summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Installer/assets/windows-install.bat2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs20
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.ErrorHandler/ModEntry.cs29
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs67
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs (renamed from src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs)14
-rw-r--r--src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs63
-rw-r--r--src/SMAPI.Mods.ErrorHandler/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs6
-rw-r--r--src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs15
-rw-r--r--src/SMAPI.Web/Controllers/ModsController.cs2
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/content-patcher.json4
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs46
-rw-r--r--src/SMAPI/Framework/ContentManagers/BaseContentManager.cs85
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs57
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs47
-rw-r--r--src/SMAPI/Framework/ContentManagers/IContentManager.cs10
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs26
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs24
-rw-r--r--src/SMAPI/Framework/Models/SConfig.cs6
-rw-r--r--src/SMAPI/Framework/SCore.cs5
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs116
-rw-r--r--src/SMAPI/Program.cs31
-rw-r--r--src/SMAPI/SMAPI.config.json6
26 files changed, 515 insertions, 180 deletions
diff --git a/src/SMAPI.Installer/assets/windows-install.bat b/src/SMAPI.Installer/assets/windows-install.bat
index d02dd4c6..2cc54e80 100644
--- a/src/SMAPI.Installer/assets/windows-install.bat
+++ b/src/SMAPI.Installer/assets/windows-install.bat
@@ -4,5 +4,5 @@ if not errorlevel 1 (
echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first.
pause
) else (
- start /WAIT /B internal/windows-install.exe
+ start /WAIT /B ./internal/windows-install.exe
)
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs
index 6782e38a..2d4b4565 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs
@@ -1,5 +1,5 @@
-using System;
using System.Linq;
+using Microsoft.Xna.Framework;
using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
@@ -45,12 +45,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
/// <param name="time">The time of day.</param>
private void SafelySetTime(int time)
{
- // define conversion between game time and TimeSpan
- TimeSpan ToTimeSpan(int value) => new TimeSpan(0, value / 100, value % 100, 0);
- int FromTimeSpan(TimeSpan span) => (span.Hours * 100) + span.Minutes;
-
// transition to new time
- int intervals = (int)((ToTimeSpan(time) - ToTimeSpan(Game1.timeOfDay)).TotalMinutes / 10);
+ int intervals = Utility.CalculateMinutesBetweenTimes(Game1.timeOfDay, time) / 10;
if (intervals > 0)
{
for (int i = 0; i < intervals; i++)
@@ -60,10 +56,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
{
for (int i = 0; i > intervals; i--)
{
- Game1.timeOfDay = FromTimeSpan(ToTimeSpan(Game1.timeOfDay).Subtract(TimeSpan.FromMinutes(20))); // offset 20 minutes so game updates to next interval
+ Game1.timeOfDay = Utility.ModifyTime(Game1.timeOfDay, -20); // offset 20 mins so game updates to next interval
Game1.performTenMinuteClockUpdate();
}
}
+
+ // reset ambient light
+ // White is the default non-raining color. If it's raining or dark out, UpdateGameClock
+ // below will update it automatically.
+ Game1.outdoorLight = Color.White;
+ Game1.ambientLight = Color.White;
+
+ // run clock update (to correct lighting, etc)
+ Game1.gameTimeInterval = 0;
+ Game1.UpdateGameClock(Game1.currentGameTime);
}
}
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 10611e08..aa3d6ceb 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.9.1",
+ "Version": "3.9.2",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.9.1"
+ "MinimumApiVersion": "3.9.2"
}
diff --git a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs
index 2f6f1939..d9426d75 100644
--- a/src/SMAPI.Mods.ErrorHandler/ModEntry.cs
+++ b/src/SMAPI.Mods.ErrorHandler/ModEntry.cs
@@ -26,21 +26,17 @@ namespace StardewModdingAPI.Mods.ErrorHandler
public override void Entry(IModHelper helper)
{
// get SMAPI core types
- SCore core = SCore.Instance;
- LogManager logManager = core.GetType().GetField("LogManager", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(core) as LogManager;
- if (logManager == null)
- {
- this.Monitor.Log($"Can't access SMAPI's internal log manager. Error-handling patches won't be applied.", LogLevel.Error);
- return;
- }
+ IMonitor monitorForGame = this.GetMonitorForGame();
// apply patches
new GamePatcher(this.Monitor).Apply(
- new EventErrorPatch(logManager.MonitorForGame),
- new DialogueErrorPatch(logManager.MonitorForGame, this.Helper.Reflection),
+ new DialogueErrorPatch(monitorForGame, this.Helper.Reflection),
+ new EventPatches(monitorForGame),
+ new GameLocationPatches(monitorForGame),
new ObjectErrorPatch(),
new LoadErrorPatch(this.Monitor, this.OnSaveContentRemoved),
- new ScheduleErrorPatch(logManager.MonitorForGame),
+ new ScheduleErrorPatch(monitorForGame),
+ new SpriteBatchValidationPatches(),
new UtilityErrorPatches()
);
@@ -61,7 +57,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler
/// <summary>The method invoked when a save is loaded.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
- public void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
+ private void OnSaveLoaded(object sender, SaveLoadedEventArgs e)
{
// show in-game warning for removed save content
if (this.IsSaveContentRemoved)
@@ -70,5 +66,16 @@ namespace StardewModdingAPI.Mods.ErrorHandler
Game1.addHUDMessage(new HUDMessage(this.Helper.Translation.Get("warn.invalid-content-removed"), HUDMessage.error_type));
}
}
+
+ /// <summary>Get the monitor with which to log game errors.</summary>
+ private IMonitor GetMonitorForGame()
+ {
+ SCore core = SCore.Instance;
+ LogManager logManager = core.GetType().GetField("LogManager", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(core) as LogManager;
+ if (logManager == null)
+ this.Monitor.Log("Can't access SMAPI's internal log manager. Some game errors may be reported as being from Error Handler.", LogLevel.Error);
+
+ return logManager?.MonitorForGame ?? this.Monitor;
+ }
}
}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs
new file mode 100644
index 00000000..a15c1d32
--- /dev/null
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/EventPatches.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+#if HARMONY_2
+using HarmonyLib;
+#else
+using Harmony;
+#endif
+using StardewModdingAPI.Framework.Patching;
+using StardewValley;
+
+namespace StardewModdingAPI.Mods.ErrorHandler.Patches
+{
+ /// <summary>Harmony patches for <see cref="Event"/> which intercept errors to log more details.</summary>
+ /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
+ [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
+ internal class EventPatches : IHarmonyPatch
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>Writes messages to the console and log file on behalf of the game.</summary>
+ private static IMonitor MonitorForGame;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// <inheritdoc />
+ public string Name => nameof(EventPatches);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param>
+ public EventPatches(IMonitor monitorForGame)
+ {
+ EventPatches.MonitorForGame = monitorForGame;
+ }
+
+ /// <inheritdoc />
+#if HARMONY_2
+ public void Apply(Harmony harmony)
+#else
+ public void Apply(HarmonyInstance harmony)
+#endif
+ {
+ harmony.Patch(
+ original: AccessTools.Method(typeof(Event), nameof(Event.LogErrorAndHalt)),
+ postfix: new HarmonyMethod(this.GetType(), nameof(EventPatches.After_Event_LogErrorAndHalt))
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>The method to call after <see cref="Event.LogErrorAndHalt"/>.</summary>
+ /// <param name="e">The exception being logged.</param>
+ private static void After_Event_LogErrorAndHalt(Exception e)
+ {
+ EventPatches.MonitorForGame.Log(e.ToString(), LogLevel.Error);
+ }
+ }
+}
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs
index fabc6cad..c10f2de7 100644
--- a/src/SMAPI.Mods.ErrorHandler/Patches/EventErrorPatch.cs
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/GameLocationPatches.cs
@@ -15,7 +15,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
[SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
- internal class EventErrorPatch : IHarmonyPatch
+ internal class GameLocationPatches : IHarmonyPatch
{
/*********
** Fields
@@ -28,7 +28,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
** Accessors
*********/
/// <inheritdoc />
- public string Name => nameof(EventErrorPatch);
+ public string Name => nameof(GameLocationPatches);
/*********
@@ -36,9 +36,9 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
*********/
/// <summary>Construct an instance.</summary>
/// <param name="monitorForGame">Writes messages to the console and log file on behalf of the game.</param>
- public EventErrorPatch(IMonitor monitorForGame)
+ public GameLocationPatches(IMonitor monitorForGame)
{
- EventErrorPatch.MonitorForGame = monitorForGame;
+ GameLocationPatches.MonitorForGame = monitorForGame;
}
/// <inheritdoc />
@@ -55,7 +55,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
{
harmony.Patch(
original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"),
- prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition))
+ prefix: new HarmonyMethod(this.GetType(), nameof(GameLocationPatches.Before_GameLocation_CheckEventPrecondition))
);
}
#endif
@@ -89,7 +89,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
/// <returns>Returns whether to execute the original method.</returns>
private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod)
{
- const string key = nameof(EventErrorPatch.Before_GameLocation_CheckEventPrecondition);
+ const string key = nameof(GameLocationPatches.Before_GameLocation_CheckEventPrecondition);
if (!PatchHelper.StartIntercept(key))
return true;
@@ -101,7 +101,7 @@ namespace StardewModdingAPI.Mods.ErrorHandler.Patches
catch (TargetInvocationException ex)
{
__result = -1;
- EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error);
+ GameLocationPatches.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error);
return false;
}
finally
diff --git a/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
new file mode 100644
index 00000000..0211cfb1
--- /dev/null
+++ b/src/SMAPI.Mods.ErrorHandler/Patches/SpriteBatchValidationPatches.cs
@@ -0,0 +1,63 @@
+#if HARMONY_2
+using HarmonyLib;
+#else
+using Harmony;
+#endif
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Patching;
+
+namespace StardewModdingAPI.Mods.ErrorHandler.Patches
+{
+ /// <summary>Harmony patch for <see cref="SpriteBatch"/> to validate textures earlier.</summary>
+ /// <remarks>Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments.</remarks>
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
+ [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")]
+ internal class SpriteBatchValidationPatches : IHarmonyPatch
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <inheritdoc />
+ public string Name => nameof(SpriteBatchValidationPatches);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <inheritdoc />
+#if HARMONY_2
+ public void Apply(Harmony harmony)
+#else
+ public void Apply(HarmonyInstance harmony)
+#endif
+ {
+ harmony.Patch(
+#if SMAPI_FOR_WINDOWS
+ original: AccessTools.Method(typeof(SpriteBatch), "InternalDraw"),
+#else
+ original: AccessTools.Method(typeof(SpriteBatch), "CheckValid", new[] { typeof(Texture2D) }),
+#endif
+ postfix: new HarmonyMethod(this.GetType(), nameof(SpriteBatchValidationPatches.After_SpriteBatch_CheckValid))
+ );
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+#if SMAPI_FOR_WINDOWS
+ /// <summary>The method to call instead of <see cref="SpriteBatch.InternalDraw"/>.</summary>
+ /// <param name="texture">The texture to validate.</param>
+#else
+ /// <summary>The method to call instead of <see cref="SpriteBatch.CheckValid"/>.</summary>
+ /// <param name="texture">The texture to validate.</param>
+#endif
+ private static void After_SpriteBatch_CheckValid(Texture2D texture)
+ {
+ if (texture?.IsDisposed == true)
+ throw new ObjectDisposedException("Cannot draw this texture because it's disposed.");
+ }
+ }
+}
diff --git a/src/SMAPI.Mods.ErrorHandler/manifest.json b/src/SMAPI.Mods.ErrorHandler/manifest.json
index bb9942d1..b6df0f49 100644
--- a/src/SMAPI.Mods.ErrorHandler/manifest.json
+++ b/src/SMAPI.Mods.ErrorHandler/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Error Handler",
"Author": "SMAPI",
- "Version": "3.9.1",
+ "Version": "3.9.2",
"Description": "Handles some common vanilla errors to log more useful info or avoid breaking the game.",
"UniqueID": "SMAPI.ErrorHandler",
"EntryDll": "ErrorHandler.dll",
- "MinimumApiVersion": "3.9.1"
+ "MinimumApiVersion": "3.9.2"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 95ee5144..4d2003e2 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.9.1",
+ "Version": "3.9.2",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.9.1"
+ "MinimumApiVersion": "3.9.2"
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
index 89a22eaf..da312471 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs
@@ -50,13 +50,13 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki
doc.LoadHtml(html);
// fetch game versions
- string stableVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-stable-version']")?.InnerText;
- string betaVersion = doc.DocumentNode.SelectSingleNode("div[@class='game-beta-version']")?.InnerText;
+ string stableVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-stable-version']")?.InnerText;
+ string betaVersion = doc.DocumentNode.SelectSingleNode("//div[@class='game-beta-version']")?.InnerText;
if (betaVersion == stableVersion)
betaVersion = null;
// find mod entries
- HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("table[@id='mod-list']//tr[@class='mod']");
+ HtmlNodeCollection modNodes = doc.DocumentNode.SelectNodes("//table[@id='mod-list']//tr[@class='mod']");
if (modNodes == null)
throw new InvalidOperationException("Can't parse wiki compatibility list, no mods found.");
diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
index 86a97016..fd206d9d 100644
--- a/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
+++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModScanner.cs
@@ -177,12 +177,17 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning
}
// get mod type
- ModType type = ModType.Invalid;
- if (manifest != null)
+ ModType type;
{
- type = !string.IsNullOrWhiteSpace(manifest.ContentPackFor?.UniqueID)
- ? ModType.ContentPack
- : ModType.Smapi;
+ bool isContentPack = !string.IsNullOrWhiteSpace(manifest?.ContentPackFor?.UniqueID);
+ bool isSmapi = !string.IsNullOrWhiteSpace(manifest?.EntryDll);
+
+ if (isContentPack == isSmapi)
+ type = ModType.Invalid;
+ else if (isContentPack)
+ type = ModType.ContentPack;
+ else
+ type = ModType.Smapi;
}
// build result
diff --git a/src/SMAPI.Web/Controllers/ModsController.cs b/src/SMAPI.Web/Controllers/ModsController.cs
index 24e36709..c62ed605 100644
--- a/src/SMAPI.Web/Controllers/ModsController.cs
+++ b/src/SMAPI.Web/Controllers/ModsController.cs
@@ -62,7 +62,7 @@ namespace StardewModdingAPI.Web.Controllers
mods: this.Cache
.GetWikiMods()
.Select(mod => new ModModel(mod.Data))
- .OrderBy(p => Regex.Replace(p.Name.ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting
+ .OrderBy(p => Regex.Replace((p.Name ?? "").ToLower(), "[^a-z0-9]", "")), // ignore case, spaces, and special characters when sorting
lastUpdated: metadata.LastUpdated,
isStale: this.Cache.IsStale(metadata.LastUpdated, this.StaleMinutes)
);
diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
index 92149f4d..21514979 100644
--- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
+++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
@@ -11,9 +11,9 @@
"title": "Format version",
"description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.",
"type": "string",
- "const": "1.19.0",
+ "const": "1.20.0",
"@errorMessages": {
- "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.19.0'."
+ "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.20.0'."
}
},
"ConfigSchema": {
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 57c40bbf..54fb54ab 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -54,7 +54,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.1");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.9.2");
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.4");
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 77dd6c72..32195fff 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework
/// <summary>An asset key prefix for assets from SMAPI mod folders.</summary>
private readonly string ManagedPrefix = "SMAPI";
+ /// <summary>Whether to enable more aggressive memory optimizations.</summary>
+ private readonly bool AggressiveMemoryOptimizations;
+
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
@@ -51,7 +54,7 @@ namespace StardewModdingAPI.Framework
private bool IsDisposed;
/// <summary>A lock used to prevent asynchronous changes to the content manager list.</summary>
- /// <remarks>The game may adds content managers in asynchronous threads (e.g. when populating the load screen).</remarks>
+ /// <remarks>The game may add content managers in asynchronous threads (e.g. when populating the load screen).</remarks>
private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim();
/// <summary>A cache of ordered tilesheet IDs used by vanilla maps.</summary>
@@ -91,8 +94,10 @@ namespace StardewModdingAPI.Framework
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param>
- public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset)
+ /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
+ public ContentCoordinator(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onLoadingFirstAsset, bool aggressiveMemoryOptimizations)
{
+ this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.Reflection = reflection;
this.JsonHelper = jsonHelper;
@@ -108,11 +113,25 @@ namespace StardewModdingAPI.Framework
monitor: monitor,
reflection: reflection,
onDisposing: this.OnDisposing,
- onLoadingFirstAsset: onLoadingFirstAsset
+ onLoadingFirstAsset: onLoadingFirstAsset,
+ aggressiveMemoryOptimizations: aggressiveMemoryOptimizations
)
);
+ var contentManagerForAssetPropagation = new GameContentManagerForAssetPropagation(
+ name: nameof(GameContentManagerForAssetPropagation),
+ serviceProvider: serviceProvider,
+ rootDirectory: rootDirectory,
+ currentCulture: currentCulture,
+ coordinator: this,
+ monitor: monitor,
+ reflection: reflection,
+ onDisposing: this.OnDisposing,
+ onLoadingFirstAsset: onLoadingFirstAsset,
+ aggressiveMemoryOptimizations: aggressiveMemoryOptimizations
+ );
+ this.ContentManagers.Add(contentManagerForAssetPropagation);
this.VanillaContentManager = new LocalizedContentManager(serviceProvider, rootDirectory);
- this.CoreAssets = new CoreAssetPropagator(this.MainContentManager.AssertAndNormalizeAssetName, reflection);
+ this.CoreAssets = new CoreAssetPropagator(this.MainContentManager, contentManagerForAssetPropagation, reflection, aggressiveMemoryOptimizations);
}
/// <summary>Get a new content manager which handles reading files from the game content folder with support for interception.</summary>
@@ -130,7 +149,8 @@ namespace StardewModdingAPI.Framework
monitor: this.Monitor,
reflection: this.Reflection,
onDisposing: this.OnDisposing,
- onLoadingFirstAsset: this.OnLoadingFirstAsset
+ onLoadingFirstAsset: this.OnLoadingFirstAsset,
+ aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations
);
this.ContentManagers.Add(manager);
return manager;
@@ -157,7 +177,8 @@ namespace StardewModdingAPI.Framework
monitor: this.Monitor,
reflection: this.Reflection,
jsonHelper: this.JsonHelper,
- onDisposing: this.OnDisposing
+ onDisposing: this.OnDisposing,
+ aggressiveMemoryOptimizations: this.AggressiveMemoryOptimizations
);
this.ContentManagers.Add(manager);
return manager;
@@ -182,6 +203,17 @@ namespace StardewModdingAPI.Framework
});
}
+ /// <summary>Clean up when the player is returning to the title screen.</summary>
+ /// <remarks>This is called after the player returns to the title screen, but before <see cref="Game1.CleanupReturningToTitle"/> runs.</remarks>
+ public void OnReturningToTitleScreen()
+ {
+ this.ContentManagerLock.InReadLock(() =>
+ {
+ foreach (IContentManager contentManager in this.ContentManagers)
+ contentManager.OnReturningToTitleScreen();
+ });
+ }
+
/// <summary>Get whether this asset is mapped to a mod folder.</summary>
/// <param name="key">The asset key.</param>
public bool IsManagedAssetKey(string key)
@@ -290,7 +322,7 @@ namespace StardewModdingAPI.Framework
// reload core game assets
if (removedAssets.Any())
{
- IDictionary<string, bool> propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager
+ IDictionary<string, bool> propagated = this.CoreAssets.Propagate(removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager
this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace);
}
else
diff --git a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
index 92264f8c..1a64dab8 100644
--- a/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/BaseContentManager.cs
@@ -5,12 +5,12 @@ using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
-using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewValley;
+using xTile;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -29,6 +29,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Encapsulates monitoring and logging.</summary>
protected readonly IMonitor Monitor;
+ /// <summary>Whether to enable more aggressive memory optimizations.</summary>
+ protected readonly bool AggressiveMemoryOptimizations;
+
/// <summary>Whether the content coordinator has been disposed.</summary>
private bool IsDisposed;
@@ -49,16 +52,16 @@ namespace StardewModdingAPI.Framework.ContentManagers
/*********
** Accessors
*********/
- /// <summary>A name for the mod manager. Not guaranteed to be unique.</summary>
+ /// <inheritdoc />
public string Name { get; }
- /// <summary>The current language as a constant.</summary>
+ /// <inheritdoc />
public LanguageCode Language => this.GetCurrentLanguage();
- /// <summary>The absolute path to the <see cref="ContentManager.RootDirectory"/>.</summary>
+ /// <inheritdoc />
public string FullRootDirectory => Path.Combine(Constants.ExecutionPath, this.RootDirectory);
- /// <summary>Whether this content manager can be targeted by managed asset keys (e.g. to load assets from a mod folder).</summary>
+ /// <inheritdoc />
public bool IsNamespaced { get; }
@@ -75,7 +78,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
/// <param name="isNamespaced">Whether this content manager handles managed asset keys (e.g. to load assets from a mod folder).</param>
- protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced)
+ /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
+ protected BaseContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, bool isNamespaced, bool aggressiveMemoryOptimizations)
: base(serviceProvider, rootDirectory, currentCulture)
{
// init
@@ -85,59 +89,49 @@ namespace StardewModdingAPI.Framework.ContentManagers
this.Monitor = monitor ?? throw new ArgumentNullException(nameof(monitor));
this.OnDisposing = onDisposing;
this.IsNamespaced = isNamespaced;
+ this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
// get asset data
this.LanguageCodes = this.GetKeyLocales().ToDictionary(p => p.Value, p => p.Key, StringComparer.OrdinalIgnoreCase);
this.BaseDisposableReferences = reflection.GetField<List<IDisposable>>(this, "disposableAssets").GetValue();
}
- /// <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>
+ /// <inheritdoc />
public override T Load<T>(string assetName)
{
return this.Load<T>(assetName, this.Language, useCache: true);
}
- /// <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>
+ /// <inheritdoc />
public override T Load<T>(string assetName, LanguageCode language)
{
return this.Load<T>(assetName, language, useCache: true);
}
- /// <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>
- /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
+ /// <inheritdoc />
public abstract T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache);
- /// <summary>Load the base asset without localization.</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>
+ /// <inheritdoc />
[Obsolete("This method is implemented for the base game and should not be used directly. To load an asset from the underlying content manager directly, use " + nameof(BaseContentManager.RawLoad) + " instead.")]
public override T LoadBase<T>(string assetName)
{
return this.Load<T>(assetName, LanguageCode.en, useCache: true);
}
- /// <summary>Perform any cleanup needed when the locale changes.</summary>
+ /// <inheritdoc />
public virtual void OnLocaleChanged() { }
- /// <summary>Normalize path separators in a file path. For asset keys, see <see cref="AssertAndNormalizeAssetName"/> instead.</summary>
- /// <param name="path">The file path to normalize.</param>
+ /// <inheritdoc />
+ public virtual void OnReturningToTitleScreen() { }
+
+ /// <inheritdoc />
[Pure]
public string NormalizePathSeparators(string path)
{
return this.Cache.NormalizePathSeparators(path);
}
- /// <summary>Assert that the given key has a valid format and return a normalized form consistent with the underlying cache.</summary>
- /// <param name="assetName">The asset key to check.</param>
- /// <exception cref="SContentLoadException">The asset key is empty or contains invalid characters.</exception>
+ /// <inheritdoc />
[SuppressMessage("ReSharper", "ParameterOnlyUsedForPreconditionCheck.Local", Justification = "Parameter is only used for assertion checks by design.")]
public string AssertAndNormalizeAssetName(string assetName)
{
@@ -154,29 +148,26 @@ namespace StardewModdingAPI.Framework.ContentManagers
/****
** Content loading
****/
- /// <summary>Get the current content locale.</summary>
+ /// <inheritdoc />
public string GetLocale()
{
return this.GetLocale(this.GetCurrentLanguage());
}
- /// <summary>The locale for a language.</summary>
- /// <param name="language">The language.</param>
+ /// <inheritdoc />
public string GetLocale(LanguageCode language)
{
return this.LanguageCodeString(language);
}
- /// <summary>Get whether the content manager has already loaded and cached the given asset.</summary>
- /// <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.</param>
+ /// <inheritdoc />
public bool IsLoaded(string assetName, LanguageCode language)
{
assetName = this.Cache.NormalizeKey(assetName);
return this.IsNormalizedKeyLoaded(assetName, language);
}
- /// <summary>Get the cached asset keys.</summary>
+ /// <inheritdoc />
public IEnumerable<string> GetAssetKeys()
{
return this.Cache.Keys
@@ -187,10 +178,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/****
** Cache invalidation
****/
- /// <summary>Purge matched assets from the cache.</summary>
- /// <param name="predicate">Matches the asset keys to invalidate.</param>
- /// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
- /// <returns>Returns the invalidated asset names and instances.</returns>
+ /// <inheritdoc />
public IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{
IDictionary<string, object> removeAssets = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
@@ -198,21 +186,28 @@ namespace StardewModdingAPI.Framework.ContentManagers
{
this.ParseCacheKey(key, out string assetName, out _);
- if (removeAssets.ContainsKey(assetName))
- return true;
- if (predicate(assetName, asset.GetType()))
+ // check if asset should be removed
+ bool remove = removeAssets.ContainsKey(assetName);
+ if (!remove && predicate(assetName, asset.GetType()))
{
removeAssets[assetName] = asset;
- return true;
+ remove = true;
+ }
+
+ // dispose if safe
+ if (remove && this.AggressiveMemoryOptimizations)
+ {
+ if (asset is Map map)
+ map.DisposeTileSheets(Game1.mapDisplayDevice);
}
- return false;
+
+ return remove;
}, dispose);
return removeAssets;
}
- /// <summary>Dispose held resources.</summary>
- /// <param name="isDisposing">Whether the content manager is being disposed (rather than finalized).</param>
+ /// <inheritdoc />
protected override void Dispose(bool isDisposing)
{
// ignore if disposed
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 665c019b..8e78faba 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -52,17 +52,14 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
/// <param name="onLoadingFirstAsset">A callback to invoke the first time *any* game content manager loads an asset.</param>
- public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset)
- : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false)
+ /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
+ public GameContentManager(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, bool aggressiveMemoryOptimizations)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: false, aggressiveMemoryOptimizations: aggressiveMemoryOptimizations)
{
this.OnLoadingFirstAsset = onLoadingFirstAsset;
}
- /// <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>
- /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
+ /// <inheritdoc />
public override T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache)
{
// raise first-load callback
@@ -94,7 +91,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
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);
+ this.Monitor.Log($"Bypassing mod loaders for this asset. Stack trace:\n{Environment.StackTrace}");
data = this.RawLoad<T>(assetName, language, useCache);
}
else
@@ -116,7 +113,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
return data;
}
- /// <summary>Perform any cleanup needed when the locale changes.</summary>
+ /// <inheritdoc />
public override void OnLocaleChanged()
{
base.OnLocaleChanged();
@@ -136,10 +133,35 @@ namespace StardewModdingAPI.Framework.ContentManagers
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
.ToArray();
if (invalidated.Any())
- this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.", LogLevel.Trace);
+ this.Monitor.Log($"Invalidated {invalidated.Length} asset names: {string.Join(", ", invalidated)} for locale change.");
+ }
+
+ /// <inheritdoc />
+ public override void OnReturningToTitleScreen()
+ {
+ // The game clears LocalizedContentManager.localizedAssetNames after returning to the title screen. That
+ // causes an inconsistency in the SMAPI asset cache, which leads to an edge case where assets already
+ // provided by mods via IAssetLoader when playing in non-English are ignored.
+ //
+ // For example, let's say a mod provides the 'Data\mail' asset through IAssetLoader when playing in
+ // Portuguese. Here's the normal load process after it's loaded:
+ // 1. The game requests Data\mail.
+ // 2. SMAPI sees that it's already cached, and calls LoadRaw to bypass asset interception.
+ // 3. LoadRaw sees that there's a localized key mapping, and gets the mapped key.
+ // 4. In this case "Data\mail" is mapped to "Data\mail" since it was loaded by a mod, so it loads that
+ // asset.
+ //
+ // When the game clears localizedAssetNames, that process goes wrong in step 4:
+ // 3. LoadRaw sees that there's no localized key mapping *and* the locale is non-English, so it attempts
+ // to load from the localized key format.
+ // 4. In this case that's 'Data\mail.pt-BR', so it successfully loads that asset.
+ // 5. Since we've bypassed asset interception at this point, it's loaded directly from the base content
+ // manager without mod changes.
+ if (LocalizedContentManager.CurrentLanguageCode != LocalizedContentManager.LanguageCode.en)
+ this.InvalidateCache((_, _) => true);
}
- /// <summary>Create a new content manager for temporary use.</summary>
+ /// <inheritdoc />
public override LocalizedContentManager CreateTemporary()
{
return this.Coordinator.CreateGameContentManager("(temporary)");
@@ -149,9 +171,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/*********
** Private methods
*********/
- /// <summary>Get whether an asset has already been loaded.</summary>
- /// <param name="normalizedAssetName">The normalized asset name.</param>
- /// <param name="language">The language to check.</param>
+ /// <inheritdoc />
protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language)
{
string cachedKey = null;
@@ -165,12 +185,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
: this.Cache.ContainsKey(normalizedAssetName);
}
- /// <summary>Add tracking data to an asset and add it to 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>
- /// <param name="language">The language code for which to inject the asset.</param>
- /// <param name="useCache">Whether to save the asset to the asset cache.</param>
+ /// <inheritdoc />
protected override void TrackAsset<T>(string assetName, T value, LanguageCode language, bool useCache)
{
// handle explicit language in asset name
@@ -358,7 +373,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
try
{
editor.Edit<T>(asset);
- this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}.", LogLevel.Trace);
+ this.Monitor.Log($"{mod.DisplayName} edited {info.AssetName}.");
}
catch (Exception ex)
{
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
new file mode 100644
index 00000000..61683ce6
--- /dev/null
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManagerForAssetPropagation.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Globalization;
+using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Reflection;
+using StardewValley;
+
+namespace StardewModdingAPI.Framework.ContentManagers
+{
+ /// <summary>An extension of <see cref="GameContentManager"/> specifically optimized for asset propagation.</summary>
+ /// <remarks>This avoids sharing an asset cache with <see cref="Game1.content"/> or mods, so that assets can be safely disposed when the vanilla game no longer references them.</remarks>
+ internal class GameContentManagerForAssetPropagation : GameContentManager
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>A unique value used in <see cref="Texture2D"/> to identify assets loaded through this instance.</summary>
+ private readonly string Tag = $"Pathoschild.SMAPI/LoadedBy:{nameof(GameContentManagerForAssetPropagation)}";
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <inheritdoc />
+ public GameContentManagerForAssetPropagation(string name, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, Action<BaseContentManager> onDisposing, Action onLoadingFirstAsset, bool aggressiveMemoryOptimizations)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, onLoadingFirstAsset, aggressiveMemoryOptimizations) { }
+
+ /// <inheritdoc />
+ public override T Load<T>(string assetName, LanguageCode language, bool useCache)
+ {
+ T data = base.Load<T>(assetName, language, useCache);
+
+ if (data is Texture2D texture)
+ texture.Tag = this.Tag;
+
+ return data;
+ }
+
+ /// <summary>Get whether a texture was loaded by this content manager.</summary>
+ /// <param name="texture">The texture to check.</param>
+ public bool IsResponsibleFor(Texture2D texture)
+ {
+ return
+ texture?.Tag is string tag
+ && tag.Contains(this.Tag);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/ContentManagers/IContentManager.cs b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
index 0e7edd8f..1e222472 100644
--- a/src/SMAPI/Framework/ContentManagers/IContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/IContentManager.cs
@@ -36,9 +36,6 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
T Load<T>(string assetName, LocalizedContentManager.LanguageCode language, bool useCache);
- /// <summary>Perform any cleanup needed when the locale changes.</summary>
- void OnLocaleChanged();
-
/// <summary>Normalize path separators in a file path. For asset keys, see <see cref="AssertAndNormalizeAssetName"/> instead.</summary>
/// <param name="path">The file path to normalize.</param>
[Pure]
@@ -69,5 +66,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="dispose">Whether to dispose invalidated assets. This should only be <c>true</c> when they're being invalidated as part of a dispose, to avoid crashing the game.</param>
/// <returns>Returns the invalidated asset names and instances.</returns>
IDictionary<string, object> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false);
+
+ /// <summary>Perform any cleanup needed when the locale changes.</summary>
+ void OnLocaleChanged();
+
+ /// <summary>Clean up when the player is returning to the title screen.</summary>
+ /// <remarks>This is called after the player returns to the title screen, but before <see cref="Game1.CleanupReturningToTitle"/> runs.</remarks>
+ void OnReturningToTitleScreen();
}
}
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 1456d3c1..9af14cb5 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -50,36 +50,28 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
- public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
- : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
+ /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
+ public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing, bool aggressiveMemoryOptimizations)
+ : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true, aggressiveMemoryOptimizations: aggressiveMemoryOptimizations)
{
this.GameContentManager = gameContentManager;
this.JsonHelper = jsonHelper;
this.ModName = modName;
}
- /// <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>
+ /// <inheritdoc />
public override T Load<T>(string assetName)
{
return this.Load<T>(assetName, this.DefaultLanguage, useCache: false);
}
- /// <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>
+ /// <inheritdoc />
public override T Load<T>(string assetName, LanguageCode language)
{
return this.Load<T>(assetName, language, useCache: false);
}
- /// <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>
- /// <param name="useCache">Whether to read/write the loaded asset to the asset cache.</param>
+ /// <inheritdoc />
public override T Load<T>(string assetName, LanguageCode language, bool useCache)
{
assetName = this.AssertAndNormalizeAssetName(assetName);
@@ -189,7 +181,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
return asset;
}
- /// <summary>Create a new content manager for temporary use.</summary>
+ /// <inheritdoc />
public override LocalizedContentManager CreateTemporary()
{
throw new NotSupportedException("Can't create a temporary mod content manager.");
@@ -209,9 +201,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/*********
** Private methods
*********/
- /// <summary>Get whether an asset has already been loaded.</summary>
- /// <param name="normalizedAssetName">The normalized asset name.</param>
- /// <param name="language">The language to check.</param>
+ /// <inheritdoc />
protected override bool IsNormalizedKeyLoaded(string normalizedAssetName, LanguageCode language)
{
return this.Cache.ContainsKey(normalizedAssetName);
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index ff00cff7..2c7be399 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -6,6 +6,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using StardewModdingAPI.Framework.Commands;
+using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.ModLoading;
using StardewModdingAPI.Internal.ConsoleWriting;
using StardewModdingAPI.Toolkit.Framework.ModData;
@@ -284,19 +285,22 @@ namespace StardewModdingAPI.Framework.Logging
}
/// <summary>Log details for settings that don't match the default.</summary>
- /// <param name="isDeveloperMode">Whether to enable full console output for developers.</param>
- /// <param name="checkForUpdates">Whether to check for newer versions of SMAPI and mods on startup.</param>
- /// <param name="rewriteMods">Whether to rewrite mods for compatibility.</param>
- public void LogSettingsHeader(bool isDeveloperMode, bool checkForUpdates, bool rewriteMods)
+ /// <param name="settings">The settings to log.</param>
+ public void LogSettingsHeader(SConfig settings)
{
- if (isDeveloperMode)
- this.Monitor.Log("You have SMAPI for developers, so the console will be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI.", LogLevel.Info);
- if (!checkForUpdates)
- this.Monitor.Log("You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI.", LogLevel.Warn);
- if (!rewriteMods)
- this.Monitor.Log("You configured SMAPI to not rewrite broken mods. Many older mods may fail to load. You can undo this by reinstalling SMAPI.", LogLevel.Warn);
+ // developer mode
+ if (settings.DeveloperMode)
+ this.Monitor.Log("You enabled developer mode, so the console will be much more verbose. You can disable it by installing the non-developer version of SMAPI.", LogLevel.Info);
+
+ // warnings
+ if (!settings.CheckForUpdates)
+ this.Monitor.Log("You disabled update checks, so you won't be notified of new SMAPI or mod updates. Running an old version of SMAPI is not recommended. You can undo this by reinstalling SMAPI.", LogLevel.Warn);
+ if (!settings.RewriteMods)
+ this.Monitor.Log("You disabled rewriting broken mods, so many older mods may fail to load. You can undo this by reinstalling SMAPI.", LogLevel.Info);
if (!this.Monitor.WriteToConsole)
this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn);
+
+ // verbose logging
this.Monitor.VerboseLog("Verbose logging enabled.");
}
diff --git a/src/SMAPI/Framework/Models/SConfig.cs b/src/SMAPI/Framework/Models/SConfig.cs
index dea08717..4a80e34c 100644
--- a/src/SMAPI/Framework/Models/SConfig.cs
+++ b/src/SMAPI/Framework/Models/SConfig.cs
@@ -21,7 +21,8 @@ namespace StardewModdingAPI.Framework.Models
[nameof(WebApiBaseUrl)] = "https://smapi.io/api/",
[nameof(VerboseLogging)] = false,
[nameof(LogNetworkTraffic)] = false,
- [nameof(RewriteMods)] = true
+ [nameof(RewriteMods)] = true,
+ [nameof(AggressiveMemoryOptimizations)] = true
};
/// <summary>The default values for <see cref="SuppressUpdateChecks"/>, to log changes if different.</summary>
@@ -60,6 +61,9 @@ namespace StardewModdingAPI.Framework.Models
/// <summary>Whether SMAPI should rewrite mods for compatibility.</summary>
public bool RewriteMods { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.RewriteMods)];
+ /// <summary>Whether to enable more aggressive memory optimizations.</summary>
+ public bool AggressiveMemoryOptimizations { get; set; } = (bool)SConfig.DefaultValues[nameof(SConfig.AggressiveMemoryOptimizations)];
+
/// <summary>Whether SMAPI should log network traffic. Best combined with <see cref="VerboseLogging"/>, which includes network metadata.</summary>
public bool LogNetworkTraffic { get; set; }
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index cd094ff4..2d783eb2 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -277,7 +277,7 @@ namespace StardewModdingAPI.Framework
// log basic info
this.LogManager.HandleMarkerFiles();
- this.LogManager.LogSettingsHeader(this.Settings.DeveloperMode, this.Settings.CheckForUpdates, this.Settings.RewriteMods);
+ this.LogManager.LogSettingsHeader(this.Settings);
// set window titles
this.SetWindowTitles(
@@ -1118,6 +1118,7 @@ namespace StardewModdingAPI.Framework
{
// perform cleanup
this.Multiplayer.CleanupOnMultiplayerExit();
+ this.ContentCore.OnReturningToTitleScreen();
this.JustReturnedToTitle = true;
}
@@ -1149,7 +1150,7 @@ namespace StardewModdingAPI.Framework
// Game1._temporaryContent initializing from SGame constructor
if (this.ContentCore == null)
{
- this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded);
+ this.ContentCore = new ContentCoordinator(serviceProvider, rootDirectory, Thread.CurrentThread.CurrentUICulture, this.Monitor, this.Reflection, this.Toolkit.JsonHelper, this.InitializeBeforeFirstAssetLoaded, this.Settings.AggressiveMemoryOptimizations);
if (this.ContentCore.Language != this.Translator.LocaleEnum)
this.Translator.SetLocale(this.ContentCore.GetLocale(), this.ContentCore.Language);
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 063804e0..8b591bc1 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Graphics;
using Netcode;
+using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
@@ -29,6 +30,15 @@ namespace StardewModdingAPI.Metadata
/*********
** Fields
*********/
+ /// <summary>The main content manager through which to reload assets.</summary>
+ private readonly LocalizedContentManager MainContentManager;
+
+ /// <summary>An internal content manager used only for asset propagation. See remarks on <see cref="GameContentManagerForAssetPropagation"/>.</summary>
+ private readonly GameContentManagerForAssetPropagation DisposableContentManager;
+
+ /// <summary>Whether to enable more aggressive memory optimizations.</summary>
+ private readonly bool AggressiveMemoryOptimizations;
+
/// <summary>Normalizes an asset key to match the cache key and assert that it's valid.</summary>
private readonly Func<string, string> AssertAndNormalizeAssetName;
@@ -53,19 +63,24 @@ namespace StardewModdingAPI.Metadata
** Public methods
*********/
/// <summary>Initialize the core asset data.</summary>
- /// <param name="assertAndNormalizeAssetName">Normalizes an asset key to match the cache key and assert that it's valid.</param>
+ /// <param name="mainContent">The main content manager through which to reload assets.</param>
+ /// <param name="disposableContent">An internal content manager used only for asset propagation.</param>
/// <param name="reflection">Simplifies access to private code.</param>
- public CoreAssetPropagator(Func<string, string> assertAndNormalizeAssetName, Reflector reflection)
+ /// <param name="aggressiveMemoryOptimizations">Whether to enable more aggressive memory optimizations.</param>
+ public CoreAssetPropagator(LocalizedContentManager mainContent, GameContentManagerForAssetPropagation disposableContent, Reflector reflection, bool aggressiveMemoryOptimizations)
{
- this.AssertAndNormalizeAssetName = assertAndNormalizeAssetName;
+ this.MainContentManager = mainContent;
+ this.DisposableContentManager = disposableContent;
this.Reflection = reflection;
+ this.AggressiveMemoryOptimizations = aggressiveMemoryOptimizations;
+
+ this.AssertAndNormalizeAssetName = disposableContent.AssertAndNormalizeAssetName;
}
/// <summary>Reload one of the game's core assets (if applicable).</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="assets">The asset keys and types to reload.</param>
/// <returns>Returns a lookup of asset names to whether they've been propagated.</returns>
- public IDictionary<string, bool> Propagate(LocalizedContentManager content, IDictionary<string, Type> assets)
+ public IDictionary<string, bool> Propagate(IDictionary<string, Type> assets)
{
// group into optimized lists
var buckets = assets.GroupBy(p =>
@@ -86,16 +101,16 @@ namespace StardewModdingAPI.Metadata
switch (bucket.Key)
{
case AssetBucket.Sprite:
- this.ReloadNpcSprites(content, bucket.Select(p => p.Key), propagated);
+ this.ReloadNpcSprites(bucket.Select(p => p.Key), propagated);
break;
case AssetBucket.Portrait:
- this.ReloadNpcPortraits(content, bucket.Select(p => p.Key), propagated);
+ this.ReloadNpcPortraits(bucket.Select(p => p.Key), propagated);
break;
default:
foreach (var entry in bucket)
- propagated[entry.Key] = this.PropagateOther(content, entry.Key, entry.Value);
+ propagated[entry.Key] = this.PropagateOther(entry.Key, entry.Value);
break;
}
}
@@ -107,13 +122,13 @@ namespace StardewModdingAPI.Metadata
** Private methods
*********/
/// <summary>Reload one of the game's core assets (if applicable).</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
/// <param name="type">The asset type to reload.</param>
/// <returns>Returns whether an asset was loaded. The return value may be true or false, or a non-null value for true.</returns>
[SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")]
- private bool PropagateOther(LocalizedContentManager content, string key, Type type)
+ private bool PropagateOther(string key, Type type)
{
+ var content = this.MainContentManager;
key = this.AssertAndNormalizeAssetName(key);
/****
@@ -163,14 +178,19 @@ namespace StardewModdingAPI.Metadata
** Buildings
****/
case "buildings\\houses": // Farm
- reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures)).SetValue(content.Load<Texture2D>(key));
- return true;
+ {
+ var field = reflection.GetField<Texture2D>(typeof(Farm), nameof(Farm.houseTextures));
+ field.SetValue(
+ this.LoadAndDisposeIfNeeded(field.GetValue(), key)
+ );
+ return true;
+ }
/****
** Content\Characters\Farmer
****/
case "characters\\farmer\\accessories": // Game1.LoadContent
- FarmerRenderer.accessoriesTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.accessoriesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.accessoriesTexture, key);
return true;
case "characters\\farmer\\farmer_base": // Farmer
@@ -180,19 +200,19 @@ namespace StardewModdingAPI.Metadata
return this.ReloadPlayerSprites(key);
case "characters\\farmer\\hairstyles": // Game1.LoadContent
- FarmerRenderer.hairStylesTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.hairStylesTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hairStylesTexture, key);
return true;
case "characters\\farmer\\hats": // Game1.LoadContent
- FarmerRenderer.hatsTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.hatsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.hatsTexture, key);
return true;
case "characters\\farmer\\pants": // Game1.LoadContent
- FarmerRenderer.pantsTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.pantsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.pantsTexture, key);
return true;
case "characters\\farmer\\shirts": // Game1.LoadContent
- FarmerRenderer.shirtsTexture = content.Load<Texture2D>(key);
+ FarmerRenderer.shirtsTexture = this.LoadAndDisposeIfNeeded(FarmerRenderer.shirtsTexture, key);
return true;
/****
@@ -432,8 +452,7 @@ namespace StardewModdingAPI.Metadata
return true;
case "tilesheets\\chairtiles": // Game1.LoadContent
- MapSeat.mapChairTexture = content.Load<Texture2D>(key);
- return true;
+ return this.ReloadChairTiles(content, key);
case "tilesheets\\craftables": // Game1.LoadContent
Game1.bigCraftableSpriteSheet = content.Load<Texture2D>(key);
@@ -582,7 +601,7 @@ namespace StardewModdingAPI.Metadata
titleMenu.aboutButton.texture = texture;
titleMenu.languageButton.texture = texture;
foreach (ClickableTextureComponent button in titleMenu.buttons)
- button.texture = titleMenu.titleButtonsTexture;
+ button.texture = texture;
foreach (TemporaryAnimatedSprite bird in titleMenu.birds)
bird.texture = texture;
@@ -671,6 +690,28 @@ namespace StardewModdingAPI.Metadata
return false;
}
+ /// <summary>Reload map seat textures.</summary>
+ /// <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 any textures were reloaded.</returns>
+ private bool ReloadChairTiles(LocalizedContentManager content, string key)
+ {
+ MapSeat.mapChairTexture = content.Load<Texture2D>(key);
+
+ foreach (var location in this.GetLocations())
+ {
+ foreach (MapSeat seat in location.mapSeats.Where(p => p != null))
+ {
+ string curKey = this.NormalizeAssetNameIgnoringEmpty(seat._loadedTextureFile);
+
+ if (curKey == null || key.Equals(curKey, StringComparison.OrdinalIgnoreCase))
+ seat.overlayTexture = MapSeat.mapChairTexture;
+ }
+ }
+
+ return true;
+ }
+
/// <summary>Reload critter textures.</summary>
/// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="key">The asset key to reload.</param>
@@ -785,6 +826,9 @@ namespace StardewModdingAPI.Metadata
/// <param name="location">The location whose map to reload.</param>
private void ReloadMap(GameLocation location)
{
+ if (this.AggressiveMemoryOptimizations)
+ location.map.DisposeTileSheets(Game1.mapDisplayDevice);
+
// reload map
location.interiorDoors.Clear(); // prevent errors when doors try to update tiles which no longer exist
location.reloadMap();
@@ -822,10 +866,9 @@ namespace StardewModdingAPI.Metadata
}
/// <summary>Reload the sprites for matching NPCs.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="keys">The asset keys to reload.</param>
/// <param name="propagated">The asset keys which have been propagated.</param>
- private void ReloadNpcSprites(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated)
+ private void ReloadNpcSprites(IEnumerable<string> keys, IDictionary<string, bool> propagated)
{
// get NPCs
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.OrdinalIgnoreCase);
@@ -843,16 +886,15 @@ namespace StardewModdingAPI.Metadata
// update sprite
foreach (var target in characters)
{
- target.Npc.Sprite.spriteTexture = content.Load<Texture2D>(target.Key);
+ target.Npc.Sprite.spriteTexture = this.LoadAndDisposeIfNeeded(target.Npc.Sprite.spriteTexture, target.Key);
propagated[target.Key] = true;
}
}
/// <summary>Reload the portraits for matching NPCs.</summary>
- /// <param name="content">The content manager through which to reload the asset.</param>
/// <param name="keys">The asset key to reload.</param>
/// <param name="propagated">The asset keys which have been propagated.</param>
- private void ReloadNpcPortraits(LocalizedContentManager content, IEnumerable<string> keys, IDictionary<string, bool> propagated)
+ private void ReloadNpcPortraits(IEnumerable<string> keys, IDictionary<string, bool> propagated)
{
// get NPCs
HashSet<string> lookup = new HashSet<string>(keys, StringComparer.OrdinalIgnoreCase);
@@ -881,7 +923,7 @@ namespace StardewModdingAPI.Metadata
// update portrait
foreach (var target in characters)
{
- target.Npc.Portrait = content.Load<Texture2D>(target.Key);
+ target.Npc.Portrait = this.LoadAndDisposeIfNeeded(target.Npc.Portrait, target.Key);
propagated[target.Key] = true;
}
}
@@ -1146,5 +1188,27 @@ namespace StardewModdingAPI.Metadata
{
return this.GetSegments(path).Length;
}
+
+ /// <summary>Load a texture, and dispose the old one if <see cref="AggressiveMemoryOptimizations"/> is enabled and it's different from the new instance.</summary>
+ /// <param name="oldTexture">The previous texture to dispose.</param>
+ /// <param name="key">The asset key to load.</param>
+ private Texture2D LoadAndDisposeIfNeeded(Texture2D oldTexture, string key)
+ {
+ // if aggressive memory optimizations are enabled, load the asset from the disposable
+ // content manager and dispose the old instance if needed.
+ if (this.AggressiveMemoryOptimizations)
+ {
+ GameContentManagerForAssetPropagation content = this.DisposableContentManager;
+
+ Texture2D newTexture = content.Load<Texture2D>(key);
+ if (oldTexture?.IsDisposed == false && !object.ReferenceEquals(oldTexture, newTexture) && content.IsResponsibleFor(oldTexture))
+ oldTexture.Dispose();
+
+ return newTexture;
+ }
+
+ // else just (re)load it from the main content manager
+ return this.MainContentManager.Load<Texture2D>(key);
+ }
}
}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index 23ee8453..986d2780 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -73,8 +73,22 @@ namespace StardewModdingAPI
/// <remarks>This must be checked *before* any references to <see cref="Constants"/>, and this method should not reference <see cref="Constants"/> itself to avoid errors in Mono or when the game isn't present.</remarks>
private static void AssertGamePresent()
{
- if (Type.GetType($"StardewValley.Game1, {EarlyConstants.GameAssemblyName}", throwOnError: false) == null)
- Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder. See the readme.txt file for details.");
+ try
+ {
+ _ = Type.GetType($"StardewValley.Game1, {EarlyConstants.GameAssemblyName}", throwOnError: true);
+ }
+ catch (Exception ex)
+ {
+ // file doesn't exist
+ if (!File.Exists(Path.Combine(EarlyConstants.ExecutionPath, $"{EarlyConstants.GameAssemblyName}.exe")))
+ Program.PrintErrorAndExit("Oops! SMAPI can't find the game. Make sure you're running StardewModdingAPI.exe in your game folder.");
+
+ // can't load file
+ Program.PrintErrorAndExit(
+ message: "Oops! SMAPI couldn't load the game executable. The technical details below may have more info.",
+ technicalMessage: $"Technical details: {ex}"
+ );
+ }
}
/// <summary>Assert that the game version is within <see cref="Constants.MinimumGameVersion"/> and <see cref="Constants.MaximumGameVersion"/>.</summary>
@@ -130,11 +144,22 @@ namespace StardewModdingAPI
/// <summary>Write an error directly to the console and exit.</summary>
/// <param name="message">The error message to display.</param>
- private static void PrintErrorAndExit(string message)
+ /// <param name="technicalMessage">An additional message to log with technical details.</param>
+ private static void PrintErrorAndExit(string message, string technicalMessage = null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(message);
Console.ResetColor();
+
+ if (technicalMessage != null)
+ {
+ Console.WriteLine();
+ Console.ForegroundColor = ConsoleColor.Gray;
+ Console.WriteLine(technicalMessage);
+ Console.ResetColor();
+ Console.WriteLine();
+ }
+
Program.PressAnyKeyToExit(showMessage: true);
}
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 7a710f14..a9e6f389 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -40,6 +40,12 @@ copy all the settings, or you may cause bugs due to overridden changes in future
"RewriteMods": true,
/**
+ * Whether to enable more aggressive memory optimizations.
+ * You can try disabling this if you get ObjectDisposedException errors.
+ */
+ "AggressiveMemoryOptimizations": true,
+
+ /**
* Whether to add a section to the 'mod issues' list for mods which directly use potentially
* sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as
* part of their normal functionality, so these warnings are meaningless without further