From accaa6c0e03e99411d7b10f938dd17b142b8eedc Mon Sep 17 00:00:00 2001 From: berkay2578 Date: Sun, 14 Apr 2019 12:29:23 +0300 Subject: checkEventPrecondition crash fix --- src/SMAPI/Framework/SCore.cs | 418 +++++++-------------- .../Patches/CheckEventPreconditionErrorPatch.cs | 77 ++++ src/SMAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 215 insertions(+), 281 deletions(-) create mode 100644 src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs (limited to 'src') 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 @@ + -- cgit From a13af946e21b12953454540ef4e3ef54dbe50f2d Mon Sep 17 00:00:00 2001 From: berkay2578 Date: Sun, 14 Apr 2019 19:29:47 +0300 Subject: Implement the return value of the original method --- src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs index 63eca5d7..54168969 100644 --- a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs +++ b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs @@ -17,8 +17,8 @@ namespace StardewModdingAPI.Patches { /// 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 store the original method. + private static MethodInfo originalMethod; /// Local variable to check if the method was already arbitrated. private static bool isArbitrated; @@ -44,9 +44,8 @@ namespace StardewModdingAPI.Patches { /// 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)); + originalMethod = AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"); + harmony.Patch(originalMethod, new HarmonyMethod(AccessTools.Method(this.GetType(), "Prefix"))); } /********* @@ -54,19 +53,22 @@ namespace StardewModdingAPI.Patches { *********/ /// The method to call instead of the GameLocation.CheckEventPrecondition. /// The instance being patched. + /// The return value of the original method. /// 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) { + private static bool Prefix(GameLocation __instance, ref int __result, string precondition) { if (isArbitrated) { isArbitrated = false; return true; } else { isArbitrated = true; try { - method.Invoke(__instance, new object[] { precondition }); + object _ = originalMethod.Invoke(__instance, new object[] { precondition }); + __result = _ is null ? -1 : (int)_; } catch (System.Exception ex) { + __result = -1; CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event info. Event precondition: {precondition}\n{ex}", LogLevel.Error); } -- cgit From 05bfd33ac4d4d089ed88fddcc8f8304ffb99f1f4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Apr 2019 21:17:51 -0400 Subject: fix formatting and code style (#636) --- src/SMAPI/Framework/SCore.cs | 419 ++++++++++++++------- .../Patches/CheckEventPreconditionErrorPatch.cs | 59 +-- src/SMAPI/Patches/DialogueErrorPatch.cs | 2 +- src/SMAPI/Patches/LoadForNewGamePatch.cs | 2 +- 4 files changed, 317 insertions(+), 165 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a09b0f73..9090ca71 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -13,10 +13,8 @@ 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; @@ -34,15 +32,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 *********/ @@ -132,7 +130,8 @@ 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); @@ -145,7 +144,8 @@ 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,7 +167,8 @@ 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; @@ -184,9 +185,11 @@ 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); @@ -230,7 +233,7 @@ namespace StardewModdingAPI.Framework { // apply game patches new GamePatcher(this.Monitor).Apply( - new CheckEventPreconditionErrorPatch(this.MonitorForGame, this.Reflection), + new CheckEventPreconditionErrorPatch(this.MonitorForGame), new DialogueErrorPatch(this.MonitorForGame, this.Reflection), new ObjectErrorPatch(), new LoadForNewGamePatch(this.Reflection, this.GameInstance.OnLoadStageChanged) @@ -240,11 +243,15 @@ 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()}"); } @@ -259,17 +266,22 @@ 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); @@ -280,7 +292,8 @@ 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); @@ -291,29 +304,38 @@ 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; @@ -321,10 +343,14 @@ 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); } } @@ -346,7 +372,8 @@ 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); @@ -386,8 +413,10 @@ 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(), @@ -400,7 +429,8 @@ 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; } @@ -420,7 +450,8 @@ 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; @@ -432,7 +463,8 @@ 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); @@ -441,7 +473,8 @@ 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)) @@ -463,7 +496,8 @@ 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; @@ -472,9 +506,11 @@ 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; @@ -482,16 +518,19 @@ 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; } @@ -500,7 +539,8 @@ 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); } @@ -511,7 +551,8 @@ 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; @@ -527,23 +568,32 @@ 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}" @@ -556,13 +606,16 @@ 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; @@ -580,14 +633,16 @@ 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)}" @@ -615,18 +670,23 @@ 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 @@ -641,7 +701,8 @@ 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) @@ -650,11 +711,15 @@ 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()}"); } @@ -665,23 +730,27 @@ 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); } @@ -694,7 +763,8 @@ 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}" @@ -706,11 +776,13 @@ 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}" @@ -730,9 +802,11 @@ 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); @@ -745,17 +819,22 @@ 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); } @@ -763,25 +842,31 @@ 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()); } @@ -792,7 +877,8 @@ 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); } @@ -813,7 +899,8 @@ 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 @@ -832,7 +919,8 @@ 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; @@ -848,9 +936,12 @@ 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; @@ -861,7 +952,8 @@ 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); @@ -874,7 +966,8 @@ namespace StardewModdingAPI.Framework { } // load as mod - else { + else + { IManifest manifest = mod.Manifest; // load mod @@ -882,31 +975,39 @@ 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."); @@ -931,7 +1032,8 @@ 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); @@ -949,7 +1051,9 @@ 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; } @@ -959,7 +1063,8 @@ 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()) @@ -972,14 +1077,16 @@ 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; @@ -996,9 +1103,11 @@ 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(); @@ -1024,7 +1133,8 @@ 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.)" @@ -1057,23 +1167,27 @@ 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; } @@ -1084,33 +1198,42 @@ 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; @@ -1119,8 +1242,10 @@ 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); } @@ -1138,19 +1263,25 @@ 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"; @@ -1174,7 +1305,8 @@ 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; @@ -1183,8 +1315,10 @@ 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; @@ -1196,14 +1330,16 @@ 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); @@ -1213,8 +1349,10 @@ 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 @@ -1222,7 +1360,8 @@ 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}")); @@ -1231,7 +1370,8 @@ 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; @@ -1242,12 +1382,14 @@ 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; @@ -1257,9 +1399,12 @@ 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 index 54168969..b62cf41c 100644 --- a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs +++ b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs @@ -1,26 +1,25 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; - using Harmony; - using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.Reflection; - using StardewValley; -namespace StardewModdingAPI.Patches { +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 { + internal class CheckEventPreconditionErrorPatch : IHarmonyPatch + { /********* - ** Private methods + ** Fields *********/ /// Writes messages to the console and log file on behalf of the game. private static IMonitor MonitorForGame; - /// Local variable to store the original method. - private static MethodInfo originalMethod; - /// Local variable to check if the method was already arbitrated. - private static bool isArbitrated; + /// The method being wrapped. + private static MethodInfo OriginalMethod; + + /// Whether the method is currently being intercepted. + private static bool IsArbitrated; /********* @@ -35,19 +34,20 @@ namespace StardewModdingAPI.Patches { *********/ /// 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) { + public CheckEventPreconditionErrorPatch(IMonitor monitorForGame) + { CheckEventPreconditionErrorPatch.MonitorForGame = monitorForGame; } - /// Apply the Harmony patch. /// The Harmony instance. - public void Apply(HarmonyInstance harmony) { - originalMethod = AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"); - harmony.Patch(originalMethod, new HarmonyMethod(AccessTools.Method(this.GetType(), "Prefix"))); + public void Apply(HarmonyInstance harmony) + { + CheckEventPreconditionErrorPatch.OriginalMethod = AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"); + harmony.Patch(CheckEventPreconditionErrorPatch.OriginalMethod, new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(CheckEventPreconditionErrorPatch.Prefix)))); } + /********* ** Private methods *********/ @@ -58,16 +58,23 @@ namespace StardewModdingAPI.Patches { /// 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, ref int __result, string precondition) { - if (isArbitrated) { - isArbitrated = false; + private static bool Prefix(GameLocation __instance, ref int __result, string precondition) + { + if (CheckEventPreconditionErrorPatch.IsArbitrated) + { + CheckEventPreconditionErrorPatch.IsArbitrated = false; return true; - } else { - isArbitrated = true; - try { - object _ = originalMethod.Invoke(__instance, new object[] { precondition }); - __result = _ is null ? -1 : (int)_; - } catch (System.Exception ex) { + } + else + { + CheckEventPreconditionErrorPatch.IsArbitrated = true; + try + { + object isValid = CheckEventPreconditionErrorPatch.OriginalMethod.Invoke(__instance, new object[] { precondition }); + __result = isValid is null ? -1 : (int)isValid; + } + catch (System.Exception ex) + { __result = -1; CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event info. Event precondition: {precondition}\n{ex}", LogLevel.Error); } diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index d8905fd1..5af6ceef 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -13,7 +13,7 @@ namespace StardewModdingAPI.Patches internal class DialogueErrorPatch : IHarmonyPatch { /********* - ** Private methods + ** Fields *********/ /// Writes messages to the console and log file on behalf of the game. private static IMonitor MonitorForGame; diff --git a/src/SMAPI/Patches/LoadForNewGamePatch.cs b/src/SMAPI/Patches/LoadForNewGamePatch.cs index 9e788e84..f4ce2023 100644 --- a/src/SMAPI/Patches/LoadForNewGamePatch.cs +++ b/src/SMAPI/Patches/LoadForNewGamePatch.cs @@ -16,7 +16,7 @@ namespace StardewModdingAPI.Patches internal class LoadForNewGamePatch : IHarmonyPatch { /********* - ** Accessors + ** Fields *********/ /// Simplifies access to private code. private static Reflector Reflection; -- cgit From bac92d6f26af84f3492db1fc8d1724155d6245ee Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Apr 2019 21:32:49 -0400 Subject: log underlying error (#636) --- src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs index b62cf41c..673fae1c 100644 --- a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs +++ b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs @@ -70,16 +70,15 @@ namespace StardewModdingAPI.Patches CheckEventPreconditionErrorPatch.IsArbitrated = true; try { - object isValid = CheckEventPreconditionErrorPatch.OriginalMethod.Invoke(__instance, new object[] { precondition }); - __result = isValid is null ? -1 : (int)isValid; + __result = (int)CheckEventPreconditionErrorPatch.OriginalMethod.Invoke(__instance, new object[] { precondition }); + return false; } - catch (System.Exception ex) + catch (TargetInvocationException ex) { __result = -1; - CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event info. Event precondition: {precondition}\n{ex}", LogLevel.Error); + CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + return false; } - - return false; } } } -- cgit From 9732ddb2759774e6b22c9414e3f5341865257270 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Apr 2019 21:55:11 -0400 Subject: avoid possible invalid state if checkEventPrecondition is called asynchronously (#636) --- .../Patches/CheckEventPreconditionErrorPatch.cs | 45 +++++++++++----------- 1 file changed, 22 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs index 673fae1c..74cfbfb0 100644 --- a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs +++ b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs @@ -15,11 +15,8 @@ namespace StardewModdingAPI.Patches /// Writes messages to the console and log file on behalf of the game. private static IMonitor MonitorForGame; - /// The method being wrapped. - private static MethodInfo OriginalMethod; - /// Whether the method is currently being intercepted. - private static bool IsArbitrated; + private static bool IsIntercepted; /********* @@ -43,8 +40,10 @@ namespace StardewModdingAPI.Patches /// The Harmony instance. public void Apply(HarmonyInstance harmony) { - CheckEventPreconditionErrorPatch.OriginalMethod = AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"); - harmony.Patch(CheckEventPreconditionErrorPatch.OriginalMethod, new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(CheckEventPreconditionErrorPatch.Prefix)))); + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + prefix: new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(CheckEventPreconditionErrorPatch.Prefix))) + ); } @@ -55,30 +54,30 @@ namespace StardewModdingAPI.Patches /// The instance being patched. /// The return value of the original method. /// The precondition to be parsed. + /// The method being wrapped. /// 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, ref int __result, string precondition) + private static bool Prefix(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) { - if (CheckEventPreconditionErrorPatch.IsArbitrated) - { - CheckEventPreconditionErrorPatch.IsArbitrated = false; + if (CheckEventPreconditionErrorPatch.IsIntercepted) return true; + + try + { + CheckEventPreconditionErrorPatch.IsIntercepted = true; + __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); + return false; + } + catch (TargetInvocationException ex) + { + __result = -1; + CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + return false; } - else + finally { - CheckEventPreconditionErrorPatch.IsArbitrated = true; - try - { - __result = (int)CheckEventPreconditionErrorPatch.OriginalMethod.Invoke(__instance, new object[] { precondition }); - return false; - } - catch (TargetInvocationException ex) - { - __result = -1; - CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); - return false; - } + CheckEventPreconditionErrorPatch.IsIntercepted = false; } } } -- cgit From 26cac2c12a2b9ae7d486c000406deb2958f5fe30 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 8 Mar 2019 21:49:35 -0500 Subject: prevent invalid items from breaking menus on hover --- docs/release-notes.md | 6 ++++++ src/SMAPI/Patches/ObjectErrorPatch.cs | 32 +++++++++++++++++++++++++++----- 2 files changed, 33 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 56ed2c8f..0b0bfbbc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,4 +1,10 @@ # Release notes +## 3.0 (upcoming release) +These changes have not been released yet. + +* For players: + * SMAPI now prevents invalid items from breaking menus on hover. + ## 2.11.2 Released 23 April 2019 for Stardew Valley 1.3.36. diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index 0481259d..d72201c3 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -1,8 +1,8 @@ using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Harmony; using StardewModdingAPI.Framework.Patching; using StardewValley; +using StardewValley.Menus; using SObject = StardewValley.Object; namespace StardewModdingAPI.Patches @@ -24,10 +24,17 @@ namespace StardewModdingAPI.Patches /// The Harmony instance. public void Apply(HarmonyInstance harmony) { - MethodInfo method = AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(ObjectErrorPatch.Prefix)); + // object.getDescription + harmony.Patch( + original: AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)), + prefix: new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(ObjectErrorPatch.Object_GetDescription_Prefix))) + ); - harmony.Patch(method, new HarmonyMethod(prefix), null); + // IClickableMenu.drawToolTip + harmony.Patch( + original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)), + prefix: new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(ObjectErrorPatch.IClickableMenu_DrawTooltip_Prefix))) + ); } @@ -40,7 +47,7 @@ namespace StardewModdingAPI.Patches /// 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(SObject __instance, ref string __result) + private static bool Object_GetDescription_Prefix(SObject __instance, ref string __result) { // invalid bigcraftables crash instead of showing '???' like invalid non-bigcraftables if (!__instance.IsRecipe && __instance.bigCraftable.Value && !Game1.bigCraftablesInformation.ContainsKey(__instance.ParentSheetIndex)) @@ -51,5 +58,20 @@ namespace StardewModdingAPI.Patches return true; } + + /// The method to call instead of . + /// The instance being patched. + /// The item for which to draw a tooltip. + /// 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 IClickableMenu_DrawTooltip_Prefix(IClickableMenu __instance, Item hoveredItem) + { + // invalid edible item cause crash when drawing tooltips + if (hoveredItem is SObject obj && obj.Edibility != -300 && !Game1.objectInformation.ContainsKey(obj.ParentSheetIndex)) + return false; + + return true; + } } } -- cgit From 64331ffe8c8cb0fc19878bbf5349299c483f8d9d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 13 Mar 2019 22:20:18 -0400 Subject: default Monitor.Log to trace --- docs/release-notes.md | 3 +++ src/SMAPI.Mods.SaveBackup/ModEntry.cs | 6 +++--- src/SMAPI/Framework/DeprecationManager.cs | 2 +- src/SMAPI/Framework/Monitor.cs | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 0b0bfbbc..1df3aa53 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,9 @@ These changes have not been released yet. * For players: * SMAPI now prevents invalid items from breaking menus on hover. +* For modders: + * `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. + ## 2.11.2 Released 23 April 2019 for Stardew Valley 1.3.36. diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index d10131b3..30dbfbe6 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -46,7 +46,7 @@ namespace StardewModdingAPI.Mods.SaveBackup } catch (Exception ex) { - this.Monitor.Log($"Error backing up saves: {ex}"); + this.Monitor.Log($"Error backing up saves: {ex}", LogLevel.Error); } } @@ -87,7 +87,7 @@ namespace StardewModdingAPI.Mods.SaveBackup catch (Exception ex) when (ex is TypeLoadException || ex.InnerException is TypeLoadException) { // create uncompressed backup if compression fails - this.Monitor.Log("Couldn't zip the save backup, creating uncompressed backup instead."); + this.Monitor.Log("Couldn't zip the save backup, creating uncompressed backup instead.", LogLevel.Debug); this.Monitor.Log(ex.ToString(), LogLevel.Trace); this.RecursiveCopy(new DirectoryInfo(Constants.SavesPath), fallbackDir, copyRoot: false); } @@ -137,7 +137,7 @@ namespace StardewModdingAPI.Mods.SaveBackup } catch (Exception ex) { - this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}"); + this.Monitor.Log($"Error deleting old save backup '{file.Name}': {ex}", LogLevel.Error); } } } diff --git a/src/SMAPI/Framework/DeprecationManager.cs b/src/SMAPI/Framework/DeprecationManager.cs index 3153bbb4..984bb487 100644 --- a/src/SMAPI/Framework/DeprecationManager.cs +++ b/src/SMAPI/Framework/DeprecationManager.cs @@ -132,7 +132,7 @@ namespace StardewModdingAPI.Framework else { this.Monitor.Log(message, level); - this.Monitor.Log(warning.StackTrace); + this.Monitor.Log(warning.StackTrace, LogLevel.Debug); } } } diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 47ebc2d7..617bfd85 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework /// Log a message for the player or developer. /// The message to log. /// The log severity level. - public void Log(string message, LogLevel level = LogLevel.Debug) + public void Log(string message, LogLevel level = LogLevel.Trace) { this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } -- cgit From 20912724a0dc61562ea00e12e9bbc472c93e6aa4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 27 Mar 2019 21:13:46 -0400 Subject: fix errors during early startup not shown before exit --- docs/release-notes.md | 1 + src/SMAPI/Program.cs | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5ba19e43..b22909f7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ These changes have not been released yet. * For players: * SMAPI now prevents invalid items from breaking menus on hover. + * Fixed errors during early startup not shown before exit. * For modders: * `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 2eec371c..3a34872a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -30,10 +30,18 @@ namespace StardewModdingAPI /// The command-line arguments. public static void Main(string[] args) { - AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; - Program.AssertGamePresent(); - Program.AssertGameVersion(); - Program.Start(args); + try + { + AppDomain.CurrentDomain.AssemblyResolve += Program.CurrentDomain_AssemblyResolve; + Program.AssertGamePresent(); + Program.AssertGameVersion(); + Program.Start(args); + } + catch (Exception ex) + { + Console.WriteLine($"SMAPI failed to initialise: {ex}"); + Program.PressAnyKeyToExit(true); + } } -- cgit From 4b9afa4ca3622d96bcd090674512846f71ec6c4a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 28 Mar 2019 13:45:25 -0400 Subject: tweak smapi.io logic to allow hidden non-old downloads --- src/SMAPI.Web/Controllers/IndexController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Web/Controllers/IndexController.cs b/src/SMAPI.Web/Controllers/IndexController.cs index ea1a52b2..4e3602d5 100644 --- a/src/SMAPI.Web/Controllers/IndexController.cs +++ b/src/SMAPI.Web/Controllers/IndexController.cs @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Web.Controllers foreach (GitAsset asset in release.Assets) { - if (asset.FileName.StartsWith("Z_OLD")) + if (asset.FileName.StartsWith("Z_")) continue; Match match = Regex.Match(asset.FileName, @"SMAPI-(?[\d\.]+(?:-.+)?)-installer(?-for-developers)?.zip"); -- cgit From f250afbaddece5d0a6a5ec4e28f49c3be63c80a0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 7 Apr 2019 15:37:39 -0400 Subject: fix unspecified log levels --- src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 1 - src/SMAPI/Framework/SCore.cs | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs index 77dace26..4807c46d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI.Events; using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; namespace StardewModdingAPI.Mods.ConsoleCommands diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9090ca71..a0fe1241 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -160,7 +160,7 @@ namespace StardewModdingAPI.Framework // init logging this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info); - this.Monitor.Log($"Mods go here: {modsPath}"); + this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info); if (modsPath != Constants.DefaultModsPath) this.Monitor.Log("(Using custom --mods-path argument.)", LogLevel.Trace); this.Monitor.Log($"Log started at {DateTime.UtcNow:s} UTC", LogLevel.Trace); @@ -252,7 +252,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}"); + this.Monitor.Log($"SMAPI failed trying to track the crash details: {ex.GetLogSummary()}", LogLevel.Error); } this.GameInstance.Exit(); @@ -577,7 +577,7 @@ namespace StardewModdingAPI.Framework 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)}"); + this.Monitor.Log($"Error: {string.Join("\n", response.Errors)}", LogLevel.Trace); } else if (this.IsValidUpdate(Constants.ApiVersion, latestBeta, this.Settings.UseBetaChannel)) { @@ -597,7 +597,7 @@ namespace StardewModdingAPI.Framework 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}" - : $"Error: {ex.GetLogSummary()}" + : $"Error: {ex.GetLogSummary()}", LogLevel.Trace ); } @@ -690,7 +690,7 @@ namespace StardewModdingAPI.Framework 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 - : ex.ToString() + : ex.ToString(), LogLevel.Trace ); } } -- cgit From e43f01ffce7238d40d275c47615c24b9134b4954 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 14 Apr 2019 22:14:43 -0400 Subject: tweak patch code style --- src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs | 2 +- src/SMAPI/Patches/DialogueErrorPatch.cs | 8 ++++---- src/SMAPI/Patches/LoadForNewGamePatch.cs | 15 +++++++-------- src/SMAPI/Patches/ObjectErrorPatch.cs | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs index 74cfbfb0..c2a16391 100644 --- a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs +++ b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs @@ -42,7 +42,7 @@ namespace StardewModdingAPI.Patches { harmony.Patch( original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), - prefix: new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(CheckEventPreconditionErrorPatch.Prefix))) + prefix: new HarmonyMethod(this.GetType(), nameof(CheckEventPreconditionErrorPatch.Prefix)) ); } diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index 5af6ceef..7d57464c 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -46,10 +46,10 @@ namespace StardewModdingAPI.Patches /// The Harmony instance. public void Apply(HarmonyInstance harmony) { - ConstructorInfo constructor = AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(DialogueErrorPatch.Prefix)); - - harmony.Patch(constructor, new HarmonyMethod(prefix), null); + harmony.Patch( + original: AccessTools.Constructor(typeof(Dialogue), new[] { typeof(string), typeof(NPC) }), + prefix: new HarmonyMethod(this.GetType(), nameof(DialogueErrorPatch.Prefix)) + ); } diff --git a/src/SMAPI/Patches/LoadForNewGamePatch.cs b/src/SMAPI/Patches/LoadForNewGamePatch.cs index f4ce2023..def7134d 100644 --- a/src/SMAPI/Patches/LoadForNewGamePatch.cs +++ b/src/SMAPI/Patches/LoadForNewGamePatch.cs @@ -1,7 +1,6 @@ using System; using System.Collections.ObjectModel; using System.Collections.Specialized; -using System.Reflection; using Harmony; using StardewModdingAPI.Enums; using StardewModdingAPI.Framework.Patching; @@ -28,7 +27,7 @@ namespace StardewModdingAPI.Patches private static bool IsCreating; /// The number of times that has been cleared since started. - private static int TimesLocationsCleared = 0; + private static int TimesLocationsCleared; /********* @@ -54,11 +53,11 @@ namespace StardewModdingAPI.Patches /// The Harmony instance. public void Apply(HarmonyInstance harmony) { - MethodInfo method = AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)); - MethodInfo prefix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Prefix)); - MethodInfo postfix = AccessTools.Method(this.GetType(), nameof(LoadForNewGamePatch.Postfix)); - - harmony.Patch(method, new HarmonyMethod(prefix), new HarmonyMethod(postfix)); + harmony.Patch( + original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadForNewGamePatch.Prefix)), + postfix: new HarmonyMethod(this.GetType(), nameof(LoadForNewGamePatch.Postfix)) + ); } @@ -89,7 +88,7 @@ namespace StardewModdingAPI.Patches if (LoadForNewGamePatch.IsCreating) { // clean up - ObservableCollection locations = (ObservableCollection) Game1.locations; + ObservableCollection locations = (ObservableCollection)Game1.locations; locations.CollectionChanged -= LoadForNewGamePatch.OnLocationListChanged; // raise stage changed diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index d72201c3..0b17b101 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -27,13 +27,13 @@ namespace StardewModdingAPI.Patches // object.getDescription harmony.Patch( original: AccessTools.Method(typeof(SObject), nameof(SObject.getDescription)), - prefix: new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(ObjectErrorPatch.Object_GetDescription_Prefix))) + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Object_GetDescription_Prefix)) ); // IClickableMenu.drawToolTip harmony.Patch( original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)), - prefix: new HarmonyMethod(AccessTools.Method(this.GetType(), nameof(ObjectErrorPatch.IClickableMenu_DrawTooltip_Prefix))) + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.IClickableMenu_DrawTooltip_Prefix)) ); } -- cgit From e3a2c56a6d8c827240a2bd4ead86c7b412e826ab Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 9 May 2019 23:03:45 -0400 Subject: fix 'location list changed' verbose log not correctly listing changes --- docs/release-notes.md | 1 + src/SMAPI/Framework/SGame.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 6c9636c7..fb6be0c0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,7 @@ These changes have not been released yet. * For modders: * `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. + * Fixed 'location list changed' verbose log not correctly listing changes. ## 2.11.2 Released 23 April 2019 for Stardew Valley 1.3.36. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 9818314a..704eb6bc 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -705,8 +705,8 @@ namespace StardewModdingAPI.Framework if (this.Monitor.IsVerbose) { - string addedText = this.Watchers.LocationsWatcher.Added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; - string removedText = this.Watchers.LocationsWatcher.Removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; + string addedText = added.Any() ? string.Join(", ", added.Select(p => p.Name)) : "none"; + string removedText = removed.Any() ? string.Join(", ", removed.Select(p => p.Name)) : "none"; this.Monitor.Log($"Context: location list changed (added {addedText}; removed {removedText}).", LogLevel.Trace); } -- cgit From 8595a2a6faffe7af81c6401cd6308f84fbefdd01 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 10 May 2019 23:25:07 -0400 Subject: make Harmony patch names more consistent --- src/SMAPI/Framework/SCore.cs | 4 +- .../Patches/CheckEventPreconditionErrorPatch.cs | 84 ---------------- src/SMAPI/Patches/EventErrorPatch.cs | 84 ++++++++++++++++ src/SMAPI/Patches/LoadContextPatch.cs | 108 +++++++++++++++++++++ src/SMAPI/Patches/LoadForNewGamePatch.cs | 108 --------------------- src/SMAPI/StardewModdingAPI.csproj | 6 +- 6 files changed, 197 insertions(+), 197 deletions(-) delete mode 100644 src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs create mode 100644 src/SMAPI/Patches/EventErrorPatch.cs create mode 100644 src/SMAPI/Patches/LoadContextPatch.cs delete mode 100644 src/SMAPI/Patches/LoadForNewGamePatch.cs (limited to 'src') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index a0fe1241..5dd52992 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -233,10 +233,10 @@ namespace StardewModdingAPI.Framework // apply game patches new GamePatcher(this.Monitor).Apply( - new CheckEventPreconditionErrorPatch(this.MonitorForGame), + new EventErrorPatch(this.MonitorForGame), new DialogueErrorPatch(this.MonitorForGame, this.Reflection), new ObjectErrorPatch(), - new LoadForNewGamePatch(this.Reflection, this.GameInstance.OnLoadStageChanged) + new LoadContextPatch(this.Reflection, this.GameInstance.OnLoadStageChanged) ); // add exit handler diff --git a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs b/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs deleted file mode 100644 index c2a16391..00000000 --- a/src/SMAPI/Patches/CheckEventPreconditionErrorPatch.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Harmony; -using StardewModdingAPI.Framework.Patching; -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 - { - /********* - ** Fields - *********/ - /// Writes messages to the console and log file on behalf of the game. - private static IMonitor MonitorForGame; - - /// Whether the method is currently being intercepted. - private static bool IsIntercepted; - - - /********* - ** 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. - public CheckEventPreconditionErrorPatch(IMonitor monitorForGame) - { - CheckEventPreconditionErrorPatch.MonitorForGame = monitorForGame; - } - - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - harmony.Patch( - original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), - prefix: new HarmonyMethod(this.GetType(), nameof(CheckEventPreconditionErrorPatch.Prefix)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of the GameLocation.CheckEventPrecondition. - /// The instance being patched. - /// The return value of the original method. - /// The precondition to be parsed. - /// The method being wrapped. - /// 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, ref int __result, string precondition, MethodInfo __originalMethod) - { - if (CheckEventPreconditionErrorPatch.IsIntercepted) - return true; - - try - { - CheckEventPreconditionErrorPatch.IsIntercepted = true; - __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); - return false; - } - catch (TargetInvocationException ex) - { - __result = -1; - CheckEventPreconditionErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); - return false; - } - finally - { - CheckEventPreconditionErrorPatch.IsIntercepted = false; - } - } - } -} diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs new file mode 100644 index 00000000..b0074356 --- /dev/null +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +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 EventErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + /// Whether the method is currently being intercepted. + private static bool IsIntercepted; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(EventErrorPatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + public EventErrorPatch(IMonitor monitorForGame) + { + EventErrorPatch.MonitorForGame = monitorForGame; + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(GameLocation), "checkEventPrecondition"), + prefix: new HarmonyMethod(this.GetType(), nameof(EventErrorPatch.Prefix)) + ); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of the GameLocation.CheckEventPrecondition. + /// The instance being patched. + /// The return value of the original method. + /// The precondition to be parsed. + /// The method being wrapped. + /// 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, ref int __result, string precondition, MethodInfo __originalMethod) + { + if (EventErrorPatch.IsIntercepted) + return true; + + try + { + EventErrorPatch.IsIntercepted = true; + __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); + return false; + } + catch (TargetInvocationException ex) + { + __result = -1; + EventErrorPatch.MonitorForGame.Log($"Failed parsing event precondition ({precondition}):\n{ex.InnerException}", LogLevel.Error); + return false; + } + finally + { + EventErrorPatch.IsIntercepted = false; + } + } + } +} diff --git a/src/SMAPI/Patches/LoadContextPatch.cs b/src/SMAPI/Patches/LoadContextPatch.cs new file mode 100644 index 00000000..43e09573 --- /dev/null +++ b/src/SMAPI/Patches/LoadContextPatch.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using Harmony; +using StardewModdingAPI.Enums; +using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.Reflection; +using StardewValley; +using StardewValley.Menus; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for which notifies SMAPI for save creation load stages. + /// This patch hooks into , checks if TitleMenu.transitioningCharacterCreationMenu is true (which means the player is creating a new save file), then raises after the location list is cleared twice (the second clear happens right before locations are created), and when the method ends. + internal class LoadContextPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Simplifies access to private code. + private static Reflector Reflection; + + /// A callback to invoke when the load stage changes. + private static Action OnStageChanged; + + /// Whether was called as part of save creation. + private static bool IsCreating; + + /// The number of times that has been cleared since started. + private static int TimesLocationsCleared; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => $"{nameof(LoadContextPatch)}"; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Simplifies access to private code. + /// A callback to invoke when the load stage changes. + public LoadContextPatch(Reflector reflection, Action onStageChanged) + { + LoadContextPatch.Reflection = reflection; + LoadContextPatch.OnStageChanged = onStageChanged; + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)), + prefix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Prefix)), + postfix: new HarmonyMethod(this.GetType(), nameof(LoadContextPatch.Postfix)) + ); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// Returns whether to execute the original method. + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + private static bool Prefix() + { + LoadContextPatch.IsCreating = Game1.activeClickableMenu is TitleMenu menu && LoadContextPatch.Reflection.GetField(menu, "transitioningCharacterCreationMenu").GetValue(); + LoadContextPatch.TimesLocationsCleared = 0; + if (LoadContextPatch.IsCreating) + { + // raise CreatedBasicInfo after locations are cleared twice + ObservableCollection locations = (ObservableCollection)Game1.locations; + locations.CollectionChanged += LoadContextPatch.OnLocationListChanged; + } + + return true; + } + + /// The method to call instead after . + /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. + private static void Postfix() + { + if (LoadContextPatch.IsCreating) + { + // clean up + ObservableCollection locations = (ObservableCollection)Game1.locations; + locations.CollectionChanged -= LoadContextPatch.OnLocationListChanged; + + // raise stage changed + LoadContextPatch.OnStageChanged(LoadStage.CreatedLocations); + } + } + + /// Raised when changes. + /// The event sender. + /// The event arguments. + private static void OnLocationListChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (++LoadContextPatch.TimesLocationsCleared == 2) + LoadContextPatch.OnStageChanged(LoadStage.CreatedBasicInfo); + } + } +} diff --git a/src/SMAPI/Patches/LoadForNewGamePatch.cs b/src/SMAPI/Patches/LoadForNewGamePatch.cs deleted file mode 100644 index def7134d..00000000 --- a/src/SMAPI/Patches/LoadForNewGamePatch.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Collections.Specialized; -using Harmony; -using StardewModdingAPI.Enums; -using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.Reflection; -using StardewValley; -using StardewValley.Menus; - -namespace StardewModdingAPI.Patches -{ - /// A Harmony patch for which notifies SMAPI for save creation load stages. - /// This patch hooks into , checks if TitleMenu.transitioningCharacterCreationMenu is true (which means the player is creating a new save file), then raises after the location list is cleared twice (the second clear happens right before locations are created), and when the method ends. - internal class LoadForNewGamePatch : IHarmonyPatch - { - /********* - ** Fields - *********/ - /// Simplifies access to private code. - private static Reflector Reflection; - - /// A callback to invoke when the load stage changes. - private static Action OnStageChanged; - - /// Whether was called as part of save creation. - private static bool IsCreating; - - /// The number of times that has been cleared since started. - private static int TimesLocationsCleared; - - - /********* - ** Accessors - *********/ - /// A unique name for this patch. - public string Name => $"{nameof(LoadForNewGamePatch)}"; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// Simplifies access to private code. - /// A callback to invoke when the load stage changes. - public LoadForNewGamePatch(Reflector reflection, Action onStageChanged) - { - LoadForNewGamePatch.Reflection = reflection; - LoadForNewGamePatch.OnStageChanged = onStageChanged; - } - - /// Apply the Harmony patch. - /// The Harmony instance. - public void Apply(HarmonyInstance harmony) - { - harmony.Patch( - original: AccessTools.Method(typeof(Game1), nameof(Game1.loadForNewGame)), - prefix: new HarmonyMethod(this.GetType(), nameof(LoadForNewGamePatch.Prefix)), - postfix: new HarmonyMethod(this.GetType(), nameof(LoadForNewGamePatch.Postfix)) - ); - } - - - /********* - ** Private methods - *********/ - /// The method to call instead of . - /// Returns whether to execute the original method. - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static bool Prefix() - { - LoadForNewGamePatch.IsCreating = Game1.activeClickableMenu is TitleMenu menu && LoadForNewGamePatch.Reflection.GetField(menu, "transitioningCharacterCreationMenu").GetValue(); - LoadForNewGamePatch.TimesLocationsCleared = 0; - if (LoadForNewGamePatch.IsCreating) - { - // raise CreatedBasicInfo after locations are cleared twice - ObservableCollection locations = (ObservableCollection)Game1.locations; - locations.CollectionChanged += LoadForNewGamePatch.OnLocationListChanged; - } - - return true; - } - - /// The method to call instead after . - /// This method must be static for Harmony to work correctly. See the Harmony documentation before renaming arguments. - private static void Postfix() - { - if (LoadForNewGamePatch.IsCreating) - { - // clean up - ObservableCollection locations = (ObservableCollection)Game1.locations; - locations.CollectionChanged -= LoadForNewGamePatch.OnLocationListChanged; - - // raise stage changed - LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedLocations); - } - } - - /// Raised when changes. - /// The event sender. - /// The event arguments. - private static void OnLocationListChanged(object sender, NotifyCollectionChangedEventArgs e) - { - if (++LoadForNewGamePatch.TimesLocationsCleared == 2) - LoadForNewGamePatch.OnStageChanged(LoadStage.CreatedBasicInfo); - } - } -} diff --git a/src/SMAPI/StardewModdingAPI.csproj b/src/SMAPI/StardewModdingAPI.csproj index 5991dd7e..673deab0 100644 --- a/src/SMAPI/StardewModdingAPI.csproj +++ b/src/SMAPI/StardewModdingAPI.csproj @@ -330,10 +330,10 @@ - - - + + + -- cgit From 77f85a701a1487539e1c54a4a7d7f92d58f99cc9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 15 May 2019 20:51:28 -0400 Subject: update mod list filters to always display clicked mod link --- docs/release-notes.md | 3 +++ src/SMAPI.Web/Views/LogParser/Index.cshtml | 4 ++-- src/SMAPI.Web/wwwroot/Content/js/mods.js | 9 +++++++-- 3 files changed, 12 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index fb6be0c0..99e05016 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,9 @@ These changes have not been released yet. * SMAPI now prevents invalid event preconditions from crashing the game (thanks to berkayylmao!). * Fixed errors during early startup not shown before exit. +* For the web UI: + * When filtering the mod list, clicking a mod link now automatically adds it to the visible mods. + * For modders: * `this.Monitor.Log` now defaults to the `Trace` log level instead of `Debug`. * Fixed 'location list changed' verbose log not correctly listing changes. diff --git a/src/SMAPI.Web/Views/LogParser/Index.cshtml b/src/SMAPI.Web/Views/LogParser/Index.cshtml index babd0bd3..dff37d7d 100644 --- a/src/SMAPI.Web/Views/LogParser/Index.cshtml +++ b/src/SMAPI.Web/Views/LogParser/Index.cshtml @@ -17,10 +17,10 @@ { } - + - + - + }

@@ -79,27 +79,32 @@ else

See the release notes and mod compatibility list for more info.

} - +

SMAPI is an open-source project by Pathoschild. It will always be free, but donations are much appreciated to help pay for development, server hosting, domain fees, coffee, etc.

-