diff options
Diffstat (limited to 'src/SMAPI/Framework/ContentManagers/ModContentManager.cs')
| -rw-r--r-- | src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 273 | 
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;              } | 
