summaryrefslogtreecommitdiff
path: root/src/StardewModdingAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-04-29 14:13:55 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-04-29 14:13:55 -0400
commit9b615fadaa3bb8fbf4fe011320aa1cc709113f3f (patch)
tree2bea5b0699288c7521ec994be992a7f4c73fa313 /src/StardewModdingAPI/Framework
parent6b9372237c79517a44a4ce3e096634f0273f5ba3 (diff)
downloadSMAPI-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.cs147
-rw-r--r--src/StardewModdingAPI/Framework/ModHelper.cs17
-rw-r--r--src/StardewModdingAPI/Framework/SContentManager.cs33
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