summaryrefslogtreecommitdiff
path: root/src/StardewModdingAPI/Program.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/StardewModdingAPI/Program.cs')
-rw-r--r--src/StardewModdingAPI/Program.cs127
1 files changed, 95 insertions, 32 deletions
diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs
index 31aeb3a6..1e5fcfc3 100644
--- a/src/StardewModdingAPI/Program.cs
+++ b/src/StardewModdingAPI/Program.cs
@@ -16,7 +16,9 @@ using StardewModdingAPI.Framework;
using StardewModdingAPI.Framework.Logging;
using StardewModdingAPI.Framework.Models;
using StardewModdingAPI.Framework.Serialisation;
+using StardewValley;
using Monitor = StardewModdingAPI.Framework.Monitor;
+using SObject = StardewValley.Object;
namespace StardewModdingAPI
{
@@ -32,7 +34,7 @@ namespace StardewModdingAPI
/// <summary>Manages console output interception.</summary>
private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager();
- /// <summary>The core logger for SMAPI.</summary>
+ /// <summary>The core logger and monitor for SMAPI.</summary>
private readonly Monitor Monitor;
/// <summary>Tracks whether the game should exit immediately and any pending initialisation should be cancelled.</summary>
@@ -99,7 +101,7 @@ namespace StardewModdingAPI
public Program(bool writeToConsole, string logPath)
{
this.LogFile = new LogFileManager(logPath);
- this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole };
+ this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = writeToConsole };
}
/// <summary>Launch SMAPI.</summary>
@@ -142,6 +144,17 @@ namespace StardewModdingAPI
this.GameInstance = new SGame(this.Monitor);
StardewValley.Program.gamePtr = this.GameInstance;
+ // add exit handler
+ new Thread(() =>
+ {
+ this.CancellationTokenSource.Token.WaitHandle.WaitOne();
+ if (this.IsGameRunning)
+ {
+ this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit();
+ this.GameInstance.Exit();
+ }
+ }).Start();
+
// hook into game events
#if SMAPI_FOR_WINDOWS
((Form)Control.FromHandle(this.GameInstance.Window.Handle)).FormClosing += (sender, args) => this.Dispose();
@@ -180,20 +193,6 @@ namespace StardewModdingAPI
}
}
- /// <summary>Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs.</summary>
- /// <param name="module">The module which requested an immediate exit.</param>
- /// <param name="reason">The reason provided for the shutdown.</param>
- public void ExitGameImmediately(string module, string reason)
- {
- this.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}");
- this.CancellationTokenSource.Cancel();
- if (this.IsGameRunning)
- {
- this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit();
- this.GameInstance.Exit();
- }
- }
-
/// <summary>Get a monitor for legacy code which doesn't have one passed in.</summary>
[Obsolete("This method should only be used when needed for backwards compatibility.")]
internal IMonitor GetLegacyMonitorForMod()
@@ -205,10 +204,16 @@ namespace StardewModdingAPI
/// <summary>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</summary>
public void Dispose()
{
+ // skip if already disposed
if (this.IsDisposed)
return;
this.IsDisposed = true;
+ // dispose mod helpers
+ foreach (var mod in this.ModRegistry.GetMods())
+ (mod.Helper as IDisposable)?.Dispose();
+
+ // dispose core components
this.IsGameRunning = false;
this.LogFile?.Dispose();
this.ConsoleManager?.Dispose();
@@ -247,27 +252,30 @@ namespace StardewModdingAPI
// redirect direct console output
{
Monitor monitor = this.GetSecondaryMonitor("Console.Out");
- monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion
if (monitor.WriteToConsole)
- this.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace);
+ this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(monitor, message);
}
- // add warning headers
+ // add headers
if (this.Settings.DeveloperMode)
{
this.Monitor.ShowTraceInConsole = true;
- this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Warn);
+ this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Info);
}
if (!this.Settings.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 or editing {Constants.ApiConfigPath}.", LogLevel.Warn);
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);
+ // validate XNB integrity
+ if (!this.ValidateContentIntegrity())
+ this.Monitor.Log("SMAPI found problems in the game's XNB files which may cause errors or crashes while you're playing. Consider uninstalling XNB mods or reinstalling the game.", LogLevel.Warn);
+
// load mods
int modsLoaded = this.LoadMods();
- if (this.CancellationTokenSource.IsCancellationRequested)
+ if (this.Monitor.IsExiting)
{
- this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error);
+ this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn);
return;
}
@@ -308,12 +316,59 @@ namespace StardewModdingAPI
inputThread.Start();
// keep console thread alive while the game is running
- while (this.IsGameRunning)
+ while (this.IsGameRunning && !this.Monitor.IsExiting)
Thread.Sleep(1000 / 10);
if (inputThread.ThreadState == ThreadState.Running)
inputThread.Abort();
}
+ /// <summary>Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated.</summary>
+ /// <returns>Returns whether all integrity checks passed.</returns>
+ private bool ValidateContentIntegrity()
+ {
+ this.Monitor.Log("Detecting common issues...");
+ bool issuesFound = false;
+
+
+ // object format (commonly broken by outdated files)
+ {
+ void LogIssue(int id, string issue) => this.Monitor.Log($"Detected issue: item #{id} in Content\\Data\\ObjectInformation is invalid ({issue}).", LogLevel.Warn);
+ foreach (KeyValuePair<int, string> entry in Game1.objectInformation)
+ {
+ // must not be empty
+ if (string.IsNullOrWhiteSpace(entry.Value))
+ {
+ LogIssue(entry.Key, "entry is empty");
+ issuesFound = true;
+ continue;
+ }
+
+ // require core fields
+ string[] fields = entry.Value.Split('/');
+ if (fields.Length < SObject.objectInfoDescriptionIndex + 1)
+ {
+ LogIssue(entry.Key, $"too few fields for an object");
+ issuesFound = true;
+ continue;
+ }
+
+ // check min length for specific types
+ switch (fields[SObject.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0])
+ {
+ case "Cooking":
+ if (fields.Length < SObject.objectInfoBuffDurationIndex + 1)
+ {
+ LogIssue(entry.Key, "too few fields for a cooking item");
+ issuesFound = true;
+ }
+ break;
+ }
+ }
+ }
+
+ return !issuesFound;
+ }
+
/// <summary>Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available.</summary>
private void CheckForUpdateAsync()
{
@@ -369,18 +424,17 @@ namespace StardewModdingAPI
List<Action> deprecationWarnings = new List<Action>(); // queue up deprecation warnings to show after mod list
foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath))
{
+ if (this.Monitor.IsExiting)
+ {
+ this.Monitor.Log("SMAPI shutting down: aborting mod scan.", LogLevel.Warn);
+ return modsLoaded;
+ }
+
// passthrough empty directories
DirectoryInfo directory = new DirectoryInfo(directoryPath);
while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1)
directory = directory.GetDirectories().First();
- // check for cancellation
- if (this.CancellationTokenSource.IsCancellationRequested)
- {
- this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error);
- return modsLoaded;
- }
-
// get manifest path
string manifestPath = Path.Combine(directory.FullName, "manifest.json");
if (!File.Exists(manifestPath))
@@ -542,7 +596,7 @@ namespace StardewModdingAPI
// inject data
// get helper
mod.ModManifest = manifest;
- mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager);
+ mod.Helper = new ModHelper(manifest, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager, (SContentManager)Game1.content);
mod.Monitor = this.GetSecondaryMonitor(manifest.Name);
mod.PathOnDisk = directory.FullName;
@@ -604,6 +658,15 @@ namespace StardewModdingAPI
}
}
+ /// <summary>Redirect messages logged directly to the console to the given monitor.</summary>
+ /// <param name="monitor">The monitor with which to log messages.</param>
+ /// <param name="message">The message to log.</param>
+ private void HandleConsoleMessage(IMonitor monitor, string message)
+ {
+ LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace; // intercept potential exceptions
+ monitor.Log(message, level);
+ }
+
/// <summary>Show a 'press any key to exit' message, and exit when they press a key.</summary>
private void PressAnyKeyToExit()
{
@@ -617,7 +680,7 @@ namespace StardewModdingAPI
/// <param name="name">The name of the module which will log messages with this instance.</param>
private Monitor GetSecondaryMonitor(string name)
{
- return new Monitor(name, this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode };
+ return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode };
}
/// <summary>Get a human-readable name for the current platform.</summary>