From 4af917335b424a77edd33aa132aad55dd7979233 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 Nov 2024 14:34:10 -0500 Subject: [PATCH 1/3] remove new private assembly references feature This caused a wide array of mod errors and crashes due to common assemblies like 0Harmony.dll and Newtonsoft.Json.dll getting reloaded (even when not marked as a private assembly). For repro steps and affected files, see this thread in #modded-tech-support on the Stardew Valley Discord: > Content Patcher "Can't parse JSON file ... Unable to find a constructor" error after recent updates We can revisit the feature in later versions. --- docs/release-notes.md | 18 +++-- src/SMAPI.Tests/Core/ModResolverTests.cs | 1 - src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs | 3 - .../IManifestPrivateAssembly.cs | 11 --- .../Framework/ManifestValidator.cs | 39 ----------- .../ManifestPrivateAssemblyArrayConverter.cs | 52 -------------- .../Serialization/Models/Manifest.cs | 9 +-- .../Models/ManifestPrivateAssembly.cs | 27 -------- .../Framework/ModLoading/AssemblyLoader.cs | 33 ++------- .../ModLoading/ModAssemblyLoadContext.cs | 69 ------------------- src/SMAPI/Framework/SCore.cs | 10 +-- 11 files changed, 25 insertions(+), 247 deletions(-) delete mode 100644 src/SMAPI.Toolkit.CoreInterfaces/IManifestPrivateAssembly.cs delete mode 100644 src/SMAPI.Toolkit/Serialization/Converters/ManifestPrivateAssemblyArrayConverter.cs delete mode 100644 src/SMAPI.Toolkit/Serialization/Models/ManifestPrivateAssembly.cs delete mode 100644 src/SMAPI/Framework/ModLoading/ModAssemblyLoadContext.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index d1cd05abc..fb0b99156 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,21 +1,31 @@ ← [README](README.md) # Release notes +## Upcoming release +* For players: + * Fixed a wide variety of mod errors and crashes after SMAPI 4.1.0 in some specific cases (e.g. Content Patcher "unable to find constructor" errors). + +* For mod authors: + * Removed the new private assembly references feature. This may be revisited in a future update once the dust settles on 1.6.9. + ## 4.1.3 Released 04 November 2024 for Stardew Valley 1.6.10 or later. -* Improved compatibility rewriters for Stardew Valley 1.6.9+. +* For players: + * Improved compatibility rewriters for Stardew Valley 1.6.9+. ## 4.1.2 Released 04 November 2024 for Stardew Valley 1.6.10 or later. -* Updated for Stardew Valley 1.6.10. -* Fixed various issues with custom maps loaded from `.tmx` files in Stardew Valley 1.6.9. +* For players: + * Updated for Stardew Valley 1.6.10. + * Fixed various issues with custom maps loaded from `.tmx` files in Stardew Valley 1.6.9. ## 4.1.1 Released 04 November 2024 for Stardew Valley 1.6.9 or later. -* Fixed crash when loading saves containing a custom spouse room loaded from a `.tmx` file. +* For players: + * Fixed crash when loading saves containing a custom spouse room loaded from a `.tmx` file. ## 4.1.0 Released 04 November 2024 for Stardew Valley 1.6.9 or later. See [release highlights](https://www.patreon.com/posts/115304143). diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index dd26355b4..15b61f9de 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -526,7 +526,6 @@ private Manifest GetManifest(string? id = null, string? name = null, string? ver minimumApiVersion: minimumApiVersion != null ? new SemanticVersion(minimumApiVersion) : null, minimumGameVersion: minimumGameVersion != null ? new SemanticVersion(minimumGameVersion) : null, dependencies: dependencies ?? Array.Empty(), - privateAssemblies: Array.Empty(), updateKeys: Array.Empty() ); } diff --git a/src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs b/src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs index 227446f13..e0c6c9cab 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs +++ b/src/SMAPI.Toolkit.CoreInterfaces/IManifest.cs @@ -38,9 +38,6 @@ public interface IManifest /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } - /// The assemblies in the mod folder which should only be referenced by this mod. These will be ignored when another mod tries to use assemblies with the same names. - IManifestPrivateAssembly[] PrivateAssemblies { get; } - /// The namespaced mod IDs to query for updates (like Nexus:541). string[] UpdateKeys { get; } diff --git a/src/SMAPI.Toolkit.CoreInterfaces/IManifestPrivateAssembly.cs b/src/SMAPI.Toolkit.CoreInterfaces/IManifestPrivateAssembly.cs deleted file mode 100644 index a3b08ebd4..000000000 --- a/src/SMAPI.Toolkit.CoreInterfaces/IManifestPrivateAssembly.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace StardewModdingAPI; - -/// An assembly which should only be referenced by the current mod. It will be ignored when another mod tries to use an assembly with the same name. -public interface IManifestPrivateAssembly -{ - /// The assembly name without metadata, like 'Newtonsoft.Json'. - public string Name { get; } - - /// Whether to disable warnings that an assembly seems to be unused, e.g. because it's accessed via reflection. - public bool UsedDynamically { get; } -} diff --git a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs index 164df58cf..b322afcd5 100644 --- a/src/SMAPI.Toolkit/Framework/ManifestValidator.cs +++ b/src/SMAPI.Toolkit/Framework/ManifestValidator.cs @@ -99,45 +99,6 @@ public static bool TryValidateFields(IManifest manifest, out string error) } } - // validate private assemblies format - if (!hasDll) - { - if (manifest.PrivateAssemblies.Length > 0) - { - error = $"manifest includes {nameof(IManifest.PrivateAssemblies)}, which isn't valid for a content pack."; - return false; - } - } - else - { - foreach (IManifestPrivateAssembly? assembly in manifest.PrivateAssemblies) - { - if (assembly is null) - { - error = $"manifest has a null entry under {nameof(IManifest.PrivateAssemblies)}."; - return false; - } - - if (string.IsNullOrWhiteSpace(assembly.Name)) - { - error = $"manifest has a {nameof(IManifest.PrivateAssemblies)} entry with no {nameof(IManifestPrivateAssembly.Name)} field."; - return false; - } - - if (assembly.Name.Contains('/') || assembly.Name.Contains('\\') || assembly.Name.Contains(".dll")) - { - error = $"manifest has a {nameof(IManifest.PrivateAssemblies)} entry with an invalid {nameof(IManifestPrivateAssembly.Name)} field (must be the assembly name without the file path, extension, or metadata)."; - return false; - } - - if (assembly.Name is "0Harmony" or "MonoGame.Framework" or "StardewModdingAPI" or "Stardew Valley" or "StardewValley.GameData") - { - error = $"manifest has a {nameof(IManifest.PrivateAssemblies)} entry with an invalid {nameof(IManifestPrivateAssembly.Name)} field (the '{assembly.Name}' assembly can't be private)."; - return false; - } - } - } - error = ""; return true; } diff --git a/src/SMAPI.Toolkit/Serialization/Converters/ManifestPrivateAssemblyArrayConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/ManifestPrivateAssemblyArrayConverter.cs deleted file mode 100644 index 5e13988d6..000000000 --- a/src/SMAPI.Toolkit/Serialization/Converters/ManifestPrivateAssemblyArrayConverter.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using StardewModdingAPI.Toolkit.Serialization.Models; - -namespace StardewModdingAPI.Toolkit.Serialization.Converters; - -/// Handles deserialization of arrays. -internal class ManifestPrivateAssemblyArrayConverter : JsonConverter -{ - /********* - ** Accessors - *********/ - /// - public override bool CanWrite => false; - - - /********* - ** Public methods - *********/ - /// - public override bool CanConvert(Type objectType) - { - return objectType == typeof(ManifestPrivateAssembly[]); - } - - - /********* - ** Protected methods - *********/ - /// - public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - List result = new List(); - - foreach (JObject obj in JArray.Load(reader).Children()) - { - string name = obj.ValueIgnoreCase(nameof(ManifestPrivateAssembly.Name))!; // will be validated separately if null - bool usedDynamically = obj.ValueIgnoreCase(nameof(ManifestPrivateAssembly.UsedDynamically)) ?? false; - result.Add(new ManifestPrivateAssembly(name, usedDynamically)); - } - - return result.ToArray(); - } - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - throw new InvalidOperationException("This converter does not write JSON."); - } -} diff --git a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs index 6db05d532..052132756 100644 --- a/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs +++ b/src/SMAPI.Toolkit/Serialization/Models/Manifest.cs @@ -42,10 +42,6 @@ public class Manifest : IManifest [JsonConverter(typeof(ManifestDependencyArrayConverter))] public IManifestDependency[] Dependencies { get; } - /// - [JsonConverter(typeof(ManifestPrivateAssemblyArrayConverter))] - public IManifestPrivateAssembly[] PrivateAssemblies { get; } - /// public string[] UpdateKeys { get; private set; } @@ -80,7 +76,6 @@ public Manifest(string uniqueID, string name, string author, string description, contentPackFor: contentPackFor != null ? new ManifestContentPackFor(contentPackFor, null) : null, - privateAssemblies: null, dependencies: null, updateKeys: null ) @@ -97,10 +92,9 @@ public Manifest(string uniqueID, string name, string author, string description, /// The name of the DLL in the directory that has the Entry method. Mutually exclusive with . /// The modID which will read this as a content pack. /// The other mods that must be loaded before this mod. - /// The names of assemblies that should be private to this mod. These assemblies will not be directly accessible by other mods and will be ignored when a mod tries to use an assembly with the same name in a public manner. /// The namespaced mod IDs to query for updates (like Nexus:541). [JsonConstructor] - public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, ISemanticVersion? minimumGameVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, IManifestPrivateAssembly[]? privateAssemblies, string[]? updateKeys) + public Manifest(string uniqueId, string name, string author, string description, ISemanticVersion version, ISemanticVersion? minimumApiVersion, ISemanticVersion? minimumGameVersion, string? entryDll, IManifestContentPackFor? contentPackFor, IManifestDependency[]? dependencies, string[]? updateKeys) { this.UniqueID = this.NormalizeField(uniqueId); this.Name = this.NormalizeField(name, replaceSquareBrackets: true); @@ -112,7 +106,6 @@ public Manifest(string uniqueId, string name, string author, string description, this.EntryDll = this.NormalizeField(entryDll); this.ContentPackFor = contentPackFor; this.Dependencies = dependencies ?? Array.Empty(); - this.PrivateAssemblies = privateAssemblies ?? Array.Empty(); this.UpdateKeys = updateKeys ?? Array.Empty(); } diff --git a/src/SMAPI.Toolkit/Serialization/Models/ManifestPrivateAssembly.cs b/src/SMAPI.Toolkit/Serialization/Models/ManifestPrivateAssembly.cs deleted file mode 100644 index 6323837af..000000000 --- a/src/SMAPI.Toolkit/Serialization/Models/ManifestPrivateAssembly.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace StardewModdingAPI.Toolkit.Serialization.Models; - -/// -public class ManifestPrivateAssembly : IManifestPrivateAssembly -{ - /********* - ** Accessors - *********/ - /// - public string Name { get; } - - /// - public bool UsedDynamically { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The assembly name. - /// Whether to disable warnings that an assembly appears to be unused, e.g. because it's accessed via reflection. - public ManifestPrivateAssembly(string name, bool usedDynamically) - { - this.Name = Manifest.NormalizeWhitespace(name); - this.UsedDynamically = usedDynamically; - } -} diff --git a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs index 5c37c4b27..d017f2e45 100644 --- a/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs +++ b/src/SMAPI/Framework/ModLoading/AssemblyLoader.cs @@ -106,10 +106,9 @@ public AssemblyLoader(Platform targetPlatform, IMonitor monitor, bool paranoidMo /// The mod for which the assembly is being loaded. /// The assembly file. /// Assume the mod is compatible, even if incompatible code is detected. - /// The assembly load context with which to load assemblies for the current mod. /// Returns the rewrite metadata for the preprocessed assembly. /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(IModMetadata mod, FileInfo assemblyFile, bool assumeCompatible, ModAssemblyLoadContext assemblyLoadContext) + public Assembly Load(IModMetadata mod, FileInfo assemblyFile, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -117,26 +116,12 @@ public Assembly Load(IModMetadata mod, FileInfo assemblyFile, bool assumeCompati HashSet visitedAssemblyNames = new( // don't try loading assemblies that are already loaded from assembly in AppDomain.CurrentDomain.GetAssemblies() let name = assembly.GetName().Name - where - name != null - && ( - !assemblyLoadContext.IsPrivateAssembly(name) - && assemblyLoadContext.IsLoadedPublicAssembly(name) - ) + where name != null select name ); assemblies = this.GetReferencedLocalAssemblies(assemblyFile, visitedAssemblyNames, this.AssemblyDefinitionResolver).ToArray(); } - // validate private assemblies - foreach (IManifestPrivateAssembly entry in mod.Manifest.PrivateAssemblies) - { - string assemblyName = entry.Name; - - if (!entry.UsedDynamically && assemblies.All(a => a.Definition?.Name.Name != assemblyName)) - this.Monitor.Log($" Mod '{mod.DisplayName}' refers to private assembly '{assemblyName}' in its manifest, but doesn't use it. This is a bug that should be reported to that mod's author.", LogLevel.Warn); - } - // validate load if (!assemblies.Any() || assemblies[0].Status == AssemblyLoadStatus.Failed) { @@ -174,27 +159,23 @@ select name } // load assembly - bool loadAsPrivate = assemblyLoadContext.IsPrivateAssembly(assembly.Definition.Name.Name); if (changed) { if (!oneAssembly) - this.Monitor.Log($" Loading{(loadAsPrivate ? " private" : "")} assembly '{assembly.File.Name}' (rewritten)..."); + this.Monitor.Log($" Loading assembly '{assembly.File.Name}' (rewritten)..."); // load assembly using MemoryStream outAssemblyStream = new(); using MemoryStream outSymbolStream = new(); assembly.Definition.Write(outAssemblyStream, new WriterParameters { WriteSymbols = true, SymbolStream = outSymbolStream, SymbolWriterProvider = this.SymbolWriterProvider }); - outAssemblyStream.Position = 0; - outSymbolStream.Position = 0; - lastAssembly = assemblyLoadContext.LoadFromStream(outAssemblyStream, outSymbolStream); - assemblyLoadContext.OnLoadedAssembly(lastAssembly); + byte[] bytes = outAssemblyStream.ToArray(); + lastAssembly = Assembly.Load(bytes, outSymbolStream.ToArray()); } else { if (!oneAssembly) - this.Monitor.Log($" Loading{(loadAsPrivate ? " private" : "")} assembly '{assembly.File.Name}'..."); - lastAssembly = assemblyLoadContext.LoadFromAssemblyPath(assembly.File.FullName); - assemblyLoadContext.OnLoadedAssembly(lastAssembly); + this.Monitor.Log($" Loading assembly '{assembly.File.Name}'..."); + lastAssembly = Assembly.UnsafeLoadFrom(assembly.File.FullName); } // track loaded assembly for definition resolution diff --git a/src/SMAPI/Framework/ModLoading/ModAssemblyLoadContext.cs b/src/SMAPI/Framework/ModLoading/ModAssemblyLoadContext.cs deleted file mode 100644 index 562f38235..000000000 --- a/src/SMAPI/Framework/ModLoading/ModAssemblyLoadContext.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Runtime.Loader; - -namespace StardewModdingAPI.Framework.ModLoading; - -/// An assembly load context which contains all the assemblies loaded by a particular mod, and redirects requests for public assemblies already loaded by another mod. -internal class ModAssemblyLoadContext : AssemblyLoadContext -{ - /********* - ** Fields - *********/ - /// A lookup of public assembly names to the load context which contains them. - private static readonly Dictionary LoadContextsByPublicAssemblyName = new(); - - /// The list of private assembly names handled by this instance. - private readonly HashSet PrivateAssemblyNames; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The mod this context is for. - public ModAssemblyLoadContext(IModMetadata mod) - : base(mod.Manifest.UniqueID) - { - this.PrivateAssemblyNames = new HashSet(mod.Manifest.PrivateAssemblies.Select(p => p.Name)); - } - - /// Cache an assembly added to this load context by SMAPI. - /// The assembly that was loaded. - public void OnLoadedAssembly(Assembly assembly) - { - string? name = assembly.GetName().Name; - - if (name != null && !this.PrivateAssemblyNames.Contains(name)) - ModAssemblyLoadContext.LoadContextsByPublicAssemblyName.TryAdd(name, this); - } - - /// Get whether an assembly is loaded and publicly available. - /// The assembly name. - public bool IsLoadedPublicAssembly(string? assemblyName) - { - return assemblyName != null && ModAssemblyLoadContext.LoadContextsByPublicAssemblyName.ContainsKey(assemblyName); - } - - /// Get whether an assembly is private to this context. - /// The assembly name. - public bool IsPrivateAssembly(string? assemblyName) - { - return assemblyName != null && this.PrivateAssemblyNames.Contains(assemblyName); - } - - - /********* - ** Protected methods - *********/ - /// - protected override Assembly? Load(AssemblyName assemblyName) - { - string? name = assemblyName.Name; - - return name is not null && ModAssemblyLoadContext.LoadContextsByPublicAssemblyName.TryGetValue(name, out ModAssemblyLoadContext? otherContext) && otherContext.Name != this.Name - ? otherContext.LoadFromAssemblyName(assemblyName) - : base.Load(assemblyName); - } -} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index cba12eb60..8aee4ca7c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1811,13 +1811,12 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin { // init HashSet suppressUpdateChecks = this.Settings.SuppressUpdateChecks; - List modAssemblyLoadContexts = new(); IInterfaceProxyFactory proxyFactory = new InterfaceProxyFactory(); // load mods foreach (IModMetadata mod in mods) { - if (!this.TryLoadMod(mod, mods, modAssemblyLoadContexts, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string? errorPhrase, out string? errorDetails)) + if (!this.TryLoadMod(mod, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out ModFailReason? failReason, out string? errorPhrase, out string? errorDetails)) { mod.SetStatus(ModMetadataStatus.Failed, failReason.Value, errorPhrase, errorDetails); skippedMods.Add(mod); @@ -1894,7 +1893,6 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin /// Load a given mod. /// The mod to load. /// The mods being loaded. - /// The assembly load contexts containing assemblies loaded by mods, which should be updated with the context for the mod being loaded. /// Preprocesses and loads mod assemblies. /// Generates proxy classes to access mod APIs through an arbitrary interface. /// The JSON helper with which to read mods' JSON files. @@ -1905,7 +1903,7 @@ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordin /// The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable). /// More detailed info about the error intended for developers (if any). /// Returns whether the mod was successfully loaded. - private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, List modAssemblyLoadContexts, AssemblyLoader assemblyLoader, IInterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, [NotNullWhen(false)] out ModFailReason? failReason, out string? errorReasonPhrase, out string? errorDetails) + private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, IInterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, [NotNullWhen(false)] out ModFailReason? failReason, out string? errorReasonPhrase, out string? errorDetails) { errorDetails = null; @@ -1989,12 +1987,10 @@ private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, List Date: Tue, 5 Nov 2024 14:34:10 -0500 Subject: [PATCH 2/3] fix asset propagation for Data/ChairTiles --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index fb0b99156..035a8781a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * For mod authors: * Removed the new private assembly references feature. This may be revisited in a future update once the dust settles on 1.6.9. + * Fixed error propagating edits to `Data/ChairTiles`. ## 4.1.3 Released 04 November 2024 for Stardew Valley 1.6.10 or later. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b33216a18..aefdb2b54 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -334,7 +334,7 @@ static ISet GetWarpSet(GameLocation location) Utility.ForEachLocation(location => { if (Context.IsMainPlayer || location.IsTemporary) - this.Reflection.GetField(location, "__mapSeatsDirty").SetValue(true); + this.Reflection.GetField(location, "_mapSeatsDirty").SetValue(true); return true; }); From 2769248ad66f9e0aa27b7422b45c4e732e0a61d6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 5 Nov 2024 14:34:11 -0500 Subject: [PATCH 3/3] prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 4 +++- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/build/common.targets b/build/common.targets index b894b315d..f2b05acfc 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed. - 4.1.3 + 4.1.4 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index 035a8781a..e50b98c18 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,7 +1,9 @@ ← [README](README.md) # Release notes -## Upcoming release +## 4.1.4 +Released 05 November 2024 for Stardew Valley 1.6.10 or later. + * For players: * Fixed a wide variety of mod errors and crashes after SMAPI 4.1.0 in some specific cases (e.g. Content Patcher "unable to find constructor" errors). diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 7bfce26f6..f180e6388 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": "4.1.3", + "Version": "4.1.4", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "4.1.3" + "MinimumApiVersion": "4.1.4" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 24abc4354..d13f07350 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": "4.1.3", + "Version": "4.1.4", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "4.1.3" + "MinimumApiVersion": "4.1.4" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 29fd12cf6..69e7c9037 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -49,7 +49,7 @@ internal static class EarlyConstants internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "4.1.3"; + internal static string RawApiVersion = "4.1.4"; } /// Contains SMAPI's constants and assumptions.