using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Metadata; using StardewValley; namespace StardewModdingAPI.Framework { /// The central logic for creating content managers, invalidating caches, and propagating asset changes. internal class ContentCoordinator : IDisposable { /********* ** Properties *********/ /// Encapsulates monitoring and logging. private readonly IMonitor Monitor; /// Provides metadata for core game assets. private readonly CoreAssetPropagator CoreAssets; /// Simplifies access to private code. private readonly Reflector Reflection; /// The loaded content managers (including the ). private readonly IList ContentManagers = new List(); /********* ** Accessors *********/ /// The primary content manager used for most assets. public SContentManager MainContentManager { get; private set; } /// The current language as a constant. public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; /// Interceptors which provide the initial versions of matching assets. public IDictionary> Loaders { get; } = new Dictionary>(); /// Interceptors which edit matching assets after they're loaded. public IDictionary> Editors { get; } = new Dictionary>(); /// The absolute path to the . public string FullRootDirectory { get; } /********* ** 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. /// Encapsulates monitoring and logging. /// Simplifies access to private code. public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection) { this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); this.Reflection = reflection; this.FullRootDirectory = Path.Combine(Constants.ExecutionPath, rootDirectory); this.ContentManagers.Add( this.MainContentManager = new SContentManager("Game1.content", serviceProvider, rootDirectory, currentCulture, this, monitor, reflection, isModFolder: false) ); this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.NormaliseAssetName, reflection); } /// Get a new content manager which defers loading to the content core. /// A name for the mod manager. Not guaranteed to be unique. /// Whether this content manager is wrapped around a mod folder. /// The root directory to search for content (or null. for the default) public SContentManager CreateContentManager(string name, bool isModFolder, string rootDirectory = null) { return new SContentManager(name, this.MainContentManager.ServiceProvider, rootDirectory ?? this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, isModFolder); } /// Get the current content locale. public string GetLocale() => this.MainContentManager.GetLocale(LocalizedContentManager.CurrentLanguageCode); /// Convert an absolute file path into a appropriate asset name. /// The absolute path to the file. public string GetAssetNameFromFilePath(string absolutePath) => this.MainContentManager.GetAssetNameFromFilePath(absolutePath, ContentSource.GameContent); /// Purge assets from the cache that match one of the interceptors. /// The asset editors for which to purge matching assets. /// The asset loaders for which to purge matching assets. /// Returns the invalidated asset names. public IEnumerable InvalidateCacheFor(IAssetEditor[] editors, IAssetLoader[] loaders) { if (!editors.Any() && !loaders.Any()) return new string[0]; // get CanEdit/Load methods MethodInfo canEdit = typeof(IAssetEditor).GetMethod(nameof(IAssetEditor.CanEdit)); MethodInfo canLoad = typeof(IAssetLoader).GetMethod(nameof(IAssetLoader.CanLoad)); if (canEdit == null || canLoad == null) throw new InvalidOperationException("SMAPI could not access the interceptor methods."); // should never happen // invalidate matching keys return this.InvalidateCache(asset => { // check loaders MethodInfo canLoadGeneric = canLoad.MakeGenericMethod(asset.DataType); if (loaders.Any(loader => (bool)canLoadGeneric.Invoke(loader, new object[] { asset }))) return true; // check editors MethodInfo canEditGeneric = canEdit.MakeGenericMethod(asset.DataType); return editors.Any(editor => (bool)canEditGeneric.Invoke(editor, new object[] { asset })); }); } /// 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 invalidated asset keys. public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { string locale = this.GetLocale(); return this.InvalidateCache((assetName, type) => { IAssetInfo info = new AssetInfo(locale, assetName, type, this.MainContentManager.NormaliseAssetName); return predicate(info); }); } /// 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 invalidated asset names. public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { // invalidate cache HashSet removedAssetNames = new HashSet(); foreach (SContentManager contentManager in this.ContentManagers) { foreach (string name in contentManager.InvalidateCache(predicate, dispose)) removedAssetNames.Add(name); } // reload core game assets int reloaded = 0; foreach (string key in removedAssetNames) { if (this.CoreAssets.Propagate(this.MainContentManager, key)) // use an intercepted content manager reloaded++; } // report result if (removedAssetNames.Any()) this.Monitor.Log($"Invalidated {removedAssetNames.Count} asset names: {string.Join(", ", removedAssetNames.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}. Reloaded {reloaded} core assets.", LogLevel.Trace); this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); return removedAssetNames; } /// Dispose held resources. public void Dispose() { if (this.MainContentManager == null) return; // already disposed this.Monitor.Log("Disposing the content coordinator. Content managers will no longer be usable after this point.", LogLevel.Trace); foreach (SContentManager contentManager in this.ContentManagers) contentManager.Dispose(); this.ContentManagers.Clear(); this.MainContentManager = null; } } }