using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using Microsoft.Xna.Framework; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; 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 { /********* ** Accessors *********/ /// 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; /********* ** Public methods *********/ /// Construct an instance. /// The service provider to use to locate services. /// The root directory to search for content. /// Encapsulates monitoring and logging. public SContentManager(IServiceProvider serviceProvider, string rootDirectory, IMonitor monitor) : this(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, null, monitor) { } /// 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 this.Monitor = monitor; IReflectionHelper reflection = new ReflectionHelper(); // 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 } /// 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) { // normalise asset name so can override the cache value later assetName = this.NormaliseAssetName(assetName); // skip if no event handlers or already loaded if (!ContentEvents.HasAssetLoadingListeners() || this.Cache.ContainsKey(assetName)) return base.Load(assetName); // intercept load T data = base.Load(assetName); string cacheLocale = this.GetCacheLocale(assetName, this.Cache); IContentEventHelper helper = new ContentEventHelper(cacheLocale, assetName, data, this.NormaliseAssetName); ContentEvents.InvokeAssetLoading(this.Monitor, helper); this.Cache[assetName] = helper.Data; return (T)helper.Data; } /********* ** Private methods *********/ /// Normalise an asset name so it's consistent with the underlying cache. /// The asset key. private string NormaliseAssetName(string assetName) { // ensure name format is consistent string[] parts = assetName.Split(SContentManager.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries); assetName = string.Join(SContentManager.PreferredPathSeparator, parts); // apply platform normalisation logic return this.NormaliseAssetNameForPlatform(assetName); } /// Get the locale for which the asset name was saved, if any. /// The normalised asset name. /// The cache to search. private string GetCacheLocale(string normalisedAssetName, IDictionary cache) { string locale = this.GetKeyLocale.Invoke(); return this.Cache.ContainsKey($"{normalisedAssetName}.{this.GetKeyLocale.Invoke()}") ? locale : null; } } }