using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Deprecations;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
namespace StardewModdingAPI.Framework.ModHelpers
{
/// Provides an API for loading content assets.
[Obsolete($"Use {nameof(IMod.Helper)}.{nameof(IModHelper.GameContent)} or {nameof(IMod.Helper)}.{nameof(IModHelper.ModContent)} instead. This interface will be removed in SMAPI 4.0.0.")]
internal class ContentHelper : BaseHelper, IContentHelper
{
/*********
** Fields
*********/
/// SMAPI's core content logic.
private readonly ContentCoordinator ContentCore;
/// A content manager for this mod which manages files from the game's Content folder.
private readonly IContentManager GameContentManager;
/// A content manager for this mod which manages files from the mod's folder.
private readonly ModContentManager ModContentManager;
/// Encapsulates monitoring and logging.
private readonly IMonitor Monitor;
/// Simplifies access to private code.
private readonly Reflector Reflection;
/*********
** Accessors
*********/
///
public string CurrentLocale => this.GameContentManager.GetLocale();
///
public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.GameContentManager.Language;
/// The observable implementation of .
internal ObservableCollection ObservableAssetEditors { get; } = new();
/// The observable implementation of .
internal ObservableCollection ObservableAssetLoaders { get; } = new();
///
public IList AssetLoaders
{
get
{
SCore.DeprecationManager.Warn(
source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetLoaders)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
);
return this.ObservableAssetLoaders;
}
}
///
public IList AssetEditors
{
get
{
SCore.DeprecationManager.Warn(
source: this.Mod,
nounPhrase: $"{nameof(IContentHelper)}.{nameof(IContentHelper.AssetEditors)}",
version: "3.14.0",
severity: DeprecationLevel.Notice
);
return this.ObservableAssetEditors;
}
}
/*********
** Public methods
*********/
/// Construct an instance.
/// SMAPI's core content logic.
/// The absolute path to the mod folder.
/// The mod using this instance.
/// Encapsulates monitoring and logging.
/// Simplifies access to private code.
public ContentHelper(ContentCoordinator contentCore, string modFolderPath, IModMetadata mod, IMonitor monitor, Reflector reflection)
: base(mod)
{
string managedAssetPrefix = contentCore.GetManagedAssetPrefix(mod.Manifest.UniqueID);
this.ContentCore = contentCore;
this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content");
this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, this.Mod.DisplayName, modFolderPath, this.GameContentManager);
this.Monitor = monitor;
this.Reflection = reflection;
}
///
public T Load(string key, ContentSource source = ContentSource.ModFolder)
where T : notnull
{
IAssetName assetName = this.ContentCore.ParseAssetName(key, allowLocales: source == ContentSource.GameContent);
try
{
this.AssertAndNormalizeAssetName(key);
switch (source)
{
case ContentSource.GameContent:
if (assetName.Name.EndsWith(".xnb", StringComparison.OrdinalIgnoreCase))
{
assetName = this.ContentCore.ParseAssetName(assetName.Name[..^4], allowLocales: true);
SCore.DeprecationManager.Warn(
this.Mod,
"loading assets from the Content folder with a .xnb file extension",
"3.14.0",
DeprecationLevel.Notice
);
}
return this.GameContentManager.LoadLocalized(assetName, this.CurrentLocaleConstant, useCache: false);
case ContentSource.ModFolder:
try
{
return this.ModContentManager.LoadExact(assetName, useCache: false);
}
catch (SContentLoadException ex) when (ex.ErrorType == ContentLoadErrorType.AssetDoesNotExist)
{
// legacy behavior: you can load a .xnb file without the file extension
try
{
IAssetName newName = this.ContentCore.ParseAssetName(assetName.Name + ".xnb", allowLocales: false);
if (this.ModContentManager.DoesAssetExist(newName))
{
T data = this.ModContentManager.LoadExact(newName, useCache: false);
SCore.DeprecationManager.Warn(
this.Mod,
"loading XNB files from the mod folder without the .xnb file extension",
"3.14.0",
DeprecationLevel.Notice
);
return data;
}
}
catch { /* legacy behavior failed, rethrow original error */ }
throw;
}
default:
throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}: unknown content source '{source}'.");
}
}
catch (Exception ex) when (ex is not SContentLoadException)
{
throw new SContentLoadException(ContentLoadErrorType.Other, $"{this.Mod.DisplayName} failed loading content asset '{key}' from {source}.", ex);
}
}
///
[Pure]
public string NormalizeAssetName(string? assetName)
{
return this.ModContentManager.AssertAndNormalizeAssetName(assetName);
}
///
public string GetActualAssetKey(string key, ContentSource source = ContentSource.ModFolder)
{
switch (source)
{
case ContentSource.GameContent:
return this.GameContentManager.AssertAndNormalizeAssetName(key);
case ContentSource.ModFolder:
return this.ModContentManager.GetInternalAssetKey(key).Name;
default:
throw new NotSupportedException($"Unknown content source '{source}'.");
}
}
///
public bool InvalidateCache(string key)
{
string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent);
this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.");
return this.ContentCore.InvalidateCache(asset => asset.Name.IsEquivalentTo(actualKey)).Any();
}
///
public bool InvalidateCache()
where T : notnull
{
this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.");
return this.ContentCore.InvalidateCache((_, _, type) => typeof(T).IsAssignableFrom(type)).Any();
}
///
public bool InvalidateCache(Func predicate)
{
this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.");
return this.ContentCore.InvalidateCache(predicate).Any();
}
///
public IAssetData GetPatchHelper(T data, string? assetName = null)
where T : notnull
{
if (data == null)
throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value.");
assetName ??= $"temp/{Guid.NewGuid():N}";
return new AssetDataForObject(
locale: this.CurrentLocale,
assetName: this.ContentCore.ParseAssetName(assetName, allowLocales: true/* no way to know if it's a game or mod asset here*/),
data: data,
getNormalizedPath: this.NormalizeAssetName,
reflection: this.Reflection
);
}
/*********
** Private methods
*********/
/// Assert that the given key has a valid format.
/// The asset key to check.
/// The asset key is empty or contains invalid characters.
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
private void AssertAndNormalizeAssetName(string key)
{
this.ModContentManager.AssertAndNormalizeAssetName(key);
if (Path.IsPathRooted(key))
throw new ArgumentException("The asset key must not be an absolute path.");
}
}
}