using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
namespace StardewModdingAPI.Framework.ContentManagers
{
/// A content manager which handles reading files from a SMAPI mod folder with support for unpacked files.
internal abstract class BaseContentManager : LocalizedContentManager, IContentManager
{
/*********
** Properties
*********/
/// The central coordinator which manages content managers.
protected readonly ContentCoordinator Coordinator;
/// The underlying asset cache.
protected readonly ContentCache Cache;
/// Encapsulates monitoring and logging.
protected readonly IMonitor Monitor;
/// Whether the content coordinator has been disposed.
private bool IsDisposed;
/// The language enum values indexed by locale code.
private readonly IDictionary LanguageCodes;
/// A callback to invoke when the content manager is being disposed.
private readonly Action OnDisposing;
/*********
** Accessors
*********/
/// A name for the mod manager. Not guaranteed to be unique.
public string Name { get; }
/// The current language as a constant.
public LanguageCode Language => this.GetCurrentLanguage();
/// The absolute path to the .
public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
/// Whether this content manager is for a mod folder.
public bool IsModContentManager { get; }
/*********
** Public methods
*********/
/// Construct an instance.
/// A name for the mod manager. Not guaranteed to be unique.
/// The service provider to use to locate services.
/// The root directory to search for content.
/// The current culture for which to localise content.
/// The central coordinator which manages content managers.
/// Encapsulates monitoring and logging.
/// Simplifies access to private code.
/// A callback to invoke when the content manager is being disposed.
/// Whether this content manager is for a mod folder.
protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing, bool isModFolder)
: base(serviceProvider, rootDirectory, currentCulture)
{
// init
this.Name = name;
this.Coordinator = coordinator ?? throw new ArgumentNullException(nameof(coordinator));
this.Cache = new ContentCache(this, reflection);
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.OnDisposing = onDisposing;
this.IsModContentManager = isModFolder;
// get asset data
this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase);
}
/// Load an asset that has been processed by the content pipeline.
/// The type of asset to load.
/// The asset path relative to the loader root directory, not including the .xnb extension.
public override T Load(string assetName)
{
return this.Load(assetName, LocalizedContentManager.CurrentLanguageCode);
}
/// Load the base asset without localisation.
/// The type of asset to load.
/// The asset path relative to the loader root directory, not including the .xnb extension.
public override T LoadBase(string assetName)
{
return this.Load(assetName, LanguageCode.en);
}
/// Inject an asset into the cache.
/// The type of asset to inject.
/// The asset path relative to the loader root directory, not including the .xnb extension.
/// The asset value.
public void Inject(string assetName, T value)
{
assetName = this.AssertAndNormaliseAssetName(assetName);
this.Cache[assetName] = value;
}
/// Get a copy of the given asset if supported.
/// The asset type.
/// The asset to clone.
public T CloneIfPossible(T asset)
{
switch (asset as object)
{
case Texture2D source:
{
int[] pixels = new int[source.Width * source.Height];
source.GetData(pixels);
Texture2D clone = new Texture2D(source.GraphicsDevice, source.Width, source.Height);
clone.SetData(pixels);
return (T)(object)clone;
}
case Dictionary source:
return (T)(object)new Dictionary(source);
case Dictionary source:
return (T)(object)new Dictionary(source);
default:
return asset;
}
}
/// Normalise path separators in a file path. For asset keys, see instead.
/// The file path to normalise.
[Pure]
public string NormalisePathSeparators(string path)
{
return this.Cache.NormalisePathSeparators(path);
}
/// Assert that the given key has a valid format and return a normalised form consistent with the underlying cache.
/// The asset key to check.
/// The asset key is empty or contains invalid characters.
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
public string AssertAndNormaliseAssetName(string assetName)
{
// NOTE: the game checks for ContentLoadException to handle invalid keys, so avoid
// throwing other types like ArgumentException here.
if (string.IsNullOrWhiteSpace(assetName))
throw new SContentLoadException("The asset key or local path is empty.");
if (assetName.Intersect(Path.GetInvalidPathChars()).Any())
throw new SContentLoadException("The asset key or local path contains invalid characters.");
return this.Cache.NormaliseKey(assetName);
}
/****
** Content loading
****/
/// Get the current content locale.
public string GetLocale()
{
return this.GetLocale(this.GetCurrentLanguage());
}
/// The locale for a language.
/// The language.
public string GetLocale(LanguageCode language)
{
return this.LanguageCodeString(language);
}
/// Get whether the content manager has already loaded and cached the given asset.
/// The asset path relative to the loader root directory, not including the .xnb extension.
public bool IsLoaded(string assetName)
{
assetName = this.Cache.NormaliseKey(assetName);
return this.IsNormalisedKeyLoaded(assetName);
}
/// Get the cached asset keys.
public IEnumerable GetAssetKeys()
{
return this.Cache.Keys
.Select(this.GetAssetName)
.Distinct();
}
/****
** Cache invalidation
****/
/// Purge matched assets from the cache.
/// Matches the asset keys to invalidate.
/// Whether to dispose invalidated assets. This should only be true when they're being invalidated as part of a dispose, to avoid crashing the game.
/// Returns the number of invalidated assets.
public IEnumerable InvalidateCache(Func predicate, bool dispose = false)
{
HashSet removeAssetNames = new HashSet(StringComparer.InvariantCultureIgnoreCase);
this.Cache.Remove((key, type) =>
{
this.ParseCacheKey(key, out string assetName, out _);
if (removeAssetNames.Contains(assetName) || predicate(assetName, type))
{
removeAssetNames.Add(assetName);
return true;
}
return false;
});
return removeAssetNames;
}
/// Dispose held resources.
/// Whether the content manager is being disposed (rather than finalized).
protected override void Dispose(bool isDisposing)
{
if (this.IsDisposed)
return;
this.IsDisposed = true;
this.OnDisposing(this);
base.Dispose(isDisposing);
}
///
public override void Unload()
{
if (this.IsDisposed)
return; // base logic doesn't allow unloading twice, which happens due to SMAPI and the game both unloading
base.Unload();
}
/*********
** Private methods
*********/
/// Get the locale codes (like ja-JP) used in asset keys.
private IDictionary GetKeyLocales()
{
// create locale => code map
IDictionary map = new Dictionary();
foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode)))
map[code] = this.GetLocale(code);
return map;
}
/// Get the asset name from a cache key.
/// The input cache key.
private string GetAssetName(string cacheKey)
{
this.ParseCacheKey(cacheKey, out string assetName, out string _);
return assetName;
}
/// Parse a cache key into its component parts.
/// The input cache key.
/// The original asset name.
/// The asset locale code (or null if not localised).
protected void ParseCacheKey(string cacheKey, out string assetName, out string localeCode)
{
// handle localised key
if (!string.IsNullOrWhiteSpace(cacheKey))
{
int lastSepIndex = cacheKey.LastIndexOf(".", StringComparison.InvariantCulture);
if (lastSepIndex >= 0)
{
string suffix = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
if (this.LanguageCodes.ContainsKey(suffix))
{
assetName = cacheKey.Substring(0, lastSepIndex);
localeCode = cacheKey.Substring(lastSepIndex + 1, cacheKey.Length - lastSepIndex - 1);
return;
}
}
}
// handle simple key
assetName = cacheKey;
localeCode = null;
}
/// Get whether an asset has already been loaded.
/// The normalised asset name.
protected abstract bool IsNormalisedKeyLoaded(string normalisedAssetName);
}
}