using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.AssemblyRewriters;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
namespace StardewModdingAPI.Framework
{
/// SMAPI's implementation of the game's content manager which lets it raise content events.
internal class SContentManager : LocalizedContentManager
{
/*********
** Properties
*********/
/// The possible directory separator characters in an asset key.
private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
/// The preferred directory separator chaeacter in an asset key.
private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
/// Encapsulates monitoring and logging.
private readonly IMonitor Monitor;
/// The underlying content manager's asset cache.
private readonly IDictionary Cache;
/// Applies platform-specific asset key normalisation so it's consistent with the underlying cache.
private readonly Func NormaliseAssetNameForPlatform;
/// The private method which generates the locale portion of an asset name.
private readonly IPrivateMethod GetKeyLocale;
/*********
** Accessors
*********/
/// Implementations which change assets after they're loaded.
internal IDictionary> Editors { get; } = new Dictionary>();
/// The absolute path to the .
public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
/*********
** Public methods
*********/
/// Construct an instance.
/// The service provider to use to locate services.
/// The root directory to search for content.
/// The current culture for which to localise content.
/// The current language code for which to localise content.
/// Encapsulates monitoring and logging.
public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor)
: base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride)
{
// initialise
IReflectionHelper reflection = new ReflectionHelper();
this.Monitor = monitor;
// get underlying fields for interception
this.Cache = reflection.GetPrivateField>(this, "loadedAssets").GetValue();
this.GetKeyLocale = reflection.GetPrivateMethod(this, "languageCode");
// get asset key normalisation logic
if (Constants.TargetPlatform == Platform.Windows)
{
IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath");
this.NormaliseAssetNameForPlatform = path => method.Invoke(path);
}
else
this.NormaliseAssetNameForPlatform = key => key.Replace('\\', '/'); // based on MonoGame's ContentManager.Load logic
}
/// Normalise path separators in a file path. For asset keys, see instead.
/// The file path to normalise.
public string NormalisePathSeparators(string path)
{
string[] parts = path.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
string normalised = string.Join(SContentManager.PreferredPathSeparator, parts);
if (path.StartsWith(SContentManager.PreferredPathSeparator))
normalised = SContentManager.PreferredPathSeparator + normalised; // keep root slash
return normalised;
}
/// Normalise an asset name so it's consistent with the underlying cache.
/// The asset key.
public string NormaliseAssetName(string assetName)
{
assetName = this.NormalisePathSeparators(assetName);
if (assetName.EndsWith(".xnb", StringComparison.InvariantCultureIgnoreCase))
return assetName.Substring(0, assetName.Length - 4);
return this.NormaliseAssetNameForPlatform(assetName);
}
/// 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.NormaliseAssetName(assetName);
return this.IsNormalisedKeyLoaded(assetName);
}
/// 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)
{
assetName = this.NormaliseAssetName(assetName);
// skip if already loaded
if (this.IsNormalisedKeyLoaded(assetName))
return base.Load(assetName);
// load asset
T asset = this.GetAssetWithInterceptors(this.GetLocale(), assetName, () => base.Load(assetName));
this.Cache[assetName] = asset;
return asset;
}
/// 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.NormaliseAssetName(assetName);
this.Cache[assetName] = value;
}
/// Get the current content locale.
public string GetLocale()
{
return this.GetKeyLocale.Invoke();
}
/*********
** Private methods
*********/
/// Get whether an asset has already been loaded.
/// The normalised asset name.
private bool IsNormalisedKeyLoaded(string normalisedAssetName)
{
return this.Cache.ContainsKey(normalisedAssetName)
|| this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}"); // translated asset
}
/// Read an asset with support for asset interceptors.
/// The asset type.
/// The current content locale.
/// The normalised asset path relative to the loader root directory, not including the .xnb extension.
/// Get the asset from the underlying content manager.
private T GetAssetWithInterceptors(string locale, string normalisedKey, Func getData)
{
// get metadata
IAssetInfo info = new AssetInfo(locale, normalisedKey, typeof(T), this.NormaliseAssetName);
// load asset
T asset = getData();
// edit asset
IAssetData data = new AssetDataForObject(info.Locale, info.AssetName, asset, this.NormaliseAssetName);
foreach (var modEditors in this.Editors)
{
IModMetadata mod = modEditors.Key;
foreach (IAssetEditor editor in modEditors.Value)
{
if (!editor.CanEdit(info))
continue;
this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace);
editor.Edit(data);
if (data.Data == null)
throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to a null value.");
if (!(data.Data is T))
throw new InvalidOperationException($"{mod.DisplayName} incorrectly set asset '{normalisedKey}' to incompatible type '{data.Data.GetType()}', expected '{typeof(T)}'.");
}
}
// return result
return (T)data.Data;
}
}
}