summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-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
11 files changed, 207 insertions, 119 deletions
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);