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;
}
}
}