summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs13
-rw-r--r--src/SMAPI.ModBuildConfig/DeployModTask.cs3
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/WebApiClient.cs56
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs10
-rw-r--r--src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs9
-rw-r--r--src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs2
-rw-r--r--src/SMAPI.Toolkit/ModToolkit.cs2
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs7
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs7
-rw-r--r--src/SMAPI.Toolkit/Serialization/JsonHelper.cs26
-rw-r--r--src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs2
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/LogParser.cs2
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs32
-rw-r--r--src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs3
-rw-r--r--src/SMAPI.Web/Startup.cs2
-rw-r--r--src/SMAPI.Web/Views/LogParser/Index.cshtml127
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/log-parser.css34
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json10
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs3
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs27
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs2
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModHelper.cs1
-rw-r--r--src/SMAPI/Framework/ModLoading/ModMetadata.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs4
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs2
-rw-r--r--src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs1
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs2
-rw-r--r--src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs2
-rw-r--r--src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs2
-rw-r--r--src/SMAPI/Framework/SCore.cs54
-rw-r--r--src/SMAPI/Framework/SGame.cs3
-rw-r--r--src/SMAPI/Framework/Serialization/KeybindConverter.cs5
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs66
-rw-r--r--src/SMAPI/SMAPI.csproj1
-rw-r--r--src/SMAPI/Translation.cs2
-rw-r--r--src/SMAPI/Utilities/SDate.cs6
41 files changed, 320 insertions, 228 deletions
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index 5a6aa747..d00a5df4 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -206,7 +206,7 @@ namespace StardewModdingApi.Installer
Console.WriteLine();
// handle choice
- string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" });
+ string choice = this.InteractivelyChoose("Type 1 or 2, then press enter.", new[] { "1", "2" }, printLine: Console.WriteLine);
switch (choice)
{
case "1":
@@ -453,6 +453,7 @@ namespace StardewModdingApi.Installer
}
// find target folder
+ // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract -- avoid error if the Mods folder has invalid mods, since they're not validated yet
ModFolder? targetMod = targetMods.FirstOrDefault(p => p.Manifest?.UniqueID?.Equals(sourceMod.Manifest.UniqueID, StringComparison.OrdinalIgnoreCase) == true);
DirectoryInfo defaultTargetFolder = new(Path.Combine(paths.ModsPath, sourceMod.Directory.Name));
DirectoryInfo targetFolder = targetMod?.Directory ?? defaultTargetFolder;
@@ -628,22 +629,22 @@ namespace StardewModdingApi.Installer
}
/// <summary>Interactively ask the user to choose a value.</summary>
- /// <param name="print">A callback which prints a message to the console.</param>
+ /// <param name="printLine">A callback which prints a message to the console.</param>
/// <param name="message">The message to print.</param>
/// <param name="options">The allowed options (not case sensitive).</param>
/// <param name="indent">The indentation to prefix to output.</param>
- private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? print = null)
+ private string InteractivelyChoose(string message, string[] options, string indent = "", Action<string>? printLine = null)
{
- print ??= this.PrintInfo;
+ printLine ??= this.PrintInfo;
while (true)
{
- print(indent + message);
+ printLine(indent + message);
Console.Write(indent);
string? input = Console.ReadLine()?.Trim().ToLowerInvariant();
if (input == null || !options.Contains(input))
{
- print($"{indent}That's not a valid option.");
+ printLine($"{indent}That's not a valid option.");
continue;
}
return input;
diff --git a/src/SMAPI.ModBuildConfig/DeployModTask.cs b/src/SMAPI.ModBuildConfig/DeployModTask.cs
index c7026ee1..88412d92 100644
--- a/src/SMAPI.ModBuildConfig/DeployModTask.cs
+++ b/src/SMAPI.ModBuildConfig/DeployModTask.cs
@@ -227,8 +227,7 @@ namespace StardewModdingAPI.ModBuildConfig
string fromPath = entry.Value.FullName;
string toPath = Path.Combine(modFolderPath, entry.Key);
- // ReSharper disable once AssignNullToNotNullAttribute -- not applicable in this context
- Directory.CreateDirectory(Path.GetDirectoryName(toPath));
+ Directory.CreateDirectory(Path.GetDirectoryName(toPath)!);
File.Copy(fromPath, toPath, overwrite: true);
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
index 3722e155..88ddfe6b 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs
@@ -30,7 +30,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
/// <summary>Get all spawnable items.</summary>
/// <param name="itemTypes">The item types to fetch (or null for any type).</param>
/// <param name="includeVariants">Whether to include flavored variants like "Sunflower Honey".</param>
- [SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "TryCreate invokes the lambda immediately.")]
+ [SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = $"{nameof(ItemRepository.TryCreate)} invokes the lambda immediately.")]
public IEnumerable<SearchableItem> GetAll(ItemType[]? itemTypes = null, bool includeVariants = true)
{
//
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 300de9d2..3c2dec19 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.15.0",
+ "Version": "3.15.1",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.15.0"
+ "MinimumApiVersion": "3.15.1"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index 15a1e0f3..28b4b149 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
- "Version": "3.15.0",
+ "Version": "3.15.1",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.15.0"
+ "MinimumApiVersion": "3.15.1"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 1a11742c..1944575b 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.15.0",
+ "Version": "3.15.1",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.15.0"
+ "MinimumApiVersion": "3.15.1"
}
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/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
index 7f06d170..3bdd145a 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
@@ -283,8 +283,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
}
/// <summary>The response model for the MediaWiki parse API.</summary>
- [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
- [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
+ [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialization.")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialization.")]
private class ResponseModel
{
/*********
@@ -306,9 +306,9 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
}
/// <summary>The inner response model for the MediaWiki parse API.</summary>
- [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")]
- [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local")]
- [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
+ [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local", Justification = "Used via JSON deserialization.")]
+ [SuppressMessage("ReSharper", "CollectionNeverUpdated.Local", Justification = "Used via JSON deserialization.")]
+ [SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local", Justification = "Used via JSON deserialization.")]
private class ResponseParseModel
{
/*********
diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
index 6978567e..f464f4bb 100644
--- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs
@@ -21,7 +21,8 @@ namespace StardewModdingAPI.Toolkit.Framework
/// <summary>Get the OS name from the system uname command.</summary>
/// <param name="buffer">The buffer to fill with the resulting string.</param>
[DllImport("libc")]
- static extern int uname(IntPtr buffer);
+ [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "This is the actual external command name.")]
+ private static extern int uname(IntPtr buffer);
/*********
@@ -51,7 +52,6 @@ namespace StardewModdingAPI.Toolkit.Framework
/// <summary>Get the human-readable OS name and version.</summary>
/// <param name="platform">The current platform.</param>
- [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")]
public static string GetFriendlyPlatformName(string platform)
{
#if SMAPI_FOR_WINDOWS
@@ -65,7 +65,10 @@ namespace StardewModdingAPI.Toolkit.Framework
return result ?? "Windows";
}
- catch { }
+ catch
+ {
+ // fallback to default behavior
+ }
#endif
string name = Environment.OSVersion.ToString();
diff --git a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
index 338192af..4c76f417 100644
--- a/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
+++ b/src/SMAPI.Toolkit/Framework/ModData/ModWarning.cs
@@ -18,7 +18,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModData
/// <summary>The mod patches the game in a way that may impact stability.</summary>
PatchesGame = 4,
-#if SMAPI_FOR_WINDOWS
+#if SMAPI_DEPRECATED
/// <summary>The mod uses the <c>dynamic</c> keyword which won't work on Linux/macOS.</summary>
[Obsolete("This value is no longer used by SMAPI and will be removed in the upcoming SMAPI 4.0.0.")]
UsesDynamic = 8,
diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs
index 0df75a31..55b9bdd8 100644
--- a/src/SMAPI.Toolkit/ModToolkit.cs
+++ b/src/SMAPI.Toolkit/ModToolkit.cs
@@ -65,7 +65,7 @@ namespace StardewModdingAPI.Toolkit
/// <param name="metadataPath">The file path for the SMAPI metadata file.</param>
public ModDatabase GetModDatabase(string metadataPath)
{
- MetadataModel metadata = JsonConvert.DeserializeObject<MetadataModel>(File.ReadAllText(metadataPath));
+ MetadataModel metadata = JsonConvert.DeserializeObject<MetadataModel>(File.ReadAllText(metadataPath)) ?? new MetadataModel();
ModDataRecord[] records = metadata.ModData.Select(pair => new ModDataRecord(pair.Key, pair.Value)).ToArray();
return new ModDatabase(records, this.GetUpdateUrl);
}
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
index c32c3185..913d54e0 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
@@ -48,7 +48,12 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
return this.ReadObject(JObject.Load(reader));
case JsonToken.String:
- return this.ReadString(JToken.Load(reader).Value<string>(), path);
+ {
+ string? value = JToken.Load(reader).Value<string>();
+ return value is not null
+ ? this.ReadString(value, path)
+ : null;
+ }
default:
throw new SParseException($"Can't parse {nameof(ISemanticVersion)} from {reader.TokenType} node (path: {reader.Path}).");
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
index 1c59f5e7..cdf2ed77 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SimpleReadOnlyConverter.cs
@@ -42,7 +42,12 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
return this.ReadObject(JObject.Load(reader), path);
case JsonToken.String:
- return this.ReadString(JToken.Load(reader).Value<string>(), path);
+ {
+ string? value = JToken.Load(reader).Value<string>();
+ return value is not null
+ ? this.ReadString(value, path)
+ : null;
+ }
default:
throw new SParseException($"Can't parse {typeof(T).Name} from {reader.TokenType} node (path: {reader.Path}).");
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/Framework/Clients/ModDrop/ModDropClient.cs b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
index c60b2c90..f5a5f930 100644
--- a/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
+++ b/src/SMAPI.Web/Framework/Clients/ModDrop/ModDropClient.cs
@@ -42,7 +42,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.ModDrop
/// <summary>Get update check info about a mod.</summary>
/// <param name="id">The mod ID.</param>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this method.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The nullability is validated in this method.")]
public async Task<IModPage?> GetModData(string id)
{
IModPage page = new GenericModPage(this.SiteKey, id);
diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
index 7fc8f958..0efa62c5 100644
--- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs
@@ -200,6 +200,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing
log.GameVersion = match.Groups["gameVersion"].Value;
log.OperatingSystem = match.Groups["os"].Value;
smapiMod.OverrideVersion(log.ApiVersion);
+
+ log.ApiVersionParsed = smapiMod.GetParsedVersion();
}
// mod path line
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs
index 4b80a830..557f08ff 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/LogModInfo.cs
@@ -1,4 +1,6 @@
+using System;
using System.Diagnostics.CodeAnalysis;
+using StardewModdingAPI.Toolkit;
namespace StardewModdingAPI.Web.Framework.LogParsing.Models
{
@@ -6,6 +8,13 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
public class LogModInfo
{
/*********
+ ** Private fields
+ *********/
+ /// <summary>The parsed mod version, if valid.</summary>
+ private Lazy<ISemanticVersion?> ParsedVersionImpl;
+
+
+ /*********
** Accessors
*********/
/// <summary>The mod name.</summary>
@@ -68,7 +77,6 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
{
this.Name = name;
this.Author = author;
- this.Version = version;
this.Description = description;
this.UpdateVersion = updateVersion;
this.UpdateLink = updateLink;
@@ -82,6 +90,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
this.IsContentPack = !string.IsNullOrWhiteSpace(this.ContentPackFor);
this.IsCodeMod = !this.IsContentPack;
}
+
+ this.OverrideVersion(version);
}
/// <summary>Add an update alert for this mod.</summary>
@@ -95,9 +105,29 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
/// <summary>Override the version number, for cases like SMAPI itself where the version is only known later during parsing.</summary>
/// <param name="version">The new mod version.</param>
+ [MemberNotNull(nameof(LogModInfo.Version), nameof(LogModInfo.ParsedVersionImpl))]
public void OverrideVersion(string version)
{
this.Version = version;
+ this.ParsedVersionImpl = new Lazy<ISemanticVersion?>(this.ParseVersion);
+ }
+
+ /// <summary>Get the semantic version for this mod, if it's valid.</summary>
+ public ISemanticVersion? GetParsedVersion()
+ {
+ return this.ParsedVersionImpl.Value;
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Get the semantic version for this mod, if it's valid.</summary>
+ public ISemanticVersion? ParseVersion()
+ {
+ return !string.IsNullOrWhiteSpace(this.Version) && SemanticVersion.TryParse(this.Version, out ISemanticVersion? version)
+ ? version
+ : null;
}
}
}
diff --git a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
index 6951e434..3f649199 100644
--- a/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
+++ b/src/SMAPI.Web/Framework/LogParsing/Models/ParsedLog.cs
@@ -28,6 +28,9 @@ namespace StardewModdingAPI.Web.Framework.LogParsing.Models
/// <summary>The SMAPI version.</summary>
public string? ApiVersion { get; set; }
+ /// <summary>The parsed SMAPI version, if it's valid.</summary>
+ public ISemanticVersion? ApiVersionParsed { get; set; }
+
/// <summary>The game version.</summary>
public string? GameVersion { get; set; }
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.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml
index 33239a2b..57e26ace 100644
--- a/src/SMAPI.Web/Views/LogParser/Index.cshtml
+++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml
@@ -8,24 +8,30 @@
@{
ViewData["Title"] = "SMAPI log parser";
+ // get log info
ParsedLog? log = Model!.ParsedLog;
-
IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod();
+ ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>());
+
+ // detect suggested fixes
+ LogModInfo[] outdatedMods = log?.Mods.Where(mod => mod.HasUpdate).ToArray() ?? Array.Empty<LogModInfo>();
+ LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p.IsCodeMod && p.Name == "Error Handler");
+ bool hasOlderErrorHandler = errorHandler?.GetParsedVersion() is not null && log?.ApiVersionParsed is not null && log.ApiVersionParsed.IsNewerThan(errorHandler.GetParsedVersion());
+ bool isPyTkCompatibilityMode = log?.ApiVersionParsed?.IsOlderThan("3.15.0") is false && log.Mods.Any(p => p.IsCodeMod && p.Name == "PyTK" && p.GetParsedVersion()?.IsOlderThan("1.23.1") is true);
+
+ // get filters
IDictionary<string, bool> defaultFilters = Enum
.GetValues<LogLevel>()
.ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
-
IDictionary<int, string> logLevels = Enum
.GetValues<LogLevel>()
.ToDictionary(level => (int)level, level => level.ToString().ToLower());
-
IDictionary<int, string> logSections = Enum
.GetValues<LogSection>()
.ToDictionary(section => (int)section, section => section.ToString());
+ // get form
string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID }, absoluteUrl: true)!;
-
- ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>());
}
@section Head {
@@ -34,7 +40,7 @@
<meta name="robots" content="noindex" />
}
<link rel="stylesheet" href="~/Content/css/file-upload.css" />
- <link rel="stylesheet" href="~/Content/css/log-parser.css?r=20220409" />
+ <link rel="stylesheet" href="~/Content/css/log-parser.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3/dist/css/tabby-ui-vertical.min.css" />
<script src="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3" crossorigin="anonymous"></script>
@@ -69,7 +75,7 @@
</text>
}
</script>
-
+
<script>
$(function() {
smapi.logParser(
@@ -158,7 +164,7 @@ else if (log?.IsValid == true)
<div id="os-instructions">
<div>
<ul data-tabs>
- @foreach (Platform platform in new[] {Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows})
+ @foreach (Platform platform in new[] { Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows })
{
@if (platform == Platform.Windows)
{
@@ -237,55 +243,70 @@ else if (log?.IsValid == true)
@if (log?.IsValid == true)
{
<div id="output">
- @if (log.Mods.Any(mod => mod.HasUpdate))
+ @if (outdatedMods.Any() || errorHandler is null || hasOlderErrorHandler || isPyTkCompatibilityMode)
{
<h2>Suggested fixes</h2>
<ul id="fix-list">
- <li>
- Consider updating these mods to fix problems:
+ @if (errorHandler is null)
+ {
+ <li class="important">You don't have the <strong>Error Handler</strong> mod installed. This automatically prevents many game or mod errors. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to re-add it.</li>
+ }
+ @if (hasOlderErrorHandler)
+ {
+ <li>Your <strong>Error Handler</strong> mod is older than SMAPI. You may be missing some game/mod error fixes. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to update it.</li>
+ }
+ @if (isPyTkCompatibilityMode)
+ {
+ <li>PyTK 1.23.0 or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.</li>
+ }
+ @if (outdatedMods.Any())
+ {
+ <li>
+ Consider updating these mods to fix problems:
- <table id="updates" class="table">
- @foreach (LogModInfo mod in log.Mods.Where(mod => (mod.HasUpdate && !mod.IsContentPack) || (contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) && contentPackList.Any(pack => pack.HasUpdate))))
- {
- <tr class="mod-entry">
- <td>
- <strong class=@(!mod.HasUpdate ? "hidden" : "")>@mod.Name</strong>
- @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList))
- {
- <div class="content-packs">
- @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
- {
- <text>+ @contentPack.Name</text><br />
- }
- </div>
- }
- </td>
- <td>
- @if (mod.HasUpdate)
- {
- <a href="@mod.UpdateLink" target="_blank">
- @(mod.Version == null ? mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}")
- </a>
- }
- else
- {
- <text>&nbsp;</text>
- }
+ <table id="updates" class="table">
+ @foreach (LogModInfo mod in log.Mods.Where(mod => (mod.HasUpdate && !mod.IsContentPack) || (contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) && contentPackList.Any(pack => pack.HasUpdate))))
+ {
+ <tr class="mod-entry">
+ <td>
+ <strong class=@(!mod.HasUpdate ? "hidden" : "")>@mod.Name</strong>
+ @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList))
+ {
+ <div class="content-packs">
+ @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
+ {
+ <text>+ @contentPack.Name</text><br />
+ }
+ </div>
+ }
+ </td>
+ <td>
+ @if (mod.HasUpdate)
+ {
+ <a href="@mod.UpdateLink" target="_blank">
+ @(mod.Version == null ? mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}")
+ </a>
+ }
+ else
+ {
+ <text>&nbsp;</text>
+ }
- @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList))
- {
- <div>
- @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
- {
- <a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br />
- }
- </div>
- }
- </td>
- </tr>
- }
- </table>
- </li>
+ @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList))
+ {
+ <div>
+ @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
+ {
+ <a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br />
+ }
+ </div>
+ }
+ </td>
+ </tr>
+ }
+ </table>
+ </li>
+ }
</ul>
}
@@ -434,7 +455,7 @@ else if (log?.IsValid == true)
<div>
This website uses JavaScript to display a filterable table. To view this log, please enable JavaScript or <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteID, format = LogViewFormat.RawView })">view the raw log</a>.
</div>
- <br/>
+ <br />
</noscript>
<log-table>
diff --git a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
index 1d457e35..f136a96f 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/log-parser.css
@@ -54,6 +54,36 @@ table caption {
}
/*********
+** Suggested fixes
+*********/
+#fix-list {
+ padding-left: 1em;
+ margin-bottom: 2em;
+}
+
+#fix-list li {
+ padding: 0.5em;
+ background: #FFC;
+ border: 1px solid #880;
+ border-radius: 5px;
+ list-style-type: none;
+}
+
+#fix-list li:not(:last-child) {
+ margin-bottom: 0.5em;
+}
+
+#fix-list li.important {
+ background: #FCC;
+ border-color: #800;
+}
+
+#fix-list li::before {
+ content: "⚠ ";
+}
+
+
+/*********
** Log metadata & filters
*********/
.table, #filters {
@@ -84,10 +114,6 @@ table caption {
min-height: 1.3em;
}
-#fix-list {
- margin-bottom: 2em;
-}
-
#updates {
min-width: 10em;
}
diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
index 16a89647..d654b181 100644
--- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
@@ -107,12 +107,6 @@
"Default | UpdateKey": "Nexus:1726"
},
- "Rubydew": {
- "ID": "bwdy.rubydew",
- "SuppressWarnings": "UsesDynamic", // mod explicitly loads DLLs for Linux/macOS compatibility
- "Default | UpdateKey": "Nexus:3656"
- },
-
"SpaceCore": {
"ID": "spacechase0.SpaceCore",
"Default | UpdateKey": "Nexus:1348"
@@ -172,8 +166,8 @@
*********/
"CFAutomate": {
"ID": "Platonymous.CFAutomate",
- "~2.12.9 | Status": "AssumeBroken",
- "~2.12.9 | StatusReasonDetails": "causes runtime errors in newer versions of Automate"
+ "~2.12.11 | Status": "AssumeBroken",
+ "~2.12.11 | StatusReasonDetails": "causes runtime errors in newer versions of Automate"
},
"Dynamic Game Assets": {
"ID": "spacechase0.DynamicGameAssets",
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 442f2ec8..c59af612 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -52,7 +52,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.15.0";
+ internal static string RawApiVersion = "3.15.1";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index d7be0c37..54f8e2a2 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -111,7 +111,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
/// <inheritdoc />
- [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Copied as-is from game code")]
public sealed override string LoadBaseString(string path)
{
try
@@ -119,7 +118,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// copied as-is from LocalizedContentManager.LoadBaseString
// This is only changed to call this.Load instead of base.Load, to support mod assets
this.ParseStringPath(path, out string assetName, out string key);
- Dictionary<string, string> strings = this.Load<Dictionary<string, string>>(assetName, LanguageCode.en);
+ Dictionary<string, string>? strings = this.Load<Dictionary<string, string>?>(assetName, LanguageCode.en);
return strings != null && strings.ContainsKey(key)
? this.GetString(strings, key)
: path;
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 8c5d0f84..f3cf05d9 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -1,5 +1,4 @@
using System;
-using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
@@ -47,9 +46,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>If a map tilesheet's image source has no file extensions, the file extensions to check for in the local mod folder.</summary>
private static readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" };
- /// <summary>A lookup of image file paths to whether they have PyTK scaling information.</summary>
- private static readonly Dictionary<string, bool> IsPyTkScaled = new(StringComparer.OrdinalIgnoreCase);
-
/*********
** Accessors
@@ -211,24 +207,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
{
if (ModContentManager.EnablePyTkLegacyMode)
{
- if (!ModContentManager.IsPyTkScaled.TryGetValue(file.FullName, out bool isScaled))
- {
- string? dirPath = file.DirectoryName;
- string fileName = $"{Path.GetFileNameWithoutExtension(file.Name)}.pytk.json";
-
- string path = dirPath is not null
- ? Path.Combine(dirPath, fileName)
- : fileName;
-
- ModContentManager.IsPyTkScaled[file.FullName] = isScaled = File.Exists(path);
- }
-
- asRawData = !isScaled;
- if (!asRawData)
- this.Monitor.LogOnce("Enabled compatibility mode for PyTK scaled textures. This won't cause any issues, but may impact performance.", LogLevel.Warn);
+ // PyTK intercepts Texture2D file loads to rescale them (e.g. for HD portraits),
+ // but doesn't support IRawTextureData loads yet. We can't just check if the
+ // current file has a '.pytk.json' rescale file though, since PyTK may still
+ // rescale it if the original asset or another edit gets rescaled.
+ asRawData = false;
+ this.Monitor.LogOnce("Enabled compatibility mode for PyTK 1.23.0 or earlier. This won't cause any issues, but may impact performance.", LogLevel.Warn);
}
- else
- asRawData = true;
}
// load
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index d811ed5c..c0b7c0ba 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -400,7 +400,7 @@ namespace StardewModdingAPI.Framework.Logging
/// <param name="mods">The loaded mods.</param>
/// <param name="skippedMods">The mods which could not be loaded.</param>
/// <param name="logParanoidWarnings">Whether to log issues for mods which directly use potentially sensitive .NET APIs like file or shell access.</param>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "Manifests aren't guaranteed non-null at this point in the loading process.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "Manifests aren't guaranteed non-null at this point in the loading process.")]
private void LogModWarnings(IEnumerable<IModMetadata> mods, IModMetadata[] skippedMods, bool logParanoidWarnings)
{
// get mods with warnings
diff --git a/src/SMAPI/Framework/ModHelpers/ModHelper.cs b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
index 9ac3b6f7..caa66bad 100644
--- a/src/SMAPI/Framework/ModHelpers/ModHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModHelper.cs
@@ -88,6 +88,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="modDirectory">The full path to the mod's folder.</param>
/// <param name="currentInputState">Manages the game's input state for the current player instance. That may not be the main player in split-screen mode.</param>
/// <param name="events">Manages access to events raised by SMAPI.</param>
+ /// <param name="contentHelper">An API for loading content assets.</param>
/// <param name="gameContentHelper">An API for loading content assets from the game's <c>Content</c> folder or via <see cref="IModEvents.Content"/>.</param>
/// <param name="modContentHelper">An API for loading content assets from your mod's files.</param>
/// <param name="contentPackHelper">An API for managing content packs.</param>
diff --git a/src/SMAPI/Framework/ModLoading/ModMetadata.cs b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
index aa4d2d8c..ac7a6bbd 100644
--- a/src/SMAPI/Framework/ModLoading/ModMetadata.cs
+++ b/src/SMAPI/Framework/ModLoading/ModMetadata.cs
@@ -83,7 +83,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <inheritdoc />
[MemberNotNullWhen(true, nameof(ModMetadata.ContentPack))]
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The manifest may be null for broken mods while loading.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The manifest may be null for broken mods while loading.")]
public bool IsContentPack => this.Manifest?.ContentPackFor != null;
/// <summary>The fake content packs created by this mod, if any.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 3e7144f9..abc46d47 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -60,8 +60,8 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
/// <param name="getFileLookup">Get a file lookup for the given directory.</param>
/// <param name="validateFilesExist">Whether to validate that files referenced in the manifest (like <see cref="IManifest.EntryDll"/>) exist on disk. This can be disabled to only validate the manifest itself.</param>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "Manifest values may be null before they're validated.")]
- [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Manifest values may be null before they're validated.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "Manifest values may be null before they're validated.")]
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "Manifest values may be null before they're validated.")]
public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string?> getUpdateUrl, Func<string, IFileLookup> getFileLookup, bool validateFilesExist = true)
{
mods = mods.ToArray();
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
index 9c8ba2b0..be45272e 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/HarmonyInstanceFacade.cs
@@ -28,7 +28,7 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
return new Harmony(id);
}
- [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "If the user passes a null original method, we let it fail in the underlying Harmony instance instead of handling it here.")]
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract", Justification = "If the user passes a null original method, we let it fail in the underlying Harmony instance instead of handling it here.")]
public DynamicMethod Patch(MethodBase original, HarmonyMethod? prefix = null, HarmonyMethod? postfix = null, HarmonyMethod? transpiler = null)
{
// In Harmony 1.x you could target a virtual method that's not implemented by the
diff --git a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs
index 67569424..3eb31df3 100644
--- a/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs
+++ b/src/SMAPI/Framework/ModLoading/RewriteFacades/SpriteBatchFacade.cs
@@ -10,7 +10,6 @@ namespace StardewModdingAPI.Framework.ModLoading.RewriteFacades
/// <summary>Provides <see cref="SpriteBatch"/> method signatures that can be injected into mod code for compatibility with mods written for XNA Framework before Stardew Valley 1.5.5.</summary>
/// <remarks>This is public to support SMAPI rewriting and should not be referenced directly by mods.</remarks>
[SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "Used via assembly rewriting")]
- [SuppressMessage("ReSharper", "CS0109", Justification = "The 'new' modifier applies when compiled on Linux/macOS.")]
[SuppressMessage("ReSharper", "CS1591", Justification = "Documentation not needed for facade classes.")]
public class SpriteBatchFacade : SpriteBatch
{
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index 62b15405..9444c046 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -100,7 +100,7 @@ namespace StardewModdingAPI.Framework.Models
/// <param name="logNetworkTraffic">Whether SMAPI should log network traffic.</param>
/// <param name="consoleColors">The colors to use for text written to the SMAPI console.</param>
/// <param name="suppressUpdateChecks">The mod IDs SMAPI should ignore when performing update checks or validating update keys.</param>
- public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? usePintail, bool? useRawImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
+ public SConfig(bool developerMode, bool? checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, string[]? verboseLogging, bool? rewriteMods, bool? useRawImageLoading, bool? useCaseInsensitivePaths, bool? logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates ?? (bool)SConfig.DefaultValues[nameof(this.CheckForUpdates)];
diff --git a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs
index 1e150508..cc936489 100644
--- a/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs
+++ b/src/SMAPI/Framework/Networking/MultiplayerPeerMod.cs
@@ -22,7 +22,7 @@ namespace StardewModdingAPI.Framework.Networking
*********/
/// <summary>Construct an instance.</summary>
/// <param name="mod">The mod metadata.</param>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The ID shouldn't be null, but we should handle it to avoid an error just in case.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The ID shouldn't be null, but we should handle it to avoid an error just in case.")]
public MultiplayerPeerMod(RemoteContextModModel mod)
{
this.Name = mod.Name;
diff --git a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
index 94b13378..dac41629 100644
--- a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
+++ b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs
@@ -14,7 +14,7 @@ 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>
- [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Field naming deliberately matches " + nameof(XnaDisplayDevice) + " to minimize differences.")]
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = $"Field naming deliberately matches {nameof(XnaDisplayDevice)} to minimize differences.")]
internal class SXnaDisplayDevice : IDisplayDevice
{
/*********
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 16c168a0..46d65f6a 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;
@@ -190,7 +191,7 @@ namespace StardewModdingAPI.Framework
string logPath = this.GetLogPath();
// init basics
- this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath));
+ this.Settings = JsonConvert.DeserializeObject<SConfig>(File.ReadAllText(Constants.ApiConfigPath)) ?? throw new InvalidOperationException("The 'smapi-internal/config.json' file is missing or invalid. You can reinstall SMAPI to fix this.");
if (File.Exists(Constants.ApiUserConfigPath))
JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings);
if (developerMode.HasValue)
@@ -324,7 +325,7 @@ namespace StardewModdingAPI.Framework
}
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "May be disposed before SMAPI is fully initialized.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "May be disposed before SMAPI is fully initialized.")]
public void Dispose()
{
// skip if already disposed
@@ -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
@@ -1284,7 +1285,7 @@ namespace StardewModdingAPI.Framework
private LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory)
{
// Game1._temporaryContent initializing from SGame constructor
- // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- this is the method that initializes it
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract -- this is the method that initializes it
if (this.ContentCore == null)
{
this.ContentCore = new ContentCoordinator(
@@ -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>>();
@@ -1559,7 +1565,7 @@ namespace StardewModdingAPI.Framework
this.Monitor.Newline();
this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert);
foreach ((IModMetadata mod, ISemanticVersion newVersion, string newUrl) in updates)
- this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert);
+ this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl} (you have {mod.Manifest.Version})", LogLevel.Alert);
}
else
this.Monitor.Log(" All mods up to date.");
@@ -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>
@@ -1794,7 +1808,7 @@ namespace StardewModdingAPI.Framework
string relativePath = mod.GetRelativePathWithRoot();
if (mod.IsContentPack)
this.Monitor.Log($" {mod.DisplayName} (from {relativePath}) [content pack]...");
- // ReSharper disable once ConstantConditionalAccessQualifier -- mod may be invalid at this point
+ // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract -- mod may be invalid at this point
else if (mod.Manifest?.EntryDll != null)
this.Monitor.Log($" {mod.DisplayName} (from {relativePath}{Path.DirectorySeparatorChar}{mod.Manifest.EntryDll})..."); // don't use Path.Combine here, since EntryDLL might not be valid
else
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 38043e1c..feb0988a 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -252,6 +252,7 @@ namespace StardewModdingAPI.Framework
[SuppressMessage("ReSharper", "LocalVariableHidesMember", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "PossibleLossOfFraction", Justification = "copied from game code as-is")]
+ [SuppressMessage("ReSharper", "PossibleNullReferenceException", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "RedundantArgumentDefaultValue", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "RedundantBaseQualifier", Justification = "copied from game code as-is")]
[SuppressMessage("ReSharper", "RedundantCast", Justification = "copied from game code as-is")]
@@ -261,6 +262,8 @@ namespace StardewModdingAPI.Framework
[SuppressMessage("ReSharper", "MergeIntoPattern", Justification = "copied from game code as-is")]
[SuppressMessage("SMAPI.CommonErrors", "AvoidImplicitNetFieldCast", Justification = "copied from game code as-is")]
[SuppressMessage("SMAPI.CommonErrors", "AvoidNetField", Justification = "copied from game code as-is")]
+
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Deliberate to minimize chance of errors when copying event calls into new versions of this code.")]
private void DrawImpl(GameTime gameTime, RenderTarget2D target_screen)
{
var events = this.Events;
diff --git a/src/SMAPI/Framework/Serialization/KeybindConverter.cs b/src/SMAPI/Framework/Serialization/KeybindConverter.cs
index 539f1291..f7b8e67e 100644
--- a/src/SMAPI/Framework/Serialization/KeybindConverter.cs
+++ b/src/SMAPI/Framework/Serialization/KeybindConverter.cs
@@ -49,7 +49,10 @@ namespace StardewModdingAPI.Framework.Serialization
case JsonToken.String:
{
- string str = JToken.Load(reader).Value<string>();
+ string? str = JToken.Load(reader).Value<string>();
+
+ if (str is null)
+ return new Keybind(Array.Empty<SButton>());
if (objectType == typeof(Keybind))
{
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index b783b2b9..1ef9a8f2 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -164,6 +164,7 @@ namespace StardewModdingAPI.Metadata
var content = this.MainContentManager;
string key = assetName.BaseName;
changedWarpRoutes = false;
+ bool changed = false;
/****
** Special case: current map tilesheet
@@ -175,7 +176,10 @@ namespace StardewModdingAPI.Metadata
foreach (TileSheet tilesheet in Game1.currentLocation.map.TileSheets)
{
if (this.IsSameBaseName(assetName, tilesheet.ImageSource))
+ {
Game1.mapDisplayDevice.LoadTileSheet(tilesheet);
+ changed = true;
+ }
}
}
@@ -184,8 +188,6 @@ namespace StardewModdingAPI.Metadata
****/
if (type == typeof(Map))
{
- bool anyChanged = false;
-
if (!ignoreWorld)
{
foreach (LocationInfo info in this.GetLocationsWithInfo())
@@ -206,12 +208,12 @@ namespace StardewModdingAPI.Metadata
var newWarps = GetWarpSet(location);
changedWarpRoutes = changedWarpRoutes || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p));
- anyChanged = true;
+ changed = true;
}
}
}
- return anyChanged;
+ return changed;
}
/****
@@ -223,7 +225,7 @@ namespace StardewModdingAPI.Metadata
** Animals
****/
case "animals/horse":
- return !ignoreWorld && this.UpdatePetOrHorseSprites<Horse>(assetName);
+ return changed | (!ignoreWorld && this.UpdatePetOrHorseSprites<Horse>(assetName));
/****
** Buildings
@@ -239,7 +241,7 @@ namespace StardewModdingAPI.Metadata
Farm farm = Game1.getFarm();
farm?.ApplyHousePaint();
- return removedFromCache || farm != null;
+ return changed | (removedFromCache || farm != null);
}
/****
@@ -253,7 +255,7 @@ namespace StardewModdingAPI.Metadata
case "characters/farmer/farmer_base_bald":
case "characters/farmer/farmer_girl_base":
case "characters/farmer/farmer_girl_base_bald":
- return !ignoreWorld && this.UpdatePlayerSprites(assetName);
+ return changed | (!ignoreWorld && this.UpdatePlayerSprites(assetName));
case "characters/farmer/hairstyles": // Game1.LoadContent
FarmerRenderer.hairStylesTexture = this.LoadTexture(key);
@@ -305,10 +307,10 @@ namespace StardewModdingAPI.Metadata
return true;
case "data/farmanimals": // FarmAnimal constructor
- return !ignoreWorld && this.UpdateFarmAnimalData();
+ return changed | (!ignoreWorld && this.UpdateFarmAnimalData());
case "data/hairdata": // Farmer.GetHairStyleMetadataFile
- return this.UpdateHairData();
+ return changed | this.UpdateHairData();
case "data/movies": // MovieTheater.GetMovieData
case "data/moviesreactions": // MovieTheater.GetMovieReactions
@@ -316,7 +318,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "data/npcdispositions": // NPC constructor
- return !ignoreWorld && this.UpdateNpcDispositions(content, assetName);
+ return changed | (!ignoreWorld && this.UpdateNpcDispositions(content, assetName));
case "data/npcgifttastes": // Game1.LoadContent
Game1.NPCGiftTastes = content.Load<Dictionary<string, string>>(key);
@@ -428,7 +430,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "loosesprites/suspensionbridge": // SuspensionBridge constructor
- return !ignoreWorld && this.UpdateSuspensionBridges(content, assetName);
+ return changed | (!ignoreWorld && this.UpdateSuspensionBridges(content, assetName));
/****
** Content\Maps
@@ -456,16 +458,16 @@ namespace StardewModdingAPI.Metadata
return true;
}
}
- return false;
+ return changed;
case "minigames/titlebuttons": // TitleMenu
- return this.UpdateTitleButtons(content, assetName);
+ return changed | this.UpdateTitleButtons(content, assetName);
/****
** Content\Strings
****/
case "strings/stringsfromcsfiles":
- return this.UpdateStringsFromCsFiles(content);
+ return changed | this.UpdateStringsFromCsFiles(content);
/****
** Content\TileSheets
@@ -490,7 +492,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "tilesheets/critters": // Critter constructor
- return !ignoreWorld && this.UpdateCritterTextures(assetName);
+ return changed | (!ignoreWorld && this.UpdateCritterTextures(assetName));
case "tilesheets/crops": // Game1.LoadContent
Game1.cropSpriteSheet = content.Load<Texture2D>(key);
@@ -559,27 +561,27 @@ namespace StardewModdingAPI.Metadata
return true;
case "terrainfeatures/mushroom_tree": // from Tree
- return !ignoreWorld && this.UpdateTreeTextures(Tree.mushroomTree);
+ return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.mushroomTree));
case "terrainfeatures/tree_palm": // from Tree
- return !ignoreWorld && this.UpdateTreeTextures(Tree.palmTree);
+ return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.palmTree));
case "terrainfeatures/tree1_fall": // from Tree
case "terrainfeatures/tree1_spring": // from Tree
case "terrainfeatures/tree1_summer": // from Tree
case "terrainfeatures/tree1_winter": // from Tree
- return !ignoreWorld && this.UpdateTreeTextures(Tree.bushyTree);
+ return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.bushyTree));
case "terrainfeatures/tree2_fall": // from Tree
case "terrainfeatures/tree2_spring": // from Tree
case "terrainfeatures/tree2_summer": // from Tree
case "terrainfeatures/tree2_winter": // from Tree
- return !ignoreWorld && this.UpdateTreeTextures(Tree.leafyTree);
+ return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.leafyTree));
case "terrainfeatures/tree3_fall": // from Tree
case "terrainfeatures/tree3_spring": // from Tree
case "terrainfeatures/tree3_winter": // from Tree
- return !ignoreWorld && this.UpdateTreeTextures(Tree.pineTree);
+ return changed | (!ignoreWorld && this.UpdateTreeTextures(Tree.pineTree));
}
/****
@@ -588,25 +590,29 @@ namespace StardewModdingAPI.Metadata
if (!ignoreWorld)
{
// dynamic textures
- if (assetName.StartsWith("animals/cat"))
- return this.UpdatePetOrHorseSprites<Cat>(assetName);
- if (assetName.StartsWith("animals/dog"))
- return this.UpdatePetOrHorseSprites<Dog>(assetName);
if (assetName.IsDirectlyUnderPath("Animals"))
- return this.UpdateFarmAnimalSprites(assetName);
+ {
+ if (assetName.StartsWith("animals/cat"))
+ return changed | this.UpdatePetOrHorseSprites<Cat>(assetName);
+
+ if (assetName.StartsWith("animals/dog"))
+ return changed | this.UpdatePetOrHorseSprites<Dog>(assetName);
+
+ return changed | this.UpdateFarmAnimalSprites(assetName);
+ }
if (assetName.IsDirectlyUnderPath("Buildings"))
- return this.UpdateBuildings(assetName);
+ return changed | this.UpdateBuildings(assetName);
if (assetName.StartsWith("LooseSprites/Fence"))
- return this.UpdateFenceTextures(assetName);
+ return changed | this.UpdateFenceTextures(assetName);
// dynamic data
if (assetName.IsDirectlyUnderPath("Characters/Dialogue"))
- return this.UpdateNpcDialogue(assetName);
+ return changed | this.UpdateNpcDialogue(assetName);
if (assetName.IsDirectlyUnderPath("Characters/schedules"))
- return this.UpdateNpcSchedules(assetName);
+ return changed | this.UpdateNpcSchedules(assetName);
}
return false;
@@ -960,7 +966,7 @@ namespace StardewModdingAPI.Metadata
{
// get suspension bridges field
var field = this.Reflection.GetField<IEnumerable<SuspensionBridge>?>(location, nameof(IslandNorth.suspensionBridges), required: false);
- // ReSharper disable once ConditionIsAlwaysTrueOrFalse -- field is nullable when required: false
+ // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract -- field is nullable when required: false
if (field == null || !typeof(IEnumerable<SuspensionBridge>).IsAssignableFrom(field.FieldInfo.FieldType))
continue;
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" />
diff --git a/src/SMAPI/Translation.cs b/src/SMAPI/Translation.cs
index 01cb92b2..5cc119d9 100644
--- a/src/SMAPI/Translation.cs
+++ b/src/SMAPI/Translation.cs
@@ -121,7 +121,7 @@ namespace StardewModdingAPI
/// <summary>Get a string representation of the given translation.</summary>
/// <param name="translation">The translation key.</param>
/// <remarks><strong>Limitation with nullable reference types: if there's no text and you disabled the fallback via <see cref="UsePlaceholder"/>, this will return null but the return value will still be marked non-nullable.</strong></remarks>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The null check is required due to limitations in nullable type annotations (see remarks).")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The null check is required due to limitations in nullable type annotations (see remarks).")]
public static implicit operator string(Translation translation)
{
return translation?.ToString()!;
diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs
index 1d4e4489..06ee8b91 100644
--- a/src/SMAPI/Utilities/SDate.cs
+++ b/src/SMAPI/Utilities/SDate.cs
@@ -250,7 +250,7 @@ namespace StardewModdingAPI.Utilities
/// <param name="year">The year.</param>
/// <param name="allowDayZero">Whether to allow 0 spring Y1 as a valid date.</param>
/// <exception cref="ArgumentException">One of the arguments has an invalid value (like day 35).</exception>
- [SuppressMessage("ReSharper", "ConstantConditionalAccessQualifier", Justification = "The nullability is validated in this constructor.")]
+ [SuppressMessage("ReSharper", "ConditionalAccessQualifierIsNonNullableAccordingToAPIContract", Justification = "The nullability is validated in this constructor.")]
private SDate(int day, string season, int year, bool allowDayZero)
{
season = season?.Trim().ToLowerInvariant()!; // null-checked below
@@ -278,11 +278,11 @@ namespace StardewModdingAPI.Utilities
/// <summary>Get whether a date represents 0 spring Y1, which is the date during the in-game intro.</summary>
/// <param name="day">The day of month.</param>
- /// <param name="season">The season name.</param>
+ /// <param name="season">The normalized season name.</param>
/// <param name="year">The year.</param>
private bool IsDayZero(int day, string season, int year)
{
- return day == 0 && season?.Trim().ToLower() == "spring" && year == 1;
+ return day == 0 && season == "spring" && year == 1;
}
/// <summary>Get the day of week for a given date.</summary>