summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets2
-rw-r--r--docs/release-notes.md13
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs4
-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.Tests/Core/ModResolverTests.cs21
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs18
-rw-r--r--src/SMAPI.Toolkit/ModToolkit.cs10
-rw-r--r--src/SMAPI.Toolkit/Serialization/JsonHelper.cs5
-rw-r--r--src/SMAPI.Toolkit/Utilities/PathLookups/CaseInsensitivePathLookup.cs (renamed from src/SMAPI.Toolkit/Utilities/CaseInsensitivePathLookup.cs)17
-rw-r--r--src/SMAPI.Toolkit/Utilities/PathLookups/IFilePathLookup.cs20
-rw-r--r--src/SMAPI.Toolkit/Utilities/PathLookups/MinimalPathLookup.cs31
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json9
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs124
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs65
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs5
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs16
-rw-r--r--src/SMAPI/Framework/ContentPack.cs9
-rw-r--r--src/SMAPI/Framework/ModHelpers/ModContentHelper.cs18
-rw-r--r--src/SMAPI/Framework/ModLoading/ModResolver.cs12
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs18
-rw-r--r--src/SMAPI/Framework/SCore.cs26
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs29
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs51
-rw-r--r--src/SMAPI/SMAPI.config.json7
28 files changed, 339 insertions, 209 deletions
diff --git a/build/common.targets b/build/common.targets
index a8dda9e0..44ee0caf 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -1,7 +1,7 @@
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<!--set general build properties -->
- <Version>3.14.0</Version>
+ <Version>3.14.1</Version>
<Product>SMAPI</Product>
<LangVersion>latest</LangVersion>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
diff --git a/docs/release-notes.md b/docs/release-notes.md
index 499c1d9b..82cf51db 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,6 +1,19 @@
← [README](README.md)
# Release notes
+## 3.14.1
+Released 06 May 2022 for Stardew Valley 1.5.6 or later.
+
+* For players:
+ * Improved performance for mods still using the previous content API.
+ * Disabled case-insensitive file paths (introduced in 3.14.0) by default.
+ _You can enable them by editing `smapi-internal/config.json` if needed. They'll be re-enabled in an upcoming version after they're reworked a bit._
+ * Removed experimental 'aggressive memory optimizations' option.
+ _This was disabled by default and is no longer needed in most cases. Memory usage will be better reduced by reworked asset propagation in the upcoming SMAPI 4.0.0._
+ * Fixed 'content file was not found' error when the game tries to load unlocalized text from a localizable mod data asset in 3.14.0.
+ * Fixed error reading empty JSON files. These are now treated as if they didn't exist (matching pre-3.14.0 behavior).
+ * Updated compatibility list.
+
## 3.14.0
Released 01 May 2022 for Stardew Valley 1.5.6 or later. See [release highlights](https://www.patreon.com/posts/65265507).
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index 5138173a..19cefd32 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -435,8 +435,8 @@ namespace StardewModdingApi.Installer
{
this.PrintDebug("Adding bundled mods...");
- ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath).ToArray();
- foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName))
+ ModFolder[] targetMods = toolkit.GetModFolders(paths.ModsPath, useCaseInsensitiveFilePaths: true).ToArray();
+ foreach (ModFolder sourceMod in toolkit.GetModFolders(bundledModsDir.FullName, useCaseInsensitiveFilePaths: true))
{
// validate source mod
if (sourceMod.Manifest == null)
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 38945c5d..edbdd081 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.14.0",
+ "Version": "3.14.1",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.14.0"
+ "MinimumApiVersion": "3.14.1"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index c082bf75..af67453d 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.14.0",
+ "Version": "3.14.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.14.0"
+ "MinimumApiVersion": "3.14.1"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 28bffa98..14b68cb4 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.14.0",
+ "Version": "3.14.1",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.14.0"
+ "MinimumApiVersion": "3.14.1"
}
diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs
index 6b2746f5..3dfc9461 100644
--- a/src/SMAPI.Tests/Core/ModResolverTests.cs
+++ b/src/SMAPI.Tests/Core/ModResolverTests.cs
@@ -12,6 +12,7 @@ using StardewModdingAPI.Toolkit;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Toolkit.Serialization.Models;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
using SemanticVersion = StardewModdingAPI.SemanticVersion;
namespace SMAPI.Tests.Core
@@ -34,7 +35,7 @@ namespace SMAPI.Tests.Core
Directory.CreateDirectory(rootFolder);
// act
- IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
+ IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
// assert
Assert.AreEqual(0, mods.Length, 0, $"Expected to find zero manifests, found {mods.Length} instead.");
@@ -52,7 +53,7 @@ namespace SMAPI.Tests.Core
Directory.CreateDirectory(modFolder);
// act
- IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
+ IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
IModMetadata? mod = mods.FirstOrDefault();
// assert
@@ -94,7 +95,7 @@ namespace SMAPI.Tests.Core
File.WriteAllText(filename, JsonConvert.SerializeObject(original));
// act
- IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase()).ToArray();
+ IModMetadata[] mods = new ModResolver().ReadManifests(new ModToolkit(), rootFolder, new ModDatabase(), useCaseInsensitiveFilePaths: true).ToArray();
IModMetadata? mod = mods.FirstOrDefault();
// assert
@@ -132,7 +133,7 @@ namespace SMAPI.Tests.Core
[Test(Description = "Assert that validation doesn't fail if there are no mods installed.")]
public void ValidateManifests_NoMods_DoesNothing()
{
- new ModResolver().ValidateManifests(Array.Empty<ModMetadata>(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
+ new ModResolver().ValidateManifests(Array.Empty<ModMetadata>(), apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFilePathLookup: _ => MinimalPathLookup.Instance, validateFilesExist: false);
}
[Test(Description = "Assert that validation skips manifests that have already failed without calling any other properties.")]
@@ -143,7 +144,7 @@ namespace SMAPI.Tests.Core
mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed);
// act
- new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
+ new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFilePathLookup: _ => MinimalPathLookup.Instance, validateFilesExist: false);
// assert
mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status.");
@@ -160,7 +161,7 @@ namespace SMAPI.Tests.Core
});
// act
- new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
+ new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFilePathLookup: _ => MinimalPathLookup.Instance, validateFilesExist: false);
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -174,7 +175,7 @@ namespace SMAPI.Tests.Core
mock.Setup(p => p.Manifest).Returns(this.GetManifest(minimumApiVersion: "1.1"));
// act
- new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
+ new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFilePathLookup: _ => MinimalPathLookup.Instance, validateFilesExist: false);
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -189,7 +190,7 @@ namespace SMAPI.Tests.Core
Directory.CreateDirectory(directoryPath);
// act
- new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null);
+ new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFilePathLookup: _ => MinimalPathLookup.Instance);
// assert
mock.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny<ModFailReason>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once, "The validation did not fail the metadata.");
@@ -206,7 +207,7 @@ namespace SMAPI.Tests.Core
Mock<IModMetadata> modB = this.GetMetadata(this.GetManifest(id: "Mod A", name: "Mod B", version: "1.0"), allowStatusChange: true);
// act
- new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, validateFilesExist: false);
+ new ModResolver().ValidateManifests(new[] { modA.Object, modB.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFilePathLookup: _ => MinimalPathLookup.Instance, validateFilesExist: false);
// assert
modA.Verify(p => p.SetStatus(ModMetadataStatus.Failed, ModFailReason.Duplicate, It.IsAny<string>(), It.IsAny<string>()), Times.AtLeastOnce, "The validation did not fail the first mod with a unique ID.");
@@ -232,7 +233,7 @@ namespace SMAPI.Tests.Core
mock.Setup(p => p.DirectoryPath).Returns(modFolder);
// act
- new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null);
+ new ModResolver().ValidateManifests(new[] { mock.Object }, apiVersion: new SemanticVersion("1.0"), getUpdateUrl: _ => null, getFilePathLookup: _ => MinimalPathLookup.Instance);
// assert
// if Moq doesn't throw a method-not-setup exception, the validation didn't override the status.
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index 24485620..aa4c3338 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -5,7 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Serialization.Models;
-using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
namespace StardewModdingAPI.Toolkit.Framework.ModScanning
{
@@ -95,19 +95,20 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>Extract information about all mods in the given folder.</summary>
/// <param name="rootPath">The root folder containing mods.</param>
- public IEnumerable<ModFolder> GetModFolders(string rootPath)
+ /// <param name="useCaseInsensitiveFilePaths">Whether to match file paths case-insensitively, even on Linux.</param>
+ public IEnumerable<ModFolder> GetModFolders(string rootPath, bool useCaseInsensitiveFilePaths)
{
DirectoryInfo root = new(rootPath);
- return this.GetModFolders(root, root);
+ return this.GetModFolders(root, root, useCaseInsensitiveFilePaths);
}
/// <summary>Extract information about all mods in the given folder.</summary>
/// <param name="rootPath">The root folder containing mods. Only the <paramref name="modPath"/> will be searched, but this field allows it to be treated as a potential mod folder of its own.</param>
/// <param name="modPath">The mod path to search.</param>
- // /// <param name="tryConsolidateMod">If the folder contains multiple XNB mods, treat them as subfolders of a single mod. This is useful when reading a single mod archive, as opposed to a mods folder.</param>
- public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath)
+ /// <param name="useCaseInsensitiveFilePaths">Whether to match file paths case-insensitively, even on Linux.</param>
+ public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath, bool useCaseInsensitiveFilePaths)
{
- return this.GetModFolders(root: new DirectoryInfo(rootPath), folder: new DirectoryInfo(modPath));
+ return this.GetModFolders(root: new DirectoryInfo(rootPath), folder: new DirectoryInfo(modPath), useCaseInsensitiveFilePaths: useCaseInsensitiveFilePaths);
}
/// <summary>Extract information from a mod folder.</summary>
@@ -195,7 +196,8 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
/// <summary>Recursively extract information about all mods in the given folder.</summary>
/// <param name="root">The root mod folder.</param>
/// <param name="folder">The folder to search for mods.</param>
- private IEnumerable<ModFolder> GetModFolders(DirectoryInfo root, DirectoryInfo folder)
+ /// <param name="useCaseInsensitiveFilePaths">Whether to match file paths case-insensitively, even on Linux.</param>
+ private IEnumerable<ModFolder> GetModFolders(DirectoryInfo root, DirectoryInfo folder, bool useCaseInsensitiveFilePaths)
{
bool isRoot = folder.FullName == root.FullName;
@@ -214,7 +216,7 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
// find mods in subfolders
if (this.IsModSearchFolder(root, folder))
{
- IEnumerable<ModFolder> subfolders = folder.EnumerateDirectories().SelectMany(sub => this.GetModFolders(root, sub));
+ IEnumerable<ModFolder> subfolders = folder.EnumerateDirectories().SelectMany(sub => this.GetModFolders(root, sub, useCaseInsensitiveFilePaths));
if (!isRoot)
subfolders = this.TryConsolidate(root, folder, subfolders.ToArray());
foreach (ModFolder subfolder in subfolders)
diff --git a/src/SMAPI.Toolkit/ModToolkit.cs b/src/SMAPI.Toolkit/ModToolkit.cs
index 51f6fa24..ce14b057 100644
--- a/src/SMAPI.Toolkit/ModToolkit.cs
+++ b/src/SMAPI.Toolkit/ModToolkit.cs
@@ -72,17 +72,19 @@ namespace StardewModdingAPI.Toolkit
/// <summary>Extract information about all mods in the given folder.</summary>
/// <param name="rootPath">The root folder containing mods.</param>
- public IEnumerable<ModFolder> GetModFolders(string rootPath)
+ /// <param name="useCaseInsensitiveFilePaths">Whether to match file paths case-insensitively, even on Linux.</param>
+ public IEnumerable<ModFolder> GetModFolders(string rootPath, bool useCaseInsensitiveFilePaths)
{
- return new ModScanner(this.JsonHelper).GetModFolders(rootPath);
+ return new ModScanner(this.JsonHelper).GetModFolders(rootPath, useCaseInsensitiveFilePaths);
}
/// <summary>Extract information about all mods in the given folder.</summary>
/// <param name="rootPath">The root folder containing mods. Only the <paramref name="modPath"/> will be searched, but this field allows it to be treated as a potential mod folder of its own.</param>
/// <param name="modPath">The mod path to search.</param>
- public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath)
+ /// <param name="useCaseInsensitiveFilePaths">Whether to match file paths case-insensitively, even on Linux.</param>
+ public IEnumerable<ModFolder> GetModFolders(string rootPath, string modPath, bool useCaseInsensitiveFilePaths)
{
- return new ModScanner(this.JsonHelper).GetModFolders(rootPath, modPath);
+ return new ModScanner(this.JsonHelper).GetModFolders(rootPath, modPath, useCaseInsensitiveFilePaths);
}
/// <summary>Get an update URL for an update key (if valid).</summary>
diff --git a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
index 3c9308f2..1a003c51 100644
--- a/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
+++ b/src/SMAPI.Toolkit/Serialization/JsonHelper.cs
@@ -108,12 +108,11 @@ namespace StardewModdingAPI.Toolkit.Serialization
/// <summary>Deserialize JSON text if possible.</summary>
/// <typeparam name="TModel">The model type.</typeparam>
/// <param name="json">The raw JSON text.</param>
- public TModel Deserialize<TModel>(string json)
+ public TModel? Deserialize<TModel>(string json)
{
try
{
- return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings)
- ?? throw new InvalidOperationException($"Couldn't deserialize model type '{typeof(TModel)}' from empty or null JSON.");
+ return JsonConvert.DeserializeObject<TModel>(json, this.JsonSettings);
}
catch (JsonReaderException)
{
diff --git a/src/SMAPI.Toolkit/Utilities/CaseInsensitivePathLookup.cs b/src/SMAPI.Toolkit/Utilities/PathLookups/CaseInsensitivePathLookup.cs
index 12fad008..9cc00737 100644
--- a/src/SMAPI.Toolkit/Utilities/CaseInsensitivePathLookup.cs
+++ b/src/SMAPI.Toolkit/Utilities/PathLookups/CaseInsensitivePathLookup.cs
@@ -2,10 +2,10 @@ using System;
using System.Collections.Generic;
using System.IO;
-namespace StardewModdingAPI.Toolkit.Utilities
+namespace StardewModdingAPI.Toolkit.Utilities.PathLookups
{
- /// <summary>Provides an API for case-insensitive relative path lookups within a root directory.</summary>
- internal class CaseInsensitivePathLookup
+ /// <summary>An API for case-insensitive relative path lookups within a root directory.</summary>
+ internal class CaseInsensitivePathLookup : IFilePathLookup
{
/*********
** Fields
@@ -32,24 +32,19 @@ namespace StardewModdingAPI.Toolkit.Utilities
this.RelativePathCache = new(() => this.GetRelativePathCache(searchOption));
}
- /// <summary>Get the exact capitalization for a given relative file path.</summary>
- /// <param name="relativePath">The relative path.</param>
- /// <remarks>Returns the resolved path in file path format, else the normalized <paramref name="relativePath"/>.</remarks>
+ /// <inheritdoc />
public string GetFilePath(string relativePath)
{
return this.GetImpl(PathUtilities.NormalizePath(relativePath));
}
- /// <summary>Get the exact capitalization for a given asset name.</summary>
- /// <param name="relativePath">The relative path.</param>
- /// <remarks>Returns the resolved path in asset name format, else the normalized <paramref name="relativePath"/>.</remarks>
+ /// <inheritdoc />
public string GetAssetName(string relativePath)
{
return this.GetImpl(PathUtilities.NormalizeAssetName(relativePath));
}
- /// <summary>Add a relative path that was just created by a SMAPI API.</summary>
- /// <param name="relativePath">The relative path. This must already be normalized in asset name or file path format.</param>
+ /// <inheritdoc />
public void Add(string relativePath)
{
// skip if cache isn't created yet (no need to add files manually in that case)
diff --git a/src/SMAPI.Toolkit/Utilities/PathLookups/IFilePathLookup.cs b/src/SMAPI.Toolkit/Utilities/PathLookups/IFilePathLookup.cs
new file mode 100644
index 00000000..678e1383
--- /dev/null
+++ b/src/SMAPI.Toolkit/Utilities/PathLookups/IFilePathLookup.cs
@@ -0,0 +1,20 @@
+namespace StardewModdingAPI.Toolkit.Utilities.PathLookups
+{
+ /// <summary>An API for relative path lookups within a root directory.</summary>
+ internal interface IFilePathLookup
+ {
+ /// <summary>Get the actual path for a given relative file path.</summary>
+ /// <param name="relativePath">The relative path.</param>
+ /// <remarks>Returns the resolved path in file path format, else the normalized <paramref name="relativePath"/>.</remarks>
+ string GetFilePath(string relativePath);
+
+ /// <summary>Get the actual path for a given asset name.</summary>
+ /// <param name="relativePath">The relative path.</param>
+ /// <remarks>Returns the resolved path in asset name format, else the normalized <paramref name="relativePath"/>.</remarks>
+ string GetAssetName(string relativePath);
+
+ /// <summary>Add a relative path that was just created by a SMAPI API.</summary>
+ /// <param name="relativePath">The relative path. This must already be normalized in asset name or file path format.</param>
+ void Add(string relativePath);
+ }
+}
diff --git a/src/SMAPI.Toolkit/Utilities/PathLookups/MinimalPathLookup.cs b/src/SMAPI.Toolkit/Utilities/PathLookups/MinimalPathLookup.cs
new file mode 100644
index 00000000..2cf14704
--- /dev/null
+++ b/src/SMAPI.Toolkit/Utilities/PathLookups/MinimalPathLookup.cs
@@ -0,0 +1,31 @@
+namespace StardewModdingAPI.Toolkit.Utilities.PathLookups
+{
+ /// <summary>An API for relative path lookups within a root directory with minimal preprocessing.</summary>
+ internal class MinimalPathLookup : IFilePathLookup
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>A singleton instance for reuse.</summary>
+ public static readonly MinimalPathLookup Instance = new();
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <inheritdoc />
+ public string GetFilePath(string relativePath)
+ {
+ return PathUtilities.NormalizePath(relativePath);
+ }
+
+ /// <inheritdoc />
+ public string GetAssetName(string relativePath)
+ {
+ return PathUtilities.NormalizeAssetName(relativePath);
+ }
+
+ /// <inheritdoc />
+ public void Add(string relativePath) { }
+ }
+}
diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
index 75a3f8c7..53c9db82 100644
--- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
@@ -168,6 +168,15 @@
},
/*********
+ ** Broke in SMAPI 3.14.0
+ *********/
+ "Dynamic Game Assets": {
+ "ID": "spacechase0.DynamicGameAssets",
+ "~1.4.1 | Status": "AssumeBroken",
+ "~1.4.1 | StatusReasonDetails": "causes runtime errors fixed in Dynamic Game Assets 1.4.2"
+ },
+
+ /*********
** Broke in SDV 1.5.5
*********/
"Animated Portrait Framework": {
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index f5da286a..b1a9cc82 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -50,7 +50,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.14.0";
+ internal static string RawApiVersion = "3.14.1";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 84fff250..4f52d57e 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -16,6 +16,7 @@ using StardewModdingAPI.Internal;
using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
using StardewValley;
using StardewValley.GameData;
using xTile;
@@ -31,8 +32,8 @@ namespace StardewModdingAPI.Framework
/// <summary>An asset key prefix for assets from SMAPI mod folders.</summary>
private readonly string ManagedPrefix = "SMAPI";
- /// <summary>Whether to enable more aggressive memory optimizations.</summary>
- private readonly bool AggressiveMemoryOptimizations;
+ /// <summary>Get a file path lookup for the given directory.</summary>
+ private readonly Func<string, IFilePathLookup> GetFilePathLookup;
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
@@ -80,6 +81,14 @@ namespace StardewModdingAPI.Framework
/// <summary>The cached asset load/edit operations to apply, indexed by asset name.</summary>
private readonly TickCacheDictionary<IAssetName, AssetOperationGroup[]> AssetOperationsByKey = new();
+ /// <summary>A cache of asset operation groups created for legacy <see cref="IAssetLoader"/> implementations.</summary>
+ [Obsolete]
+ private readonly Dictionary<IAssetLoader, Dictionary<Type, AssetOperationGroup>> LegacyLoaderCache = new(ReferenceEqualityComparer.Instance);
+
+ /// <summary>A cache of asset operation groups created for legacy <see cref="IAssetEditor"/> implementations.</summary>
+ [Obsolete]
+ private readonly Dictionary<IAssetEditor, Dictionary<Type, AssetOperationGroup>> LegacyEditorCache = new(ReferenceEqualityComparer.Instance);
+
/*********
** Accessors
@@ -114,12 +123,12 @@ namespace StardewModdingAPI.Framework
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param>
/// <param name="onAssetLoaded">A callback to invoke when an asset is fully loaded.</param>
- /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
+ /// <param name="getFilePathLookup">Get a file path lookup for the given directory.</param>
/// <param name="onAssetsInvalidated">A callback to invoke when any asset names have been invalidated from the cache.</param>
/// <param name="requestAssetOperations">Get the load/edit operations to apply to an asset by querying registered <see cref="IContentEvents.AssetRequested"/> event handlers.</param>
- public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, bool aggressiveMemoryOptimizations, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, IList<AssetOperationGroup>> requestAssetOperations)
+ public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, Func<string, IFilePathLookup> getFilePathLookup, Action<IList<IAssetName>> onAssetsInvalidated, Func<IAssetInfo, IList<AssetOperationGroup>> requestAssetOperations)
{
- this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
+ this.GetFilePathLookup = getFilePathLookup;
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Reflection = reflection;
this.JsonHelper = jsonHelper;
@@ -139,26 +148,11 @@ namespace StardewModdingAPI.Framework
reflection: reflection,
onDisposing: this.OnDisposing,
onLoadingFirstAsset: onLoadingFirstAsset,
- onAssetLoaded: onAssetLoaded,
- aggressiveMemoryOptimizations: aggressiveMemoryOptimizations
+ onAssetLoaded: onAssetLoaded
)
);
- var contentManagerForAssetPropagation = new GameContentManagerForAssetPropagation(
- name: nameof(GameContentManagerForAssetPropagation),
- serviceProvider: serviceProvider,
- rootDirectory: rootDirectory,
- currentCulture: currentCulture,
- coordinator: this,
- monitor: monitor,
- reflection: reflection,
- onDisposing: this.OnDisposing,
- onLoadingFirstAsset: onLoadingFirstAsset,
- onAssetLoaded: onAssetLoaded,
- aggressiveMemoryOptimizations: aggressiveMemoryOptimizations
- );
- this.ContentManagers.Add(contentManagerForAssetPropagation);
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
- this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, this.Monitor, reflection, aggressiveMemoryOptimizations, name => this.ParseAssetName(name, allowLocales: true));
+ this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, this.Monitor, reflection, name => this.ParseAssetName(name, allowLocales: true));
this.LocaleCodes = new Lazy<Dictionary<string, LocalizedContentManager.LanguageCode>>(() => this.GetLocaleCodes(customLanguages: Enumerable.Empty<ModLanguage>()));
}
@@ -178,8 +172,7 @@ namespace StardewModdingAPI.Framework
reflection: this.Reflection,
onDisposing: this.OnDisposing,
onLoadingFirstAsset: this.OnLoadingFirstAsset,
- onAssetLoaded: this.OnAssetLoaded,
- aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations
+ onAssetLoaded: this.OnAssetLoaded
);
this.ContentManagers.Add(manager);
return manager;
@@ -207,8 +200,7 @@ namespace StardewModdingAPI.Framework
reflection: this.Reflection,
jsonHelper: this.JsonHelper,
onDisposing: this.OnDisposing,
- aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations,
- relativePathCache: CaseInsensitivePathLookup.GetCachedFor(rootDirectory)
+ relativePathLookup: this.GetFilePathLookup(rootDirectory)
);
this.ContentManagers.Add(manager);
return manager;
@@ -614,20 +606,25 @@ namespace StardewModdingAPI.Framework
}
// add operation
- yield return new AssetOperationGroup(
- mod: loader.Mod,
- loadOperations: new[]
- {
- new AssetLoadOperation(
- mod: loader.Mod,
- priority: AssetLoadPriority.Exclusive,
- onBehalfOf: null,
- getData: assetInfo => loader.Data.Load<T>(
- this.GetLegacyAssetInfo(assetInfo)
+ yield return this.GetOrCreateLegacyOperationGroup(
+ cache: this.LegacyLoaderCache,
+ editor: loader.Data,
+ dataType: info.DataType,
+ createGroup: () => new AssetOperationGroup(
+ mod: loader.Mod,
+ loadOperations: new[]
+ {
+ new AssetLoadOperation(
+ mod: loader.Mod,
+ priority: AssetLoadPriority.Exclusive,
+ onBehalfOf: null,
+ getData: assetInfo => loader.Data.Load<T>(
+ this.GetLegacyAssetInfo(assetInfo)
+ )
)
- )
- },
- editOperations: Array.Empty<AssetEditOperation>()
+ },
+ editOperations: Array.Empty<AssetEditOperation>()
+ )
);
}
@@ -668,25 +665,48 @@ namespace StardewModdingAPI.Framework
};
// add operation
- yield return new AssetOperationGroup(
- mod: editor.Mod,
- loadOperations: Array.Empty<AssetLoadOperation>(),
- editOperations: new[]
- {
- new AssetEditOperation(
- mod: editor.Mod,
- priority: priority,
- onBehalfOf: null,
- applyEdit: assetData => editor.Data.Edit<T>(
- this.GetLegacyAssetData(assetData)
+ yield return this.GetOrCreateLegacyOperationGroup(
+ cache: this.LegacyEditorCache,
+ editor: editor.Data,
+ dataType: info.DataType,
+ createGroup: () => new AssetOperationGroup(
+ mod: editor.Mod,
+ loadOperations: Array.Empty<AssetLoadOperation>(),
+ editOperations: new[]
+ {
+ new AssetEditOperation(
+ mod: editor.Mod,
+ priority: priority,
+ onBehalfOf: null,
+ applyEdit: assetData => editor.Data.Edit<T>(
+ this.GetLegacyAssetData(assetData)
+ )
)
- )
- }
+ }
+ )
);
}
#pragma warning restore CS0612, CS0618
}
+ /// <summary>Get a cached asset operation group for a legacy <see cref="IAssetLoader"/> or <see cref="IAssetEditor"/> instance, creating it if needed.</summary>
+ /// <typeparam name="TInterceptor">The editor type (one of <see cref="IAssetLoader"/> or <see cref="IAssetEditor"/>).</typeparam>
+ /// <param name="cache">The cached operation groups for the interceptor type.</param>
+ /// <param name="editor">The legacy asset interceptor.</param>
+ /// <param name="dataType">The asset data type.</param>
+ /// <param name="createGroup">Create the asset operation group if it's not cached yet.</param>
+ private AssetOperationGroup GetOrCreateLegacyOperationGroup<TInterceptor>(Dictionary<TInterceptor, Dictionary<Type, AssetOperationGroup>> cache, TInterceptor editor, Type dataType, Func<AssetOperationGroup> createGroup)
+ where TInterceptor : class
+ {
+ if (!cache.TryGetValue(editor, out Dictionary<Type, AssetOperationGroup>? cacheByType))
+ cache[editor] = cacheByType = new Dictionary<Type, AssetOperationGroup>();
+
+ if (!cacheByType.TryGetValue(dataType, out AssetOperationGroup? group))
+ cacheByType[dataType] = group = createGroup();
+
+ return group;
+ }
+
/// <summary>Get an asset info compatible with legacy <see cref="IAssetLoader"/> and <see cref="IAssetEditor"/> instances, which always expect the base name.</summary>
/// <param name="asset">The asset info.</param>
private IAssetInfo GetLegacyAssetInfo(IAssetInfo asset)
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index b2e3ec0f..e4695588 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -11,7 +11,6 @@ using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
-using xTile;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -33,9 +32,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Simplifies access to private code.</summary>
protected readonly Reflector Reflection;
- /// <summary>Whether to enable more aggressive memory optimizations.</summary>
- protected readonly bool AggressiveMemoryOptimizations;
-
/// <summary>Whether to automatically try resolving keys to a localized form if available.</summary>
protected bool TryLocalizeKeys = true;
@@ -82,8 +78,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
/// <param name="isNamespaced">Whether this content manager handles managed asset keys (e.g. to load assets from a mod folder).</param>
- /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
- protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced, bool aggressiveMemoryOptimizations)
+ protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced)
: base(serviceProvider, rootDirectory, currentCulture)
{
// init
@@ -95,7 +90,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
this.Reflection = reflection;
this.OnDisposing = onDisposing;
this.IsNamespaced = isNamespaced;
- this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
// get asset data
this.BaseDisposableReferences = reflection.GetField<List<IDisposable>?>(this, "disposableAssets").GetValue()
@@ -117,6 +111,26 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
/// <inheritdoc />
+ [SuppressMessage("ReSharper", "ConditionIsAlwaysTrueOrFalse", Justification = "Copied as-is from game code")]
+ public sealed override string LoadBaseString(string path)
+ {
+ try
+ {
+ // 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);
+ return strings != null && strings.ContainsKey(key)
+ ? this.GetString(strings, key)
+ : path;
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed loading string path '{path}' from '{this.Name}'.", ex);
+ }
+ }
+
+ /// <inheritdoc />
public sealed override T Load<T>(string assetName)
{
return this.Load<T>(assetName, this.Language);
@@ -231,14 +245,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
removeAssets[baseAssetName] = asset;
remove = true;
}
-
- // dispose if safe
- if (remove && this.AggressiveMemoryOptimizations)
- {
- if (asset is Map map)
- map.DisposeTileSheets(Game1.mapDisplayDevice);
- }
-
return remove;
}, dispose);
@@ -345,5 +351,34 @@ namespace StardewModdingAPI.Framework.ContentManagers
// avoid hard disposable references; see remarks on the field
this.BaseDisposableReferences.Clear();
}
+
+ /****
+ ** Private methods copied from the game code
+ ****/
+#pragma warning disable CS1574 // <see cref /> can't be resolved: the reference is valid but private
+ /// <summary>Parse a string path like <c>assetName:key</c>.</summary>
+ /// <param name="path">The string path.</param>
+ /// <param name="assetName">The extracted asset name.</param>
+ /// <param name="key">The extracted entry key.</param>
+ /// <exception cref="ContentLoadException">The string path is not in a valid format.</exception>
+ /// <remarks>This is copied as-is from <see cref="LocalizedContentManager.parseStringPath"/>.</remarks>
+ private void ParseStringPath(string path, out string assetName, out string key)
+ {
+ int length = path.IndexOf(':');
+ assetName = length != -1 ? path.Substring(0, length) : throw new ContentLoadException("Unable to parse string path: " + path);
+ key = path.Substring(length + 1, path.Length - length - 1);
+ }
+
+ /// <summary>Get a string value from a dictionary asset.</summary>
+ /// <param name="strings">The asset to read.</param>
+ /// <param name="key">The string key to find.</param>
+ /// <remarks>This is copied as-is from <see cref="LocalizedContentManager.GetString"/>.</remarks>
+ private string GetString(Dictionary<string, string> strings, string key)
+ {
+ return strings.TryGetValue(key + ".desktop", out string? str)
+ ? str
+ : strings[key];
+ }
+#pragma warning restore CS1574
}
}
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 083df454..c53040e1 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -51,9 +51,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
/// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param>
/// <param name="onAssetLoaded">A callback to invoke when an asset is fully loaded.</param>
- /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
- public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, bool aggressiveMemoryOptimizations)
- : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false, aggressiveMemoryOptimizations: aggressiveMemoryOptimizations)
+ public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false)
{
this.OnLoadingFirstAsset = onLoadingFirstAsset;
this.OnAssetLoaded = onAssetLoaded;
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
index 1b0e1016..5c574a1a 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
@@ -21,8 +21,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
** Public methods
*********/
/// <inheritdoc />
- public GameContentManagerForAssetPropagation(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded, bool aggressiveMemoryOptimizations)
- : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, onLoadingFirstAsset, onAssetLoaded, aggressiveMemoryOptimizations) { }
+ public GameContentManagerForAssetPropagation(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, Action<BaseContentManager, IAssetName> onAssetLoaded)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, onLoadingFirstAsset, onAssetLoaded) { }
/// <inheritdoc />
public override T LoadExact<T>(IAssetName assetName, bool useCache)
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 8f64c5a8..65dffd8b 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -10,6 +10,7 @@ using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
using StardewValley;
using xTile;
using xTile.Format;
@@ -32,8 +33,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>The game content manager used for map tilesheets not provided by the mod.</summary>
private readonly IContentManager GameContentManager;
- /// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
- private readonly CaseInsensitivePathLookup RelativePathCache;
+ /// <summary>A lookup for relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
+ private readonly IFilePathLookup RelativePathLookup;
/// <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 readonly string[] LocalTilesheetExtensions = { ".png", ".xnb" };
@@ -54,13 +55,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
- /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
- /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="rootDirectory"/>.</param>
- public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, bool aggressiveMemoryOptimizations, CaseInsensitivePathLookup relativePathCache)
- : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true, aggressiveMemoryOptimizations: aggressiveMemoryOptimizations)
+ /// <param name="relativePathLookup">A lookup for relative paths within the <paramref name="rootDirectory"/>.</param>
+ public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, IFilePathLookup relativePathLookup)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
{
this.GameContentManager = gameContentManager;
- this.RelativePathCache = relativePathCache;
+ this.RelativePathLookup = relativePathLookup;
this.JsonHelper = jsonHelper;
this.ModName = modName;
@@ -257,7 +257,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
private FileInfo GetModFile(string path)
{
// map to case-insensitive path if needed
- path = this.RelativePathCache.GetFilePath(path);
+ path = this.RelativePathLookup.GetFilePath(path);
// try exact match
FileInfo file = new(Path.Combine(this.FullRootDirectory, path));
diff --git a/src/SMAPI/Framework/ContentPack.cs b/src/SMAPI/Framework/ContentPack.cs
index dde33c95..9503a0e6 100644
--- a/src/SMAPI/Framework/ContentPack.cs
+++ b/src/SMAPI/Framework/ContentPack.cs
@@ -3,6 +3,7 @@ using System.IO;
using StardewModdingAPI.Framework.ModHelpers;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
namespace StardewModdingAPI.Framework
{
@@ -15,8 +16,8 @@ namespace StardewModdingAPI.Framework
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
- /// <summary>A case-insensitive lookup of relative paths within the <see cref="DirectoryPath"/>.</summary>
- private readonly CaseInsensitivePathLookup RelativePathCache;
+ /// <summary>A lookup for relative paths within the <see cref="DirectoryPath"/>.</summary>
+ private readonly IFilePathLookup RelativePathCache;
/*********
@@ -47,8 +48,8 @@ namespace StardewModdingAPI.Framework
/// <param name="content">Provides an API for loading content assets from the content pack's folder.</param>
/// <param name="translation">Provides translations stored in the content pack's <c>i18n</c> folder.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
- /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="directoryPath"/>.</param>
- public ContentPack(string directoryPath, IManifest manifest, IModContentHelper content, TranslationHelper translation, JsonHelper jsonHelper, CaseInsensitivePathLookup relativePathCache)
+ /// <param name="relativePathCache">A lookup for relative paths within the <paramref name="directoryPath"/>.</param>
+ public ContentPack(string directoryPath, IManifest manifest, IModContentHelper content, TranslationHelper translation, JsonHelper jsonHelper, IFilePathLookup relativePathCache)
{
this.DirectoryPath = directoryPath;
this.Manifest = manifest;
diff --git a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
index def0b728..74ea73de 100644
--- a/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ModContentHelper.cs
@@ -4,7 +4,7 @@ using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
-using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
namespace StardewModdingAPI.Framework.ModHelpers
{
@@ -23,8 +23,8 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The friendly mod name for use in errors.</summary>
private readonly string ModName;
- /// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
- private readonly CaseInsensitivePathLookup RelativePathCache;
+ /// <summary>A lookup for relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
+ private readonly IFilePathLookup RelativePathLookup;
/// <summary>Simplifies access to private code.</summary>
private readonly Reflector Reflection;
@@ -39,9 +39,9 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <param name="mod">The mod using this instance.</param>
/// <param name="modName">The friendly mod name for use in errors.</param>
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
- /// <param name="relativePathCache">A case-insensitive lookup of relative paths within the <paramref name="relativePathCache"/>.</param>
+ /// <param name="relativePathLookup">A lookup for relative paths within the <paramref name="relativePathLookup"/>.</param>
/// <param name="reflection">Simplifies access to private code.</param>
- public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, string modName, IContentManager gameContentManager, CaseInsensitivePathLookup relativePathCache, Reflector reflection)
+ public ModContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, string modName, IContentManager gameContentManager, IFilePathLookup relativePathLookup, Reflector reflection)
: base(mod)
{
string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID);
@@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
this.ContentCore = contentCore;
this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, gameContentManager);
this.ModName = modName;
- this.RelativePathCache = relativePathCache;
+ this.RelativePathLookup = relativePathLookup;
this.Reflection = reflection;
}
@@ -57,7 +57,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
public T Load<T>(string relativePath)
where T : notnull
{
- relativePath = this.RelativePathCache.GetAssetName(relativePath);
+ relativePath = this.RelativePathLookup.GetAssetName(relativePath);
IAssetName assetName = this.ContentCore.ParseAssetName(relativePath, allowLocales: false);
@@ -74,7 +74,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <inheritdoc />
public IAssetName GetInternalAssetName(string relativePath)
{
- relativePath = this.RelativePathCache.GetAssetName(relativePath);
+ relativePath = this.RelativePathLookup.GetAssetName(relativePath);
return this.ModContentManager.GetInternalAssetKey(relativePath);
}
@@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value.");
relativePath = relativePath != null
- ? this.RelativePathCache.GetAssetName(relativePath)
+ ? this.RelativePathLookup.GetAssetName(relativePath)
: $"temp/{Guid.NewGuid():N}";
return new AssetDataForObject(
diff --git a/src/SMAPI/Framework/ModLoading/ModResolver.cs b/src/SMAPI/Framework/ModLoading/ModResolver.cs
index 74e7cb32..1b1fa04e 100644
--- a/src/SMAPI/Framework/ModLoading/ModResolver.cs
+++ b/src/SMAPI/Framework/ModLoading/ModResolver.cs
@@ -9,6 +9,7 @@ using StardewModdingAPI.Toolkit.Framework.ModScanning;
using StardewModdingAPI.Toolkit.Framework.UpdateData;
using StardewModdingAPI.Toolkit.Serialization.Models;
using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
namespace StardewModdingAPI.Framework.ModLoading
{
@@ -22,10 +23,11 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="toolkit">The mod toolkit.</param>
/// <param name="rootPath">The root path to search for mods.</param>
/// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param>
+ /// <param name="useCaseInsensitiveFilePaths">Whether to match file paths case-insensitively, even on Linux.</param>
/// <returns>Returns the manifests by relative folder.</returns>
- public IEnumerable<IModMetadata> ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase)
+ public IEnumerable<IModMetadata> ReadManifests(ModToolkit toolkit, string rootPath, ModDatabase modDatabase, bool useCaseInsensitiveFilePaths)
{
- foreach (ModFolder folder in toolkit.GetModFolders(rootPath))
+ foreach (ModFolder folder in toolkit.GetModFolders(rootPath, useCaseInsensitiveFilePaths))
{
Manifest? manifest = folder.Manifest;
@@ -56,10 +58,11 @@ namespace StardewModdingAPI.Framework.ModLoading
/// <param name="mods">The mod manifests to validate.</param>
/// <param name="apiVersion">The current SMAPI version.</param>
/// <param name="getUpdateUrl">Get an update URL for an update key (if valid).</param>
+ /// <param name="getFilePathLookup">Get a file path 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.")]
- public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string?> getUpdateUrl, bool validateFilesExist = true)
+ public void ValidateManifests(IEnumerable<IModMetadata> mods, ISemanticVersion apiVersion, Func<string, string?> getUpdateUrl, Func<string, IFilePathLookup> getFilePathLookup, bool validateFilesExist = true)
{
mods = mods.ToArray();
@@ -144,7 +147,8 @@ namespace StardewModdingAPI.Framework.ModLoading
// file doesn't exist
if (validateFilesExist)
{
- string fileName = CaseInsensitivePathLookup.GetCachedFor(mod.DirectoryPath).GetFilePath(mod.Manifest.EntryDll!);
+ IFilePathLookup pathLookup = getFilePathLookup(mod.DirectoryPath);
+ string fileName = pathLookup.GetFilePath(mod.Manifest.EntryDll!);
if (!File.Exists(Path.Combine(mod.DirectoryPath, fileName)))
{
mod.SetStatus(ModMetadataStatus.Failed, ModFailReason.InvalidManifest, $"its DLL '{mod.Manifest.EntryDll}' doesn't exist.");
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index 1a43c1fc..80d0d9ba 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -22,8 +22,8 @@ namespace StardewModdingAPI.Framework.Models
[nameof(VerboseLogging)] = false,
[nameof(LogNetworkTraffic)] = false,
[nameof(RewriteMods)] = true,
- [nameof(AggressiveMemoryOptimizations)] = false,
- [nameof(UsePintail)] = true
+ [nameof(UsePintail)] = true,
+ [nameof(UseCaseInsensitivePaths)] = false
};
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
@@ -62,12 +62,12 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether SMAPI should rewrite mods for compatibility.</summary>
public bool RewriteMods { get; }
- /// <summary>Whether to enable more aggressive memory optimizations.</summary>
- public bool AggressiveMemoryOptimizations { get; }
-
/// <summary>Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</summary>
public bool UsePintail { get; }
+ /// <summary>Whether to make SMAPI file APIs case-insensitive, even on Linux.</summary>
+ public bool UseCaseInsensitivePaths { get; }
+
/// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary>
public bool LogNetworkTraffic { get; }
@@ -90,12 +90,12 @@ namespace StardewModdingAPI.Framework.Models
/// <param name="webApiBaseUrl">The base URL for SMAPI's web API, used to perform update checks.</param>
/// <param name="verboseLogging">Whether SMAPI should log more information about the game context.</param>
/// <param name="rewriteMods">Whether SMAPI should rewrite mods for compatibility.</param>
- /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
/// <param name="usePintail">Whether to use the experimental Pintail API proxying library, instead of the original proxying built into SMAPI itself.</param>
+ /// <param name="useCaseInsensitivePaths">>Whether to make SMAPI file APIs case-insensitive, even on Linux.</param>
/// <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, bool verboseLogging, bool? rewriteMods, bool? aggressiveMemoryOptimizations, bool usePintail, bool logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
+ public SConfig(bool developerMode, bool checkForUpdates, bool? paranoidWarnings, bool? useBetaChannel, string gitHubProjectName, string webApiBaseUrl, bool verboseLogging, bool? rewriteMods, bool? usePintail, bool? useCaseInsensitivePaths, bool logNetworkTraffic, ColorSchemeConfig consoleColors, string[]? suppressUpdateChecks)
{
this.DeveloperMode = developerMode;
this.CheckForUpdates = checkForUpdates;
@@ -105,8 +105,8 @@ namespace StardewModdingAPI.Framework.Models
this.WebApiBaseUrl = webApiBaseUrl;
this.VerboseLogging = verboseLogging;
this.RewriteMods = rewriteMods ?? (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)];
- this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations ?? (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)];
- this.UsePintail = usePintail;
+ this.UsePintail = usePintail ?? (bool)SConfig.DefaultValues[nameof(SConfig.UsePintail)];
+ this.UseCaseInsensitivePaths = useCaseInsensitivePaths ?? (bool)SConfig.DefaultValues[nameof(SConfig.UseCaseInsensitivePaths)];
this.LogNetworkTraffic = logNetworkTraffic;
this.ConsoleColors = consoleColors;
this.SuppressUpdateChecks = suppressUpdateChecks ?? Array.Empty<string>();
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index e64318a5..a9296d9b 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -43,6 +43,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Toolkit.Utilities.PathLookups;
using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.Menus;
@@ -396,7 +397,7 @@ namespace StardewModdingAPI.Framework
}
// load manifests
- IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase).ToArray();
+ IModMetadata[] mods = resolver.ReadManifests(toolkit, this.ModsPath, modDatabase, useCaseInsensitiveFilePaths: this.Settings.UseCaseInsensitivePaths).ToArray();
// filter out ignored mods
foreach (IModMetadata mod in mods.Where(p => p.IsIgnored))
@@ -404,7 +405,7 @@ namespace StardewModdingAPI.Framework
mods = mods.Where(p => !p.IsIgnored).ToArray();
// load mods
- resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl);
+ resolver.ValidateManifests(mods, Constants.ApiVersion, toolkit.GetUpdateUrl, getFilePathLookup: this.GetFilePathLookup);
mods = resolver.ProcessDependencies(mods, modDatabase).ToArray();
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
@@ -1252,7 +1253,7 @@ namespace StardewModdingAPI.Framework
onLoadingFirstAsset: this.InitializeBeforeFirstAssetLoaded,
onAssetLoaded: this.OnAssetLoaded,
onAssetsInvalidated: this.OnAssetsInvalidated,
- aggressiveMemoryOptimizations: this.Settings.AggressiveMemoryOptimizations,
+ getFilePathLookup: this.GetFilePathLookup,
requestAssetOperations: this.RequestAssetOperations
);
if (this.ContentCore.Language != this.Translator.LocaleEnum)
@@ -1753,7 +1754,7 @@ namespace StardewModdingAPI.Framework
if (mod.IsContentPack)
{
IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName);
- CaseInsensitivePathLookup relativePathCache = CaseInsensitivePathLookup.GetCachedFor(mod.DirectoryPath);
+ IFilePathLookup relativePathCache = this.GetFilePathLookup(mod.DirectoryPath);
GameContentHelper gameContentHelper = new(this.ContentCore, mod, mod.DisplayName, monitor, this.Reflection);
IModContentHelper modContentHelper = new ModContentHelper(this.ContentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection);
TranslationHelper translationHelper = new(mod, contentCore.GetLocale(), contentCore.Language);
@@ -1772,7 +1773,7 @@ namespace StardewModdingAPI.Framework
// get mod info
string assemblyPath = Path.Combine(
mod.DirectoryPath,
- CaseInsensitivePathLookup.GetCachedFor(mod.DirectoryPath).GetFilePath(manifest.EntryDll!)
+ this.GetFilePathLookup(mod.DirectoryPath).GetFilePath(manifest.EntryDll!)
);
// load mod
@@ -1838,7 +1839,7 @@ namespace StardewModdingAPI.Framework
{
IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name);
- CaseInsensitivePathLookup relativePathCache = CaseInsensitivePathLookup.GetCachedFor(packDirPath);
+ IFilePathLookup relativePathCache = this.GetFilePathLookup(packDirPath);
GameContentHelper gameContentHelper = new(contentCore, mod, packManifest.Name, packMonitor, this.Reflection);
IModContentHelper packContentHelper = new ModContentHelper(contentCore, packDirPath, mod, packManifest.Name, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection);
@@ -1852,12 +1853,12 @@ namespace StardewModdingAPI.Framework
IModEvents events = new ModEvents(mod, this.EventManager);
ICommandHelper commandHelper = new CommandHelper(mod, this.CommandManager);
- CaseInsensitivePathLookup relativePathCache = CaseInsensitivePathLookup.GetCachedFor(mod.DirectoryPath);
+ IFilePathLookup relativePathLookup = this.GetFilePathLookup(mod.DirectoryPath);
#pragma warning disable CS0612 // deprecated code
ContentHelper contentHelper = new(contentCore, mod.DirectoryPath, mod, monitor, this.Reflection);
#pragma warning restore CS0612
GameContentHelper gameContentHelper = new(contentCore, mod, mod.DisplayName, monitor, this.Reflection);
- IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathCache, this.Reflection);
+ IModContentHelper modContentHelper = new ModContentHelper(contentCore, mod.DirectoryPath, mod, mod.DisplayName, gameContentHelper.GetUnderlyingContentManager(), relativePathLookup, this.Reflection);
IContentPackHelper contentPackHelper = new ContentPackHelper(mod, new Lazy<IContentPack[]>(GetContentPacks), CreateFakeContentPack);
IDataHelper dataHelper = new DataHelper(mod, mod.DirectoryPath, jsonHelper);
IReflectionHelper reflectionHelper = new ReflectionHelper(mod, mod.DisplayName, this.Reflection);
@@ -2035,6 +2036,15 @@ namespace StardewModdingAPI.Framework
return translations;
}
+ /// <summary>Get a file path lookup for the given directory.</summary>
+ /// <param name="rootDirectory">The root path to scan.</param>
+ private IFilePathLookup GetFilePathLookup(string rootDirectory)
+ {
+ return this.Settings.UseCaseInsensitivePaths
+ ? CaseInsensitivePathLookup.GetCachedFor(rootDirectory)
+ : MinimalPathLookup.Instance;
+ }
+
/// <summary>Get the map display device which applies SMAPI features like tile rotation to loaded maps.</summary>
/// <remarks>This is separate to let mods like PyTK wrap it with their own functionality.</remarks>
private IDisplayDevice GetMapDisplayDevice()
diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs
index e41e7edc..2badcbbf 100644
--- a/src/SMAPI/Framework/SMultiplayer.cs
+++ b/src/SMAPI/Framework/SMultiplayer.cs
@@ -9,6 +9,7 @@ using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Utilities;
using StardewValley;
@@ -489,8 +490,8 @@ namespace StardewModdingAPI.Framework
private RemoteContextModel? ReadContext(BinaryReader reader)
{
string data = reader.ReadString();
- RemoteContextModel model = this.JsonHelper.Deserialize<RemoteContextModel>(data);
- return model.ApiVersion != null
+ RemoteContextModel? model = this.JsonHelper.Deserialize<RemoteContextModel>(data);
+ return model?.ApiVersion != null
? model
: null; // no data available for vanilla players
}
@@ -499,13 +500,31 @@ namespace StardewModdingAPI.Framework
/// <param name="message">The raw message to parse.</param>
private void ReceiveModMessage(IncomingMessage message)
{
- // parse message
+ // read message JSON
string json = message.Reader.ReadString();
- ModMessageModel model = this.JsonHelper.Deserialize<ModMessageModel>(json);
- HashSet<long> playerIDs = new HashSet<long>(model.ToPlayerIDs ?? this.GetKnownPlayerIDs());
if (this.LogNetworkTraffic)
this.Monitor.Log($"Received message: {json}.");
+ // deserialize model
+ ModMessageModel? model;
+ try
+ {
+ model = this.JsonHelper.Deserialize<ModMessageModel>(json);
+ if (model is null)
+ {
+ this.Monitor.Log($"Received invalid mod message from {message.FarmerID}.\nRaw message data: {json}");
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ this.Monitor.Log($"Received invalid mod message from {message.FarmerID}.\nRaw message data: {json}\nError details: {ex.GetLogSummary()}");
+ return;
+ }
+
+ // get player IDs
+ HashSet<long> playerIDs = new HashSet<long>(model.ToPlayerIDs ?? this.GetKnownPlayerIDs());
+
// notify local mods
if (playerIDs.Contains(Game1.player.UniqueMultiplayerID))
this.OnModMessageReceived(model);
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 5dee2c4d..12b73515 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -5,7 +5,6 @@ using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
-using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Internal;
using StardewModdingAPI.Toolkit.Utilities;
@@ -33,18 +32,12 @@ namespace StardewModdingAPI.Metadata
/// <summary>The main content manager through which to reload assets.</summary>
private readonly LocalizedContentManager MainContentManager;
- /// <summary>An internal content manager used only for asset propagation. See remarks on <see cref="GameContentManagerForAssetPropagation"/>.</summary>
- private readonly GameContentManagerForAssetPropagation DisposableContentManager;
-
/// <summary>Writes messages to the console.</summary>
private readonly IMonitor Monitor;
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection;
- /// <summary>Whether to enable more aggressive memory optimizations.</summary>
- private readonly bool AggressiveMemoryOptimizations;
-
/// <summary>Parse a raw asset name.</summary>
private readonly Func<string, IAssetName> ParseAssetName;
@@ -67,18 +60,14 @@ namespace StardewModdingAPI.Metadata
*********/
/// <summary>Initialize the core asset data.</summary>
/// <param name="mainContent">The main content manager through which to reload assets.</param>
- /// <param name="disposableContent">An internal content manager used only for asset propagation.</param>
/// <param name="monitor">Writes messages to the console.</param>
/// <param name="reflection">Simplifies access to private code.</param>
- /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
/// <param name="parseAssetName">Parse a raw asset name.</param>
- public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, IMonitor monitor, Reflector reflection, bool aggressiveMemoryOptimizations, Func<string, IAssetName> parseAssetName)
+ public CoreAssetPropagator(LocalizedContentManager mainContent, IMonitor monitor, Reflector reflection, Func<string, IAssetName> parseAssetName)
{
this.MainContentManager = mainContent;
- this.DisposableContentManager = disposableContent;
this.Monitor = monitor;
this.Reflection = reflection;
- this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
this.ParseAssetName = parseAssetName;
}
@@ -230,7 +219,7 @@ namespace StardewModdingAPI.Metadata
** Buildings
****/
case "buildings/houses": // Farm
- Farm.houseTextures = this.LoadAndDisposeIfNeeded(Farm.houseTextures, key);
+ Farm.houseTextures = this.LoadTexture(key);
return true;
case "buildings/houses_paintmask": // Farm
@@ -247,7 +236,7 @@ namespace StardewModdingAPI.Metadata
** Content\Characters\Farmer
****/
case "characters/farmer/accessories": // Game1.LoadContent
- FarmerRenderer.accessoriesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.accessoriesTexture, key);
+ FarmerRenderer.accessoriesTexture = this.LoadTexture(key);
return true;
case "characters/farmer/farmer_base": // Farmer
@@ -257,19 +246,19 @@ namespace StardewModdingAPI.Metadata
return !ignoreWorld && this.ReloadPlayerSprites(assetName);
case "characters/farmer/hairstyles": // Game1.LoadContent
- FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key);
+ FarmerRenderer.hairStylesTexture = this.LoadTexture(key);
return true;
case "characters/farmer/hats": // Game1.LoadContent
- FarmerRenderer.hatsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hatsTexture, key);
+ FarmerRenderer.hatsTexture = this.LoadTexture(key);
return true;
case "characters/farmer/pants": // Game1.LoadContent
- FarmerRenderer.pantsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.pantsTexture, key);
+ FarmerRenderer.pantsTexture = this.LoadTexture(key);
return true;
case "characters/farmer/shirts": // Game1.LoadContent
- FarmerRenderer.shirtsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.shirtsTexture, key);
+ FarmerRenderer.shirtsTexture = this.LoadTexture(key);
return true;
/****
@@ -905,9 +894,6 @@ namespace StardewModdingAPI.Metadata
GameLocation location = locationInfo.Location;
Vector2? playerPos = Game1.player?.Position;
- if (this.AggressiveMemoryOptimizations)
- location.map.DisposeTileSheets(Game1.mapDisplayDevice);
-
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
location.reloadMap();
@@ -973,7 +959,7 @@ namespace StardewModdingAPI.Metadata
// update sprite
foreach (var target in characters)
{
- target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.AssetName.BaseName);
+ target.Npc.Sprite.spriteTexture = this.LoadTexture(target.AssetName.BaseName);
propagated[target.AssetName] = true;
}
}
@@ -1012,7 +998,7 @@ namespace StardewModdingAPI.Metadata
// update portrait
foreach (var target in characters)
{
- target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.AssetName.BaseName);
+ target.Npc.Portrait = this.LoadTexture(target.AssetName.BaseName);
propagated[target.AssetName] = true;
}
}
@@ -1284,25 +1270,10 @@ namespace StardewModdingAPI.Metadata
: Array.Empty<string>();
}
- /// <summary>Load a texture, and dispose the old one if <see cref="AggressiveMemoryOptimizations"/> is enabled and it's different from the new instance.</summary>
- /// <param name="oldTexture">The previous texture to dispose.</param>
+ /// <summary>Load a texture from the main content manager.</summary>
/// <param name="key">The asset key to load.</param>
- private Texture2D LoadAndDisposeIfNeeded(Texture2D? oldTexture, string key)
+ private Texture2D LoadTexture(string key)
{
- // if aggressive memory optimizations are enabled, load the asset from the disposable
- // content manager and dispose the old instance if needed.
- if (this.AggressiveMemoryOptimizations)
- {
- GameContentManagerForAssetPropagation content = this.DisposableContentManager;
-
- Texture2D newTexture = content.Load<Texture2D>(key);
- if (oldTexture?.IsDisposed == false && !object.ReferenceEquals(oldTexture, newTexture) && content.IsResponsibleFor(oldTexture))
- oldTexture.Dispose();
-
- return newTexture;
- }
-
- // else just (re)load it from the main content manager
return this.MainContentManager.Load<Texture2D>(key);
}
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 065dfa8c..bdd6374a 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -40,11 +40,10 @@ copy all the settings, or you may cause bugs due to overridden changes in future
"RewriteMods": true,
/**
- * Whether to enable more aggressive memory optimizations.
- * If you get frequent 'OutOfMemoryException' errors, you can try enabling this to reduce their
- * frequency. This may cause crashes for farmhands in multiplayer.
+ * Whether to make SMAPI file APIs case-insensitive, even on Linux.
+ * This is experimental, and the initial implementation may impact load times.
*/
- "AggressiveMemoryOptimizations": false,
+ "UseCaseInsensitivePaths": false,
/**
* Whether to use the experimental Pintail API proxying library, instead of the original