using System; using System.Collections.Generic; using System.Globalization; 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; 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 *********/ /// 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. /// 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 } /// 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) { // get normalised metadata assetName = this.NormaliseAssetName(assetName); string cacheLocale = this.GetCacheLocale(assetName); // skip if already loaded if (this.IsNormalisedKeyLoaded(assetName)) return base.Load(assetName); // load data T data = base.Load(assetName); // let mods intercept content IContentEventHelper helper = new ContentEventHelper(cacheLocale, assetName, data, this.NormaliseAssetName); ContentEvents.InvokeAfterAssetLoaded(this.Monitor, helper); this.Cache[assetName] = helper.Data; return (T)helper.Data; } /// 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; } /********* ** 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 } /// Get the locale for which the asset name was saved, if any. /// The normalised asset name. private string GetCacheLocale(string normalisedAssetName) { string locale = this.GetKeyLocale.Invoke(); return this.Cache.ContainsKey($"{normalisedAssetName}.{locale}") ? locale : null; } } }