summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework/ContentManagers/ModContentManager.cs')
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs273
1 files changed, 158 insertions, 115 deletions
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index beb90a5d..8f64c5a8 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -32,8 +32,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>The language code for language-agnostic mod assets.</summary>
- private readonly LanguageCode DefaultLanguage = Constants.DefaultLanguage;
+ /// <summary>A case-insensitive lookup of relative paths within the <see cref="ContentManager.RootDirectory"/>.</summary>
+ private readonly CaseInsensitivePathLookup RelativePathCache;
/// <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" };
@@ -55,33 +55,31 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <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>
- 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)
+ /// <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)
{
this.GameContentManager = gameContentManager;
+ this.RelativePathCache = relativePathCache;
this.JsonHelper = jsonHelper;
this.ModName = modName;
- }
- /// <inheritdoc />
- public override T Load<T>(string assetName)
- {
- return this.Load<T>(assetName, this.DefaultLanguage, useCache: false);
+ this.TryLocalizeKeys = false;
}
/// <inheritdoc />
- public override T Load<T>(string assetName, LanguageCode language)
+ public override bool DoesAssetExist<T>(IAssetName assetName)
{
- return this.Load<T>(assetName, language, useCache: false);
+ if (base.DoesAssetExist<T>(assetName))
+ return true;
+
+ FileInfo file = this.GetModFile(assetName.Name);
+ return file.Exists;
}
/// <inheritdoc />
- public override T Load<T>(string assetName, LanguageCode language, bool useCache)
+ public override T LoadExact<T>(IAssetName assetName, bool useCache)
{
- // normalize key
- bool isXnbFile = Path.GetExtension(assetName).ToLower() == ".xnb";
- assetName = this.AssertAndNormalizeAssetName(assetName);
-
// disable caching
// This is necessary to avoid assets being shared between content managers, which can
// cause changes to an asset through one content manager affecting the same asset in
@@ -90,106 +88,43 @@ namespace StardewModdingAPI.Framework.ContentManagers
if (useCache)
throw new InvalidOperationException("Mod content managers don't support asset caching.");
- // disable language handling
- // Mod files don't support automatic translation logic, so this should never happen.
- if (language != this.DefaultLanguage)
- throw new InvalidOperationException("Localized assets aren't supported by the mod content manager.");
-
// resolve managed asset key
{
- if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath))
+ if (this.Coordinator.TryParseManagedAssetKey(assetName.Name, out string? contentManagerID, out IAssetName? relativePath))
{
if (contentManagerID != this.Name)
- throw new SContentLoadException($"Can't load managed asset key '{assetName}' through content manager '{this.Name}' for a different mod.");
+ throw this.GetLoadError(assetName, "can't load a different mod's managed asset key through this mod content manager.");
assetName = relativePath;
}
}
// get local asset
- SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}");
T asset;
try
{
// get file
- FileInfo file = this.GetModFile(isXnbFile ? $"{assetName}.xnb" : assetName); // .xnb extension is stripped from asset names passed to the content manager
+ FileInfo file = this.GetModFile(assetName.Name);
if (!file.Exists)
- throw GetContentError("the specified path doesn't exist.");
+ throw this.GetLoadError(assetName, "the specified path doesn't exist.");
// load content
- switch (file.Extension.ToLower())
+ asset = file.Extension.ToLower() switch
{
- // XNB file
- case ".xnb":
- {
- asset = this.RawLoad<T>(assetName, useCache: false);
- if (asset is Map map)
- {
- map.assetPath = assetName;
- this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: true);
- }
- }
- break;
-
- // unpacked Bitmap font
- case ".fnt":
- {
- string source = File.ReadAllText(file.FullName);
- asset = (T)(object)new XmlSource(source);
- }
- break;
-
- // unpacked data
- case ".json":
- {
- if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out asset))
- throw GetContentError("the JSON file is invalid."); // should never happen since we check for file existence above
- }
- break;
-
- // unpacked image
- case ".png":
- {
- // validate
- if (typeof(T) != typeof(Texture2D))
- throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
-
- // fetch & cache
- using FileStream stream = File.OpenRead(file.FullName);
-
- Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
- texture = this.PremultiplyTransparency(texture);
- asset = (T)(object)texture;
- }
- break;
-
- // unpacked map
- case ".tbin":
- case ".tmx":
- {
- // validate
- if (typeof(T) != typeof(Map))
- throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
-
- // fetch & cache
- FormatManager formatManager = FormatManager.Instance;
- Map map = formatManager.LoadMap(file.FullName);
- map.assetPath = assetName;
- this.FixTilesheetPaths(map, relativeMapPath: assetName, fixEagerPathPrefixes: false);
- asset = (T)(object)map;
- }
- break;
-
- default:
- throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', or '.xnb'.");
- }
+ ".fnt" => this.LoadFont<T>(assetName, file),
+ ".json" => this.LoadDataFile<T>(assetName, file),
+ ".png" => this.LoadImageFile<T>(assetName, file),
+ ".tbin" or ".tmx" => this.LoadMapFile<T>(assetName, file),
+ ".xnb" => this.LoadXnbFile<T>(assetName),
+ _ => this.HandleUnknownFileType<T>(assetName, file)
+ };
}
- catch (Exception ex) when (!(ex is SContentLoadException))
+ catch (Exception ex) when (ex is not SContentLoadException)
{
- throw new SContentLoadException($"The content manager failed loading content asset '{assetName}' from {this.Name}.", ex);
+ throw this.GetLoadError(assetName, "an unexpected occurred.", ex);
}
// track & return asset
- this.TrackAsset(assetName, asset, language, useCache);
+ this.TrackAsset(assetName, asset, useCache: false);
return asset;
}
@@ -202,36 +137,137 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Get the underlying key in the game's content cache for an asset. This does not validate whether the asset exists.</summary>
/// <param name="key">The local path to a content file relative to the mod folder.</param>
/// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception>
- public string GetInternalAssetKey(string key)
+ public IAssetName GetInternalAssetKey(string key)
{
FileInfo file = this.GetModFile(key);
- string relativePath = PathUtilities.GetRelativePath(this.RootDirectory, file.FullName);
- return Path.Combine(this.Name, relativePath);
+ string relativePath = Path.GetRelativePath(this.RootDirectory, file.FullName);
+ string internalKey = Path.Combine(this.Name, relativePath);
+
+ return this.Coordinator.ParseAssetName(internalKey, allowLocales: false);
}
/*********
** Private methods
*********/
- /// <inheritdoc />
- protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language)
+ /// <summary>Load an unpacked font file (<c>.fnt</c>).</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset name relative to the loader root directory.</param>
+ /// <param name="file">The file to load.</param>
+ private T LoadFont<T>(IAssetName assetName, FileInfo file)
{
- return this.Cache.ContainsKey(normalizedAssetName);
+ // validate
+ if (!typeof(T).IsAssignableFrom(typeof(XmlSource)))
+ throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(XmlSource)}'.");
+
+ // load
+ string source = File.ReadAllText(file.FullName);
+ return (T)(object)new XmlSource(source);
+ }
+
+ /// <summary>Load an unpacked data file (<c>.json</c>).</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset name relative to the loader root directory.</param>
+ /// <param name="file">The file to load.</param>
+ private T LoadDataFile<T>(IAssetName assetName, FileInfo file)
+ {
+ if (!this.JsonHelper.ReadJsonFileIfExists(file.FullName, out T? asset))
+ throw this.GetLoadError(assetName, "the JSON file is invalid."); // should never happen since we check for file existence before calling this method
+
+ return asset;
+ }
+
+ /// <summary>Load an unpacked image file (<c>.json</c>).</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset name relative to the loader root directory.</param>
+ /// <param name="file">The file to load.</param>
+ private T LoadImageFile<T>(IAssetName assetName, FileInfo file)
+ {
+ // validate
+ if (typeof(T) != typeof(Texture2D))
+ throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'.");
+
+ // load
+ using FileStream stream = File.OpenRead(file.FullName);
+ Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream);
+ texture = this.PremultiplyTransparency(texture);
+ return (T)(object)texture;
+ }
+
+ /// <summary>Load an unpacked image file (<c>.tbin</c> or <c>.tmx</c>).</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset name relative to the loader root directory.</param>
+ /// <param name="file">The file to load.</param>
+ private T LoadMapFile<T>(IAssetName assetName, FileInfo file)
+ {
+ // validate
+ if (typeof(T) != typeof(Map))
+ throw this.GetLoadError(assetName, $"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'.");
+
+ // load
+ FormatManager formatManager = FormatManager.Instance;
+ Map map = formatManager.LoadMap(file.FullName);
+ map.assetPath = assetName.Name;
+ this.FixTilesheetPaths(map, relativeMapPath: assetName.Name, fixEagerPathPrefixes: false);
+ return (T)(object)map;
+ }
+
+ /// <summary>Load a packed file (<c>.xnb</c>).</summary>
+ /// <typeparam name="T">The type of asset to load.</typeparam>
+ /// <param name="assetName">The asset name relative to the loader root directory.</param>
+ private T LoadXnbFile<T>(IAssetName assetName)
+ {
+ // the underlying content manager adds a .xnb extension implicitly, so
+ // we need to strip it here to avoid trying to load a '.xnb.xnb' file.
+ IAssetName loadName = assetName.Name.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase)
+ ? this.Coordinator.ParseAssetName(assetName.Name[..^".xnb".Length], allowLocales: false)
+ : assetName;
+
+ // load asset
+ T asset = this.RawLoad<T>(loadName, useCache: false);
+ if (asset is Map map)
+ {
+ map.assetPath = loadName.Name;
+ this.FixTilesheetPaths(map, relativeMapPath: loadName.Name, fixEagerPathPrefixes: true);
+ }
+
+ return asset;
+ }
+
+ /// <summary>Handle a request to load a file type that isn't supported by SMAPI.</summary>
+ /// <typeparam name="T">The expected file type.</typeparam>
+ /// <param name="assetName">The asset name relative to the loader root directory.</param>
+ /// <param name="file">The file to load.</param>
+ private T HandleUnknownFileType<T>(IAssetName assetName, FileInfo file)
+ {
+ throw this.GetLoadError(assetName, $"unknown file extension '{file.Extension}'; must be one of '.fnt', '.json', '.png', '.tbin', '.tmx', or '.xnb'.");
+ }
+
+ /// <summary>Get an error which indicates that an asset couldn't be loaded.</summary>
+ /// <param name="assetName">The asset name that failed to load.</param>
+ /// <param name="reasonPhrase">The reason the file couldn't be loaded.</param>
+ /// <param name="exception">The underlying exception, if applicable.</param>
+ private SContentLoadException GetLoadError(IAssetName assetName, string reasonPhrase, Exception? exception = null)
+ {
+ return new($"Failed loading asset '{assetName}' from {this.Name}: {reasonPhrase}", exception);
}
/// <summary>Get a file from the mod folder.</summary>
/// <param name="path">The asset path relative to the content folder.</param>
private FileInfo GetModFile(string path)
{
+ // map to case-insensitive path if needed
+ path = this.RelativePathCache.GetFilePath(path);
+
// try exact match
- FileInfo file = new FileInfo(Path.Combine(this.FullRootDirectory, path));
+ FileInfo file = new(Path.Combine(this.FullRootDirectory, path));
// try with default extension
- if (!file.Exists && file.Extension == string.Empty)
+ if (!file.Exists)
{
foreach (string extension in this.LocalTilesheetExtensions)
{
- FileInfo result = new FileInfo(file.FullName + extension);
+ FileInfo result = new(file.FullName + extension);
if (result.Exists)
{
file = result;
@@ -252,16 +288,20 @@ namespace StardewModdingAPI.Framework.ContentManagers
// premultiply pixels
Color[] data = new Color[texture.Width * texture.Height];
texture.GetData(data);
+ bool changed = false;
for (int i = 0; i < data.Length; i++)
{
- var pixel = data[i];
- if (pixel.A == byte.MinValue || pixel.A == byte.MaxValue)
+ Color pixel = data[i];
+ if (pixel.A is (byte.MinValue or byte.MaxValue))
continue; // no need to change fully transparent/opaque pixels
data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4())
+ changed = true;
}
- texture.SetData(data);
+ if (changed)
+ texture.SetData(data);
+
return texture;
}
@@ -296,15 +336,18 @@ namespace StardewModdingAPI.Framework.ContentManagers
// load best match
try
{
- if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out string assetName, out string error))
+ if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out IAssetName? assetName, out string? error))
throw new SContentLoadException($"{errorPrefix} {error}");
- if (assetName != tilesheet.ImageSource)
- this.Monitor.VerboseLog($" Mapped tilesheet '{tilesheet.ImageSource}' to '{assetName}'.");
+ if (assetName is not null)
+ {
+ if (!assetName.IsEquivalentTo(tilesheet.ImageSource))
+ this.Monitor.VerboseLog($" Mapped tilesheet '{tilesheet.ImageSource}' to '{assetName}'.");
- tilesheet.ImageSource = assetName;
+ tilesheet.ImageSource = assetName.Name;
+ }
}
- catch (Exception ex) when (!(ex is SContentLoadException))
+ catch (Exception ex) when (ex is not SContentLoadException)
{
throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex);
}
@@ -318,7 +361,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="error">A message indicating why the file couldn't be loaded.</param>
/// <returns>Returns whether the asset name was found.</returns>
/// <remarks>See remarks on <see cref="FixTilesheetPaths"/>.</remarks>
- private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out string assetName, out string error)
+ private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName? assetName, out string? error)
{
assetName = null;
error = null;
@@ -326,7 +369,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// nothing to do
if (string.IsNullOrWhiteSpace(relativePath))
{
- assetName = relativePath;
+ assetName = null;
return true;
}
@@ -350,10 +393,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// get from game assets
- string contentKey = this.GetContentKeyForTilesheetImageSource(relativePath);
+ IAssetName contentKey = this.Coordinator.ParseAssetName(this.GetContentKeyForTilesheetImageSource(relativePath), allowLocales: false);
try
{
- this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
+ this.GameContentManager.LoadLocalized<Texture2D>(contentKey, this.GameContentManager.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
assetName = contentKey;
return true;
}
@@ -366,7 +409,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
// if the content file doesn't exist, that doesn't mean the error here is a
// content-not-found error. Unfortunately XNA doesn't provide a good way to
// detect the error type.
- if (this.GetContentFolderFileExists(contentKey))
+ if (this.GetContentFolderFileExists(contentKey.Name))
throw;
}