diff options
author | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-04-29 14:13:55 -0400 |
---|---|---|
committer | Jesse Plamondon-Willard <github@jplamondonw.com> | 2017-04-29 14:13:55 -0400 |
commit | 9b615fadaa3bb8fbf4fe011320aa1cc709113f3f (patch) | |
tree | 2bea5b0699288c7521ec994be992a7f4c73fa313 /src/StardewModdingAPI/Framework | |
parent | 6b9372237c79517a44a4ce3e096634f0273f5ba3 (diff) | |
download | SMAPI-9b615fadaa3bb8fbf4fe011320aa1cc709113f3f.tar.gz SMAPI-9b615fadaa3bb8fbf4fe011320aa1cc709113f3f.tar.bz2 SMAPI-9b615fadaa3bb8fbf4fe011320aa1cc709113f3f.zip |
add initial content API (#257)
Diffstat (limited to 'src/StardewModdingAPI/Framework')
-rw-r--r-- | src/StardewModdingAPI/Framework/ContentHelper.cs | 147 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/ModHelper.cs | 17 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/SContentManager.cs | 33 |
3 files changed, 188 insertions, 9 deletions
diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs new file mode 100644 index 00000000..0d063ef0 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -0,0 +1,147 @@ +using System; +using System.IO; +using System.Linq; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// <summary>Provides an API for loading content assets.</summary> + internal class ContentHelper : IContentHelper + { + /********* + ** Properties + *********/ + /// <summary>SMAPI's underlying content manager.</summary> + private readonly SContentManager ContentManager; + + /// <summary>The absolute path to the mod folder.</summary> + private readonly string ModFolderPath; + + /// <summary>The path to the mod's folder, relative to the game's content folder (e.g. "../Mods/ModName").</summary> + private readonly string RelativeContentFolder; + + /// <summary>The friendly mod name for use in errors.</summary> + private readonly string ModName; + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="contentManager">SMAPI's underlying content manager.</param> + /// <param name="modFolderPath">The absolute path to the mod folder.</param> + /// <param name="modName">The friendly mod name for use in errors.</param> + public ContentHelper(SContentManager contentManager, string modFolderPath, string modName) + { + this.ContentManager = contentManager; + this.ModFolderPath = modFolderPath; + this.ModName = modName; + this.RelativeContentFolder = this.GetRelativePath(contentManager.FullRootDirectory, modFolderPath); + } + + /// <summary>Fetch and cache content from the game content or mod folder (if not already cached), and return it.</summary> + /// <typeparam name="T">The expected data type. The main supported types are <see cref="Texture2D"/> and dictionaries; other types may be supported by the game's content pipeline.</typeparam> + /// <param name="key">The asset key to fetch (if the <paramref name="source"/> is <see cref="ContentSource.GameContent"/>), or the local path to an XNB file relative to the mod folder.</param> + /// <param name="source">Where to search for a matching content asset.</param> + /// <exception cref="ArgumentException">The <paramref name="key"/> is empty or contains invalid characters.</exception> + /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> + public T Load<T>(string key, ContentSource source) + { + // validate + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("The asset key or local path is empty."); + if (key.Intersect(Path.GetInvalidPathChars()).Any()) + throw new ArgumentException("The asset key or local path contains invalid characters."); + + // load content + switch (source) + { + case ContentSource.GameContent: + return this.LoadFromGameContent<T>(key, key, source); + + case ContentSource.ModFolder: + // find content file + FileInfo file = new FileInfo(Path.Combine(this.ModFolderPath, key)); + if (!file.Exists && file.Extension == "") + file = new FileInfo(Path.Combine(this.ModFolderPath, key + ".xnb")); + if (!file.Exists) + throw new ContentLoadException($"There is no file at path '{file.FullName}'."); + + // get content-relative path + string contentPath = Path.Combine(this.RelativeContentFolder, key); + if (contentPath.EndsWith(".xnb")) + contentPath = contentPath.Substring(0, contentPath.Length - 4); + + // load content + switch (file.Extension.ToLower()) + { + case ".xnb": + return this.LoadFromGameContent<T>(contentPath, key, source); + + case ".png": + // validate + if (typeof(T) != typeof(Texture2D)) + throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + + // try cache + if (this.ContentManager.IsLoaded(contentPath)) + return this.LoadFromGameContent<T>(contentPath, key, source); + + // fetch & cache + using (FileStream stream = File.OpenRead(file.FullName)) + { + Texture2D texture = Texture2D.FromStream(Game1.graphics.GraphicsDevice, stream); + this.ContentManager.Inject(contentPath, texture); + return (T)(object)texture; + } + + default: + throw new ContentLoadException($"Unknown file extension '{file.Extension}'; must be '.xnb' or '.png'."); + } + + default: + throw new NotSupportedException($"Unknown content source '{source}'."); + } + } + + + /********* + ** Private methods + *********/ + /// <summary>Load a content asset through the underlying content manager, and throw a friendly error if it fails.</summary> + /// <typeparam name="T">The expected data type.</typeparam> + /// <param name="assetKey">The content key.</param> + /// <param name="friendlyKey">The friendly content key to show in errors.</param> + /// <param name="source">The content source for use in errors.</param> + /// <exception cref="ContentLoadException">The content couldn't be loaded.</exception> + private T LoadFromGameContent<T>(string assetKey, string friendlyKey, ContentSource source) + { + try + { + return this.ContentManager.Load<T>(assetKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"{this.ModName} failed loading content asset '{friendlyKey}' from {source}.", ex); + } + } + + /// <summary>Get a directory path relative to a given root.</summary> + /// <param name="rootPath">The root path from which the path should be relative.</param> + /// <param name="targetPath">The target file path.</param> + private string GetRelativePath(string rootPath, string targetPath) + { + // convert to URIs + Uri from = new Uri(rootPath + "/"); + Uri to = new Uri(targetPath + "/"); + if (from.Scheme != to.Scheme) + throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{rootPath}'."); + + // get relative path + return Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()) + .Replace(Path.DirectorySeparatorChar == '/' ? '\\' : '/', Path.DirectorySeparatorChar); // use correct separator for platform + } + } +} diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 52e482f2..09297a65 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -18,9 +18,12 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// <summary>The mod directory path.</summary> + /// <summary>The full path to the mod's folder.</summary> public string DirectoryPath { get; } + /// <summary>An API for loading content assets.</summary> + public IContentHelper Content { get; } + /// <summary>Simplifies access to private game code.</summary> public IReflectionHelper Reflection { get; } = new ReflectionHelper(); @@ -35,14 +38,15 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="modName">The friendly mod name.</param> - /// <param name="modDirectory">The mod directory path.</param> + /// <param name="manifest">The manifest for the associated mod.</param> + /// <param name="modDirectory">The full path to the mod's folder.</param> /// <param name="jsonHelper">Encapsulate SMAPI's JSON parsing.</param> /// <param name="modRegistry">Metadata about loaded mods.</param> /// <param name="commandManager">Manages console commands.</param> + /// <param name="contentManager">The content manager which loads content assets.</param> /// <exception cref="ArgumentNullException">An argument is null or empty.</exception> /// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception> - public ModHelper(string modName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager) + public ModHelper(IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -55,10 +59,11 @@ namespace StardewModdingAPI.Framework throw new InvalidOperationException("The specified mod directory does not exist."); // initialise - this.JsonHelper = jsonHelper; this.DirectoryPath = modDirectory; + this.JsonHelper = jsonHelper; + this.Content = new ContentHelper(contentManager, modDirectory, manifest.Name); this.ModRegistry = modRegistry; - this.ConsoleCommands = new CommandHelper(modName, commandManager); + this.ConsoleCommands = new CommandHelper(manifest.Name, commandManager); } /**** diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index ef5855b2..e363e6b4 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Threading; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Content; @@ -17,7 +18,7 @@ namespace StardewModdingAPI.Framework internal class SContentManager : LocalizedContentManager { /********* - ** Accessors + ** Properties *********/ /// <summary>The possible directory separator characters in an asset key.</summary> private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray(); @@ -39,6 +40,13 @@ namespace StardewModdingAPI.Framework /********* + ** Accessors + *********/ + /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary> + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> @@ -85,7 +93,7 @@ namespace StardewModdingAPI.Framework string cacheLocale = this.GetCacheLocale(assetName); // skip if already loaded - if (this.IsLoaded(assetName)) + if (this.IsNormalisedKeyLoaded(assetName)) return base.Load<T>(assetName); // load data @@ -98,6 +106,25 @@ namespace StardewModdingAPI.Framework return (T)helper.Data; } + /// <summary>Inject an asset into the cache.</summary> + /// <typeparam name="T">The type of asset to inject.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="value">The asset value.</param> + public void Inject<T>(string assetName, T value) + { + assetName = this.NormaliseAssetName(assetName); + this.Cache[assetName] = value; + } + + /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + public bool IsLoaded(string assetName) + { + assetName = this.NormaliseAssetName(assetName); + return this.IsNormalisedKeyLoaded(assetName); + + } + /********* ** Private methods @@ -116,7 +143,7 @@ namespace StardewModdingAPI.Framework /// <summary>Get whether an asset has already been loaded.</summary> /// <param name="normalisedAssetName">The normalised asset name.</param> - private bool IsLoaded(string normalisedAssetName) + private bool IsNormalisedKeyLoaded(string normalisedAssetName) { return this.Cache.ContainsKey(normalisedAssetName) || this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke<string>()}"); // translated asset |