summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs6
-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/AssemblyDefinitionResolver.cs70
-rw-r--r--src/SMAPI/Framework/ModLoading/AssemblyLoader.cs13
-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.cs10
-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/Translation.cs2
-rw-r--r--src/SMAPI/Utilities/SDate.cs6
20 files changed, 148 insertions, 81 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 33468717..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>
@@ -244,8 +244,8 @@ namespace StardewModdingAPI
internal static void ConfigureAssemblyResolver(AssemblyDefinitionResolver resolver)
{
// add search paths
- resolver.AddSearchDirectory(Constants.GamePath);
- resolver.AddSearchDirectory(Constants.InternalFilesPath);
+ resolver.TryAddSearchDirectory(Constants.GamePath);
+ resolver.TryAddSearchDirectory(Constants.InternalFilesPath);
// add SMAPI explicitly
// Normally this would be handled automatically by the search paths, but for some reason there's a specific
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/AssemblyDefinitionResolver.cs b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
index b3378ad1..5a850255 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyDefinitionResolver.cs
@@ -4,18 +4,31 @@ using Mono.Cecil;
namespace StardewModdingAPI.Framework.ModLoading
{
/// <summary>A minimal assembly definition resolver which resolves references to known assemblies.</summary>
- internal class AssemblyDefinitionResolver : DefaultAssemblyResolver
+ internal class AssemblyDefinitionResolver : IAssemblyResolver
{
/*********
** Fields
*********/
+ /// <summary>The underlying assembly resolver.</summary>
+ private readonly DefaultAssemblyResolverWrapper Resolver = new();
+
/// <summary>The known assemblies.</summary>
private readonly IDictionary<string, AssemblyDefinition> Lookup = new Dictionary<string, AssemblyDefinition>();
+ /// <summary>The directory paths to search for assemblies.</summary>
+ private readonly HashSet<string> SearchPaths = new();
+
/*********
** Public methods
*********/
+ /// <summary>Construct an instance.</summary>
+ public AssemblyDefinitionResolver()
+ {
+ foreach (string path in this.Resolver.GetSearchDirectories())
+ this.SearchPaths.Add(path);
+ }
+
/// <summary>Add known assemblies to the resolver.</summary>
/// <param name="assemblies">The known assemblies.</param>
public void Add(params AssemblyDefinition[] assemblies)
@@ -29,7 +42,7 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="names">The assembly names for which it should be returned.</param>
public void AddWithExplicitNames(AssemblyDefinition assembly, params string[] names)
{
- this.RegisterAssembly(assembly);
+ this.Resolver.AddAssembly(assembly);
foreach (string name in names)
this.Lookup[name] = assembly;
}
@@ -37,18 +50,52 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <summary>Resolve an assembly reference.</summary>
/// <param name="name">The assembly name.</param>
/// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
- public override AssemblyDefinition Resolve(AssemblyNameReference name)
+ public AssemblyDefinition Resolve(AssemblyNameReference name)
{
- return this.ResolveName(name.Name) ?? base.Resolve(name);
+ return this.ResolveName(name.Name) ?? this.Resolver.Resolve(name);
}
/// <summary>Resolve an assembly reference.</summary>
/// <param name="name">The assembly name.</param>
/// <param name="parameters">The assembly reader parameters.</param>
/// <exception cref="AssemblyResolutionException">The assembly can't be resolved.</exception>
- public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
+ public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters)
{
- return this.ResolveName(name.Name) ?? base.Resolve(name, parameters);
+ return this.ResolveName(name.Name) ?? this.Resolver.Resolve(name, parameters);
+ }
+
+ /// <summary>Add a directory path to search for assemblies, if it's non-null and not already added.</summary>
+ /// <param name="path">The path to search.</param>
+ /// <returns>Returns whether the path was successfully added.</returns>
+ public bool TryAddSearchDirectory(string? path)
+ {
+ if (path is not null && this.SearchPaths.Add(path))
+ {
+ this.Resolver.AddSearchDirectory(path);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <summary>Remove a directory path to search for assemblies, if it's non-null.</summary>
+ /// <param name="path">The path to remove.</param>
+ /// <returns>Returns whether the path was in the list and removed.</returns>
+ public bool RemoveSearchDirectory(string? path)
+ {
+ if (path is not null && this.SearchPaths.Remove(path))
+ {
+ this.Resolver.RemoveSearchDirectory(path);
+ return true;
+ }
+
+ return false;
+ }
+
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ this.Resolver.Dispose();
}
@@ -63,5 +110,16 @@ namespace StardewModdingAPI.Framework.ModLoading
? match
: null;
}
+
+ /// <summary>An internal wrapper around <see cref="DefaultAssemblyResolver"/> to allow access to its protected methods.</summary>
+ private class DefaultAssemblyResolverWrapper : DefaultAssemblyResolver
+ {
+ /// <summary>Add an assembly to the resolver.</summary>
+ /// <param name="assembly">The assembly to add.</param>
+ public void AddAssembly(AssemblyDefinition assembly)
+ {
+ this.RegisterAssembly(assembly);
+ }
+ }
}
}
diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
index eb940c41..01037870 100644
--- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
+++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs
@@ -264,8 +264,15 @@ namespace StardewModdingAPI.Framework.ModLoading
if (!file.Exists)
yield break; // not a local assembly
+ // add the assembly's directory temporarily if needed
+ // this is needed by F# mods which bundle FSharp.Core.dll, for example
+ string? temporarySearchDir = null;
+ if (this.AssemblyDefinitionResolver.TryAddSearchDirectory(file.DirectoryName))
+ temporarySearchDir = file.DirectoryName;
+
// read assembly
AssemblyDefinition assembly;
+ try
{
byte[] assemblyBytes = File.ReadAllBytes(file.FullName);
Stream readStream = this.TrackForDisposal(new MemoryStream(assemblyBytes));
@@ -286,6 +293,12 @@ namespace StardewModdingAPI.Framework.ModLoading
assembly = this.TrackForDisposal(AssemblyDefinition.ReadAssembly(readStream, new ReaderParameters(ReadingMode.Immediate) { AssemblyResolver = assemblyResolver, InMemory = true }));
}
}
+ finally
+ {
+ // clean up temporary search directory
+ if (temporarySearchDir is not null)
+ this.AssemblyDefinitionResolver.RemoveSearchDirectory(temporarySearchDir);
+ }
// skip if already visited
if (visitedAssemblyNames.Contains(assembly.Name.Name))
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 fdfe70fc..46d65f6a 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -191,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)
@@ -325,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
@@ -1285,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(
@@ -1565,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.");
@@ -1808,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/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>