using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewValley;
namespace StardewModdingAPI.Framework.ContentManagers
{
/// A content manager which handles reading files from the game content folder with support for interception.
internal class GameContentManager : BaseContentManager
{
/*********
** Properties
*********/
/// The assets currently being intercepted by instances. This is used to prevent infinite loops when a loader loads a new asset.
private readonly ContextHash AssetsBeingLoaded = new ContextHash();
/// Interceptors which provide the initial versions of matching assets.
private IDictionary> Loaders => this.Coordinator.Loaders;
/// Interceptors which edit matching assets after they're loaded.
private IDictionary> Editors => this.Coordinator.Editors;
/// A lookup which indicates whether the asset is localisable (i.e. the filename contains the locale), if previously loaded.
private readonly IDictionary IsLocalisableLookup;
/*********
** Public methods
*********/
/// Construct an instance.
/// A name for the mod manager. Not guaranteed to be unique.
/// The service provider to use to locate services.
/// The root directory to search for content.
/// The current culture for which to localise content.
/// The central coordinator which manages content managers.
/// Encapsulates monitoring and logging.
/// Simplifies access to private code.
/// A callback to invoke when the content manager is being disposed.
public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action onDisposing)
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isModFolder: false)
{
this.IsLocalisableLookup = reflection.GetField>(this, "_localizedAsset").GetValue();
}
/// 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.
/// The language code for which to load content.
public override T Load(string assetName, LanguageCode language)
{
assetName = this.AssertAndNormaliseAssetName(assetName);
// get from cache
if (this.IsLoaded(assetName))
return base.Load(assetName, language);
// get managed asset
if (this.Coordinator.TryParseManagedAssetKey(assetName, out string contentManagerID, out string relativePath))
{
T managedAsset = this.Coordinator.LoadAndCloneManagedAsset(assetName, contentManagerID, relativePath, language);
this.Inject(assetName, managedAsset);
return managedAsset;
}
// load asset
T data;
if (this.AssetsBeingLoaded.Contains(assetName))
{
this.Monitor.Log($"Broke loop while loading asset '{assetName}'.", LogLevel.Warn);
this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}", LogLevel.Trace);
data = base.Load(assetName, language);
}
else
{
data = this.AssetsBeingLoaded.Track(assetName, () =>
{
string locale = this.GetLocale(language);
IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.AssertAndNormaliseAssetName);
IAssetData asset =
this.ApplyLoader(info)
?? new AssetDataForObject(info, base.Load(assetName, language), this.AssertAndNormaliseAssetName);
asset = this.ApplyEditors(info, asset);
return (T)asset.Data;
});
}
// update cache & return data
this.Inject(assetName, data);
return data;
}
/// Create a new content manager for temporary use.
public override LocalizedContentManager CreateTemporary()
{
return this.Coordinator.CreateGameContentManager("(temporary)");
}
/*********
** Private methods
*********/
/// Get whether an asset has already been loaded.
/// The normalised asset name.
protected override bool IsNormalisedKeyLoaded(string normalisedAssetName)
{
// default English
if (this.Language == LocalizedContentManager.LanguageCode.en || this.Coordinator.IsManagedAssetKey(normalisedAssetName))
return this.Cache.ContainsKey(normalisedAssetName);
// translated
string localeKey = $"{normalisedAssetName}.{this.GetLocale(this.GetCurrentLanguage())}";
if (this.IsLocalisableLookup.TryGetValue(localeKey, out bool localisable))
{
return localisable
? this.Cache.ContainsKey(localeKey)
: this.Cache.ContainsKey(normalisedAssetName);
}
// not loaded yet
return false;
}
/// Load the initial asset from the registered .
/// The basic asset metadata.
/// Returns the loaded asset metadata, or null if no loader matched.
private IAssetData ApplyLoader(IAssetInfo info)
{
// find matching loaders
var loaders = this.GetInterceptors(this.Loaders)
.Where(entry =>
{
try
{
return entry.Value.CanLoad(info);
}
catch (Exception ex)
{
entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return false;
}
})
.ToArray();
// validate loaders
if (!loaders.Any())
return null;
if (loaders.Length > 1)
{
string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray();
this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn);
return null;
}
// fetch asset from loader
IModMetadata mod = loaders[0].Key;
IAssetLoader loader = loaders[0].Value;
T data;
try
{
data = this.CloneIfPossible(loader.Load(info));
this.Monitor.Log($"{mod.DisplayName} loaded asset '{info.AssetName}'.", LogLevel.Trace);
}
catch (Exception ex)
{
mod.LogAsMod($"Mod crashed when loading asset '{info.AssetName}'. SMAPI will use the default asset instead. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
return null;
}
// validate asset
if (data == null)
{
mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Error);
return null;
}
// return matched asset
return new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName);
}
/// Apply any to a loaded asset.
/// The asset type.
/// The basic asset metadata.
/// The loaded asset.
private IAssetData ApplyEditors(IAssetInfo info, IAssetData asset)
{
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormaliseAssetName);
// edit asset
foreach (var entry in this.GetInterceptors(this.Editors))
{
// check for match
IModMetadata mod = entry.Key;
IAssetEditor editor = entry.Value;
try
{
if (!editor.CanEdit(info))
continue;
}
catch (Exception ex)
{
mod.LogAsMod($"Mod crashed when checking whether it could edit asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
continue;
}
// try edit
object prevAsset = asset.Data;
try
{
editor.Edit(asset);
this.Monitor.Log($"{mod.DisplayName} intercepted {info.AssetName}.", LogLevel.Trace);
}
catch (Exception ex)
{
mod.LogAsMod($"Mod crashed when editing asset '{info.AssetName}', which may cause errors in-game. Error details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
// validate edit
if (asset.Data == null)
{
mod.LogAsMod($"Mod incorrectly set asset '{info.AssetName}' to a null value; ignoring override.", LogLevel.Warn);
asset = GetNewData(prevAsset);
}
else if (!(asset.Data is T))
{
mod.LogAsMod($"Mod incorrectly set asset '{asset.AssetName}' to incompatible type '{asset.Data.GetType()}', expected '{typeof(T)}'; ignoring override.", LogLevel.Warn);
asset = GetNewData(prevAsset);
}
}
// return result
return asset;
}
/// Get all registered interceptors from a list.
private IEnumerable> GetInterceptors(IDictionary> entries)
{
foreach (var entry in entries)
{
IModMetadata mod = entry.Key;
IList interceptors = entry.Value;
// registered editors
foreach (T interceptor in interceptors)
yield return new KeyValuePair(mod, interceptor);
}
}
}
}