diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 9ffa46a5..a09b0f73 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -13,8 +13,10 @@ using System.Text.RegularExpressions;
using System.Threading;
#if SMAPI_FOR_WINDOWS
using System.Windows.Forms;
+
#endif
using Newtonsoft.Json;
+
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Exceptions;
@@ -32,15 +34,15 @@ using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Serialisation;
using StardewModdingAPI.Toolkit.Utilities;
+
using StardewValley;
+
using Object = StardewValley.Object;
using ThreadState = System.Threading.ThreadState;
-namespace StardewModdingAPI.Framework
-{
+namespace StardewModdingAPI.Framework {
/// The core class which initialises and manages SMAPI.
- internal class SCore : IDisposable
- {
+ internal class SCore : IDisposable {
/*********
** Fields
*********/
@@ -130,8 +132,7 @@ namespace StardewModdingAPI.Framework
/// Construct an instance.
/// The path to search for mods.
/// Whether to output log messages to the console.
- public SCore(string modsPath, bool writeToConsole)
- {
+ public SCore(string modsPath, bool writeToConsole) {
// init paths
this.VerifyPath(modsPath);
this.VerifyPath(Constants.LogDir);
@@ -144,8 +145,7 @@ namespace StardewModdingAPI.Framework
// init basics
this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath));
this.LogFile = new LogFileManager(logPath);
- this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging)
- {
+ this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) {
WriteToConsole = writeToConsole,
ShowTraceInConsole = this.Settings.DeveloperMode,
ShowFullStampInConsole = this.Settings.DeveloperMode
@@ -167,8 +167,7 @@ namespace StardewModdingAPI.Framework
// validate platform
#if SMAPI_FOR_WINDOWS
- if (Constants.Platform != Platform.Windows)
- {
+ if (Constants.Platform != Platform.Windows) {
this.Monitor.Log("Oops! You're running Windows, but this version of SMAPI is for Linux or Mac. Please reinstall SMAPI to fix this.", LogLevel.Error);
this.PressAnyKeyToExit();
return;
@@ -185,11 +184,9 @@ namespace StardewModdingAPI.Framework
/// Launch SMAPI.
[HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions
- public void RunInteractively()
- {
+ public void RunInteractively() {
// initialise SMAPI
- try
- {
+ try {
#if !SMAPI_3_0_STRICT
// hook up events
ContentEvents.Init(this.EventManager);
@@ -233,6 +230,7 @@ namespace StardewModdingAPI.Framework
// apply game patches
new GamePatcher(this.Monitor).Apply(
+ new CheckEventPreconditionErrorPatch(this.MonitorForGame, this.Reflection),
new DialogueErrorPatch(this.MonitorForGame, this.Reflection),
new ObjectErrorPatch(),
new LoadForNewGamePatch(this.Reflection, this.GameInstance.OnLoadStageChanged)
@@ -242,15 +240,11 @@ namespace StardewModdingAPI.Framework
new Thread(() =>
{
this.CancellationTokenSource.Token.WaitHandle.WaitOne();
- if (this.IsGameRunning)
- {
- try
- {
+ if (this.IsGameRunning) {
+ try {
File.WriteAllText(Constants.FatalCrashMarker, string.Empty);
File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true);
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}");
}
@@ -265,22 +259,17 @@ namespace StardewModdingAPI.Framework
this.GameInstance.Window.Title += " [SMAPI 3.0 strict mode]";
Console.Title += " [SMAPI 3.0 strict mode]";
#endif
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
this.Monitor.Log($"SMAPI failed to initialise: {ex.GetLogSummary()}", LogLevel.Error);
this.PressAnyKeyToExit();
return;
}
// check update marker
- if (File.Exists(Constants.UpdateMarker))
- {
+ if (File.Exists(Constants.UpdateMarker)) {
string rawUpdateFound = File.ReadAllText(Constants.UpdateMarker);
- if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound))
- {
- if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion))
- {
+ if (SemanticVersion.TryParse(rawUpdateFound, out ISemanticVersion updateFound)) {
+ if (Constants.ApiVersion.IsPrerelease() && updateFound.IsNewerThan(Constants.ApiVersion)) {
this.Monitor.Log("A new version of SMAPI was detected last time you played.", LogLevel.Error);
this.Monitor.Log($"You can update to {updateFound}: https://smapi.io.", LogLevel.Error);
this.Monitor.Log("Press any key to continue playing anyway. (This only appears when using a SMAPI beta.)", LogLevel.Info);
@@ -291,8 +280,7 @@ namespace StardewModdingAPI.Framework
}
// show details if game crashed during last session
- if (File.Exists(Constants.FatalCrashMarker))
- {
+ if (File.Exists(Constants.FatalCrashMarker)) {
this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: https://community.playstarbound.com/threads/108375/.", LogLevel.Error);
this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://log.smapi.io.", LogLevel.Error);
this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info);
@@ -303,38 +291,29 @@ namespace StardewModdingAPI.Framework
// start game
this.Monitor.Log("Starting game...", LogLevel.Debug);
- try
- {
+ try {
this.IsGameRunning = true;
StardewValley.Program.releaseBuild = true; // game's debug logic interferes with SMAPI opening the game window
this.GameInstance.Run();
- }
- catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor"))
- {
+ } catch (InvalidOperationException ex) when (ex.Source == "Microsoft.Xna.Framework.Xact" && ex.StackTrace.Contains("Microsoft.Xna.Framework.Audio.AudioEngine..ctor")) {
this.Monitor.Log("The game couldn't load audio. Do you have speakers or headphones plugged in?", LogLevel.Error);
this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace);
this.PressAnyKeyToExit();
- }
- catch (FileNotFoundException ex) when (ex.Message == "Could not find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.") // path in error is hardcoded regardless of install path
- {
+ } catch (FileNotFoundException ex) when (ex.Message == "Could not find file 'C:\\Program Files (x86)\\Steam\\SteamApps\\common\\Stardew Valley\\Content\\XACT\\FarmerSounds.xgs'.") // path in error is hardcoded regardless of install path
+ {
this.Monitor.Log("The game can't find its Content\\XACT\\FarmerSounds.xgs file. You can usually fix this by resetting your content files (see https://smapi.io/troubleshoot#reset-content ), or by uninstalling and reinstalling the game.", LogLevel.Error);
this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace);
this.PressAnyKeyToExit();
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
this.MonitorForGame.Log($"The game failed to launch: {ex.GetLogSummary()}", LogLevel.Error);
this.PressAnyKeyToExit();
- }
- finally
- {
+ } finally {
this.Dispose();
}
}
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
- public void Dispose()
- {
+ public void Dispose() {
// skip if already disposed
if (this.IsDisposed)
return;
@@ -342,14 +321,10 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Disposing...", LogLevel.Trace);
// dispose mod data
- foreach (IModMetadata mod in this.ModRegistry.GetAll())
- {
- try
- {
+ foreach (IModMetadata mod in this.ModRegistry.GetAll()) {
+ try {
(mod.Mod as IDisposable)?.Dispose();
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
mod.LogAsMod($"Mod failed during disposal: {ex.GetLogSummary()}.", LogLevel.Warn);
}
}
@@ -371,8 +346,7 @@ namespace StardewModdingAPI.Framework
** Private methods
*********/
/// Initialise SMAPI and mods after the game starts.
- private void InitialiseAfterGameStart()
- {
+ private void InitialiseAfterGameStart() {
// add headers
#if SMAPI_3_0_STRICT
this.Monitor.Log($"You're running SMAPI 3.0 strict mode, so most mods won't work correctly. If that wasn't intended, install the normal version of SMAPI from https://smapi.io instead.", LogLevel.Warn);
@@ -412,10 +386,8 @@ namespace StardewModdingAPI.Framework
this.LoadMods(mods, this.Toolkit.JsonHelper, this.ContentCore, modDatabase);
// write metadata file
- if (this.Settings.DumpMetadata)
- {
- ModFolderExport export = new ModFolderExport
- {
+ if (this.Settings.DumpMetadata) {
+ ModFolderExport export = new ModFolderExport {
Exported = DateTime.UtcNow.ToString("O"),
ApiVersion = Constants.ApiVersion.ToString(),
GameVersion = Constants.GameVersion.ToString(),
@@ -428,8 +400,7 @@ namespace StardewModdingAPI.Framework
// check for updates
this.CheckForUpdatesAsync(mods);
}
- if (this.Monitor.IsExiting)
- {
+ if (this.Monitor.IsExiting) {
this.Monitor.Log("SMAPI shutting down: aborting initialisation.", LogLevel.Warn);
return;
}
@@ -449,8 +420,7 @@ namespace StardewModdingAPI.Framework
}
/// Handle the game changing locale.
- private void OnLocaleChanged()
- {
+ private void OnLocaleChanged() {
// get locale
string locale = this.ContentCore.GetLocale();
LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language;
@@ -462,8 +432,7 @@ namespace StardewModdingAPI.Framework
/// Run a loop handling console input.
[SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")]
- private void RunConsoleLoop()
- {
+ private void RunConsoleLoop() {
// prepare console
this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info);
this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand);
@@ -472,8 +441,7 @@ namespace StardewModdingAPI.Framework
// start handling command line input
Thread inputThread = new Thread(() =>
{
- while (true)
- {
+ while (true) {
// get input
string input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
@@ -495,8 +463,7 @@ namespace StardewModdingAPI.Framework
/// Look for common issues with the game's XNB content, and log warnings if anything looks broken or outdated.
/// Returns whether all integrity checks passed.
- private bool ValidateContentIntegrity()
- {
+ private bool ValidateContentIntegrity() {
this.Monitor.Log("Detecting common issues...", LogLevel.Trace);
bool issuesFound = false;
@@ -505,11 +472,9 @@ namespace StardewModdingAPI.Framework
// detect issues
bool hasObjectIssues = false;
void LogIssue(int id, string issue) => this.Monitor.Log($@"Detected issue: item #{id} in Content\Data\ObjectInformation.xnb is invalid ({issue}).", LogLevel.Trace);
- foreach (KeyValuePair entry in Game1.objectInformation)
- {
+ foreach (KeyValuePair entry in Game1.objectInformation) {
// must not be empty
- if (string.IsNullOrWhiteSpace(entry.Value))
- {
+ if (string.IsNullOrWhiteSpace(entry.Value)) {
LogIssue(entry.Key, "entry is empty");
hasObjectIssues = true;
continue;
@@ -517,19 +482,16 @@ namespace StardewModdingAPI.Framework
// require core fields
string[] fields = entry.Value.Split('/');
- if (fields.Length < Object.objectInfoDescriptionIndex + 1)
- {
+ if (fields.Length < Object.objectInfoDescriptionIndex + 1) {
LogIssue(entry.Key, "too few fields for an object");
hasObjectIssues = true;
continue;
}
// check min length for specific types
- switch (fields[Object.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0])
- {
+ switch (fields[Object.objectInfoTypeIndex].Split(new[] { ' ' }, 2)[0]) {
case "Cooking":
- if (fields.Length < Object.objectInfoBuffDurationIndex + 1)
- {
+ if (fields.Length < Object.objectInfoBuffDurationIndex + 1) {
LogIssue(entry.Key, "too few fields for a cooking item");
hasObjectIssues = true;
}
@@ -538,8 +500,7 @@ namespace StardewModdingAPI.Framework
}
// log error
- if (hasObjectIssues)
- {
+ if (hasObjectIssues) {
issuesFound = true;
this.Monitor.Log(@"Your Content\Data\ObjectInformation.xnb file seems to be broken or outdated.", LogLevel.Warn);
}
@@ -550,8 +511,7 @@ namespace StardewModdingAPI.Framework
/// Asynchronously check for a new version of SMAPI and any installed mods, and print alerts to the console if an update is available.
/// The mods to include in the update check (if eligible).
- private void CheckForUpdatesAsync(IModMetadata[] mods)
- {
+ private void CheckForUpdatesAsync(IModMetadata[] mods) {
if (!this.Settings.CheckForUpdates)
return;
@@ -567,32 +527,23 @@ namespace StardewModdingAPI.Framework
// check SMAPI version
ISemanticVersion updateFound = null;
- try
- {
+ try {
ModEntryModel response = client.GetModInfo(new[] { new ModSearchEntryModel("Pathoschild.SMAPI", new[] { $"GitHub:{this.Settings.GitHubProjectName}" }) }).Single().Value;
ISemanticVersion latestStable = response.Main?.Version;
ISemanticVersion latestBeta = response.Optional?.Version;
- if (latestStable == null && response.Errors.Any())
- {
+ if (latestStable == null && response.Errors.Any()) {
this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you may not be notified of new versions if this keeps happening.", LogLevel.Warn);
this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}");
- }
- else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel))
- {
+ } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) {
updateFound = latestBeta;
this.Monitor.Log($"You can update SMAPI to {latestBeta}: {Constants.HomePageUrl}", LogLevel.Alert);
- }
- else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel))
- {
+ } else if (this.IsValidUpdate(Constants.ApiVersion, latestStable, this.Settings.UseBetaChannel)) {
updateFound = latestStable;
this.Monitor.Log($"You can update SMAPI to {latestStable}: {Constants.HomePageUrl}", LogLevel.Alert);
- }
- else
+ } else
this.Monitor.Log(" SMAPI okay.", LogLevel.Trace);
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
this.Monitor.Log("Couldn't check for a new version of SMAPI. This won't affect your game, but you won't be notified of new versions if this keeps happening.", LogLevel.Warn);
this.Monitor.Log(ex is WebException && ex.InnerException == null
? $"Error: {ex.Message}"
@@ -605,16 +556,13 @@ namespace StardewModdingAPI.Framework
File.WriteAllText(Constants.UpdateMarker, updateFound.ToString());
// check mod versions
- if (mods.Any())
- {
- try
- {
+ if (mods.Any()) {
+ try {
HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase);
// prepare search model
List searchMods = new List();
- foreach (IModMetadata mod in mods)
- {
+ foreach (IModMetadata mod in mods) {
if (!mod.HasID() || suppressUpdateChecks.Contains(mod.Manifest.UniqueID))
continue;
@@ -632,16 +580,14 @@ namespace StardewModdingAPI.Framework
// extract update alerts & errors
var updates = new List>();
var errors = new StringBuilder();
- foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName))
- {
+ foreach (IModMetadata mod in mods.OrderBy(p => p.DisplayName)) {
// link to update-check data
if (!mod.HasID() || !results.TryGetValue(mod.Manifest.UniqueID, out ModEntryModel result))
continue;
mod.SetUpdateData(result);
// handle errors
- if (result.Errors != null && result.Errors.Any())
- {
+ if (result.Errors != null && result.Errors.Any()) {
errors.AppendLine(result.Errors.Length == 1
? $" {mod.DisplayName}: {result.Errors[0]}"
: $" {mod.DisplayName}:\n - {string.Join("\n - ", result.Errors)}"
@@ -669,23 +615,18 @@ namespace StardewModdingAPI.Framework
this.Monitor.Log("Got update-check errors for some mods:\n" + errors.ToString().TrimEnd(), LogLevel.Trace);
// show update alerts
- if (updates.Any())
- {
+ if (updates.Any()) {
this.Monitor.Newline();
this.Monitor.Log($"You can update {updates.Count} mod{(updates.Count != 1 ? "s" : "")}:", LogLevel.Alert);
- foreach (var entry in updates)
- {
+ foreach (var entry in updates) {
IModMetadata mod = entry.Item1;
ISemanticVersion newVersion = entry.Item2;
string newUrl = entry.Item3;
this.Monitor.Log($" {mod.DisplayName} {newVersion}: {newUrl}", LogLevel.Alert);
}
- }
- else
+ } else
this.Monitor.Log(" All mods up to date.", LogLevel.Trace);
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
this.Monitor.Log("Couldn't check for new mod versions. This won't affect your game, but you won't be notified of mod updates if this keeps happening.", LogLevel.Warn);
this.Monitor.Log(ex is WebException && ex.InnerException == null
? ex.Message
@@ -700,8 +641,7 @@ namespace StardewModdingAPI.Framework
/// The current semantic version.
/// The target semantic version.
/// Whether the user enabled the beta channel and should be offered pre-release updates.
- private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel)
- {
+ private bool IsValidUpdate(ISemanticVersion currentVersion, ISemanticVersion newVersion, bool useBetaChannel) {
return
newVersion != null
&& newVersion.IsNewerThan(currentVersion)
@@ -710,15 +650,11 @@ namespace StardewModdingAPI.Framework
/// Create a directory path if it doesn't exist.
/// The directory path.
- private void VerifyPath(string path)
- {
- try
- {
+ private void VerifyPath(string path) {
+ try {
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
// note: this happens before this.Monitor is initialised
Console.WriteLine($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}");
}
@@ -729,27 +665,23 @@ namespace StardewModdingAPI.Framework
/// The JSON helper with which to read mods' JSON files.
/// The content manager to use for mod content.
/// Handles access to SMAPI's internal mod metadata list.
- private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase)
- {
+ private void LoadMods(IModMetadata[] mods, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase) {
this.Monitor.Log("Loading mods...", LogLevel.Trace);
// load mods
IDictionary> skippedMods = new Dictionary>();
- using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings))
- {
+ using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings)) {
// init
HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.InvariantCultureIgnoreCase);
InterfaceProxyFactory proxyFactory = new InterfaceProxyFactory();
- void LogSkip(IModMetadata mod, string errorPhrase, string errorDetails)
- {
+ void LogSkip(IModMetadata mod, string errorPhrase, string errorDetails) {
skippedMods[mod] = Tuple.Create(errorPhrase, errorDetails);
if (mod.Status != ModMetadataStatus.Failed)
mod.SetStatus(ModMetadataStatus.Failed, errorPhrase);
}
// load mods
- foreach (IModMetadata contentPack in mods)
- {
+ foreach (IModMetadata contentPack in mods) {
if (!this.TryLoadMod(contentPack, mods, modAssemblyLoader, proxyFactory, jsonHelper, contentCore, modDatabase, suppressUpdateChecks, out string errorPhrase, out string errorDetails))
LogSkip(contentPack, errorPhrase, errorDetails);
}
@@ -762,8 +694,7 @@ namespace StardewModdingAPI.Framework
// log loaded mods
this.Monitor.Log($"Loaded {loadedMods.Length} mods" + (loadedMods.Length > 0 ? ":" : "."), LogLevel.Info);
- foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName))
- {
+ foreach (IModMetadata metadata in loadedMods.OrderBy(p => p.DisplayName)) {
IManifest manifest = metadata.Manifest;
this.Monitor.Log(
$" {metadata.DisplayName} {manifest.Version}"
@@ -775,13 +706,11 @@ namespace StardewModdingAPI.Framework
this.Monitor.Newline();
// log loaded content packs
- if (loadedContentPacks.Any())
- {
+ if (loadedContentPacks.Any()) {
string GetModDisplayName(string id) => loadedMods.FirstOrDefault(p => p.HasID(id))?.DisplayName;
this.Monitor.Log($"Loaded {loadedContentPacks.Length} content packs:", LogLevel.Info);
- foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName))
- {
+ foreach (IModMetadata metadata in loadedContentPacks.OrderBy(p => p.DisplayName)) {
IManifest manifest = metadata.Manifest;
this.Monitor.Log(
$" {metadata.DisplayName} {manifest.Version}"
@@ -801,11 +730,9 @@ namespace StardewModdingAPI.Framework
this.ReloadTranslations(loadedMods);
// initialise loaded non-content-pack mods
- foreach (IModMetadata metadata in loadedMods)
- {
+ foreach (IModMetadata metadata in loadedMods) {
// add interceptors
- if (metadata.Mod.Helper.Content is ContentHelper helper)
- {
+ if (metadata.Mod.Helper.Content is ContentHelper helper) {
// ReSharper disable SuspiciousTypeConversion.Global
if (metadata.Mod is IAssetEditor editor)
helper.ObservableAssetEditors.Add(editor);
@@ -818,22 +745,17 @@ namespace StardewModdingAPI.Framework
}
// call entry method
- try
- {
+ try {
IMod mod = metadata.Mod;
mod.Entry(mod.Helper);
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
metadata.LogAsMod($"Mod crashed on entry and might not work correctly. Technical details:\n{ex.GetLogSummary()}", LogLevel.Error);
}
// get mod API
- try
- {
+ try {
object api = metadata.Mod.GetApi();
- if (api != null && !api.GetType().IsPublic)
- {
+ if (api != null && !api.GetType().IsPublic) {
api = null;
this.Monitor.Log($"{metadata.DisplayName} provides an API instance with a non-public type. This isn't currently supported, so the API won't be available to other mods.", LogLevel.Warn);
}
@@ -841,31 +763,25 @@ namespace StardewModdingAPI.Framework
if (api != null)
this.Monitor.Log($" Found mod-provided API ({api.GetType().FullName}).", LogLevel.Trace);
metadata.SetApi(api);
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
this.Monitor.Log($"Failed loading mod-provided API for {metadata.DisplayName}. Integrations with other mods may not work. Error: {ex.GetLogSummary()}", LogLevel.Error);
}
}
// invalidate cache entries when needed
// (These listeners are registered after Entry to avoid repeatedly reloading assets as mods initialise.)
- foreach (IModMetadata metadata in loadedMods)
- {
- if (metadata.Mod.Helper.Content is ContentHelper helper)
- {
+ foreach (IModMetadata metadata in loadedMods) {
+ if (metadata.Mod.Helper.Content is ContentHelper helper) {
helper.ObservableAssetEditors.CollectionChanged += (sender, e) =>
{
- if (e.NewItems?.Count > 0)
- {
+ if (e.NewItems?.Count > 0) {
this.Monitor.Log("Invalidating cache entries for new asset editors...", LogLevel.Trace);
this.ContentCore.InvalidateCacheFor(e.NewItems.Cast().ToArray(), new IAssetLoader[0]);
}
};
helper.ObservableAssetLoaders.CollectionChanged += (sender, e) =>
{
- if (e.NewItems?.Count > 0)
- {
+ if (e.NewItems?.Count > 0) {
this.Monitor.Log("Invalidating cache entries for new asset loaders...", LogLevel.Trace);
this.ContentCore.InvalidateCacheFor(new IAssetEditor[0], e.NewItems.Cast().ToArray());
}
@@ -876,8 +792,7 @@ namespace StardewModdingAPI.Framework
// reset cache now if any editors or loaders were added during entry
IAssetEditor[] editors = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetEditors).ToArray();
IAssetLoader[] loaders = loadedMods.SelectMany(p => p.Mod.Helper.Content.AssetLoaders).ToArray();
- if (editors.Any() || loaders.Any())
- {
+ if (editors.Any() || loaders.Any()) {
this.Monitor.Log("Invalidating cached assets for new editors & loaders...", LogLevel.Trace);
this.ContentCore.InvalidateCacheFor(editors, loaders);
}
@@ -898,8 +813,7 @@ namespace StardewModdingAPI.Framework
/// The user-facing reason phrase explaining why the mod couldn't be loaded (if applicable).
/// More detailed details about the error intended for developers (if any).
/// Returns whether the mod was successfully loaded.
- private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails)
- {
+ private bool TryLoadMod(IModMetadata mod, IModMetadata[] mods, AssemblyLoader assemblyLoader, InterfaceProxyFactory proxyFactory, JsonHelper jsonHelper, ContentCoordinator contentCore, ModDatabase modDatabase, HashSet suppressUpdateChecks, out string errorReasonPhrase, out string errorDetails) {
errorDetails = null;
// log entry
@@ -918,8 +832,7 @@ namespace StardewModdingAPI.Framework
mod.SetWarning(ModWarning.NoUpdateKeys);
// validate status
- if (mod.Status == ModMetadataStatus.Failed)
- {
+ if (mod.Status == ModMetadataStatus.Failed) {
this.Monitor.Log($" Failed: {mod.Error}", LogLevel.Trace);
errorReasonPhrase = mod.Error;
return false;
@@ -935,12 +848,9 @@ namespace StardewModdingAPI.Framework
// validate dependencies
// Although dependences are validated before mods are loaded, a dependency may have failed to load.
- if (mod.Manifest.Dependencies?.Any() == true)
- {
- foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired))
- {
- if (this.ModRegistry.Get(dependency.UniqueID) == null)
- {
+ if (mod.Manifest.Dependencies?.Any() == true) {
+ foreach (IManifestDependency dependency in mod.Manifest.Dependencies.Where(p => p.IsRequired)) {
+ if (this.ModRegistry.Get(dependency.UniqueID) == null) {
string dependencyName = mods
.FirstOrDefault(otherMod => otherMod.HasID(dependency.UniqueID))
?.DisplayName ?? dependency.UniqueID;
@@ -951,8 +861,7 @@ namespace StardewModdingAPI.Framework
}
// load as content pack
- if (mod.IsContentPack)
- {
+ if (mod.IsContentPack) {
IManifest manifest = mod.Manifest;
IMonitor monitor = this.GetSecondaryMonitor(mod.DisplayName);
IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor);
@@ -965,8 +874,7 @@ namespace StardewModdingAPI.Framework
}
// load as mod
- else
- {
+ else {
IManifest manifest = mod.Manifest;
// load mod
@@ -974,39 +882,31 @@ namespace StardewModdingAPI.Framework
? Path.Combine(mod.DirectoryPath, manifest.EntryDll)
: null;
Assembly modAssembly;
- try
- {
+ try {
modAssembly = assemblyLoader.Load(mod, assemblyPath, assumeCompatible: mod.DataRecord?.Status == ModStatus.AssumeCompatible);
this.ModRegistry.TrackAssemblies(mod, modAssembly);
- }
- catch (IncompatibleInstructionException) // details already in trace logs
- {
+ } catch (IncompatibleInstructionException) // details already in trace logs
+ {
string[] updateUrls = new[] { modDatabase.GetModPageUrlFor(manifest.UniqueID), "https://mods.smapi.io" }.Where(p => p != null).ToArray();
errorReasonPhrase = $"it's no longer compatible. Please check for a new version at {string.Join(" or ", updateUrls)}";
return false;
- }
- catch (SAssemblyLoadFailedException ex)
- {
+ } catch (SAssemblyLoadFailedException ex) {
errorReasonPhrase = $"it DLL couldn't be loaded: {ex.Message}";
return false;
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
errorReasonPhrase = "its DLL couldn't be loaded.";
errorDetails = $"Error: {ex.GetLogSummary()}";
return false;
}
// initialise mod
- try
- {
+ try {
// get mod instance
if (!this.TryLoadModEntry(modAssembly, out Mod modEntry, out errorReasonPhrase))
return false;
// get content packs
- IContentPack[] GetContentPacks()
- {
+ IContentPack[] GetContentPacks() {
if (!this.ModRegistry.AreAllModsLoaded)
throw new InvalidOperationException("Can't access content packs before SMAPI finishes loading mods.");
@@ -1031,8 +931,7 @@ namespace StardewModdingAPI.Framework
IMultiplayerHelper multiplayerHelper = new MultiplayerHelper(manifest.UniqueID, this.GameInstance.Multiplayer);
ITranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, manifest.Name, contentCore.GetLocale(), contentCore.Language);
- IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest)
- {
+ IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) {
IMonitor packMonitor = this.GetSecondaryMonitor(packManifest.Name);
IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor);
return new ContentPack(packDirPath, packManifest, packContentHelper, this.Toolkit.JsonHelper);
@@ -1050,9 +949,7 @@ namespace StardewModdingAPI.Framework
mod.SetMod(modEntry);
this.ModRegistry.Add(mod);
return true;
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
errorReasonPhrase = $"initialisation failed:\n{ex.GetLogSummary()}";
return false;
}
@@ -1062,8 +959,7 @@ namespace StardewModdingAPI.Framework
/// Write a summary of mod warnings to the console and log.
/// The loaded mods.
/// The mods which were skipped, along with the friendly and developer reasons.
- private void LogModWarnings(IModMetadata[] mods, IDictionary> skippedMods)
- {
+ private void LogModWarnings(IModMetadata[] mods, IDictionary> skippedMods) {
// get mods with warnings
IModMetadata[] modsWithWarnings = mods.Where(p => p.Warnings != ModWarning.None).ToArray();
if (!modsWithWarnings.Any() && !skippedMods.Any())
@@ -1076,16 +972,14 @@ namespace StardewModdingAPI.Framework
}
// log skipped mods
- if (skippedMods.Any())
- {
+ if (skippedMods.Any()) {
this.Monitor.Log(" Skipped mods", LogLevel.Error);
this.Monitor.Log(" " + "".PadRight(50, '-'), LogLevel.Error);
this.Monitor.Log(" These mods could not be added to your game.", LogLevel.Error);
this.Monitor.Newline();
HashSet logged = new HashSet();
- foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName))
- {
+ foreach (var pair in skippedMods.OrderBy(p => p.Key.DisplayName)) {
IModMetadata mod = pair.Key;
string errorReason = pair.Value.Item1;
string errorDetails = pair.Value.Item2;
@@ -1102,11 +996,9 @@ namespace StardewModdingAPI.Framework
}
// log warnings
- if (modsWithWarnings.Any())
- {
+ if (modsWithWarnings.Any()) {
// issue block format logic
- void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb)
- {
+ void LogWarningGroup(ModWarning warning, LogLevel logLevel, string heading, params string[] blurb) {
IModMetadata[] matches = modsWithWarnings
.Where(mod => mod.HasUnsuppressWarning(warning))
.ToArray();
@@ -1132,8 +1024,7 @@ namespace StardewModdingAPI.Framework
"These mods change the save serialiser. They may corrupt your save files, or make them unusable if",
"you uninstall these mods."
);
- if (this.Settings.ParanoidWarnings)
- {
+ if (this.Settings.ParanoidWarnings) {
LogWarningGroup(ModWarning.AccessesFilesystem, LogLevel.Warn, "Accesses filesystem directly",
"These mods directly access the filesystem, and you enabled paranoid warnings. (Note that this may be",
"legitimate and innocent usage; this warning is meaningless without further investigation.)"
@@ -1166,27 +1057,23 @@ namespace StardewModdingAPI.Framework
/// The loaded instance.
/// The error indicating why loading failed (if applicable).
/// Returns whether the mod entry class was successfully loaded.
- private bool TryLoadModEntry(Assembly modAssembly, out Mod mod, out string error)
- {
+ private bool TryLoadModEntry(Assembly modAssembly, out Mod mod, out string error) {
mod = null;
// find type
TypeInfo[] modEntries = modAssembly.DefinedTypes.Where(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract).Take(2).ToArray();
- if (modEntries.Length == 0)
- {
+ if (modEntries.Length == 0) {
error = $"its DLL has no '{nameof(Mod)}' subclass.";
return false;
}
- if (modEntries.Length > 1)
- {
+ if (modEntries.Length > 1) {
error = $"its DLL contains multiple '{nameof(Mod)}' subclasses.";
return false;
}
// get implementation
mod = (Mod)modAssembly.CreateInstance(modEntries[0].ToString());
- if (mod == null)
- {
+ if (mod == null) {
error = "its entry class couldn't be instantiated.";
return false;
}
@@ -1197,42 +1084,33 @@ namespace StardewModdingAPI.Framework
/// Reload translations for all mods.
/// The mods for which to reload translations.
- private void ReloadTranslations(IEnumerable mods)
- {
+ private void ReloadTranslations(IEnumerable mods) {
JsonHelper jsonHelper = this.Toolkit.JsonHelper;
- foreach (IModMetadata metadata in mods)
- {
+ foreach (IModMetadata metadata in mods) {
if (metadata.IsContentPack)
throw new InvalidOperationException("Can't reload translations for a content pack.");
// read translation files
IDictionary> translations = new Dictionary>();
DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n"));
- if (translationsDir.Exists)
- {
- foreach (FileInfo file in translationsDir.EnumerateFiles("*.json"))
- {
+ if (translationsDir.Exists) {
+ foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) {
string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim());
- try
- {
+ try {
if (jsonHelper.ReadJsonFileIfExists(file.FullName, out IDictionary data))
translations[locale] = data;
else
metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed.", LogLevel.Warn);
- }
- catch (Exception ex)
- {
+ } catch (Exception ex) {
metadata.LogAsMod($"Mod's i18n/{locale}.json file couldn't be parsed: {ex.GetLogSummary()}", LogLevel.Warn);
}
}
}
// validate translations
- foreach (string locale in translations.Keys.ToArray())
- {
+ foreach (string locale in translations.Keys.ToArray()) {
// skip empty files
- if (translations[locale] == null || !translations[locale].Keys.Any())
- {
+ if (translations[locale] == null || !translations[locale].Keys.Any()) {
metadata.LogAsMod($"Mod's i18n/{locale}.json is empty and will be ignored.", LogLevel.Warn);
translations.Remove(locale);
continue;
@@ -1241,10 +1119,8 @@ namespace StardewModdingAPI.Framework
// handle duplicates
HashSet keys = new HashSet(StringComparer.InvariantCultureIgnoreCase);
HashSet duplicateKeys = new HashSet(StringComparer.InvariantCultureIgnoreCase);
- foreach (string key in translations[locale].Keys.ToArray())
- {
- if (!keys.Add(key))
- {
+ foreach (string key in translations[locale].Keys.ToArray()) {
+ if (!keys.Add(key)) {
duplicateKeys.Add(key);
translations[locale].Remove(key);
}
@@ -1262,25 +1138,19 @@ namespace StardewModdingAPI.Framework
/// The method called when the user submits a core SMAPI command in the console.
/// The command name.
/// The command arguments.
- private void HandleCommand(string name, string[] arguments)
- {
- switch (name)
- {
+ private void HandleCommand(string name, string[] arguments) {
+ switch (name) {
case "help":
- if (arguments.Any())
- {
+ if (arguments.Any()) {
Command result = this.GameInstance.CommandManager.Get(arguments[0]);
if (result == null)
this.Monitor.Log("There's no command with that name.", LogLevel.Error);
else
this.Monitor.Log($"{result.Name}: {result.Documentation}{(result.Mod != null ? $"\n(Added by {result.Mod.DisplayName}.)" : "")}", LogLevel.Info);
- }
- else
- {
+ } else {
string message = "The following commands are registered:\n";
IGrouping[] groups = (from command in this.GameInstance.CommandManager.GetAll() orderby command.Mod?.DisplayName, command.Name group command.Name by command.Mod?.DisplayName).ToArray();
- foreach (var group in groups)
- {
+ foreach (var group in groups) {
string modName = group.Key ?? "SMAPI";
string[] commandNames = group.ToArray();
message += $"{modName}:\n {string.Join("\n ", commandNames)}\n\n";
@@ -1304,8 +1174,7 @@ namespace StardewModdingAPI.Framework
/// Redirect messages logged directly to the console to the given monitor.
/// The monitor with which to log messages as the game.
/// The message to log.
- private void HandleConsoleMessage(IMonitor gameMonitor, string message)
- {
+ private void HandleConsoleMessage(IMonitor gameMonitor, string message) {
// detect exception
LogLevel level = message.Contains("Exception") ? LogLevel.Error : LogLevel.Trace;
@@ -1314,10 +1183,8 @@ namespace StardewModdingAPI.Framework
return;
// show friendly error if applicable
- foreach (var entry in this.ReplaceConsolePatterns)
- {
- if (entry.Item1.IsMatch(message))
- {
+ foreach (var entry in this.ReplaceConsolePatterns) {
+ if (entry.Item1.IsMatch(message)) {
this.Monitor.Log(entry.Item2, entry.Item3);
gameMonitor.Log(message, LogLevel.Trace);
return;
@@ -1329,16 +1196,14 @@ namespace StardewModdingAPI.Framework
}
/// Show a 'press any key to exit' message, and exit when they press a key.
- private void PressAnyKeyToExit()
- {
+ private void PressAnyKeyToExit() {
this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info);
this.PressAnyKeyToExit(showMessage: false);
}
/// Show a 'press any key to exit' message, and exit when they press a key.
/// Whether to print a 'press any key to exit' message to the console.
- private void PressAnyKeyToExit(bool showMessage)
- {
+ private void PressAnyKeyToExit(bool showMessage) {
if (showMessage)
Console.WriteLine("Game has ended. Press any key to exit.");
Thread.Sleep(100);
@@ -1348,10 +1213,8 @@ namespace StardewModdingAPI.Framework
/// Get a monitor instance derived from SMAPI's current settings.
/// The name of the module which will log messages with this instance.
- private Monitor GetSecondaryMonitor(string name)
- {
- return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging)
- {
+ private Monitor GetSecondaryMonitor(string name) {
+ return new Monitor(name, this.ConsoleManager, this.LogFile, this.CancellationTokenSource, this.Settings.ColorScheme, this.Settings.VerboseLogging) {
WriteToConsole = this.Monitor.WriteToConsole,
ShowTraceInConsole = this.Settings.DeveloperMode,
ShowFullStampInConsole = this.Settings.DeveloperMode
@@ -1359,8 +1222,7 @@ namespace StardewModdingAPI.Framework
}
/// Get the absolute path to the next available log file.
- private string GetLogPath()
- {
+ private string GetLogPath() {
// default path
{
FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}"));
@@ -1369,8 +1231,7 @@ namespace StardewModdingAPI.Framework
}
// get first disambiguated path
- for (int i = 2; i < int.MaxValue; i++)
- {
+ for (int i = 2; i < int.MaxValue; i++) {
FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}"));
if (!file.Exists)
return file.FullName;
@@ -1381,14 +1242,12 @@ namespace StardewModdingAPI.Framework
}
/// Delete normal (non-crash) log files created by SMAPI.
- private void PurgeNormalLogs()
- {
+ private void PurgeNormalLogs() {
DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir);
if (!logsDir.Exists)
return;
- foreach (FileInfo logFile in logsDir.EnumerateFiles())
- {
+ foreach (FileInfo logFile in logsDir.EnumerateFiles()) {
// skip non-SMAPI file
if (!logFile.Name.StartsWith(Constants.LogNamePrefix, StringComparison.InvariantCultureIgnoreCase))
continue;
@@ -1398,12 +1257,9 @@ namespace StardewModdingAPI.Framework
continue;
// delete file
- try
- {
+ try {
FileUtilities.ForceDelete(logFile);
- }
- catch (IOException)
- {
+ } catch (IOException) {
// ignore file if it's in use
}
}
diff --git a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs
new file mode 100644
index 00000000..63eca5d7
--- /dev/null
+++ b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs
@@ -0,0 +1,77 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+using Harmony;
+
+using StardewModdingAPI.Framework.Patching;
+using StardewModdingAPI.Framework.Reflection;
+
+using StardewValley;
+
+namespace StardewModdingAPI.Patches {
+ /// A Harmony patch for the constructor which intercepts invalid dialogue lines and logs an error instead of crashing.
+ internal class CheckEventPreconditionErrorPatch : IHarmonyPatch {
+ /*********
+ ** Private methods
+ *********/
+ /// Writes messages to the console and log file on behalf of the game.
+ private static IMonitor MonitorForGame;
+
+ /// Local variable to store the patched method.
+ private static MethodInfo method;
+ /// Local variable to check if the method was already arbitrated.
+ private static bool isArbitrated;
+
+
+ /*********
+ ** Accessors
+ *********/
+ /// A unique name for this patch.
+ public string Name => $"{nameof(CheckEventPreconditionErrorPatch)}";
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// Construct an instance.
+ /// Writes messages to the console and log file on behalf of the game.
+ /// Simplifies access to private code.
+ public CheckEventPreconditionErrorPatch(IMonitor monitorForGame, Reflector reflector) {
+ CheckEventPreconditionErrorPatch.MonitorForGame = monitorForGame;
+ }
+
+
+ /// Apply the Harmony patch.
+ /// The Harmony instance.
+ public void Apply(HarmonyInstance harmony) {
+ method = AccessTools.Method(typeof(GameLocation), "checkEventPrecondition");
+ MethodInfo transpiler = AccessTools.Method(this.GetType(), nameof(CheckEventPreconditionErrorPatch.Prefix));
+ harmony.Patch(method, new HarmonyMethod(transpiler));
+ }
+
+ /*********
+ ** Private methods
+ *********/
+ /// The method to call instead of the GameLocation.CheckEventPrecondition.
+ /// The instance being patched.
+ /// The precondition to be parsed.
+ /// Returns whether to execute the original method.
+ /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments.
+ [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony.")]
+ private static bool Prefix(GameLocation __instance, string precondition) {
+ if (isArbitrated) {
+ isArbitrated = false;
+ return true;
+ } else {
+ isArbitrated = true;
+ try {
+ method.Invoke(__instance, new object[] { precondition });
+ } catch (System.Exception ex) {
+ CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event info. Event precondition: {precondition}\n{ex}", LogLevel.Error);
+ }
+
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj
index b6562eca..5991dd7e 100644
--- a/src/SMAPI/StardewModdingAPI.csproj
+++ b/src/SMAPI/StardewModdingAPI.csproj
@@ -330,6 +330,7 @@
+