diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/SMAPI/Framework/ContentCore.cs (renamed from src/SMAPI/Framework/SContentManager.cs) | 139 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagerShim.cs | 68 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 43 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 16 | ||||
-rw-r--r-- | src/SMAPI/Metadata/CoreAssets.cs | 9 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 36 | ||||
-rw-r--r-- | src/SMAPI/StardewModdingAPI.csproj | 2 |
7 files changed, 214 insertions, 99 deletions
diff --git a/src/SMAPI/Framework/SContentManager.cs b/src/SMAPI/Framework/ContentCore.cs index 29c6c684..85b8db8f 100644 --- a/src/SMAPI/Framework/SContentManager.cs +++ b/src/SMAPI/Framework/ContentCore.cs @@ -19,9 +19,9 @@ using StardewValley; namespace StardewModdingAPI.Framework { - /// <summary>A thread-safe content manager which intercepts assets being loaded to let SMAPI mods inject or edit them.</summary> + /// <summary>A thread-safe content handler which loads assets with support for mod injection and editing.</summary> /// <remarks> - /// This is the centralised content manager which manages all game assets. The game and mods don't use this class + /// This is the centralised content logic which manages all game assets. The game and mods don't use this class /// directly; instead they use one of several <see cref="ContentManagerShim"/> instances, which proxy requests to /// this class. That ensures that when the game disposes one content manager, the others can continue unaffected. /// That notably requires this class to be thread-safe, since the content managers can be disposed asynchronously. @@ -30,11 +30,14 @@ namespace StardewModdingAPI.Framework /// For English and non-translatable assets, these have the same value. The underlying cache only knows about asset /// keys, and the game and mods only know about asset names. The content manager handles resolving them. /// </remarks> - internal class SContentManager : LocalizedContentManager + internal class ContentCore : IDisposable { /********* ** Properties *********/ + /// <summary>The underlying content manager.</summary> + private readonly LocalizedContentManager Content; + /// <summary>Encapsulates monitoring and logging.</summary> private readonly IMonitor Monitor; @@ -42,10 +45,10 @@ namespace StardewModdingAPI.Framework private readonly ContentCache Cache; /// <summary>The locale codes used in asset keys indexed by enum value.</summary> - private readonly IDictionary<LanguageCode, string> Locales; + private readonly IDictionary<LocalizedContentManager.LanguageCode, string> Locales; /// <summary>The language enum values indexed by locale code.</summary> - private readonly IDictionary<string, LanguageCode> LanguageCodes; + private readonly IDictionary<string, LocalizedContentManager.LanguageCode> LanguageCodes; /// <summary>Provides metadata for core game assets.</summary> private readonly CoreAssets CoreAssets; @@ -66,15 +69,17 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ + /// <summary>The current language as a constant.</summary> + public LocalizedContentManager.LanguageCode Language => this.Content.GetCurrentLanguage(); + /// <summary>Interceptors which provide the initial versions of matching assets.</summary> - internal IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>(); + public IDictionary<IModMetadata, IList<IAssetLoader>> Loaders { get; } = new Dictionary<IModMetadata, IList<IAssetLoader>>(); /// <summary>Interceptors which edit matching assets after they're loaded.</summary> - internal IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>(); + public IDictionary<IModMetadata, IList<IAssetEditor>> Editors { get; } = new Dictionary<IModMetadata, IList<IAssetEditor>>(); /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary> - internal string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory); - + public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.Content.RootDirectory); /********* ** Public methods @@ -89,12 +94,12 @@ namespace StardewModdingAPI.Framework /// <param name="languageCodeOverride">The current language code for which to localise content.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="reflection">Simplifies access to private code.</param> - public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) - : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) + public ContentCore(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor, Reflector reflection) { // init this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor)); - this.Cache = new ContentCache(this, reflection); + this.Content = new LocalizedContentManager(serviceProvider, rootDirectory, currentCulture, languageCodeOverride); + this.Cache = new ContentCache(this.Content, reflection); this.ModContentPrefix = this.GetAssetNameFromFilePath(Constants.ModPath); // get asset data @@ -103,6 +108,14 @@ namespace StardewModdingAPI.Framework this.LanguageCodes = this.Locales.ToDictionary(p => p.Value, p => p.Key, StringComparer.InvariantCultureIgnoreCase); } + /// <summary>Get a new content manager which defers loading to the content core.</summary> + /// <param name="name">The content manager's name for logs (if any).</param> + /// <param name="rootDirectory">The root directory to search for content (or <c>null</c>. for the default)</param> + public ContentManagerShim CreateContentManager(string name, string rootDirectory = null) + { + return new ContentManagerShim(this, name, this.Content.ServiceProvider, rootDirectory ?? this.Content.RootDirectory, this.Content.CurrentCulture, this.Content.LanguageCodeOverride); + } + /**** ** Asset key/name handling ****/ @@ -153,7 +166,14 @@ namespace StardewModdingAPI.Framework /// <summary>Get the current content locale.</summary> public string GetLocale() { - return this.Locales[this.GetCurrentLanguage()]; + return this.GetLocale(this.Content.GetCurrentLanguage()); + } + + /// <summary>The locale for a language.</summary> + /// <param name="language">The language.</param> + public string GetLocale(LocalizedContentManager.LanguageCode language) + { + return this.Locales[language]; } /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary> @@ -177,18 +197,15 @@ namespace StardewModdingAPI.Framework /// <summary>Load an asset through the content pipeline. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> /// <typeparam name="T">The expected asset type.</typeparam> /// <param name="assetName">The asset path relative to the content directory.</param> - public override T Load<T>(string assetName) - { - return this.LoadFor<T>(assetName, this); - } - - /// <summary>Load an asset through the content pipeline. When loading a <c>.png</c> file, this must be called outside the game's draw loop.</summary> - /// <typeparam name="T">The expected asset type.</typeparam> - /// <param name="assetName">The asset path relative to the content directory.</param> /// <param name="instance">The content manager instance for which to load the asset.</param> + /// <param name="language">The language code for which to load content.</param> /// <exception cref="ArgumentException">The <paramref name="assetName"/> is empty or contains invalid characters.</exception> /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> - public T LoadFor<T>(string assetName, ContentManager instance) + public T Load<T>(string assetName, ContentManager instance +#if STARDEW_VALLEY_1_3 + , LocalizedContentManager.LanguageCode language +#endif + ) { // normalise asset key this.AssertValidAssetKeyFormat(assetName); @@ -196,7 +213,11 @@ namespace StardewModdingAPI.Framework // load game content if (!assetName.StartsWith(this.ModContentPrefix)) +#if STARDEW_VALLEY_1_3 + return this.LoadImpl<T>(assetName, instance, language); +#else return this.LoadImpl<T>(assetName, instance); +#endif // load mod content SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"Failed loading content asset '{assetName}': {reasonPhrase}"); @@ -206,7 +227,11 @@ namespace StardewModdingAPI.Framework { // try cache if (this.IsLoaded(assetName)) +#if STARDEW_VALLEY_1_3 + return this.LoadImpl<T>(assetName, instance, language); +#else return this.LoadImpl<T>(assetName, instance); +#endif // get file FileInfo file = this.GetModFile(assetName); @@ -218,7 +243,11 @@ namespace StardewModdingAPI.Framework { // XNB file case ".xnb": +#if STARDEW_VALLEY_1_3 + return this.LoadImpl<T>(assetName, instance, language); +#else return this.LoadImpl<T>(assetName, instance); +#endif // unpacked map case ".tbin": @@ -339,7 +368,7 @@ namespace StardewModdingAPI.Framework int reloaded = 0; foreach (string key in removeAssetNames) { - if (this.CoreAssets.ReloadForKey(this, key)) + if (this.CoreAssets.ReloadForKey(Game1.content, key)) // use an intercepted content manager reloaded++; } @@ -379,11 +408,10 @@ namespace StardewModdingAPI.Framework ** Disposal ****/ /// <summary>Dispose held resources.</summary> - /// <param name="disposing">Whether the content manager is disposing (rather than finalising).</param> - protected override void Dispose(bool disposing) + public void Dispose() { this.Monitor.Log("Disposing SMAPI's main content manager. It will no longer be usable after this point.", LogLevel.Trace); - base.Dispose(disposing); + this.Content.Dispose(); } /**** @@ -398,28 +426,28 @@ namespace StardewModdingAPI.Framework /// <summary>Get the locale codes (like <c>ja-JP</c>) used in asset keys.</summary> /// <param name="reflection">Simplifies access to private game code.</param> - private IDictionary<LanguageCode, string> GetKeyLocales(Reflector reflection) + private IDictionary<LocalizedContentManager.LanguageCode, string> GetKeyLocales(Reflector reflection) { #if !STARDEW_VALLEY_1_3 - IReflectedField<LanguageCode> codeField = reflection.GetField<LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); - LanguageCode previousCode = codeField.GetValue(); + IReflectedField<LocalizedContentManager.LanguageCode> codeField = reflection.GetField<LocalizedContentManager.LanguageCode>(typeof(LocalizedContentManager), "_currentLangCode"); + LocalizedContentManager.LanguageCode previousCode = codeField.GetValue(); #endif - string previousOverride = this.LanguageCodeOverride; + string previousOverride = this.Content.LanguageCodeOverride; try { // temporarily disable language override - this.LanguageCodeOverride = null; + this.Content.LanguageCodeOverride = null; // create locale => code map IReflectedMethod languageCodeString = reflection #if STARDEW_VALLEY_1_3 - .GetMethod(this, "languageCodeString"); + .GetMethod(this.Content, "languageCodeString"); #else - .GetMethod(this, "languageCode"); + .GetMethod(this.Content, "languageCode"); #endif - IDictionary<LanguageCode, string> map = new Dictionary<LanguageCode, string>(); - foreach (LanguageCode code in Enum.GetValues(typeof(LanguageCode))) + IDictionary<LocalizedContentManager.LanguageCode, string> map = new Dictionary<LocalizedContentManager.LanguageCode, string>(); + foreach (LocalizedContentManager.LanguageCode code in Enum.GetValues(typeof(LocalizedContentManager.LanguageCode))) { #if STARDEW_VALLEY_1_3 map[code] = languageCodeString.Invoke<string>(code); @@ -434,7 +462,7 @@ namespace StardewModdingAPI.Framework finally { // restore previous settings - this.LanguageCodeOverride = previousOverride; + this.Content.LanguageCodeOverride = previousOverride; #if !STARDEW_VALLEY_1_3 codeField.SetValue(previousCode); #endif @@ -485,7 +513,7 @@ namespace StardewModdingAPI.Framework private bool IsNormalisedKeyLoaded(string normalisedAssetName) { return this.Cache.ContainsKey(normalisedAssetName) - || this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.GetCurrentLanguage()]}"); // translated asset + || this.Cache.ContainsKey($"{normalisedAssetName}.{this.Locales[this.Content.GetCurrentLanguage()]}"); // translated asset } /// <summary>Track that a content manager loaded an asset.</summary> @@ -505,7 +533,12 @@ namespace StardewModdingAPI.Framework /// <typeparam name="T">The type of asset to load.</typeparam> /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> /// <param name="instance">The content manager instance for which to load the asset.</param> - private T LoadImpl<T>(string assetName, ContentManager instance) + /// <param name="language">The language code for which to load content.</param> + private T LoadImpl<T>(string assetName, ContentManager instance +#if STARDEW_VALLEY_1_3 + , LocalizedContentManager.LanguageCode language +#endif + ) { return this.WithWriteLock(() => { @@ -513,7 +546,13 @@ namespace StardewModdingAPI.Framework if (this.IsNormalisedKeyLoaded(assetName)) { this.TrackAssetLoader(assetName, instance); - return base.Load<T>(assetName); + return this.Content + +#if STARDEW_VALLEY_1_3 + .Load<T>(assetName, language); +#else + .Load<T>(assetName); +#endif } // load asset @@ -522,14 +561,30 @@ namespace StardewModdingAPI.Framework { 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<T>(assetName); + data = this.Content +#if STARDEW_VALLEY_1_3 + .Load<T>(assetName, language); +#else + .Load<T>(assetName); +#endif } else { data = this.AssetsBeingLoaded.Track(assetName, () => { - IAssetInfo info = new AssetInfo(this.GetLocale(), assetName, typeof(T), this.NormaliseAssetName); - IAssetData asset = this.ApplyLoader<T>(info) ?? new AssetDataForObject(info, base.Load<T>(assetName), this.NormaliseAssetName); + string locale = +#if STARDEW_VALLEY_1_3 + this.GetLocale(language); +#else + this.GetLocale(); +#endif + IAssetInfo info = new AssetInfo(locale, assetName, typeof(T), this.NormaliseAssetName); + IAssetData asset = this.ApplyLoader<T>(info) +#if STARDEW_VALLEY_1_3 + ?? new AssetDataForObject(info, this.Content.Load<T>(assetName, language), this.NormaliseAssetName); +#else + ?? new AssetDataForObject(info, this.Content.Load<T>(assetName), this.NormaliseAssetName); +#endif asset = this.ApplyEditors<T>(info, asset); return (T)asset.Data; }); diff --git a/src/SMAPI/Framework/ContentManagerShim.cs b/src/SMAPI/Framework/ContentManagerShim.cs index d46f23a3..8f88fc2d 100644 --- a/src/SMAPI/Framework/ContentManagerShim.cs +++ b/src/SMAPI/Framework/ContentManagerShim.cs @@ -1,15 +1,17 @@ +using System; +using System.Globalization; using StardewValley; namespace StardewModdingAPI.Framework { - /// <summary>A minimal content manager which defers to SMAPI's main content manager.</summary> + /// <summary>A minimal content manager which defers to SMAPI's core content logic.</summary> internal class ContentManagerShim : LocalizedContentManager { /********* ** Properties *********/ - /// <summary>SMAPI's underlying content manager.</summary> - private readonly SContentManager ContentManager; + /// <summary>SMAPI's core content logic.</summary> + private readonly ContentCore ContentCore; /********* @@ -23,12 +25,16 @@ namespace StardewModdingAPI.Framework ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="contentManager">SMAPI's underlying content manager.</param> + /// <param name="contentCore">SMAPI's core content logic.</param> /// <param name="name">The content manager's name for logs (if any).</param> - public ContentManagerShim(SContentManager contentManager, string name) - : base(contentManager.ServiceProvider, contentManager.RootDirectory, contentManager.CurrentCulture, contentManager.LanguageCodeOverride) + /// <param name="serviceProvider">The service provider to use to locate services.</param> + /// <param name="rootDirectory">The root directory to search for content.</param> + /// <param name="currentCulture">The current culture for which to localise content.</param> + /// <param name="languageCodeOverride">The current language code for which to localise content.</param> + public ContentManagerShim(ContentCore contentCore, string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride) + : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { - this.ContentManager = contentManager; + this.ContentCore = contentCore; this.Name = name; } @@ -37,14 +43,58 @@ namespace StardewModdingAPI.Framework /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> public override T Load<T>(string assetName) { - return this.ContentManager.LoadFor<T>(assetName, this); +#if STARDEW_VALLEY_1_3 + return this.Load<T>(assetName, LocalizedContentManager.CurrentLanguageCode); +#else + return this.ContentCore.Load<T>(assetName, this); +#endif } +#if STARDEW_VALLEY_1_3 + /// <summary>Load an asset that has been processed by the content pipeline.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="language">The language code for which to load content.</param> + public override T Load<T>(string assetName, LanguageCode language) + { + return this.ContentCore.Load<T>(assetName, this, language); + } + + /// <summary>Load the base asset without localisation.</summary> + /// <typeparam name="T">The type of asset to load.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + public override T LoadBase<T>(string assetName) + { + return this.Load<T>(assetName, LanguageCode.en); + } +#endif + + /// <summary>Inject an asset into the cache.</summary> + /// <typeparam name="T">The type of asset to inject.</typeparam> + /// <param name="assetName">The asset path relative to the loader root directory, not including the <c>.xnb</c> extension.</param> + /// <param name="value">The asset value.</param> + public void Inject<T>(string assetName, T value) + { + this.ContentCore.Inject<T>(assetName, value, this); + } + +#if STARDEW_VALLEY_1_3 + /// <summary>Create a new content manager for temporary use.</summary> + public override LocalizedContentManager CreateTemporary() + { + return this.ContentCore.CreateContentManager("(temporary)"); + } +#endif + + + /********* + ** Protected methods + *********/ /// <summary>Dispose held resources.</summary> /// <param name="disposing">Whether the content manager is disposing (rather than finalising).</param> protected override void Dispose(bool disposing) { - this.ContentManager.DisposeFor(this); + this.ContentCore.DisposeFor(this); } } } diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index 7d8bec1e..c7d4c39e 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -22,8 +22,11 @@ namespace StardewModdingAPI.Framework.ModHelpers /********* ** Properties *********/ - /// <summary>SMAPI's underlying content manager.</summary> - private readonly SContentManager ContentManager; + /// <summary>SMAPI's core content logic.</summary> + private readonly ContentCore ContentCore; + + /// <summary>The content manager for this mod.</summary> + private readonly ContentManagerShim ContentManager; /// <summary>The absolute path to the mod folder.</summary> private readonly string ModFolderPath; @@ -39,10 +42,10 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Accessors *********/ /// <summary>The game's current locale code (like <c>pt-BR</c>).</summary> - public string CurrentLocale => this.ContentManager.GetLocale(); + public string CurrentLocale => this.ContentCore.GetLocale(); /// <summary>The game's current locale as an enum value.</summary> - public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentManager.GetCurrentLanguage(); + public LocalizedContentManager.LanguageCode CurrentLocaleConstant => this.ContentCore.Language; /// <summary>The observable implementation of <see cref="AssetEditors"/>.</summary> internal ObservableCollection<IAssetEditor> ObservableAssetEditors { get; } = new ObservableCollection<IAssetEditor>(); @@ -61,14 +64,16 @@ namespace StardewModdingAPI.Framework.ModHelpers ** Public methods *********/ /// <summary>Construct an instance.</summary> - /// <param name="contentManager">SMAPI's underlying content manager.</param> + /// <param name="contentCore">SMAPI's core content logic.</param> + /// <param name="contentManager">The content manager for this mod.</param> /// <param name="modFolderPath">The absolute path to the mod folder.</param> /// <param name="modID">The unique ID of the relevant mod.</param> /// <param name="modName">The friendly mod name for use in errors.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> - public ContentHelper(SContentManager contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) + public ContentHelper(ContentCore contentCore, ContentManagerShim contentManager, string modFolderPath, string modID, string modName, IMonitor monitor) : base(modID) { + this.ContentCore = contentCore; this.ContentManager = contentManager; this.ModFolderPath = modFolderPath; this.ModName = modName; @@ -100,10 +105,10 @@ namespace StardewModdingAPI.Framework.ModHelpers throw GetContentError($"there's no matching file at path '{file.FullName}'."); // get asset path - string assetName = this.ContentManager.GetAssetNameFromFilePath(file.FullName); + string assetName = this.ContentCore.GetAssetNameFromFilePath(file.FullName); // try cache - if (this.ContentManager.IsLoaded(assetName)) + if (this.ContentCore.IsLoaded(assetName)) return this.ContentManager.Load<T>(assetName); // fix map tilesheets @@ -119,7 +124,7 @@ namespace StardewModdingAPI.Framework.ModHelpers this.FixCustomTilesheetPaths(map, key); // inject map - this.ContentManager.Inject(assetName, map, this.ContentManager); + this.ContentManager.Inject(assetName, map); return (T)(object)map; } @@ -141,7 +146,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [Pure] public string NormaliseAssetName(string assetName) { - return this.ContentManager.NormaliseAssetName(assetName); + return this.ContentCore.NormaliseAssetName(assetName); } /// <summary>Get the underlying key in the game's content cache for an asset. This can be used to load custom map tilesheets, but should be avoided when you can use the content API instead. This does not validate whether the asset exists.</summary> @@ -153,11 +158,11 @@ namespace StardewModdingAPI.Framework.ModHelpers switch (source) { case ContentSource.GameContent: - return this.ContentManager.NormaliseAssetName(key); + return this.ContentCore.NormaliseAssetName(key); case ContentSource.ModFolder: FileInfo file = this.GetModFile(key); - return this.ContentManager.NormaliseAssetName(this.ContentManager.GetAssetNameFromFilePath(file.FullName)); + return this.ContentCore.NormaliseAssetName(this.ContentCore.GetAssetNameFromFilePath(file.FullName)); default: throw new NotSupportedException($"Unknown content source '{source}'."); @@ -172,7 +177,7 @@ namespace StardewModdingAPI.Framework.ModHelpers { string actualKey = this.GetActualAssetKey(key, ContentSource.GameContent); this.Monitor.Log($"Requested cache invalidation for '{actualKey}'.", LogLevel.Trace); - return this.ContentManager.InvalidateCache(asset => asset.AssetNameEquals(actualKey)); + return this.ContentCore.InvalidateCache(asset => asset.AssetNameEquals(actualKey)); } /// <summary>Remove all assets of the given type from the cache so they're reloaded on the next request. <b>This can be a very expensive operation and should only be used in very specific cases.</b> This will reload core game assets if needed, but references to the former assets will still show the previous content.</summary> @@ -181,7 +186,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache<T>() { this.Monitor.Log($"Requested cache invalidation for all assets of type {typeof(T)}. This is an expensive operation and should be avoided if possible.", LogLevel.Trace); - return this.ContentManager.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); + return this.ContentCore.InvalidateCache((key, type) => typeof(T).IsAssignableFrom(type)); } /// <summary>Remove matching assets from the content cache so they're reloaded on the next request. This will reload core game assets if needed, but references to the former asset will still show the previous content.</summary> @@ -190,7 +195,7 @@ namespace StardewModdingAPI.Framework.ModHelpers public bool InvalidateCache(Func<IAssetInfo, bool> predicate) { this.Monitor.Log("Requested cache invalidation for all assets matching a predicate.", LogLevel.Trace); - return this.ContentManager.InvalidateCache(predicate); + return this.ContentCore.InvalidateCache(predicate); } /********* @@ -202,7 +207,7 @@ namespace StardewModdingAPI.Framework.ModHelpers [SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")] private void AssertValidAssetKeyFormat(string key) { - this.ContentManager.AssertValidAssetKeyFormat(key); + this.ContentCore.AssertValidAssetKeyFormat(key); if (Path.IsPathRooted(key)) throw new ArgumentException("The asset key must not be an absolute path."); } @@ -230,7 +235,7 @@ namespace StardewModdingAPI.Framework.ModHelpers // get map info if (!map.TileSheets.Any()) return; - mapKey = this.ContentManager.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators + mapKey = this.ContentCore.NormaliseAssetName(mapKey); // Mono's Path.GetDirectoryName doesn't handle Windows dir separators string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder // fix tilesheets @@ -336,7 +341,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetModFile(string path) { // try exact match - path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); + path = Path.Combine(this.ModFolderPath, this.ContentCore.NormalisePathSeparators(path)); FileInfo file = new FileInfo(path); // try with default extension @@ -355,7 +360,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private FileInfo GetContentFolderFile(string key) { // get file path - string path = Path.Combine(this.ContentManager.FullRootDirectory, key); + string path = Path.Combine(this.ContentCore.FullRootDirectory, key); if (!path.EndsWith(".xnb")) path += ".xnb"; diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index acb3e794..aeaab9ea 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -161,7 +161,7 @@ namespace StardewModdingAPI.Framework ** Accessors *********/ /// <summary>SMAPI's content manager.</summary> - public SContentManager SContentManager { get; } + public ContentCore ContentCore { get; } /// <summary>Whether SMAPI should log more information about the game context.</summary> public bool VerboseLogging { get; set; } @@ -190,10 +190,12 @@ namespace StardewModdingAPI.Framework // override content manager this.Monitor?.Log("Overriding content manager...", LogLevel.Trace); - this.SContentManager = new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); - this.Content = new ContentManagerShim(this.SContentManager, "SGame.Content"); - Game1.content = new ContentManagerShim(this.SContentManager, "Game1.content"); - reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(new ContentManagerShim(this.SContentManager, "Game1._temporaryContent")); // regenerate value with new content manager + this.ContentCore = new ContentCore(this.Content.ServiceProvider, this.Content.RootDirectory, Thread.CurrentThread.CurrentUICulture, null, this.Monitor, reflection); + this.Content = this.ContentCore.CreateContentManager("SGame.Content"); + Game1.content = this.ContentCore.CreateContentManager("Game1.content"); + + // replace already-created temporary content managers + reflection.GetField<LocalizedContentManager>(typeof(Game1), "_temporaryContent").SetValue(this.ContentCore.CreateContentManager("Game1._temporaryContent")); // regenerate value with new content manager } /**** @@ -205,7 +207,7 @@ namespace StardewModdingAPI.Framework protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) { // return default if SMAPI's content manager isn't initialised yet - if (this.SContentManager == null) + if (this.ContentCore == null) { this.Monitor?.Log("SMAPI's content manager isn't initialised; skipping content manager interception.", LogLevel.Trace); return base.CreateContentManager(serviceProvider, rootDirectory); @@ -216,7 +218,7 @@ namespace StardewModdingAPI.Framework throw new InvalidOperationException("SMAPI uses a single content manager internally. You can't get a new content manager with a different service provider."); if (rootDirectory != this.Content.RootDirectory) throw new InvalidOperationException($"SMAPI uses a single content manager internally. You can't get a new content manager with a different root directory (current is {this.Content.RootDirectory}, requested {rootDirectory})."); - return new ContentManagerShim(this.SContentManager, "(generated instance)"); + return this.ContentCore.CreateContentManager("(generated instance)"); } /// <summary>The method called when the game is updating its state. This happens roughly 60 times per second.</summary> diff --git a/src/SMAPI/Metadata/CoreAssets.cs b/src/SMAPI/Metadata/CoreAssets.cs index c027df25..378117a8 100644 --- a/src/SMAPI/Metadata/CoreAssets.cs +++ b/src/SMAPI/Metadata/CoreAssets.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework.Graphics; -using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Reflection; using StardewValley; using StardewValley.BellsAndWhistles; @@ -24,7 +23,7 @@ namespace StardewModdingAPI.Metadata protected readonly Func<string, string> GetNormalisedPath; /// <summary>Setters which update static or singleton texture fields indexed by normalised asset key.</summary> - private readonly IDictionary<string, Action<SContentManager, string>> SingletonSetters; + private readonly IDictionary<string, Action<LocalizedContentManager, string>> SingletonSetters; /********* @@ -37,7 +36,7 @@ namespace StardewModdingAPI.Metadata { this.GetNormalisedPath = getNormalisedPath; this.SingletonSetters = - new Dictionary<string, Action<SContentManager, string>> + new Dictionary<string, Action<LocalizedContentManager, string>> { // from CraftingRecipe.InitShared ["Data\\CraftingRecipes"] = (content, key) => CraftingRecipe.craftingRecipes = content.Load<Dictionary<string, string>>(key), @@ -151,10 +150,10 @@ namespace StardewModdingAPI.Metadata /// <param name="content">The content manager through which to reload the asset.</param> /// <param name="key">The asset key to reload.</param> /// <returns>Returns whether an asset was reloaded.</returns> - public bool ReloadForKey(SContentManager content, string key) + public bool ReloadForKey(LocalizedContentManager content, string key) { // static assets - if (this.SingletonSetters.TryGetValue(key, out Action<SContentManager, string> reload)) + if (this.SingletonSetters.TryGetValue(key, out Action<LocalizedContentManager, string> reload)) { reload(content, key); return true; diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 9c00e581..39a5754b 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -58,7 +58,7 @@ namespace StardewModdingAPI private SGame GameInstance; /// <summary>The underlying content manager.</summary> - private SContentManager ContentManager => this.GameInstance.SContentManager; + private ContentCore ContentCore => this.GameInstance.ContentCore; /// <summary>The SMAPI configuration settings.</summary> /// <remarks>This is initialised after the game starts.</remarks> @@ -286,10 +286,11 @@ namespace StardewModdingAPI // dispose core components this.IsGameRunning = false; - this.LogFile?.Dispose(); this.ConsoleManager?.Dispose(); + this.ContentCore?.Dispose(); this.CancellationTokenSource?.Dispose(); this.GameInstance?.Dispose(); + this.LogFile?.Dispose(); } @@ -389,7 +390,7 @@ namespace StardewModdingAPI mods = resolver.ProcessDependencies(mods, modDatabase).ToArray(); // load mods - this.LoadMods(mods, this.JsonHelper, this.ContentManager, modDatabase); + this.LoadMods(mods, this.JsonHelper, this.ContentCore, modDatabase); // check for updates this.CheckForUpdatesAsync(mods); @@ -413,8 +414,8 @@ namespace StardewModdingAPI private void OnLocaleChanged() { // get locale - string locale = this.ContentManager.GetLocale(); - LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); + string locale = this.ContentCore.GetLocale(); + LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; // update mod translation helpers foreach (IModMetadata mod in this.ModRegistry.GetAll(contentPacks: false)) @@ -671,9 +672,9 @@ namespace StardewModdingAPI /// <summary>Load and hook up the given mods.</summary> /// <param name="mods">The mods to load.</param> /// <param name="jsonHelper">The JSON helper with which to read mods' JSON files.</param> - /// <param name="contentManager">The content manager to use for mod content.</param> + /// <param name="contentCore">The content manager to use for mod content.</param> /// <param name="modDatabase">Handles access to SMAPI's internal mod metadata list.</param> - private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, SContentManager contentManager, ModDatabase modDatabase) + private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCore contentCore, ModDatabase modDatabase) { this.Monitor.Log("Loading mods...", LogLevel.Trace); @@ -697,7 +698,8 @@ namespace StardewModdingAPI // load mod as content pack IMonitor monitor = this.GetSecondaryMonitor(metadata.DisplayName); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + IContentHelper contentHelper = new ContentHelper(this.ContentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IContentPack contentPack = new ContentPack(metadata.DirectoryPath, manifest, contentHelper, jsonHelper); metadata.SetMod(contentPack, monitor); this.ModRegistry.Add(metadata); @@ -777,15 +779,17 @@ namespace StardewModdingAPI IModHelper modHelper; { ICommandHelper commandHelper = new CommandHelper(manifest.UniqueID, metadata.DisplayName, this.CommandManager); - IContentHelper contentHelper = new ContentHelper(contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); + ContentManagerShim contentManager = this.ContentCore.CreateContentManager($"Mods.{metadata.Manifest.UniqueID}", metadata.DirectoryPath); + IContentHelper contentHelper = new ContentHelper(contentCore, contentManager, metadata.DirectoryPath, manifest.UniqueID, metadata.DisplayName, monitor); IReflectionHelper reflectionHelper = new ReflectionHelper(manifest.UniqueID, metadata.DisplayName, this.Reflection, this.DeprecationManager); IModRegistry modRegistryHelper = new ModRegistryHelper(manifest.UniqueID, this.ModRegistry, proxyFactory, monitor); - ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); + ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language); IContentPack CreateTransitionalContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name); - IContentHelper packContentHelper = new ContentHelper(contentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); + ContentManagerShim packContentManager = this.ContentCore.CreateContentManager($"Mods.{packManifest.UniqueID}", packDirPath); + IContentHelper packContentHelper = new ContentHelper(contentCore, packContentManager, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); return new ContentPack(packDirPath, packManifest, packContentHelper, this.JsonHelper); } @@ -881,8 +885,8 @@ namespace StardewModdingAPI helper.ObservableAssetLoaders.Add(loader); // ReSharper restore SuspiciousTypeConversion.Global - this.ContentManager.Editors[metadata] = helper.ObservableAssetEditors; - this.ContentManager.Loaders[metadata] = helper.ObservableAssetLoaders; + this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; + this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; } // call entry method @@ -927,7 +931,7 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]); + this.ContentCore.InvalidateCacheFor(e.NewItems.Cast<IAssetEditor>().ToArray(), new IAssetLoader[0]); } }; helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => @@ -935,7 +939,7 @@ namespace StardewModdingAPI if (e.NewItems.Count > 0) { this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray()); + this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast<IAssetLoader>().ToArray()); } }; } @@ -947,7 +951,7 @@ namespace StardewModdingAPI if (editors.Any() || loaders.Any()) { this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace); - this.ContentManager.InvalidateCacheFor(editors, loaders); + this.ContentCore.InvalidateCacheFor(editors, loaders); } // unlock mod integrations diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 1dc7740e..bffb96e2 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -196,7 +196,7 @@ <Compile Include="Framework\ModLoading\ModMetadata.cs" /> <Compile Include="Framework\Reflection\ReflectedProperty.cs" /> <Compile Include="Framework\RequestExitDelegate.cs" /> - <Compile Include="Framework\SContentManager.cs" /> + <Compile Include="Framework\ContentCore.cs" /> <Compile Include="Framework\Exceptions\SParseException.cs" /> <Compile Include="Framework\Serialisation\JsonHelper.cs" /> <Compile Include="Framework\Serialisation\SmapiConverters\StringEnumConverter.cs" /> |