add initial content API (#257)
This commit is contained in:
parent
6b9372237c
commit
9b615fadaa
|
@ -0,0 +1,12 @@
|
|||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>Specifies a source containing content that can be loaded.</summary>
|
||||
public enum ContentSource
|
||||
{
|
||||
/// <summary>Assets in the game's content manager (i.e. XNBs in the game's content folder).</summary>
|
||||
GameContent,
|
||||
|
||||
/// <summary>XNB files in the current mod's folder.</summary>
|
||||
ModFolder
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/****
|
||||
|
|
|
@ -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();
|
||||
|
@ -38,6 +39,13 @@ namespace StardewModdingAPI.Framework
|
|||
private readonly IPrivateMethod GetKeyLocale;
|
||||
|
||||
|
||||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
|
||||
public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
|
||||
|
||||
|
||||
/*********
|
||||
** Public methods
|
||||
*********/
|
||||
|
@ -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
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
using Microsoft.Xna.Framework.Graphics;
|
||||
|
||||
namespace StardewModdingAPI
|
||||
{
|
||||
/// <summary>Provides an API for loading content assets.</summary>
|
||||
public interface IContentHelper
|
||||
{
|
||||
/// <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>
|
||||
T Load<T>(string key, ContentSource source);
|
||||
}
|
||||
}
|
|
@ -6,9 +6,12 @@
|
|||
/*********
|
||||
** Accessors
|
||||
*********/
|
||||
/// <summary>The mod directory path.</summary>
|
||||
/// <summary>The full path to the mod's folder.</summary>
|
||||
string DirectoryPath { get; }
|
||||
|
||||
/// <summary>An API for loading content assets.</summary>
|
||||
IContentHelper Content { get; }
|
||||
|
||||
/// <summary>Simplifies access to private game code.</summary>
|
||||
IReflectionHelper Reflection { get; }
|
||||
|
||||
|
|
|
@ -596,7 +596,7 @@ namespace StardewModdingAPI
|
|||
// inject data
|
||||
// get helper
|
||||
mod.ModManifest = manifest;
|
||||
mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager);
|
||||
mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content);
|
||||
mod.Monitor = this.GetSecondaryMonitor(manifest.Name);
|
||||
mod.PathOnDisk = directory.FullName;
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@
|
|||
<Link>Properties\GlobalAssemblyInfo.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="Command.cs" />
|
||||
<Compile Include="ContentSource.cs" />
|
||||
<Compile Include="Events\ContentEvents.cs" />
|
||||
<Compile Include="Events\EventArgsValueChanged.cs" />
|
||||
<Compile Include="Framework\Command.cs" />
|
||||
|
@ -146,6 +147,7 @@
|
|||
<Compile Include="Framework\AssemblyDefinitionResolver.cs" />
|
||||
<Compile Include="Framework\AssemblyParseResult.cs" />
|
||||
<Compile Include="Framework\CommandManager.cs" />
|
||||
<Compile Include="Framework\ContentHelper.cs" />
|
||||
<Compile Include="Framework\Content\ContentEventData.cs" />
|
||||
<Compile Include="Framework\Content\ContentEventHelper.cs" />
|
||||
<Compile Include="Framework\Content\ContentEventHelperForDictionary.cs" />
|
||||
|
@ -166,6 +168,7 @@
|
|||
<Compile Include="IContentEventHelper.cs" />
|
||||
<Compile Include="IContentEventHelperForDictionary.cs" />
|
||||
<Compile Include="IContentEventHelperForImage.cs" />
|
||||
<Compile Include="IContentHelper.cs" />
|
||||
<Compile Include="IModRegistry.cs" />
|
||||
<Compile Include="Events\LocationEvents.cs" />
|
||||
<Compile Include="Events\MenuEvents.cs" />
|
||||
|
|
Loading…
Reference in New Issue