From 95a93a05b39d2b27b538ecdb0e6a18f28096c5c2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 7 Feb 2017 20:50:41 -0500 Subject: remove oldest deprecated code (#231) Since Stardew Valley 1.2 breaks most mods anyway, this commits removes the oldest deprecations and fixes the issues that are easiest for mods to update. See documentation for details. --- src/StardewModdingAPI/Program.cs | 46 +++++++++++++++------------------------- 1 file changed, 17 insertions(+), 29 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 45bf1238..c0a05e2d 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -14,14 +14,13 @@ using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Models; -using StardewModdingAPI.Inheritance; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; namespace StardewModdingAPI { /// The main entry point for SMAPI, responsible for hooking into and launching the game. - public class Program + internal class Program { /********* ** Properties @@ -52,31 +51,27 @@ namespace StardewModdingAPI /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); - - /********* - ** Accessors - *********/ - /// The number of mods currently loaded by SMAPI. - public static int ModsLoaded; - - /// The underlying game instance. - public static SGame gamePtr; - /// Whether the game is currently running. - public static bool ready; + private static bool ready; /// The underlying game assembly. - public static Assembly StardewAssembly; + private static Assembly StardewAssembly; /// The underlying type. - public static Type StardewProgramType; + private static Type StardewProgramType; /// The field containing game's main instance. - public static FieldInfo StardewGameInfo; + private static FieldInfo StardewGameInfo; - // ReSharper disable once PossibleNullReferenceException - /// The game's build type (i.e. GOG vs Steam). - public static int BuildType => (int)Program.StardewProgramType.GetField("buildType", BindingFlags.Public | BindingFlags.Static).GetValue(null); + + /********* + ** Accessors + *********/ + /// The underlying game instance. + internal static SGame gamePtr; + + /// The number of mods currently loaded by SMAPI. + internal static int ModsLoaded; /// Tracks the installed mods. internal static readonly ModRegistry ModRegistry = new ModRegistry(); @@ -358,7 +353,7 @@ namespace StardewModdingAPI string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; // read manifest - ManifestImpl manifest; + Manifest manifest; try { // read manifest text @@ -370,7 +365,7 @@ namespace StardewModdingAPI } // deserialise manifest - manifest = helper.ReadJsonFile("manifest.json"); + manifest = helper.ReadJsonFile("manifest.json"); if (manifest == null) { Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); @@ -381,10 +376,6 @@ namespace StardewModdingAPI Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } - - // log deprecated fields - if (manifest.UsedAuthourField) - deprecationWarnings.Add(() => Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.Authour)}", "1.0", DeprecationLevel.Notice)); } catch (Exception ex) { @@ -424,7 +415,7 @@ namespace StardewModdingAPI } catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) { - Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.Version}.", LogLevel.Error); + Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); continue; } } @@ -528,14 +519,11 @@ namespace StardewModdingAPI { // call entry methods mod.Entry(); // deprecated since 1.0 - mod.Entry((ModHelper)mod.Helper); // deprecated since 1.1 mod.Entry(mod.Helper); // raise deprecation warning for old Entry() methods if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) Program.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice); - if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(ModHelper) })) - Program.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}({nameof(ModHelper)}) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.1", DeprecationLevel.PendingRemoval); } catch (Exception ex) { -- cgit From 3e91af6b06deec6aa2dca80945c82af528094c52 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 10 Feb 2017 22:52:16 -0500 Subject: mark several mods incompatible with Stardew Valley 1.2+ (#231) --- src/StardewModdingAPI/Program.cs | 2 +- src/StardewModdingAPI/StardewModdingAPI.data.json | 60 ++++++++++++++++------- 2 files changed, 44 insertions(+), 18 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index c0a05e2d..75be23f2 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -385,7 +385,7 @@ namespace StardewModdingAPI // validate known incompatible mods IncompatibleMod compatibility; - if (incompatibleMods.TryGetValue(manifest.UniqueID ?? $"{manifest.Name}|{manifest.Author}|{manifest.EntryDll}", out compatibility)) + if (incompatibleMods.TryGetValue(!string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll, out compatibility)) { if (!compatibility.IsCompatible(manifest.Version)) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.data.json b/src/StardewModdingAPI/StardewModdingAPI.data.json index 3295336f..3c9be222 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.data.json +++ b/src/StardewModdingAPI/StardewModdingAPI.data.json @@ -6,45 +6,71 @@ This file contains advanced metadata for SMAPI. You shouldn't change this file. */ [ - /* versions not compatible with Stardew Valley 1.1+ */ + /* versions which crash the game */ { - "ID": "SPDSprinklersMod", - "Name": "Better Sprinklers", - "UpperVersion": "2.1", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", - "ForceCompatibleVersion": "^2.1-EntoPatch" + "Name": "NPC Map Locations", + "ID": "NPCMapLocationsMod", + "LowerVersion": "1.42", + "UpperVersion": "1.43", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", + "ReasonPhrase": "this version has an update check error which crashes the game" }, + + /* versions not compatible with Stardew Valley 1.1+ */ { - "ID": "SPDChestLabel", "Name": "Chest Label System", + "ID": "SPDChestLabel", "UpperVersion": "1.5", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", "ForceCompatibleVersion": "^1.5-EntoPatch" }, { - "ID": "CJBCheatsMenu", "Name": "CJB Cheats Menu", + "ID": "CJBCheatsMenu", "UpperVersion": "1.12", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", "ForceCompatibleVersion": "^1.12-EntoPatch" }, { - "ID": "CJBItemSpawner", "Name": "CJB Item Spawner", + "ID": "CJBItemSpawner", "UpperVersion": "1.5", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", "ForceCompatibleVersion": "^1.5-EntoPatch" }, - /* versions which crash the game */ + /* versions not compatible with Stardew Valley 1.2+ */ { - "ID": "NPCMapLocationsMod", - "Name": "NPC Map Locations", - "LowerVersion": "1.42", - "UpperVersion": "1.43", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", - "ReasonPhrase": "this version has an update check error which crashes the game" + "Name": "Better Sprinklers", + "ID": "SPDSprinklersMod", + "UpperVersion": "2.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "ForceCompatibleVersion": "^2.1-EntoPatch.7" + }, + { + "Name": "Casks Anywhere", + "ID": "CasksAnywhere", + "UpperVersion": "1.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/878" + }, + { + "Name": "Entoarox Framework", + "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9", + "UpperVersion": "1.6.1", + "UpdateUrl": "http://community.playstarbound.com/resources/4228" + }, + { + "Name": "Get Dressed", + "ID": "GetDressed.dll", + "UpperVersion": "3.2", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331" + }, + { + "Name": "NoSoilDecay", + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "UpperVersion": "0.5", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237" } ] -- cgit From 46b7d7a4001e209d7201ed5cad38cad2f1ad2a7f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Feb 2017 01:15:56 -0500 Subject: redirect the game's debug messages into trace logs (#233) The game writes debug messages directly to the console, which shows up for SMAPI users. This commit redirects direct console messages to a monitor. --- release-notes.md | 6 +- src/StardewModdingAPI/Framework/LogFileManager.cs | 48 ------------ .../Logging/ConsoleInterceptionManager.cs | 86 ++++++++++++++++++++++ .../Framework/Logging/InterceptingTextWriter.cs | 79 ++++++++++++++++++++ .../Framework/Logging/LogFileManager.cs | 48 ++++++++++++ src/StardewModdingAPI/Framework/Monitor.cs | 56 ++++++-------- src/StardewModdingAPI/Framework/SGame.cs | 1 - src/StardewModdingAPI/Program.cs | 20 +++-- src/StardewModdingAPI/StardewModdingAPI.csproj | 4 +- 9 files changed, 257 insertions(+), 91 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/LogFileManager.cs create mode 100644 src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs create mode 100644 src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs create mode 100644 src/StardewModdingAPI/Framework/Logging/LogFileManager.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index e6becdbd..e3e8752b 100644 --- a/release-notes.md +++ b/release-notes.md @@ -4,13 +4,13 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.8...1.9). For players: -* Updated for Stardew Valley 1.2. +* Updated for Stardew Valley 1.2. Most mods are now rewritten for compatibility; some mods require an update from their authors. * Simplified log filename. -* SMAPI now rewrites most mods for Stardew Valley 1.2 compatibility. +* Fixed game's debug output being shown in the console for all users. For mod developers: * Added `SaveEvents.AfterReturnToTitle` event. -* Added `GetPrivateProperty` to reflection helper. +* Added `GetPrivateProperty` reflection helper. * Many deprecated APIs have been removed; see the [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod) for more information. * Log files now always use `\r\n` to simplify crossplatform viewing. diff --git a/src/StardewModdingAPI/Framework/LogFileManager.cs b/src/StardewModdingAPI/Framework/LogFileManager.cs deleted file mode 100644 index 80437e9c..00000000 --- a/src/StardewModdingAPI/Framework/LogFileManager.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.IO; - -namespace StardewModdingAPI.Framework -{ - /// Manages reading and writing to log file. - internal class LogFileManager : IDisposable - { - /********* - ** Properties - *********/ - /// The underlying stream writer. - private readonly StreamWriter Stream; - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The log file to write. - public LogFileManager(string path) - { - // create log directory if needed - string logDir = Path.GetDirectoryName(path); - if (logDir == null) - throw new ArgumentException($"The log path '{path}' is not valid."); - Directory.CreateDirectory(logDir); - - // open log file stream - this.Stream = new StreamWriter(path, append: false) { AutoFlush = true }; - } - - /// Write a message to the log. - /// The message to log. - public void WriteLine(string message) - { - // always use Windows-style line endings for convenience - // (Linux/Mac editors are fine with them, Windows editors often require them) - this.Stream.Write(message + "\r\n"); - } - - /// Release all resources. - public void Dispose() - { - this.Stream.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs b/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs new file mode 100644 index 00000000..d84671ee --- /dev/null +++ b/src/StardewModdingAPI/Framework/Logging/ConsoleInterceptionManager.cs @@ -0,0 +1,86 @@ +using System; + +namespace StardewModdingAPI.Framework.Logging +{ + /// Manages console output interception. + internal class ConsoleInterceptionManager : IDisposable + { + /********* + ** Properties + *********/ + /// The intercepting console writer. + private readonly InterceptingTextWriter Output; + + + /********* + ** Accessors + *********/ + /// Whether the current console supports color formatting. + public bool SupportsColor { get; } + + /// The event raised when something writes a line to the console directly. + public event Action OnLineIntercepted; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public ConsoleInterceptionManager() + { + // redirect output through interceptor + this.Output = new InterceptingTextWriter(Console.Out); + this.Output.OnLineIntercepted += line => this.OnLineIntercepted?.Invoke(line); + Console.SetOut(this.Output); + + // test color support + this.SupportsColor = this.TestColorSupport(); + } + + /// Get an exclusive lock and write to the console output without interception. + /// The action to perform within the exclusive write block. + public void ExclusiveWriteWithoutInterception(Action action) + { + lock (Console.Out) + { + try + { + this.Output.ShouldIntercept = false; + action(); + } + finally + { + this.Output.ShouldIntercept = true; + } + } + } + + /// Release all resources. + public void Dispose() + { + Console.SetOut(this.Output.Out); + this.Output.Dispose(); + } + + + /********* + ** private methods + *********/ + /// Test whether the current console supports color formatting. + private bool TestColorSupport() + { + try + { + this.ExclusiveWriteWithoutInterception(() => + { + Console.ForegroundColor = Console.ForegroundColor; + }); + return true; + } + catch (Exception) + { + return false; // Mono bug + } + } + } +} diff --git a/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs b/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs new file mode 100644 index 00000000..14789109 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Logging/InterceptingTextWriter.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace StardewModdingAPI.Framework.Logging +{ + /// A text writer which allows intercepting output. + internal class InterceptingTextWriter : TextWriter + { + /********* + ** Properties + *********/ + /// The current line being intercepted. + private readonly List Line = new List(); + + + /********* + ** Accessors + *********/ + /// The underlying console output. + public TextWriter Out { get; } + + /// The character encoding in which the output is written. + public override Encoding Encoding => this.Out.Encoding; + + /// Whether to intercept console output. + public bool ShouldIntercept { get; set; } + + /// The event raised when a line of text is intercepted. + public event Action OnLineIntercepted; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The underlying output writer. + public InterceptingTextWriter(TextWriter output) + { + this.Out = output; + } + + /// Writes a character to the text string or stream. + /// The character to write to the text stream. + public override void Write(char ch) + { + // intercept + if (this.ShouldIntercept) + { + switch (ch) + { + case '\r': + return; + + case '\n': + this.OnLineIntercepted?.Invoke(new string(this.Line.ToArray())); + this.Line.Clear(); + break; + + default: + this.Line.Add(ch); + break; + } + } + + // pass through + else + this.Out.Write(ch); + } + + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + this.OnLineIntercepted = null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs new file mode 100644 index 00000000..1f6ade1d --- /dev/null +++ b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs @@ -0,0 +1,48 @@ +using System; +using System.IO; + +namespace StardewModdingAPI.Framework.Logging +{ + /// Manages reading and writing to log file. + internal class LogFileManager : IDisposable + { + /********* + ** Properties + *********/ + /// The underlying stream writer. + private readonly StreamWriter Stream; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The log file to write. + public LogFileManager(string path) + { + // create log directory if needed + string logDir = Path.GetDirectoryName(path); + if (logDir == null) + throw new ArgumentException($"The log path '{path}' is not valid."); + Directory.CreateDirectory(logDir); + + // open log file stream + this.Stream = new StreamWriter(path, append: false) { AutoFlush = true }; + } + + /// Write a message to the log. + /// The message to log. + public void WriteLine(string message) + { + // always use Windows-style line endings for convenience + // (Linux/Mac editors are fine with them, Windows editors often require them) + this.Stream.Write(message + "\r\n"); + } + + /// Release all resources. + public void Dispose() + { + this.Stream.Dispose(); + } + } +} diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 39b567d8..33c1bbf4 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using StardewModdingAPI.Framework.Logging; namespace StardewModdingAPI.Framework { @@ -13,6 +14,9 @@ namespace StardewModdingAPI.Framework /// The name of the module which logs messages using this instance. private readonly string Source; + /// Manages access to the console output. + private readonly ConsoleInterceptionManager ConsoleManager; + /// The log file to which to write messages. private readonly LogFileManager LogFile; @@ -34,9 +38,6 @@ namespace StardewModdingAPI.Framework /********* ** Accessors *********/ - /// Whether the current console supports color codes. - internal static readonly bool ConsoleSupportsColor = Monitor.GetConsoleSupportsColor(); - /// Whether to show trace messages in the console. internal bool ShowTraceInConsole { get; set; } @@ -49,8 +50,9 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The name of the module which logs messages using this instance. + /// Manages access to the console output. /// The log file to which to write messages. - public Monitor(string source, LogFileManager logFile) + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -61,6 +63,7 @@ namespace StardewModdingAPI.Framework // initialise this.Source = source; this.LogFile = logFile; + this.ConsoleManager = consoleManager; } /// Log a message for the player or developer. @@ -68,7 +71,7 @@ namespace StardewModdingAPI.Framework /// The log severity level. public void Log(string message, LogLevel level = LogLevel.Debug) { - this.LogImpl(this.Source, message, Monitor.Colors[level], level); + this.LogImpl(this.Source, message, level, Monitor.Colors[level]); } /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. @@ -83,8 +86,7 @@ namespace StardewModdingAPI.Framework /// The message to log. internal void LogFatal(string message) { - Console.BackgroundColor = ConsoleColor.Red; - this.LogImpl(this.Source, message, ConsoleColor.White, LogLevel.Error); + this.LogImpl(this.Source, message, LogLevel.Error, ConsoleColor.White, background: ConsoleColor.Red); } /// Log a message for the player or developer, using the specified console color. @@ -95,7 +97,7 @@ namespace StardewModdingAPI.Framework [Obsolete("This method is provided for backwards compatibility and otherwise should not be used. Use " + nameof(Monitor) + "." + nameof(Monitor.Log) + " instead.")] internal void LegacyLog(string source, string message, ConsoleColor color, LogLevel level = LogLevel.Debug) { - this.LogImpl(source, message, color, level); + this.LogImpl(source, message, level, color); } @@ -105,9 +107,10 @@ namespace StardewModdingAPI.Framework /// Write a message line to the log. /// The name of the mod logging the message. /// The message to log. - /// The console color. /// The log level. - private void LogImpl(string source, string message, ConsoleColor color, LogLevel level) + /// The console foreground color. + /// The console background color (or null to leave it as-is). + private void LogImpl(string source, string message, LogLevel level, ConsoleColor color, ConsoleColor? background = null) { // generate message string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); @@ -116,30 +119,19 @@ namespace StardewModdingAPI.Framework // log if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) { - if (Monitor.ConsoleSupportsColor) + this.ConsoleManager.ExclusiveWriteWithoutInterception(() => { - Console.ForegroundColor = color; - Console.WriteLine(message); - Console.ResetColor(); - } - else - Console.WriteLine(message); + if (this.ConsoleManager.SupportsColor) + { + Console.ForegroundColor = color; + Console.WriteLine(message); + Console.ResetColor(); + } + else + Console.WriteLine(message); + }); } this.LogFile.WriteLine(message); } - - /// Test whether the current console supports color formatting. - private static bool GetConsoleSupportsColor() - { - try - { - Console.ForegroundColor = Console.ForegroundColor; - return true; - } - catch (Exception) - { - return false; // Mono bug - } - } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 6a751b85..9e269df3 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1170,7 +1170,6 @@ namespace StardewModdingAPI.Framework // after exit to title if (this.IsWorldReady && this.IsExiting && Game1.activeClickableMenu is TitleMenu) { - Console.WriteLine($"{Game1.currentGameTime.TotalGameTime}: after return to title"); SaveEvents.InvokeAfterReturnToTitle(this.Monitor); this.AfterLoadTimer = 5; this.IsExiting = false; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 75be23f2..a6302540 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -13,6 +13,7 @@ using Newtonsoft.Json; using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -42,8 +43,11 @@ namespace StardewModdingAPI /// The log file to which to write messages. private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); + /// Manages console output interception. + private static readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); + /// The core logger for SMAPI. - private static readonly Monitor Monitor = new Monitor("SMAPI", Program.LogFile); + private static readonly Monitor Monitor = new Monitor("SMAPI", Program.ConsoleManager, Program.LogFile); /// The user settings for SMAPI. private static UserSettings Settings; @@ -87,14 +91,12 @@ namespace StardewModdingAPI /// The command-line arguments. private static void Main(string[] args) { - // set log options + // initialise logging Program.Monitor.WriteToConsole = !args.Contains("--no-terminal"); Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting - - // add info header Program.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); - // initialise user settings + // read config { string settingsPath = Constants.ApiConfigPath; if (File.Exists(settingsPath)) @@ -108,6 +110,12 @@ namespace StardewModdingAPI File.WriteAllText(settingsPath, JsonConvert.SerializeObject(Program.Settings, Formatting.Indented)); } + // redirect direct console output + { + IMonitor monitor = Program.GetSecondaryMonitor("Console.Out"); + Program.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); + } + // add warning headers if (Program.Settings.DeveloperMode) { @@ -574,7 +582,7 @@ namespace StardewModdingAPI /// The name of the module which will log messages with this instance. private static Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, Program.LogFile) { WriteToConsole = Program.Monitor.WriteToConsole, ShowTraceInConsole = Program.Settings.DeveloperMode }; + return new Monitor(name, Program.ConsoleManager, Program.LogFile) { WriteToConsole = Program.Monitor.WriteToConsole, ShowTraceInConsole = Program.Settings.DeveloperMode }; } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 896721ea..ae08012d 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -145,6 +145,8 @@ + + @@ -166,7 +168,7 @@ - + -- cgit From 824ca7174a2b6a46d8a4ea50cac41c78e56dc48f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Feb 2017 02:04:01 -0500 Subject: delve into mod folders that only contain another folder (#208) This fixes a common issue when users unpack mods into a nested folder (e.g. "SomeMod-1.0.0\SomeMod\manifest.json"), which previously wouldn't be recognised as a mod. SMAPI will not do this if the folder contains files or more than one folder, to prevent backup folders and the like from being loaded. --- release-notes.md | 1 + src/StardewModdingAPI/Program.cs | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index e3e8752b..72e3b3c5 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,7 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.8...1.9). For players: * Updated for Stardew Valley 1.2. Most mods are now rewritten for compatibility; some mods require an update from their authors. * Simplified log filename. +* Simple nested mod folders are now recognised by SMAPI (e.g. `ModName-1.0\ModName\manifest.json`). * Fixed game's debug output being shown in the console for all users. For mod developers: diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a6302540..91a39042 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -337,9 +337,12 @@ namespace StardewModdingAPI // load mod assemblies List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list - foreach (string directory in Directory.GetDirectories(Program.ModPath)) + foreach (string directoryPath in Directory.GetDirectories(Program.ModPath)) { - string directoryName = new DirectoryInfo(directory).Name; + // passthrough empty directories + DirectoryInfo directory = new DirectoryInfo(directoryPath); + while (!directory.GetFiles().Any() && directory.GetDirectories().Length == 1) + directory = directory.GetDirectories().First(); // check for cancellation if (Program.CancellationTokenSource.IsCancellationRequested) @@ -349,13 +352,13 @@ namespace StardewModdingAPI } // get helper - IModHelper helper = new ModHelper(directory, Program.ModRegistry); + IModHelper helper = new ModHelper(directory.FullName, Program.ModRegistry); // get manifest path - string manifestPath = Path.Combine(directory, "manifest.json"); + string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) { - Program.Monitor.Log($"Ignored folder \"{directoryName}\" which doesn't have a manifest.json.", LogLevel.Warn); + Program.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; @@ -434,7 +437,7 @@ namespace StardewModdingAPI deprecationWarnings.Add(() => Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); try { - string psDir = Path.Combine(directory, "psconfigs"); + string psDir = Path.Combine(directory.FullName, "psconfigs"); Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { @@ -450,7 +453,7 @@ namespace StardewModdingAPI } // validate mod path to simplify errors - string assemblyPath = Path.Combine(directory, manifest.EntryDll); + string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { Program.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error); @@ -501,7 +504,7 @@ namespace StardewModdingAPI mod.ModManifest = manifest; mod.Helper = helper; mod.Monitor = Program.GetSecondaryMonitor(manifest.Name); - mod.PathOnDisk = directory; + mod.PathOnDisk = directory.FullName; // track mod Program.ModRegistry.Add(mod); -- cgit From 693f16f99ec3492e3bfcd0071af1d9d5e519dcfa Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Feb 2017 02:08:21 -0500 Subject: don't write direct console output to log file (#233) Per discussion with mod developers. --- src/StardewModdingAPI/Framework/Monitor.cs | 10 ++++++++-- src/StardewModdingAPI/Program.cs | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 33c1bbf4..3a9276a0 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -44,6 +44,9 @@ namespace StardewModdingAPI.Framework /// Whether to write anything to the console. This should be disabled if no console is available. internal bool WriteToConsole { get; set; } = true; + /// Whether to write anything to the log file. This should almost always be enabled. + internal bool WriteToFile { get; set; } = true; + /********* ** Public methods @@ -116,7 +119,7 @@ namespace StardewModdingAPI.Framework string levelStr = level.ToString().ToUpper().PadRight(Monitor.MaxLevelLength); message = $"[{DateTime.Now:HH:mm:ss} {levelStr} {source}] {message}"; - // log + // write to console if (this.WriteToConsole && (this.ShowTraceInConsole || level != LogLevel.Trace)) { this.ConsoleManager.ExclusiveWriteWithoutInterception(() => @@ -131,7 +134,10 @@ namespace StardewModdingAPI.Framework Console.WriteLine(message); }); } - this.LogFile.WriteLine(message); + + // write to log file + if (this.WriteToFile) + this.LogFile.WriteLine(message); } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 91a39042..f4518e21 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -112,8 +112,10 @@ namespace StardewModdingAPI // redirect direct console output { - IMonitor monitor = Program.GetSecondaryMonitor("Console.Out"); - Program.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); + Monitor monitor = Program.GetSecondaryMonitor("Console.Out"); + monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion + if (monitor.WriteToConsole) + Program.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); } // add warning headers -- cgit From d1080a8b2b54c777a446f08d9ecd5b76b4b2561a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 13 Feb 2017 00:13:29 -0500 Subject: move core JSON logic out of mod helper (#199) This lets SMAPI parse manifest.json files without a mod helper, so we can pass the mod name into the helper. --- src/StardewModdingAPI/Framework/ModHelper.cs | 59 +++++------------ .../Framework/Serialisation/JsonHelper.cs | 73 ++++++++++++++++++++++ src/StardewModdingAPI/Program.cs | 12 ++-- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 4 files changed, 96 insertions(+), 49 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 2b562b4f..4a3d5ed5 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -1,8 +1,7 @@ using System; using System.IO; -using Newtonsoft.Json; -using StardewModdingAPI.Advanced; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Serialisation; namespace StardewModdingAPI.Framework { @@ -12,12 +11,8 @@ namespace StardewModdingAPI.Framework /********* ** Properties *********/ - /// The JSON settings to use when serialising and deserialising files. - private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - ObjectCreationHandling = ObjectCreationHandling.Replace // avoid issue where default ICollection values are duplicated each time the config is loaded - }; + /// Encapsulates SMAPI's JSON file parsing. + private readonly JsonHelper JsonHelper; /********* @@ -38,20 +33,24 @@ namespace StardewModdingAPI.Framework *********/ /// Construct an instance. /// The mod directory path. + /// Encapsulate SMAPI's JSON parsing. /// Metadata about loaded mods. - /// An argument is null or invalid. + /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modDirectory, IModRegistry modRegistry) + public ModHelper(string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry) { // validate - if (modRegistry == null) - throw new ArgumentException("The mod registry cannot be null."); if (string.IsNullOrWhiteSpace(modDirectory)) - throw new ArgumentException("The mod directory cannot be empty."); + throw new ArgumentNullException(nameof(modDirectory)); + if (jsonHelper == null) + throw new ArgumentNullException(nameof(jsonHelper)); + if (modRegistry == null) + throw new ArgumentNullException(nameof(modRegistry)); if (!Directory.Exists(modDirectory)) throw new InvalidOperationException("The specified mod directory does not exist."); // initialise + this.JsonHelper = jsonHelper; this.DirectoryPath = modDirectory; this.ModRegistry = modRegistry; } @@ -88,28 +87,8 @@ namespace StardewModdingAPI.Framework public TModel ReadJsonFile(string path) where TModel : class { - // read file - string fullPath = Path.Combine(this.DirectoryPath, path); - string json; - try - { - json = File.ReadAllText(fullPath); - } - catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) - { - return null; - } - - // deserialise model - TModel model = JsonConvert.DeserializeObject(json, this.JsonSettings); - if (model is IConfigFile) - { - var wrapper = (IConfigFile)model; - wrapper.ModHelper = this; - wrapper.FilePath = path; - } - - return model; + path = Path.Combine(this.DirectoryPath, path); + return this.JsonHelper.ReadJsonFile(path, this); } /// Save to a JSON file. @@ -120,15 +99,7 @@ namespace StardewModdingAPI.Framework where TModel : class { path = Path.Combine(this.DirectoryPath, path); - - // create directory if needed - string dir = Path.GetDirectoryName(path); - if (!Directory.Exists(dir)) - Directory.CreateDirectory(dir); - - // write file - string json = JsonConvert.SerializeObject(model, this.JsonSettings); - File.WriteAllText(path, json); + this.JsonHelper.WriteJsonFile(path, model); } } } diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs new file mode 100644 index 00000000..3809666f --- /dev/null +++ b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs @@ -0,0 +1,73 @@ +using System; +using System.IO; +using Newtonsoft.Json; +using StardewModdingAPI.Advanced; + +namespace StardewModdingAPI.Framework.Serialisation +{ + /// Encapsulates SMAPI's JSON file parsing. + public class JsonHelper + { + /********* + ** Accessors + *********/ + /// The JSON settings to use when serialising and deserialising files. + private readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + ObjectCreationHandling = ObjectCreationHandling.Replace // avoid issue where default ICollection values are duplicated each time the config is loaded + }; + + + /********* + ** Public methods + *********/ + /// Read a JSON file. + /// The model type. + /// The absolete file path. + /// The mod helper to inject for instances. + /// Returns the deserialised model, or null if the file doesn't exist or is empty. + public TModel ReadJsonFile(string fullPath, IModHelper modHelper) + where TModel : class + { + // read file + string json; + try + { + json = File.ReadAllText(fullPath); + } + catch (Exception ex) when (ex is DirectoryNotFoundException || ex is FileNotFoundException) + { + return null; + } + + // deserialise model + TModel model = JsonConvert.DeserializeObject(json, this.JsonSettings); + if (model is IConfigFile) + { + var wrapper = (IConfigFile)model; + wrapper.ModHelper = modHelper; + wrapper.FilePath = fullPath; + } + + return model; + } + + /// Save to a JSON file. + /// The model type. + /// The absolete file path. + /// The model to save. + public void WriteJsonFile(string fullPath, TModel model) + where TModel : class + { + // create directory if needed + string dir = Path.GetDirectoryName(fullPath); + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + // write file + string json = JsonConvert.SerializeObject(model, this.JsonSettings); + File.WriteAllText(fullPath, json); + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index f4518e21..b86e186f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -15,6 +15,7 @@ using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Framework.Models; +using StardewModdingAPI.Framework.Serialisation; using StardewValley; using Monitor = StardewModdingAPI.Framework.Monitor; @@ -319,6 +320,9 @@ namespace StardewModdingAPI { Program.Monitor.Log("Loading mods..."); + // get JSON helper + JsonHelper jsonHelper = new JsonHelper(); + // get assembly loader AssemblyLoader modAssemblyLoader = new AssemblyLoader(Program.TargetPlatform, Program.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); @@ -353,9 +357,6 @@ namespace StardewModdingAPI return; } - // get helper - IModHelper helper = new ModHelper(directory.FullName, Program.ModRegistry); - // get manifest path string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) @@ -378,7 +379,7 @@ namespace StardewModdingAPI } // deserialise manifest - manifest = helper.ReadJsonFile("manifest.json"); + manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json"), null); if (manifest == null) { Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); @@ -503,8 +504,9 @@ namespace StardewModdingAPI } // inject data + // get helper mod.ModManifest = manifest; - mod.Helper = helper; + mod.Helper = new ModHelper(directory.FullName, jsonHelper, Program.ModRegistry); mod.Monitor = Program.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index ae08012d..add6ec40 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -148,6 +148,7 @@ + -- cgit From 0441d0843c65775bc72377e32ed4b3b5ee0b8f75 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 13 Feb 2017 00:40:33 -0500 Subject: add new console command API with backward compatibility (#199) --- src/StardewModdingAPI/Command.cs | 77 +++++++++------ src/StardewModdingAPI/Events/EventArgsCommand.cs | 1 + src/StardewModdingAPI/Framework/Command.cs | 40 ++++++++ src/StardewModdingAPI/Framework/CommandHelper.cs | 53 ++++++++++ src/StardewModdingAPI/Framework/CommandManager.cs | 114 ++++++++++++++++++++++ src/StardewModdingAPI/Framework/ModHelper.cs | 8 +- src/StardewModdingAPI/ICommandHelper.cs | 26 +++++ src/StardewModdingAPI/IModHelper.cs | 3 + src/StardewModdingAPI/Program.cs | 32 +++--- src/StardewModdingAPI/StardewModdingAPI.csproj | 4 + 10 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Command.cs create mode 100644 src/StardewModdingAPI/Framework/CommandHelper.cs create mode 100644 src/StardewModdingAPI/Framework/CommandManager.cs create mode 100644 src/StardewModdingAPI/ICommandHelper.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs index 6195bd8b..3b1e5632 100644 --- a/src/StardewModdingAPI/Command.cs +++ b/src/StardewModdingAPI/Command.cs @@ -1,12 +1,12 @@ using System; using System.Collections.Generic; -using System.Linq; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; namespace StardewModdingAPI { /// A command that can be submitted through the SMAPI console to interact with SMAPI. + [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] public class Command { /********* @@ -16,7 +16,7 @@ namespace StardewModdingAPI ** SMAPI ****/ /// The commands registered with SMAPI. - internal static List RegisteredCommands = new List(); + private static readonly IDictionary LegacyCommands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); /// The event raised when this command is submitted through the console. public event EventHandler CommandFired; @@ -64,6 +64,7 @@ namespace StardewModdingAPI this.CommandFired.Invoke(this, new EventArgsCommand(this)); } + /**** ** SMAPI ****/ @@ -72,27 +73,7 @@ namespace StardewModdingAPI /// Encapsulates monitoring and logging. public static void CallCommand(string input, IMonitor monitor) { - // normalise input - input = input?.Trim(); - if (string.IsNullOrWhiteSpace(input)) - return; - - // tokenise input - string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - string commandName = args[0]; - args = args.Skip(1).ToArray(); - - // get command - Command command = Command.FindCommand(commandName); - if (command == null) - { - monitor.Log("Unknown command", LogLevel.Error); - return; - } - - // fire command - command.CalledArgs = args; - command.Fire(); + Program.CommandManager.Trigger(input); } /// Register a command with SMAPI. @@ -101,11 +82,25 @@ namespace StardewModdingAPI /// A human-readable list of accepted arguments. public static Command RegisterCommand(string name, string description, string[] args = null) { - var command = new Command(name, description, args); - if (Command.RegisteredCommands.Contains(command)) - throw new InvalidOperationException($"The '{command.CommandName}' command is already registered!"); + name = name?.Trim().ToLower(); + + // raise deprecation warning + Program.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Notice); - Command.RegisteredCommands.Add(command); + // validate + if (Command.LegacyCommands.ContainsKey(name)) + throw new InvalidOperationException($"The '{name}' command is already registered!"); + + // add command + string modName = Program.ModRegistry.GetModFromStack() ?? ""; + string documentation = args?.Length > 0 + ? $"{description} - {string.Join(", ", args)}" + : description; + Program.CommandManager.Add(modName, name, documentation, Command.Fire); + + // add legacy command + Command command = new Command(name, description, args); + Command.LegacyCommands.Add(name, command); return command; } @@ -113,7 +108,33 @@ namespace StardewModdingAPI /// The command name to find. public static Command FindCommand(string name) { - return Command.RegisteredCommands.Find(x => x.CommandName.Equals(name)); + if (name == null) + return null; + + Command command; + Command.LegacyCommands.TryGetValue(name.Trim(), out command); + return command; + } + + + /********* + ** Private methods + *********/ + /// Trigger this command. + /// The command name. + /// The command arguments. + private static void Fire(string name, string[] args) + { + // get legacy command + Command command; + if (!Command.LegacyCommands.TryGetValue(name, out command)) + { + throw new InvalidOperationException($"Can't run command '{name}' because there's no such legacy command."); + return; + } + + // raise event + command.Fire(); } } } diff --git a/src/StardewModdingAPI/Events/EventArgsCommand.cs b/src/StardewModdingAPI/Events/EventArgsCommand.cs index ddf644fb..bae13694 100644 --- a/src/StardewModdingAPI/Events/EventArgsCommand.cs +++ b/src/StardewModdingAPI/Events/EventArgsCommand.cs @@ -3,6 +3,7 @@ namespace StardewModdingAPI.Events { /// Event arguments for a event. + [Obsolete("Use " + nameof(IModHelper) + "." + nameof(IModHelper.ConsoleCommands))] public class EventArgsCommand : EventArgs { /********* diff --git a/src/StardewModdingAPI/Framework/Command.cs b/src/StardewModdingAPI/Framework/Command.cs new file mode 100644 index 00000000..943e018d --- /dev/null +++ b/src/StardewModdingAPI/Framework/Command.cs @@ -0,0 +1,40 @@ +using System; + +namespace StardewModdingAPI.Framework +{ + /// A command that can be submitted through the SMAPI console to interact with SMAPI. + internal class Command + { + /********* + ** Accessor + *********/ + /// The friendly name for the mod that registered the command. + public string ModName { get; } + + /// The command name, which the user must type to trigger it. + public string Name { get; } + + /// The human-readable documentation shown when the player runs the built-in 'help' command. + public string Documentation { get; } + + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + public Action Callback { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The friendly name for the mod that registered the command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + public Command(string modName, string name, string documentation, Action callback) + { + this.ModName = modName; + this.Name = name; + this.Documentation = documentation; + this.Callback = callback; + } + } +} diff --git a/src/StardewModdingAPI/Framework/CommandHelper.cs b/src/StardewModdingAPI/Framework/CommandHelper.cs new file mode 100644 index 00000000..2e9dea8e --- /dev/null +++ b/src/StardewModdingAPI/Framework/CommandHelper.cs @@ -0,0 +1,53 @@ +using System; + +namespace StardewModdingAPI.Framework +{ + /// Provides an API for managing console commands. + internal class CommandHelper : ICommandHelper + { + /********* + ** Accessors + *********/ + /// The friendly mod name for this instance. + private readonly string ModName; + + /// Manages console commands. + private readonly CommandManager CommandManager; + + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The friendly mod name for this instance. + /// Manages console commands. + public CommandHelper(string modName, CommandManager commandManager) + { + this.ModName = modName; + this.CommandManager = commandManager; + } + + /// Add a console command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + public ICommandHelper Add(string name, string documentation, Action callback) + { + this.CommandManager.Add(this.ModName, name, documentation, callback); + return this; + } + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + public bool Trigger(string name, string[] arguments) + { + return this.CommandManager.Trigger(name, arguments); + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/StardewModdingAPI/Framework/CommandManager.cs new file mode 100644 index 00000000..3aa4bf97 --- /dev/null +++ b/src/StardewModdingAPI/Framework/CommandManager.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace StardewModdingAPI.Framework +{ + /// Manages console commands. + internal class CommandManager + { + /********* + ** Properties + *********/ + /// The commands registered with SMAPI. + private readonly IDictionary Commands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Add a console command. + /// The friendly mod name for this instance. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// Whether to allow a null argument; this should only used for backwards compatibility. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + public void Add(string modName, string name, string documentation, Action callback, bool allowNullCallback = false) + { + name = this.GetNormalisedName(name); + + // validate format + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name), "Can't register a command with no name."); + if (name.Any(char.IsWhiteSpace)) + throw new FormatException($"Can't register the '{name}' command because the name can't contain whitespace."); + if (callback == null && !allowNullCallback) + throw new ArgumentNullException(nameof(callback), $"Can't register the '{name}' command because without a callback."); + + // ensure uniqueness + if (this.Commands.ContainsKey(name)) + throw new ArgumentException(nameof(callback), $"Can't register the '{name}' command because there's already a command with that name."); + + // add command + this.Commands.Add(name, new Command(modName, name, documentation, callback)); + } + + /// Get a command by its unique name. + /// The command name. + /// Returns the matching command, or null if not found. + public Command Get(string name) + { + name = this.GetNormalisedName(name); + Command command; + this.Commands.TryGetValue(name, out command); + return command; + } + + /// Get all registered commands. + public IEnumerable GetAll() + { + return this.Commands + .Values + .OrderBy(p => p.Name); + } + + /// Trigger a command. + /// The raw command input. + /// Returns whether a matching command was triggered. + public bool Trigger(string input) + { + string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + string name = args[0]; + args = args.Skip(1).ToArray(); + + return this.Trigger(name, args); + } + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + public bool Trigger(string name, string[] arguments) + { + // get normalised name + name = this.GetNormalisedName(name); + if (name == null) + return false; + + // get command + Command command; + if (this.Commands.TryGetValue(name, out command)) + { + command.Callback.Invoke(name, arguments); + return true; + } + return false; + } + + /********* + ** Private methods + *********/ + /// Get a normalised command name. + /// The command name. + private string GetNormalisedName(string name) + { + name = name?.Trim().ToLower(); + return !string.IsNullOrWhiteSpace(name) + ? name + : null; + } + } +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 4a3d5ed5..04767de5 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -27,17 +27,22 @@ namespace StardewModdingAPI.Framework /// Metadata about loaded mods. public IModRegistry ModRegistry { get; } + /// An API for managing console commands. + public ICommandHelper ConsoleCommands { get; } + /********* ** Public methods *********/ /// Construct an instance. + /// The friendly mod name. /// The mod directory path. /// Encapsulate SMAPI's JSON parsing. /// Metadata about loaded mods. + /// Manages console commands. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry) + public ModHelper(string modName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -53,6 +58,7 @@ namespace StardewModdingAPI.Framework this.JsonHelper = jsonHelper; this.DirectoryPath = modDirectory; this.ModRegistry = modRegistry; + this.ConsoleCommands = new CommandHelper(modName, commandManager); } /**** diff --git a/src/StardewModdingAPI/ICommandHelper.cs b/src/StardewModdingAPI/ICommandHelper.cs new file mode 100644 index 00000000..3a51ffb4 --- /dev/null +++ b/src/StardewModdingAPI/ICommandHelper.cs @@ -0,0 +1,26 @@ +using System; + +namespace StardewModdingAPI +{ + /// Provides an API for managing console commands. + public interface ICommandHelper + { + /********* + ** Public methods + *********/ + /// Add a console command. + /// The command name, which the user must type to trigger it. + /// The human-readable documentation shown when the player runs the built-in 'help' command. + /// The method to invoke when the command is triggered. This method is passed the command name and arguments submitted by the user. + /// The or is null or empty. + /// The is not a valid format. + /// There's already a command with that name. + ICommandHelper Add(string name, string documentation, Action callback); + + /// Trigger a command. + /// The command name. + /// The command arguments. + /// Returns whether a matching command was triggered. + bool Trigger(string name, string[] arguments); + } +} diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs index 02f9c038..ef67cd1c 100644 --- a/src/StardewModdingAPI/IModHelper.cs +++ b/src/StardewModdingAPI/IModHelper.cs @@ -15,6 +15,9 @@ /// Metadata about loaded mods. IModRegistry ModRegistry { get; } + /// An API for managing console commands. + ICommandHelper ConsoleCommands { get; } + /********* ** Public methods diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b86e186f..ff0dff29 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -84,6 +84,9 @@ namespace StardewModdingAPI /// Manages deprecation warnings. internal static readonly DeprecationManager DeprecationManager = new DeprecationManager(Program.Monitor, Program.ModRegistry); + /// Manages console commands. + internal static readonly CommandManager CommandManager = new CommandManager(); + /********* ** Public methods @@ -262,7 +265,7 @@ namespace StardewModdingAPI while (!Program.ready) Thread.Sleep(1000); // register help command - Command.RegisterCommand("help", "Lists all commands | 'help ' returns command description").CommandFired += Program.help_CommandFired; + Program.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", Program.HandleHelpCommand); // listen for command line input Program.Monitor.Log("Starting console..."); @@ -506,7 +509,7 @@ namespace StardewModdingAPI // inject data // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(directory.FullName, jsonHelper, Program.ModRegistry); + mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, Program.ModRegistry, Program.CommandManager); mod.Monitor = Program.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; @@ -556,24 +559,29 @@ namespace StardewModdingAPI private static void ConsoleInputLoop() { while (true) - Command.CallCommand(Console.ReadLine(), Program.Monitor); + { + string input = Console.ReadLine(); + if (!Program.CommandManager.Trigger(input)) + Program.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + } } /// The method called when the user submits the help command in the console. - /// The event sender. - /// The event data. - private static void help_CommandFired(object sender, EventArgsCommand e) + /// The command name. + /// The command arguments. + private static void HandleHelpCommand(string name, string[] arguments) { - if (e.Command.CalledArgs.Length > 0) + if (arguments.Any()) { - var command = Command.FindCommand(e.Command.CalledArgs[0]); - if (command == null) - Program.Monitor.Log("The specified command could't be found", LogLevel.Error); + + Framework.Command result = Program.CommandManager.Get(arguments[0]); + if (result == null) + Program.Monitor.Log("There's no command with that name.", LogLevel.Error); else - Program.Monitor.Log(command.CommandArgs.Length > 0 ? $"{command.CommandName}: {command.CommandDesc} - {string.Join(", ", command.CommandArgs)}" : $"{command.CommandName}: {command.CommandDesc}", LogLevel.Info); + Program.Monitor.Log($"{result.Name}: {result.Documentation} [added by {result.ModName}]", LogLevel.Info); } else - Program.Monitor.Log("Commands: " + string.Join(", ", Command.RegisteredCommands.Select(x => x.CommandName)), LogLevel.Info); + Program.Monitor.Log("Commands: " + string.Join(", ", Program.CommandManager.GetAll().Select(p => p.Name)), LogLevel.Info); } /// Show a 'press any key to exit' message, and exit when they press a key. diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index add6ec40..ffeb8e2e 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -117,6 +117,7 @@ + @@ -145,11 +146,14 @@ + + + -- cgit From 845fbaab12a040464e403aa693a0eaaa9c55c9ae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 13 Feb 2017 01:19:02 -0500 Subject: migrate TrainerMod to new API (#199) --- src/StardewModdingAPI/Program.cs | 2 +- src/TrainerMod/TrainerMod.cs | 1027 +++++++++++++++++--------------------- 2 files changed, 468 insertions(+), 561 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ff0dff29..58b86e8f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -578,7 +578,7 @@ namespace StardewModdingAPI if (result == null) Program.Monitor.Log("There's no command with that name.", LogLevel.Error); else - Program.Monitor.Log($"{result.Name}: {result.Documentation} [added by {result.ModName}]", LogLevel.Info); + Program.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); } else Program.Monitor.Log("Commands: " + string.Join(", ", Program.CommandManager.GetAll().Select(p => p.Name)), LogLevel.Info); diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs index a7cedb6a..1690d5b5 100644 --- a/src/TrainerMod/TrainerMod.cs +++ b/src/TrainerMod/TrainerMod.cs @@ -44,7 +44,7 @@ namespace TrainerMod /// Provides simplified APIs for writing mods. public override void Entry(IModHelper helper) { - this.RegisterCommands(); + this.RegisterCommands(helper); GameEvents.UpdateTick += this.ReceiveUpdateTick; } @@ -73,634 +73,541 @@ namespace TrainerMod Game1.timeOfDay = this.FrozenTime; } - /// Register all trainer commands. - private void RegisterCommands() - { - Command.RegisterCommand("types", "Lists all value types | types").CommandFired += this.HandleTypes; - - Command.RegisterCommand("save", "Saves the game? Doesn't seem to work. | save").CommandFired += this.HandleSave; - Command.RegisterCommand("load", "Shows the load screen | load").CommandFired += this.HandleLoad; - - Command.RegisterCommand("player_setname", "Sets the player's name | player_setname ", new[] { "(player, pet, farm) (String) The target name" }).CommandFired += this.HandlePlayerSetName; - Command.RegisterCommand("player_setmoney", "Sets the player's money | player_setmoney |inf", new[] { "(Int32) The target money" }).CommandFired += this.HandlePlayerSetMoney; - Command.RegisterCommand("player_setstamina", "Sets the player's stamina | player_setstamina |inf", new[] { "(Int32) The target stamina" }).CommandFired += this.HandlePlayerSetStamina; - Command.RegisterCommand("player_setmaxstamina", "Sets the player's max stamina | player_setmaxstamina ", new[] { "(Int32) The target max stamina" }).CommandFired += this.HandlePlayerSetMaxStamina; - Command.RegisterCommand("player_sethealth", "Sets the player's health | player_sethealth |inf", new[] { "(Int32) The target health" }).CommandFired += this.HandlePlayerSetHealth; - Command.RegisterCommand("player_setmaxhealth", "Sets the player's max health | player_setmaxhealth ", new[] { "(Int32) The target max health" }).CommandFired += this.HandlePlayerSetMaxHealth; - Command.RegisterCommand("player_setimmunity", "Sets the player's immunity | player_setimmunity ", new[] { "(Int32) The target immunity" }).CommandFired += this.HandlePlayerSetImmunity; - - Command.RegisterCommand("player_setlevel", "Sets the player's specified skill to the specified value | player_setlevel ", new[] { "(luck, mining, combat, farming, fishing, foraging) (1-10) The target level" }).CommandFired += this.HandlePlayerSetLevel; - Command.RegisterCommand("player_setspeed", "Sets the player's speed to the specified value?", new[] { "(Int32) The target speed [0 is normal]" }).CommandFired += this.HandlePlayerSetSpeed; - Command.RegisterCommand("player_changecolour", "Sets the player's colour of the specified object | player_changecolor ", new[] { "(hair, eyes, pants) (r,g,b)" }).CommandFired += this.HandlePlayerChangeColor; - Command.RegisterCommand("player_changestyle", "Sets the player's style of the specified object | player_changecolor ", new[] { "(hair, shirt, skin, acc, shoe, swim, gender) (Int32)" }).CommandFired += this.HandlePlayerChangeStyle; - - Command.RegisterCommand("player_additem", "Gives the player an item | player_additem [count] [quality]", new[] { "(Int32) (Int32)[count] (Int32)[quality]" }).CommandFired += this.HandlePlayerAddItem; - Command.RegisterCommand("player_addmelee", "Gives the player a melee item | player_addmelee ", new[] { "?" }).CommandFired += this.HandlePlayerAddMelee; - Command.RegisterCommand("player_addring", "Gives the player a ring | player_addring ", new[] { "?" }).CommandFired += this.HandlePlayerAddRing; - - Command.RegisterCommand("list_items", "Lists items in the game data | list_items [search]", new[] { "(String)" }).CommandFired += this.HandleListItems; - - Command.RegisterCommand("world_settime", "Sets the time to the specified value | world_settime ", new[] { "(Int32) The target time [06:00 AM is 600]" }).CommandFired += this.HandleWorldSetTime; - Command.RegisterCommand("world_freezetime", "Freezes or thaws time | world_freezetime ", new[] { "(0 - 1) Whether or not to freeze time. 0 is thawed, 1 is frozen" }).CommandFired += this.HandleWorldFreezeTime; - Command.RegisterCommand("world_setday", "Sets the day to the specified value | world_setday ", new[] { "(Int32) The target day [1-28]" }).CommandFired += this.world_setDay; - Command.RegisterCommand("world_setseason", "Sets the season to the specified value | world_setseason ", new[] { "(winter, spring, summer, fall) The target season" }).CommandFired += this.HandleWorldSetSeason; - Command.RegisterCommand("world_downminelevel", "Goes down one mine level? | world_downminelevel", new[] { "" }).CommandFired += this.HandleWorldDownMineLevel; - Command.RegisterCommand("world_setminelevel", "Sets mine level? | world_setminelevel", new[] { "(Int32) The target level" }).CommandFired += this.HandleWorldSetMineLevel; - - Command.RegisterCommand("show_game_files", "Opens the game folder. | show_game_files").CommandFired += this.HandleShowGameFiles; - Command.RegisterCommand("show_data_files", "Opens the folder containing the save and log files. | show_data_files").CommandFired += this.HandleShowDataFiles; - } - /**** - ** Command handlers + ** Command definitions ****/ - /// The event raised when the 'types' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleTypes(object sender, EventArgsCommand e) - { - this.Monitor.Log($"[Int32: {int.MinValue} - {int.MaxValue}], [Int64: {long.MinValue} - {long.MaxValue}], [String: \"raw text\"], [Colour: r,g,b (EG: 128, 32, 255)]", LogLevel.Info); - } + /// Register all trainer commands. + /// Provides simplified APIs for writing mods. + private void RegisterCommands(IModHelper helper) + { + helper.ConsoleCommands + .Add("types", "Lists all value types.", this.HandleCommand) + .Add("save", "Saves the game? Doesn't seem to work.", this.HandleCommand) + .Add("load", "Shows the load screen.", this.HandleCommand) + .Add("player_setname", "Sets the player's name.\n\nUsage: player_setname \n- target: what to rename (one of 'player' or 'farm').\n- name: the new name to set.", this.HandleCommand) + .Add("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.", this.HandleCommand) + .Add("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina \n- value: an integer amount, or 'inf' for infinite stamina.", this.HandleCommand) + .Add("player_setmaxstamina", "Sets the player's max stamina.\n\nUsage: player_setmaxstamina \n- value: an integer amount.", this.HandleCommand) + .Add("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth \n- value: an integer amount, or 'inf' for infinite health.", this.HandleCommand) + .Add("player_setmaxhealth", "Sets the player's max health.\n\nUsage: player_setmaxhealth \n- value: an integer amount.", this.HandleCommand) + .Add("player_setimmunity", "Sets the player's immunity.\n\nUsage: player_setimmunity \n- value: an integer amount.", this.HandleCommand) + + .Add("player_setlevel", "Sets the player's specified skill to the specified value.\n\nUsage: player_setlevel \n- skill: the skill to set (one of 'luck', 'mining', 'combat', 'farming', 'fishing', or 'foraging').\n- value: the target level (a number from 1 to 10).", this.HandleCommand) + .Add("player_setspeed", "Sets the player's speed to the specified value?\n\nUsage: player_setspeed \n- value: an integer amount (0 is normal).", this.HandleCommand) + .Add("player_changecolour", "Sets the colour of a player feature.\n\nUsage: player_changecolor \n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- colour: a colour value in RGB format, like (255,255,255).", this.HandleCommand) + .Add("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changecolor .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.", this.HandleCommand) + + .Add("player_additem", $"Gives the player an item.\n\nUsage: player_additem [count] [quality]\n- item: the item ID (use the 'list_items' command to see a list).\n- count (optional): how many of the item to give.\n- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).", this.HandleCommand) + .Add("player_addmelee", "Gives the player a melee weapon.\n\nUsage: player_addmelee \n- item: the melee weapon ID (use the 'list_items' command to see a list).", this.HandleCommand) + .Add("player_addring", "Gives the player a ring.\n\nUsage: player_addring \n- item: the ring ID (use the 'list_items' command to see a list).", this.HandleCommand) + + .Add("list_items", "Lists and searches items in the game data.\n\nUsage: list_items [search]\n- search (optional): an arbitrary search string to filter by.", this.HandleCommand) + + .Add("world_settime", "Sets the time to the specified value.\n\nUsage: world_settime \n- value: the target time in military time (like 0600 for 6am and 1800 for 6pm)", this.HandleCommand) + .Add("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime \n- value: one of 0 (resume) or 1 (freeze).", this.HandleCommand) + .Add("world_setday", "Sets the day to the specified value.\n\nUsage: world_setday .\n- value: the target day (a number from 1 to 28).", this.HandleCommand) + .Add("world_setseason", "Sets the season to the specified value.\n\nUsage: world_setseason \n- value: the target season (one of 'spring', 'summer', 'fall', 'winter').", this.HandleCommand) + .Add("world_downminelevel", "Goes down one mine level?", this.HandleCommand) + .Add("world_setminelevel", "Sets the mine level?\n\nUsage: world_setminelevel \n- value: The target level (a number between 1 and 120).", this.HandleCommand) + + .Add("show_game_files", "Opens the game folder.", this.HandleCommand) + .Add("show_data_files", "Opens the folder containing the save and log files.", this.HandleCommand); + } + + /// Handle a TrainerMod command. + /// The command name. + /// The command arguments. + private void HandleCommand(string name, string[] args) + { + switch (name) + { + case "type": + this.Monitor.Log($"[Int32: {int.MinValue} - {int.MaxValue}], [Int64: {long.MinValue} - {long.MaxValue}], [String: \"raw text\"], [Colour: r,g,b (EG: 128, 32, 255)]", LogLevel.Info); + break; - /// The event raised when the 'save' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleSave(object sender, EventArgsCommand e) - { - SaveGame.Save(); - } + case "save": + SaveGame.Save(); + break; - /// The event raised when the 'load' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleLoad(object sender, EventArgsCommand e) - { - Game1.hasLoadedGame = false; - Game1.activeClickableMenu = new LoadGameMenu(); - } + case "load": + Game1.hasLoadedGame = false; + Game1.activeClickableMenu = new LoadGameMenu(); + break; - /// The event raised when the 'player_setName' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetName(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 1) - { - string target = e.Command.CalledArgs[0]; - string[] validTargets = { "player", "pet", "farm" }; - if (validTargets.Contains(target)) - { - switch (target) + case "player_setname": + if (args.Length > 1) { - case "player": - Game1.player.Name = e.Command.CalledArgs[1]; - break; - case "pet": - this.Monitor.Log("Pets cannot currently be renamed.", LogLevel.Info); - break; - case "farm": - Game1.player.farmName = e.Command.CalledArgs[1]; - break; + string target = args[0]; + string[] validTargets = { "player", "farm" }; + if (validTargets.Contains(target)) + { + switch (target) + { + case "player": + Game1.player.Name = args[1]; + break; + case "farm": + Game1.player.farmName = args[1]; + break; + } + } + else + this.LogObjectInvalid(); } - } - else - this.LogObjectInvalid(); - } - else - this.LogObjectValueNotSpecified(); - } + else + this.LogObjectValueNotSpecified(); + break; - /// The event raised when the 'player_setMoney' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetMoney(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string amountStr = e.Command.CalledArgs[0]; - if (amountStr == "inf") - this.InfiniteMoney = true; - else - { - this.InfiniteMoney = false; - int amount; - if (int.TryParse(amountStr, out amount)) + case "player_setmoney": + if (args.Any()) { - Game1.player.Money = amount; - this.Monitor.Log($"Set {Game1.player.Name}'s money to {Game1.player.Money}", LogLevel.Info); + string amountStr = args[0]; + if (amountStr == "inf") + this.InfiniteMoney = true; + else + { + this.InfiniteMoney = false; + int amount; + if (int.TryParse(amountStr, out amount)) + { + Game1.player.Money = amount; + this.Monitor.Log($"Set {Game1.player.Name}'s money to {Game1.player.Money}", LogLevel.Info); + } + else + this.LogValueNotInt32(); + } } else - this.LogValueNotInt32(); - } - } - else - this.LogValueNotSpecified(); - } + this.LogValueNotSpecified(); + break; - /// The event raised when the 'player_setStamina' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetStamina(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string amountStr = e.Command.CalledArgs[0]; - if (amountStr == "inf") - this.InfiniteStamina = true; - else - { - this.InfiniteStamina = false; - int amount; - if (int.TryParse(amountStr, out amount)) + case "playet_setstamina": + if (args.Any()) { - Game1.player.Stamina = amount; - this.Monitor.Log($"Set {Game1.player.Name}'s stamina to {Game1.player.Stamina}", LogLevel.Info); + string amountStr = args[0]; + if (amountStr == "inf") + this.InfiniteStamina = true; + else + { + this.InfiniteStamina = false; + int amount; + if (int.TryParse(amountStr, out amount)) + { + Game1.player.Stamina = amount; + this.Monitor.Log($"Set {Game1.player.Name}'s stamina to {Game1.player.Stamina}", LogLevel.Info); + } + else + this.LogValueNotInt32(); + } } else - this.LogValueNotInt32(); - } - } - else - this.LogValueNotSpecified(); - } + this.LogValueNotSpecified(); + break; - /// The event raised when the 'player_setMaxStamina' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetMaxStamina(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - int amount; - if (int.TryParse(e.Command.CalledArgs[0], out amount)) - { - Game1.player.MaxStamina = amount; - this.Monitor.Log($"Set {Game1.player.Name}'s max stamina to {Game1.player.MaxStamina}", LogLevel.Info); - } - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } - - /// The event raised when the 'player_setLevel' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetLevel(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 1) - { - string skill = e.Command.CalledArgs[0]; - string[] skills = { "luck", "mining", "combat", "farming", "fishing", "foraging" }; - if (skills.Contains(skill)) - { - int level; - if (int.TryParse(e.Command.CalledArgs[1], out level)) + case "player_setmaxstamina": + if (args.Any()) { - switch (skill) + int amount; + if (int.TryParse(args[0], out amount)) { - case "luck": - Game1.player.LuckLevel = level; - break; - case "mining": - Game1.player.MiningLevel = level; - break; - case "combat": - Game1.player.CombatLevel = level; - break; - case "farming": - Game1.player.FarmingLevel = level; - break; - case "fishing": - Game1.player.FishingLevel = level; - break; - case "foraging": - Game1.player.ForagingLevel = level; - break; + Game1.player.MaxStamina = amount; + this.Monitor.Log($"Set {Game1.player.Name}'s max stamina to {Game1.player.MaxStamina}", LogLevel.Info); } + else + this.LogValueNotInt32(); } else - this.LogValueNotInt32(); - } - else - this.Monitor.Log(" is invalid", LogLevel.Error); - } - else - this.Monitor.Log(" and must be specified", LogLevel.Error); - } + this.LogValueNotSpecified(); + break; - /// The event raised when the 'player_setSpeed' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetSpeed(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string amountStr = e.Command.CalledArgs[0]; - if (amountStr.IsInt()) - { - Game1.player.addedSpeed = amountStr.ToInt(); - this.Monitor.Log($"Set {Game1.player.Name}'s added speed to {Game1.player.addedSpeed}", LogLevel.Info); - } - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } - - /// The event raised when the 'player_changeColour' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerChangeColor(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 1) - { - string target = e.Command.CalledArgs[0]; - string[] validTargets = { "hair", "eyes", "pants" }; - if (validTargets.Contains(target)) - { - string[] colorHexes = e.Command.CalledArgs[1].Split(new[] { ',' }, 3); - if (colorHexes[0].IsInt() && colorHexes[1].IsInt() && colorHexes[2].IsInt()) + case "player_setlevel": + if (args.Length > 1) { - var color = new Color(colorHexes[0].ToInt(), colorHexes[1].ToInt(), colorHexes[2].ToInt()); - switch (target) + string skill = args[0]; + string[] skills = { "luck", "mining", "combat", "farming", "fishing", "foraging" }; + if (skills.Contains(skill)) { - case "hair": - Game1.player.hairstyleColor = color; - break; - case "eyes": - Game1.player.changeEyeColor(color); - break; - case "pants": - Game1.player.pantsColor = color; - break; + int level; + if (int.TryParse(args[1], out level)) + { + switch (skill) + { + case "luck": + Game1.player.LuckLevel = level; + break; + case "mining": + Game1.player.MiningLevel = level; + break; + case "combat": + Game1.player.CombatLevel = level; + break; + case "farming": + Game1.player.FarmingLevel = level; + break; + case "fishing": + Game1.player.FishingLevel = level; + break; + case "foraging": + Game1.player.ForagingLevel = level; + break; + } + } + else + this.LogValueNotInt32(); } + else + this.Monitor.Log(" is invalid", LogLevel.Error); } else - this.Monitor.Log(" is invalid", LogLevel.Error); - } - else - this.LogObjectInvalid(); - } - else - this.Monitor.Log(" and must be specified", LogLevel.Error); - } + this.Monitor.Log(" and must be specified", LogLevel.Error); + break; - /// The event raised when the 'player_changeStyle' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerChangeStyle(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 1) - { - string target = e.Command.CalledArgs[0]; - string[] validTargets = { "hair", "shirt", "skin", "acc", "shoe", "swim", "gender" }; - if (validTargets.Contains(target)) - { - if (e.Command.CalledArgs[1].IsInt()) + case "player_setspeed": + if (args.Any()) { - var styleID = e.Command.CalledArgs[1].ToInt(); - switch (target) + string amountStr = args[0]; + if (amountStr.IsInt()) { - case "hair": - Game1.player.changeHairStyle(styleID); - break; - case "shirt": - Game1.player.changeShirt(styleID); - break; - case "acc": - Game1.player.changeAccessory(styleID); - break; - case "skin": - Game1.player.changeSkinColor(styleID); - break; - case "shoe": - Game1.player.changeShoeColor(styleID); - break; - case "swim": - if (styleID == 0) - Game1.player.changeOutOfSwimSuit(); - else if (styleID == 1) - Game1.player.changeIntoSwimsuit(); - else - this.Monitor.Log(" must be 0 or 1 for this ", LogLevel.Error); - break; - case "gender": - if (styleID == 0) - Game1.player.changeGender(true); - else if (styleID == 1) - Game1.player.changeGender(false); - else - this.Monitor.Log(" must be 0 or 1 for this ", LogLevel.Error); - break; + Game1.player.addedSpeed = amountStr.ToInt(); + this.Monitor.Log($"Set {Game1.player.Name}'s added speed to {Game1.player.addedSpeed}", LogLevel.Info); } + else + this.LogValueNotInt32(); } else - this.LogValueInvalid(); - } - else - this.LogObjectInvalid(); - } - else - this.LogObjectValueNotSpecified(); - } + this.LogValueNotSpecified(); + break; - /// The event raised when the 'world_freezeTime' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleWorldFreezeTime(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string valueStr = e.Command.CalledArgs[0]; - if (valueStr.IsInt()) - { - int value = valueStr.ToInt(); - if (value == 0 || value == 1) + case "player_changecolour": + if (args.Length > 1) { - this.FreezeTime = value == 1; - this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0; - this.Monitor.Log("Time is now " + (this.FreezeTime ? "frozen" : "thawed"), LogLevel.Info); + string target = args[0]; + string[] validTargets = { "hair", "eyes", "pants" }; + if (validTargets.Contains(target)) + { + string[] colorHexes = args[1].Split(new[] { ',' }, 3); + if (colorHexes[0].IsInt() && colorHexes[1].IsInt() && colorHexes[2].IsInt()) + { + var color = new Color(colorHexes[0].ToInt(), colorHexes[1].ToInt(), colorHexes[2].ToInt()); + switch (target) + { + case "hair": + Game1.player.hairstyleColor = color; + break; + case "eyes": + Game1.player.changeEyeColor(color); + break; + case "pants": + Game1.player.pantsColor = color; + break; + } + } + else + this.Monitor.Log(" is invalid", LogLevel.Error); + } + else + this.LogObjectInvalid(); } else - this.Monitor.Log(" should be 0 or 1", LogLevel.Error); - } - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } + this.Monitor.Log(" and must be specified", LogLevel.Error); + break; - /// The event raised when the 'world_setTime' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleWorldSetTime(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string timeStr = e.Command.CalledArgs[0]; - if (timeStr.IsInt()) - { - int time = timeStr.ToInt(); + case "player_changestyle": + if (args.Length > 1) + { + string target = args[0]; + string[] validTargets = { "hair", "shirt", "skin", "acc", "shoe", "swim", "gender" }; + if (validTargets.Contains(target)) + { + if (args[1].IsInt()) + { + var styleID = args[1].ToInt(); + switch (target) + { + case "hair": + Game1.player.changeHairStyle(styleID); + break; + case "shirt": + Game1.player.changeShirt(styleID); + break; + case "acc": + Game1.player.changeAccessory(styleID); + break; + case "skin": + Game1.player.changeSkinColor(styleID); + break; + case "shoe": + Game1.player.changeShoeColor(styleID); + break; + case "swim": + if (styleID == 0) + Game1.player.changeOutOfSwimSuit(); + else if (styleID == 1) + Game1.player.changeIntoSwimsuit(); + else + this.Monitor.Log(" must be 0 or 1 for this ", LogLevel.Error); + break; + case "gender": + if (styleID == 0) + Game1.player.changeGender(true); + else if (styleID == 1) + Game1.player.changeGender(false); + else + this.Monitor.Log(" must be 0 or 1 for this ", LogLevel.Error); + break; + } + } + else + this.LogValueInvalid(); + } + else + this.LogObjectInvalid(); + } + else + this.LogObjectValueNotSpecified(); + break; - if (time <= 2600 && time >= 600) + case "world_freezetime": + if (args.Any()) { - Game1.timeOfDay = e.Command.CalledArgs[0].ToInt(); - this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0; - this.Monitor.Log($"Time set to: {Game1.timeOfDay}", LogLevel.Info); + string valueStr = args[0]; + if (valueStr.IsInt()) + { + int value = valueStr.ToInt(); + if (value == 0 || value == 1) + { + this.FreezeTime = value == 1; + this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0; + this.Monitor.Log("Time is now " + (this.FreezeTime ? "frozen" : "thawed"), LogLevel.Info); + } + else + this.Monitor.Log(" should be 0 or 1", LogLevel.Error); + } + else + this.LogValueNotInt32(); } else - this.Monitor.Log(" should be between 600 and 2600 (06:00 AM - 02:00 AM [NEXT DAY])", LogLevel.Error); - } - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } + this.LogValueNotSpecified(); + break; - /// The event raised when the 'world_setDay' command is triggered. - /// The event sender. - /// The event arguments. - private void world_setDay(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string dayStr = e.Command.CalledArgs[0]; + case "world_settime": + if (args.Any()) + { + string timeStr = args[0]; + if (timeStr.IsInt()) + { + int time = timeStr.ToInt(); - if (dayStr.IsInt()) - { - int day = dayStr.ToInt(); - if (day <= 28 && day > 0) - Game1.dayOfMonth = day; + if (time <= 2600 && time >= 600) + { + Game1.timeOfDay = args[0].ToInt(); + this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0; + this.Monitor.Log($"Time set to: {Game1.timeOfDay}", LogLevel.Info); + } + else + this.Monitor.Log(" should be between 600 and 2600 (06:00 AM - 02:00 AM [NEXT DAY])", LogLevel.Error); + } + else + this.LogValueNotInt32(); + } else - this.Monitor.Log(" must be between 1 and 28", LogLevel.Error); - } - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } - - /// The event raised when the 'world_setSeason' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleWorldSetSeason(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string season = e.Command.CalledArgs[0]; - string[] validSeasons = { "winter", "spring", "summer", "fall" }; - if (validSeasons.Contains(season)) - Game1.currentSeason = season; - else - this.LogValueInvalid(); - } - else - this.LogValueNotSpecified(); - } + this.LogValueNotSpecified(); + break; - /// The event raised when the 'player_setHealth' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetHealth(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string amountStr = e.Command.CalledArgs[0]; + case "world_setday": + if (args.Any()) + { + string dayStr = args[0]; - if (amountStr == "inf") - this.InfiniteHealth = true; - else - { - this.InfiniteHealth = false; - if (amountStr.IsInt()) - Game1.player.health = amountStr.ToInt(); + if (dayStr.IsInt()) + { + int day = dayStr.ToInt(); + if (day <= 28 && day > 0) + Game1.dayOfMonth = day; + else + this.Monitor.Log(" must be between 1 and 28", LogLevel.Error); + } + else + this.LogValueNotInt32(); + } else - this.LogValueNotInt32(); - } - } - else - this.LogValueNotSpecified(); - } - - /// The event raised when the 'player_setMaxHealth' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetMaxHealth(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string amountStr = e.Command.CalledArgs[0]; - if (amountStr.IsInt()) - Game1.player.maxHealth = amountStr.ToInt(); - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } + this.LogValueNotSpecified(); + break; - /// The event raised when the 'player_setImmunity' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerSetImmunity(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string amountStr = e.Command.CalledArgs[0]; - if (amountStr.IsInt()) - Game1.player.immunity = amountStr.ToInt(); - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } + case "world_setseason": + if (args.Any()) + { + string season = args[0]; + string[] validSeasons = { "winter", "spring", "summer", "fall" }; + if (validSeasons.Contains(season)) + Game1.currentSeason = season; + else + this.LogValueInvalid(); + } + else + this.LogValueNotSpecified(); + break; - /// The event raised when the 'player_addItem' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerAddItem(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - string itemIdStr = e.Command.CalledArgs[0]; - if (itemIdStr.IsInt()) - { - int itemID = itemIdStr.ToInt(); - int count = 1; - int quality = 0; - if (e.Command.CalledArgs.Length > 1) + case "player_sethealth": + if (args.Any()) { - if (e.Command.CalledArgs[1].IsInt()) - count = e.Command.CalledArgs[1].ToInt(); + string amountStr = args[0]; + + if (amountStr == "inf") + this.InfiniteHealth = true; else { - this.Monitor.Log("[count] is invalid", LogLevel.Error); - return; + this.InfiniteHealth = false; + if (amountStr.IsInt()) + Game1.player.health = amountStr.ToInt(); + else + this.LogValueNotInt32(); } + } + else + this.LogValueNotSpecified(); + break; + + case "player_setmaxhealth": + if (args.Any()) + { + string amountStr = args[0]; + if (amountStr.IsInt()) + Game1.player.maxHealth = amountStr.ToInt(); + else + this.LogValueNotInt32(); + } + else + this.LogValueNotSpecified(); + break; + + case "player_setimmunity": + if (args.Any()) + { + string amountStr = args[0]; + if (amountStr.IsInt()) + Game1.player.immunity = amountStr.ToInt(); + else + this.LogValueNotInt32(); + } + else + this.LogValueNotSpecified(); + break; - if (e.Command.CalledArgs.Length > 2) + case "player_additem": + if (args.Any()) + { + string itemIdStr = args[0]; + if (itemIdStr.IsInt()) { - if (e.Command.CalledArgs[2].IsInt()) - quality = e.Command.CalledArgs[2].ToInt(); - else + int itemID = itemIdStr.ToInt(); + int count = 1; + int quality = 0; + if (args.Length > 1) { - this.Monitor.Log("[quality] is invalid", LogLevel.Error); - return; + if (args[1].IsInt()) + count = args[1].ToInt(); + else + { + this.Monitor.Log("[count] is invalid", LogLevel.Error); + return; + } + + if (args.Length > 2) + { + if (args[2].IsInt()) + quality = args[2].ToInt(); + else + { + this.Monitor.Log("[quality] is invalid", LogLevel.Error); + return; + } + } } + + var item = new Object(itemID, count) { quality = quality }; + + Game1.player.addItemByMenuIfNecessary(item); } + else + this.Monitor.Log(" is invalid", LogLevel.Error); } + else + this.LogObjectValueNotSpecified(); + break; - var item = new Object(itemID, count) { quality = quality }; + case "player_addmelee": + if (args.Any()) + { + if (args[0].IsInt()) + { + MeleeWeapon weapon = new MeleeWeapon(args[0].ToInt()); + Game1.player.addItemByMenuIfNecessary(weapon); + this.Monitor.Log($"Gave {weapon.Name} to {Game1.player.Name}", LogLevel.Info); + } + else + this.Monitor.Log(" is invalid", LogLevel.Error); + } + else + this.LogObjectValueNotSpecified(); + break; - Game1.player.addItemByMenuIfNecessary(item); - } - else - this.Monitor.Log(" is invalid", LogLevel.Error); - } - else - this.LogObjectValueNotSpecified(); - } + case "player_addring": + if (args.Any()) + { + if (args[0].IsInt()) + { + Ring ring = new Ring(args[0].ToInt()); + Game1.player.addItemByMenuIfNecessary(ring); + this.Monitor.Log($"Gave {ring.Name} to {Game1.player.Name}", LogLevel.Info); + } + else + this.Monitor.Log(" is invalid", LogLevel.Error); + } + else + this.LogObjectValueNotSpecified(); + break; - /// The event raised when the 'player_addMelee' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerAddMelee(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - if (e.Command.CalledArgs[0].IsInt()) - { - MeleeWeapon weapon = new MeleeWeapon(e.Command.CalledArgs[0].ToInt()); - Game1.player.addItemByMenuIfNecessary(weapon); - this.Monitor.Log($"Gave {weapon.Name} to {Game1.player.Name}", LogLevel.Info); - } - else - this.Monitor.Log(" is invalid", LogLevel.Error); - } - else - this.LogObjectValueNotSpecified(); - } + case "list_items": + { + var matches = this.GetItems(args).ToArray(); - /// The event raised when the 'player_addRing' command is triggered. - /// The event sender. - /// The event arguments. - private void HandlePlayerAddRing(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - if (e.Command.CalledArgs[0].IsInt()) - { - Ring ring = new Ring(e.Command.CalledArgs[0].ToInt()); - Game1.player.addItemByMenuIfNecessary(ring); - this.Monitor.Log($"Gave {ring.Name} to {Game1.player.Name}", LogLevel.Info); - } - else - this.Monitor.Log(" is invalid", LogLevel.Error); - } - else - this.LogObjectValueNotSpecified(); - } + // show matches + string summary = "Searching...\n"; + if (matches.Any()) + this.Monitor.Log(summary + this.GetTableString(matches, new[] { "type", "id", "name" }, val => new[] { val.Type.ToString(), val.ID.ToString(), val.Name }), LogLevel.Info); + else + this.Monitor.Log(summary + "No items found", LogLevel.Info); + } + break; - /// The event raised when the 'list_items' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleListItems(object sender, EventArgsCommand e) - { - var matches = this.GetItems(e.Command.CalledArgs).ToArray(); - - // show matches - string summary = "Searching...\n"; - if (matches.Any()) - this.Monitor.Log(summary + this.GetTableString(matches, new[] { "type", "id", "name" }, val => new[] { val.Type.ToString(), val.ID.ToString(), val.Name }), LogLevel.Info); - else - this.Monitor.Log(summary + "No items found", LogLevel.Info); - } + case "world_downminelevel": + Game1.nextMineLevel(); + break; - /// The event raised when the 'world_downMineLevel' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleWorldDownMineLevel(object sender, EventArgsCommand e) - { - Game1.nextMineLevel(); - } + case "world_setminelevel": + if (args.Any()) + { + if (args[0].IsInt()) + Game1.enterMine(true, args[0].ToInt(), ""); + else + this.LogValueNotInt32(); + } + else + this.LogValueNotSpecified(); + break; - /// The event raised when the 'world_setMineLevel' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleWorldSetMineLevel(object sender, EventArgsCommand e) - { - if (e.Command.CalledArgs.Length > 0) - { - if (e.Command.CalledArgs[0].IsInt()) - Game1.enterMine(true, e.Command.CalledArgs[0].ToInt(), ""); - else - this.LogValueNotInt32(); - } - else - this.LogValueNotSpecified(); - } + case "show_game_files": + Process.Start(Constants.ExecutionPath); + break; - /// The event raised when the 'show_game_files' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleShowGameFiles(object sender, EventArgsCommand e) - { - Process.Start(Constants.ExecutionPath); - } + case "show_data_files": + Process.Start(Constants.DataPath); + break; - /// The event raised when the 'show_data_files' command is triggered. - /// The event sender. - /// The event arguments. - private void HandleShowDataFiles(object sender, EventArgsCommand e) - { - Process.Start(Constants.DataPath); + default: + throw new NotImplementedException($"TrainerMod received unknown command '{name}'."); + } } /**** -- cgit From f140e844ed1e52d1f5955e07a397f44ef9ab3233 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 14 Feb 2017 22:06:06 -0500 Subject: streamline startup a bit --- src/StardewModdingAPI/Framework/Monitor.cs | 2 +- src/StardewModdingAPI/Program.cs | 56 +++++++++++++----------------- 2 files changed, 26 insertions(+), 32 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index 3a9276a0..c1735917 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -82,7 +82,7 @@ namespace StardewModdingAPI.Framework public void ExitGameImmediately(string reason) { Program.ExitGameImmediately(this.Source, reason); - Program.gamePtr.Exit(); + Program.GameInstance.Exit(); } /// Log a fatal error message. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 58b86e8f..c3f8f8d8 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -59,21 +59,12 @@ namespace StardewModdingAPI /// Whether the game is currently running. private static bool ready; - /// The underlying game assembly. - private static Assembly StardewAssembly; - - /// The underlying type. - private static Type StardewProgramType; - - /// The field containing game's main instance. - private static FieldInfo StardewGameInfo; - /********* ** Accessors *********/ /// The underlying game instance. - internal static SGame gamePtr; + internal static SGame GameInstance; /// The number of mods currently loaded by SMAPI. internal static int ModsLoaded; @@ -185,8 +176,8 @@ namespace StardewModdingAPI Program.CancellationTokenSource.Cancel(); if (Program.ready) { - Program.gamePtr.Exiting += (sender, e) => Program.PressAnyKeyToExit(); - Program.gamePtr.Exit(); + Program.GameInstance.Exiting += (sender, e) => Program.PressAnyKeyToExit(); + Program.GameInstance.Exit(); } } @@ -226,29 +217,31 @@ namespace StardewModdingAPI { try { - // load the game assembly - Program.Monitor.Log("Loading game..."); - Program.StardewAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); - Program.StardewProgramType = Program.StardewAssembly.GetType("StardewValley.Program", true); - Program.StardewGameInfo = Program.StardewProgramType.GetField("gamePtr"); - Game1.version += $" | SMAPI {Constants.ApiVersion}"; - - // add error interceptors + // add error handlers #if SMAPI_FOR_WINDOWS Application.ThreadException += (sender, e) => Program.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); #endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => Program.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - // initialise game instance - Program.gamePtr = new SGame(Program.Monitor) { IsMouseVisible = false }; - Program.gamePtr.Exiting += (sender, e) => Program.ready = false; - Program.gamePtr.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(Program.Monitor, sender, e); - Program.gamePtr.Window.Title = $"Stardew Valley - Version {Game1.version}"; - Program.StardewGameInfo.SetValue(Program.StardewProgramType, Program.gamePtr); - - // patch graphics - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + // initialise game + { + // load assembly + Program.Monitor.Log("Loading game..."); + Assembly gameAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); + Type gameProgramType = gameAssembly.GetType("StardewValley.Program", true); + + // set Game1 instance + Program.GameInstance = new SGame(Program.Monitor); + Program.GameInstance.Exiting += (sender, e) => Program.ready = false; + Program.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(Program.Monitor, sender, e); + Program.GameInstance.Window.Title = $"Stardew Valley - Version {Game1.version}"; + gameProgramType.GetField("gamePtr").SetValue(gameProgramType, Program.GameInstance); + + // configure + Game1.version += $" | SMAPI {Constants.ApiVersion}"; + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; + } // load mods Program.LoadMods(); @@ -262,7 +255,8 @@ namespace StardewModdingAPI new Thread(() => { // wait for the game to load up - while (!Program.ready) Thread.Sleep(1000); + while (!Program.ready) + Thread.Sleep(1000); // register help command Program.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", Program.HandleHelpCommand); @@ -290,7 +284,7 @@ namespace StardewModdingAPI try { Program.ready = true; - Program.gamePtr.Run(); + Program.GameInstance.Run(); } finally { -- cgit From 176eddbf7b70934c2665aa3a0ac8b46bef04012a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 16 Feb 2017 00:54:41 -0500 Subject: make SMAPI core non-static, eliminate direct access between core components --- src/StardewModdingAPI/Command.cs | 39 ++- src/StardewModdingAPI/Config.cs | 18 +- src/StardewModdingAPI/Constants.cs | 3 - src/StardewModdingAPI/Events/PlayerEvents.cs | 18 +- src/StardewModdingAPI/Events/TimeEvents.cs | 16 +- .../Framework/InternalExtensions.cs | 16 +- src/StardewModdingAPI/Framework/Monitor.cs | 9 +- .../Framework/RequestExitDelegate.cs | 7 + src/StardewModdingAPI/Log.cs | 16 +- src/StardewModdingAPI/Mod.cs | 24 +- src/StardewModdingAPI/Program.cs | 287 +++++++++++---------- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 12 files changed, 294 insertions(+), 160 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/RequestExitDelegate.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs index 1cbb01ff..6b056ce7 100644 --- a/src/StardewModdingAPI/Command.cs +++ b/src/StardewModdingAPI/Command.cs @@ -12,12 +12,22 @@ namespace StardewModdingAPI /********* ** Properties *********/ - /**** - ** SMAPI - ****/ /// The commands registered with SMAPI. private static readonly IDictionary LegacyCommands = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + /// Manages console commands. + private static CommandManager CommandManager; + + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /// Tracks the installed mods. + private static ModRegistry ModRegistry; + + + /********* + ** Accessors + *********/ /// The event raised when this command is submitted through the console. public event EventHandler CommandFired; @@ -43,6 +53,17 @@ namespace StardewModdingAPI /**** ** Command ****/ + /// Injects types required for backwards compatibility. + /// Manages console commands. + /// Manages deprecation warnings. + /// Tracks the installed mods. + internal static void Shim(CommandManager commandManager, DeprecationManager deprecationManager, ModRegistry modRegistry) + { + Command.CommandManager = commandManager; + Command.DeprecationManager = deprecationManager; + Command.ModRegistry = modRegistry; + } + /// Construct an instance. /// The name of the command. /// A human-readable description of what the command does. @@ -73,8 +94,8 @@ namespace StardewModdingAPI /// Encapsulates monitoring and logging. public static void CallCommand(string input, IMonitor monitor) { - Program.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.Notice); - Program.CommandManager.Trigger(input); + Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.Notice); + Command.CommandManager.Trigger(input); } /// Register a command with SMAPI. @@ -86,18 +107,18 @@ namespace StardewModdingAPI name = name?.Trim().ToLower(); // raise deprecation warning - Program.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Notice); + Command.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Notice); // validate if (Command.LegacyCommands.ContainsKey(name)) throw new InvalidOperationException($"The '{name}' command is already registered!"); // add command - string modName = Program.ModRegistry.GetModFromStack() ?? ""; + string modName = Command.ModRegistry.GetModFromStack() ?? ""; string documentation = args?.Length > 0 ? $"{description} - {string.Join(", ", args)}" : description; - Program.CommandManager.Add(modName, name, documentation, Command.Fire); + Command.CommandManager.Add(modName, name, documentation, Command.Fire); // add legacy command Command command = new Command(name, description, args); @@ -109,7 +130,7 @@ namespace StardewModdingAPI /// The command name to find. public static Command FindCommand(string name) { - Program.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.Notice); + Command.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.Notice); if (name == null) return null; diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs index 037c0fdf..f253930d 100644 --- a/src/StardewModdingAPI/Config.cs +++ b/src/StardewModdingAPI/Config.cs @@ -11,6 +11,13 @@ namespace StardewModdingAPI [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] public abstract class Config { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Accessors *********/ @@ -26,6 +33,13 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + Config.DeprecationManager = deprecationManager; + } + /// Construct an instance of the config class. /// The config class type. [Obsolete("This base class is obsolete since SMAPI 1.0. See the latest project README for details.")] @@ -111,8 +125,8 @@ namespace StardewModdingAPI /// Construct an instance. protected Config() { - Program.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Notice); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings + Config.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Notice); + Config.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings } } diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index db0c2622..262ba61d 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -59,9 +59,6 @@ namespace StardewModdingAPI /// The GitHub repository to check for updates. internal const string GitHubRepository = "Pathoschild/SMAPI"; - /// The title of the SMAPI console window. - internal static string ConsoleTitle => $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {Program.ModsLoaded}"; - /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs index 99bdac16..996077ab 100644 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -10,6 +10,13 @@ namespace StardewModdingAPI.Events /// Events raised when the player data changes. public static class PlayerEvents { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Events *********/ @@ -31,6 +38,13 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + PlayerEvents.DeprecationManager = deprecationManager; + } + /// Raise a event. /// Encapsulates monitoring and logging. /// Whether the save has been loaded. This is always true. @@ -42,7 +56,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}"; Delegate[] handlers = PlayerEvents.LoadedGame.GetInvocationList(); - Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); monitor.SafelyRaiseGenericEvent(name, handlers, null, loaded); } @@ -58,7 +72,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}"; Delegate[] handlers = PlayerEvents.FarmerChanged.GetInvocationList(); - Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); } diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index a140a223..0f9257c1 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -6,6 +6,13 @@ namespace StardewModdingAPI.Events /// Events raised when the in-game date or time changes. public static class TimeEvents { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Events *********/ @@ -32,6 +39,13 @@ namespace StardewModdingAPI.Events /********* ** Internal methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + TimeEvents.DeprecationManager = deprecationManager; + } + /// Raise an event. /// Encapsulates monitoring and logging. internal static void InvokeAfterDayStarted(IMonitor monitor) @@ -88,7 +102,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}"; Delegate[] handlers = TimeEvents.OnNewDay.GetInvocationList(); - Program.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + TimeEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); } } diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index c4bd2d35..4ca79518 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -8,9 +8,23 @@ namespace StardewModdingAPI.Framework /// Provides extension methods for SMAPI's internal use. internal static class InternalExtensions { + /********* + ** Properties + *********/ + /// Tracks the installed mods. + private static ModRegistry ModRegistry; + + /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Tracks the installed mods. + internal static void Shim(ModRegistry modRegistry) + { + InternalExtensions.ModRegistry = modRegistry; + } + /**** ** IMonitor ****/ @@ -103,7 +117,7 @@ namespace StardewModdingAPI.Framework foreach (Delegate handler in handlers) { - string modName = Program.ModRegistry.GetModFrom(handler) ?? "an unknown mod"; // suppress stack trace for unknown mods, not helpful here + string modName = InternalExtensions.ModRegistry.GetModFrom(handler) ?? "an unknown mod"; // suppress stack trace for unknown mods, not helpful here deprecationManager.Warn(modName, nounPhrase, version, severity); } } diff --git a/src/StardewModdingAPI/Framework/Monitor.cs b/src/StardewModdingAPI/Framework/Monitor.cs index c1735917..64075f2f 100644 --- a/src/StardewModdingAPI/Framework/Monitor.cs +++ b/src/StardewModdingAPI/Framework/Monitor.cs @@ -34,6 +34,9 @@ namespace StardewModdingAPI.Framework [LogLevel.Alert] = ConsoleColor.Magenta }; + /// A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + private RequestExitDelegate RequestExit; + /********* ** Accessors @@ -55,7 +58,8 @@ namespace StardewModdingAPI.Framework /// The name of the module which logs messages using this instance. /// Manages access to the console output. /// The log file to which to write messages. - public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile) + /// A delegate which requests that SMAPI immediately exit the game. + public Monitor(string source, ConsoleInterceptionManager consoleManager, LogFileManager logFile, RequestExitDelegate requestExitDelegate) { // validate if (string.IsNullOrWhiteSpace(source)) @@ -81,8 +85,7 @@ namespace StardewModdingAPI.Framework /// The reason for the shutdown. public void ExitGameImmediately(string reason) { - Program.ExitGameImmediately(this.Source, reason); - Program.GameInstance.Exit(); + this.RequestExit(this.Source, reason); } /// Log a fatal error message. diff --git a/src/StardewModdingAPI/Framework/RequestExitDelegate.cs b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs new file mode 100644 index 00000000..12d0ea0c --- /dev/null +++ b/src/StardewModdingAPI/Framework/RequestExitDelegate.cs @@ -0,0 +1,7 @@ +namespace StardewModdingAPI.Framework +{ + /// A delegate which requests that SMAPI immediately exit the game. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. + /// The module which requested an immediate exit. + /// The reason provided for the shutdown. + internal delegate void RequestExitDelegate(string module, string reason); +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs index 5cb794f9..da98baba 100644 --- a/src/StardewModdingAPI/Log.cs +++ b/src/StardewModdingAPI/Log.cs @@ -9,6 +9,13 @@ namespace StardewModdingAPI [Obsolete("Use " + nameof(Mod) + "." + nameof(Mod.Monitor))] public static class Log { + /********* + ** Properties + *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + + /********* ** Accessors *********/ @@ -22,6 +29,13 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + Log.DeprecationManager = deprecationManager; + } + /**** ** Exceptions ****/ @@ -292,7 +306,7 @@ namespace StardewModdingAPI /// Raise a deprecation warning. private static void WarnDeprecated() { - Program.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Notice); + Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Notice); } /// Get the name of the mod logging a message from the stack. diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index c8456a29..d3fe882f 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -10,6 +10,9 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + /// The backing field for . private string _pathOnDisk; @@ -32,7 +35,7 @@ namespace StardewModdingAPI { get { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Notice); return this._pathOnDisk; } internal set { this._pathOnDisk = value; } @@ -44,8 +47,8 @@ namespace StardewModdingAPI { get { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Notice); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings return Path.Combine(this.PathOnDisk, "config.json"); } } @@ -60,8 +63,8 @@ namespace StardewModdingAPI { get { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.Info); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigPath)}", "1.0", DeprecationLevel.Info); + Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0"); // avoid redundant warnings return Constants.CurrentSavePathExists ? Path.Combine(this.PerSaveConfigFolder, Constants.SaveFolderName + ".json") : ""; } } @@ -70,6 +73,13 @@ namespace StardewModdingAPI /********* ** Public methods *********/ + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + Mod.DeprecationManager = deprecationManager; + } + /// The mod entry point, called after the mod is first loaded. [Obsolete("This overload is obsolete since SMAPI 1.0.")] public virtual void Entry(params object[] objects) { } @@ -86,8 +96,8 @@ namespace StardewModdingAPI [Obsolete] private string GetPerSaveConfigFolder() { - Program.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Notice); - Program.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings if (!((Manifest)this.ModManifest).PerSaveConfigs) { diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index c3f8f8d8..0857d41b 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -28,7 +28,7 @@ namespace StardewModdingAPI ** Properties *********/ /// The target game platform. - private static readonly Platform TargetPlatform = + private readonly Platform TargetPlatform = #if SMAPI_FOR_WINDOWS Platform.Windows; #else @@ -36,47 +36,47 @@ namespace StardewModdingAPI #endif /// The full path to the Stardew Valley executable. - private static readonly string GameExecutablePath = Path.Combine(Constants.ExecutionPath, Program.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); + private readonly string GameExecutablePath; /// The full path to the folder containing mods. - private static readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); + private readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); /// The log file to which to write messages. - private static readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); + private readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); /// Manages console output interception. - private static readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); + private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); /// The core logger for SMAPI. - private static readonly Monitor Monitor = new Monitor("SMAPI", Program.ConsoleManager, Program.LogFile); + private readonly Monitor Monitor; /// The user settings for SMAPI. - private static UserSettings Settings; + private UserSettings Settings; /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. - private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); /// Whether the game is currently running. - private static bool ready; + private bool IsGameRunning; /********* ** Accessors *********/ /// The underlying game instance. - internal static SGame GameInstance; + internal SGame GameInstance; /// The number of mods currently loaded by SMAPI. - internal static int ModsLoaded; + internal int ModsLoaded; /// Tracks the installed mods. - internal static readonly ModRegistry ModRegistry = new ModRegistry(); + internal readonly ModRegistry ModRegistry = new ModRegistry(); /// Manages deprecation warnings. - internal static readonly DeprecationManager DeprecationManager = new DeprecationManager(Program.Monitor, Program.ModRegistry); + internal readonly DeprecationManager DeprecationManager; /// Manages console commands. - internal static readonly CommandManager CommandManager = new CommandManager(); + internal readonly CommandManager CommandManager = new CommandManager(); /********* @@ -85,11 +85,36 @@ namespace StardewModdingAPI /// The main entry point which hooks into and launches the game. /// The command-line arguments. private static void Main(string[] args) + { + new Program(writeToConsole: !args.Contains("--no-terminal")) + .LaunchInteractively(); + } + + /// Construct an instance. + internal Program(bool writeToConsole) + { + this.GameExecutablePath = Path.Combine(Constants.ExecutionPath, this.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); + this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; + this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); + } + + /// Launch SMAPI. + internal void LaunchInteractively() { // initialise logging - Program.Monitor.WriteToConsole = !args.Contains("--no-terminal"); Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting - Program.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); + + // inject compatibility shims +#pragma warning disable 618 + Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); + Config.Shim(this.DeprecationManager); + InternalExtensions.Shim(this.ModRegistry); + Log.Shim(this.DeprecationManager); + Mod.Shim(this.DeprecationManager); + PlayerEvents.Shim(this.DeprecationManager); + TimeEvents.Shim(this.DeprecationManager); +#pragma warning restore 618 // read config { @@ -97,39 +122,39 @@ namespace StardewModdingAPI if (File.Exists(settingsPath)) { string json = File.ReadAllText(settingsPath); - Program.Settings = JsonConvert.DeserializeObject(json); + this.Settings = JsonConvert.DeserializeObject(json); } else - Program.Settings = new UserSettings(); + this.Settings = new UserSettings(); - File.WriteAllText(settingsPath, JsonConvert.SerializeObject(Program.Settings, Formatting.Indented)); + File.WriteAllText(settingsPath, JsonConvert.SerializeObject(this.Settings, Formatting.Indented)); } // redirect direct console output { - Monitor monitor = Program.GetSecondaryMonitor("Console.Out"); + Monitor monitor = this.GetSecondaryMonitor("Console.Out"); monitor.WriteToFile = false; // not useful for troubleshooting mods per discussion if (monitor.WriteToConsole) - Program.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); + this.ConsoleManager.OnLineIntercepted += line => monitor.Log(line, LogLevel.Trace); } // add warning headers - if (Program.Settings.DeveloperMode) + if (this.Settings.DeveloperMode) { - Program.Monitor.ShowTraceInConsole = true; - Program.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); + this.Monitor.ShowTraceInConsole = true; + this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); } - if (!Program.Settings.CheckForUpdates) - Program.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); - if (!Program.Monitor.WriteToConsole) - Program.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); + if (!this.Settings.CheckForUpdates) + this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); + if (!this.Monitor.WriteToConsole) + this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); // print file paths - Program.Monitor.Log($"Mods go here: {Program.ModPath}"); + this.Monitor.Log($"Mods go here: {this.ModPath}"); // initialise legacy log - Log.Monitor = Program.GetSecondaryMonitor("legacy mod"); - Log.ModRegistry = Program.ModRegistry; + Log.Monitor = this.GetSecondaryMonitor("legacy mod"); + Log.ModRegistry = this.ModRegistry; // hook into & launch the game try @@ -137,56 +162,56 @@ namespace StardewModdingAPI // verify version if (String.Compare(Game1.version, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0) { - Program.Monitor.Log($"Oops! You're running Stardew Valley {Game1.version}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Game1.version}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); return; } // initialise - Program.Monitor.Log("Loading SMAPI..."); - Console.Title = Constants.ConsoleTitle; - Program.VerifyPath(Program.ModPath); - Program.VerifyPath(Constants.LogDir); - if (!File.Exists(Program.GameExecutablePath)) + this.Monitor.Log("Loading SMAPI..."); + Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion}"; + this.VerifyPath(this.ModPath); + this.VerifyPath(Constants.LogDir); + if (!File.Exists(this.GameExecutablePath)) { - Program.Monitor.Log($"Couldn't find executable: {Program.GameExecutablePath}", LogLevel.Error); - Program.PressAnyKeyToExit(); + this.Monitor.Log($"Couldn't find executable: {this.GameExecutablePath}", LogLevel.Error); + this.PressAnyKeyToExit(); return; } // check for update when game loads - if (Program.Settings.CheckForUpdates) - GameEvents.GameLoaded += (sender, e) => Program.CheckForUpdateAsync(); + if (this.Settings.CheckForUpdates) + GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync(); // launch game - Program.StartGame(); + this.StartGame(); } catch (Exception ex) { - Program.Monitor.Log($"Critical error: {ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"Critical error: {ex.GetLogSummary()}", LogLevel.Error); } - Program.PressAnyKeyToExit(); + this.PressAnyKeyToExit(); } /// Immediately exit the game without saving. This should only be invoked when an irrecoverable fatal error happens that risks save corruption or game-breaking bugs. /// The module which requested an immediate exit. /// The reason provided for the shutdown. - internal static void ExitGameImmediately(string module, string reason) + internal void ExitGameImmediately(string module, string reason) { - Program.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}"); - Program.CancellationTokenSource.Cancel(); - if (Program.ready) + this.Monitor.LogFatal($"{module} requested an immediate game shutdown: {reason}"); + this.CancellationTokenSource.Cancel(); + if (this.IsGameRunning) { - Program.GameInstance.Exiting += (sender, e) => Program.PressAnyKeyToExit(); - Program.GameInstance.Exit(); + this.GameInstance.Exiting += (sender, e) => this.PressAnyKeyToExit(); + this.GameInstance.Exit(); } } /// Get a monitor for legacy code which doesn't have one passed in. [Obsolete("This method should only be used when needed for backwards compatibility.")] - internal static IMonitor GetLegacyMonitorForMod() + internal IMonitor GetLegacyMonitorForMod() { - string modName = Program.ModRegistry.GetModFromStack() ?? "unknown"; - return Program.GetSecondaryMonitor(modName); + string modName = this.ModRegistry.GetModFromStack() ?? "unknown"; + return this.GetSecondaryMonitor(modName); } @@ -194,7 +219,7 @@ namespace StardewModdingAPI ** Private methods *********/ /// Asynchronously check for a new version of SMAPI, and print a message to the console if an update is available. - private static void CheckForUpdateAsync() + private void CheckForUpdateAsync() { new Thread(() => { @@ -203,40 +228,40 @@ namespace StardewModdingAPI GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; ISemanticVersion latestVersion = new SemanticVersion(release.Tag); if (latestVersion.IsNewerThan(Constants.ApiVersion)) - Program.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); + this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); } catch (Exception ex) { - Program.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.\n{ex.GetLogSummary()}"); + 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.\n{ex.GetLogSummary()}"); } }).Start(); } /// Hook into Stardew Valley and launch the game. - private static void StartGame() + private void StartGame() { try { // add error handlers #if SMAPI_FOR_WINDOWS - Application.ThreadException += (sender, e) => Program.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); + Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException); #endif - AppDomain.CurrentDomain.UnhandledException += (sender, e) => Program.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); + AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); // initialise game { // load assembly - Program.Monitor.Log("Loading game..."); - Assembly gameAssembly = Assembly.UnsafeLoadFrom(Program.GameExecutablePath); + this.Monitor.Log("Loading game..."); + Assembly gameAssembly = Assembly.UnsafeLoadFrom(this.GameExecutablePath); Type gameProgramType = gameAssembly.GetType("StardewValley.Program", true); // set Game1 instance - Program.GameInstance = new SGame(Program.Monitor); - Program.GameInstance.Exiting += (sender, e) => Program.ready = false; - Program.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(Program.Monitor, sender, e); - Program.GameInstance.Window.Title = $"Stardew Valley - Version {Game1.version}"; - gameProgramType.GetField("gamePtr").SetValue(gameProgramType, Program.GameInstance); + this.GameInstance = new SGame(this.Monitor); + this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; + this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); + this.GameInstance.Window.Title = $"Stardew Valley - Version {Game1.version}"; + gameProgramType.GetField("gamePtr").SetValue(gameProgramType, this.GameInstance); // configure Game1.version += $" | SMAPI {Constants.ApiVersion}"; @@ -244,10 +269,10 @@ namespace StardewModdingAPI } // load mods - Program.LoadMods(); - if (Program.CancellationTokenSource.IsCancellationRequested) + this.LoadMods(); + if (this.CancellationTokenSource.IsCancellationRequested) { - Program.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); + this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); return; } @@ -255,18 +280,18 @@ namespace StardewModdingAPI new Thread(() => { // wait for the game to load up - while (!Program.ready) + while (!this.IsGameRunning) Thread.Sleep(1000); // register help command - Program.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", Program.HandleHelpCommand); + this.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", this.HandleHelpCommand); // listen for command line input - Program.Monitor.Log("Starting console..."); - Program.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - Thread consoleInputThread = new Thread(Program.ConsoleInputLoop); + this.Monitor.Log("Starting console..."); + this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); + Thread consoleInputThread = new Thread(this.ConsoleInputLoop); consoleInputThread.Start(); - while (Program.ready) + while (this.IsGameRunning) Thread.Sleep(1000 / 10); // Check if the game is still running 10 times a second // abort the console thread, we're closing @@ -275,31 +300,31 @@ namespace StardewModdingAPI }).Start(); // start game loop - Program.Monitor.Log("Starting game..."); - if (Program.CancellationTokenSource.IsCancellationRequested) + this.Monitor.Log("Starting game..."); + if (this.CancellationTokenSource.IsCancellationRequested) { - Program.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); + this.Monitor.Log("Shutdown requested; interrupting initialisation.", LogLevel.Error); return; } try { - Program.ready = true; - Program.GameInstance.Run(); + this.IsGameRunning = true; + this.GameInstance.Run(); } finally { - Program.ready = false; + this.IsGameRunning = false; } } catch (Exception ex) { - Program.Monitor.Log($"The game encountered a fatal error:\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"The game encountered a fatal error:\n{ex.GetLogSummary()}", LogLevel.Error); } } /// Create a directory path if it doesn't exist. /// The directory path. - private static void VerifyPath(string path) + private void VerifyPath(string path) { try { @@ -308,20 +333,20 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"Couldn't create a path: {path}\n\n{ex.GetLogSummary()}", LogLevel.Error); } } /// Load and hook up all mods in the mod directory. - private static void LoadMods() + private void LoadMods() { - Program.Monitor.Log("Loading mods..."); + this.Monitor.Log("Loading mods..."); // get JSON helper JsonHelper jsonHelper = new JsonHelper(); // get assembly loader - AssemblyLoader modAssemblyLoader = new AssemblyLoader(Program.TargetPlatform, Program.Monitor); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(this.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); // get known incompatible mods @@ -335,12 +360,12 @@ namespace StardewModdingAPI catch (Exception ex) { incompatibleMods = new Dictionary(); - Program.Monitor.Log($"Couldn't read metadata file at {Constants.ApiModMetadataPath}. SMAPI will still run, but some features may be disabled.\n{ex}", LogLevel.Warn); + this.Monitor.Log($"Couldn't read metadata file at {Constants.ApiModMetadataPath}. SMAPI will still run, but some features may be disabled.\n{ex}", LogLevel.Warn); } // load mod assemblies List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list - foreach (string directoryPath in Directory.GetDirectories(Program.ModPath)) + foreach (string directoryPath in Directory.GetDirectories(this.ModPath)) { // passthrough empty directories DirectoryInfo directory = new DirectoryInfo(directoryPath); @@ -348,9 +373,9 @@ namespace StardewModdingAPI directory = directory.GetDirectories().First(); // check for cancellation - if (Program.CancellationTokenSource.IsCancellationRequested) + if (this.CancellationTokenSource.IsCancellationRequested) { - Program.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); + this.Monitor.Log("Shutdown requested; interrupting mod loading.", LogLevel.Error); return; } @@ -358,7 +383,7 @@ namespace StardewModdingAPI string manifestPath = Path.Combine(directory.FullName, "manifest.json"); if (!File.Exists(manifestPath)) { - Program.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); + this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; @@ -371,7 +396,7 @@ namespace StardewModdingAPI string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { - Program.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error); continue; } @@ -379,18 +404,18 @@ namespace StardewModdingAPI manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json"), null); if (manifest == null) { - Program.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); continue; } if (string.IsNullOrEmpty(manifest.EntryDll)) { - Program.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -407,7 +432,7 @@ namespace StardewModdingAPI if (!string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl)) warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - Program.Monitor.Log(warning, LogLevel.Error); + this.Monitor.Log(warning, LogLevel.Error); continue; } } @@ -420,13 +445,13 @@ namespace StardewModdingAPI ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); if (minVersion.IsNewerThan(Constants.ApiVersion)) { - Program.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); continue; } } catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) { - Program.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); continue; } } @@ -434,20 +459,20 @@ namespace StardewModdingAPI // create per-save directory if (manifest.PerSaveConfigs) { - deprecationWarnings.Add(() => Program.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); + deprecationWarnings.Add(() => this.DeprecationManager.Warn(manifest.Name, $"{nameof(Manifest)}.{nameof(Manifest.PerSaveConfigs)}", "1.0", DeprecationLevel.Info)); try { string psDir = Path.Combine(directory.FullName, "psconfigs"); Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { - Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error); continue; } } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } } @@ -456,7 +481,7 @@ namespace StardewModdingAPI string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { - Program.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error); continue; } @@ -468,7 +493,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{manifest.EntryDll}'.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{manifest.EntryDll}'.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -477,13 +502,13 @@ namespace StardewModdingAPI { if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) { - Program.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); continue; } } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while reading the mod DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: an error occurred while reading the mod DLL.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -496,25 +521,25 @@ namespace StardewModdingAPI mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { - Program.Monitor.Log($"{errorPrefix}: the mod's entry class could not be instantiated."); + this.Monitor.Log($"{errorPrefix}: the mod's entry class could not be instantiated."); continue; } // inject data // get helper mod.ModManifest = manifest; - mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, Program.ModRegistry, Program.CommandManager); - mod.Monitor = Program.GetSecondaryMonitor(manifest.Name); + mod.Helper = new ModHelper(manifest.Name, directory.FullName, jsonHelper, this.ModRegistry, this.CommandManager); + mod.Monitor = this.GetSecondaryMonitor(manifest.Name); mod.PathOnDisk = directory.FullName; // track mod - Program.ModRegistry.Add(mod); - Program.ModsLoaded += 1; - Program.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); + this.ModRegistry.Add(mod); + this.ModsLoaded += 1; + this.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) { - Program.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } } @@ -525,7 +550,7 @@ namespace StardewModdingAPI deprecationWarnings = null; // initialise mods - foreach (Mod mod in Program.ModRegistry.GetMods()) + foreach (Mod mod in this.ModRegistry.GetMods()) { try { @@ -534,54 +559,54 @@ namespace StardewModdingAPI mod.Entry(mod.Helper); // raise deprecation warning for old Entry() methods - if (Program.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - Program.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice); + if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) + this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice); } catch (Exception ex) { - Program.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn); + this.Monitor.Log($"The {mod.ModManifest.Name} mod failed on entry initialisation. It will still be loaded, but may not function correctly.\n{ex.GetLogSummary()}", LogLevel.Warn); } } // print result - Program.Monitor.Log($"Loaded {Program.ModsLoaded} mods."); - Console.Title = Constants.ConsoleTitle; + this.Monitor.Log($"Loaded {this.ModsLoaded} mods."); + Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {this.ModsLoaded}"; } // ReSharper disable once FunctionNeverReturns /// Run a loop handling console input. - private static void ConsoleInputLoop() + private void ConsoleInputLoop() { while (true) { string input = Console.ReadLine(); - if (!Program.CommandManager.Trigger(input)) - Program.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + if (!this.CommandManager.Trigger(input)) + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); } } /// The method called when the user submits the help command in the console. /// The command name. /// The command arguments. - private static void HandleHelpCommand(string name, string[] arguments) + private void HandleHelpCommand(string name, string[] arguments) { if (arguments.Any()) { - Framework.Command result = Program.CommandManager.Get(arguments[0]); + Framework.Command result = this.CommandManager.Get(arguments[0]); if (result == null) - Program.Monitor.Log("There's no command with that name.", LogLevel.Error); + this.Monitor.Log("There's no command with that name.", LogLevel.Error); else - Program.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); + this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); } else - Program.Monitor.Log("Commands: " + string.Join(", ", Program.CommandManager.GetAll().Select(p => p.Name)), LogLevel.Info); + this.Monitor.Log("Commands: " + string.Join(", ", this.CommandManager.GetAll().Select(p => p.Name)), LogLevel.Info); } /// Show a 'press any key to exit' message, and exit when they press a key. - private static void PressAnyKeyToExit() + private void PressAnyKeyToExit() { - Program.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); + this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); Thread.Sleep(100); Console.ReadKey(); Environment.Exit(0); @@ -589,9 +614,9 @@ namespace StardewModdingAPI /// Get a monitor instance derived from SMAPI's current settings. /// The name of the module which will log messages with this instance. - private static Monitor GetSecondaryMonitor(string name) + private Monitor GetSecondaryMonitor(string name) { - return new Monitor(name, Program.ConsoleManager, Program.LogFile) { WriteToConsole = Program.Monitor.WriteToConsole, ShowTraceInConsole = Program.Settings.DeveloperMode }; + return new Monitor(name, this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode }; } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 1e896d4b..35dd6513 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -151,6 +151,7 @@ + -- cgit From 960507879eacc0a760862c269b39f9d7448a7bd5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 16 Feb 2017 01:03:50 -0500 Subject: remove unneeded property for game exe path --- src/StardewModdingAPI/Log.cs | 14 +++++++------- src/StardewModdingAPI/Program.cs | 28 ++++++++++++---------------- 2 files changed, 19 insertions(+), 23 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs index da98baba..a8d78e55 100644 --- a/src/StardewModdingAPI/Log.cs +++ b/src/StardewModdingAPI/Log.cs @@ -15,15 +15,11 @@ namespace StardewModdingAPI /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; - - /********* - ** Accessors - *********/ /// The underlying logger. - internal static Monitor Monitor; + private static Monitor Monitor; /// Tracks the installed mods. - internal static ModRegistry ModRegistry; + private static ModRegistry ModRegistry; /********* @@ -31,9 +27,13 @@ namespace StardewModdingAPI *********/ /// Injects types required for backwards compatibility. /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) + /// The underlying logger. + /// Tracks the installed mods. + internal static void Shim(DeprecationManager deprecationManager, Monitor monitor, ModRegistry modRegistry) { Log.DeprecationManager = deprecationManager; + Log.Monitor = monitor; + Log.ModRegistry = modRegistry; } /**** diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 0857d41b..41e12394 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -105,18 +105,7 @@ namespace StardewModdingAPI Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); - // inject compatibility shims -#pragma warning disable 618 - Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); - Config.Shim(this.DeprecationManager); - InternalExtensions.Shim(this.ModRegistry); - Log.Shim(this.DeprecationManager); - Mod.Shim(this.DeprecationManager); - PlayerEvents.Shim(this.DeprecationManager); - TimeEvents.Shim(this.DeprecationManager); -#pragma warning restore 618 - - // read config + // read settings { string settingsPath = Constants.ApiConfigPath; if (File.Exists(settingsPath)) @@ -130,6 +119,17 @@ namespace StardewModdingAPI File.WriteAllText(settingsPath, JsonConvert.SerializeObject(this.Settings, Formatting.Indented)); } + // inject compatibility shims +#pragma warning disable 618 + Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); + Config.Shim(this.DeprecationManager); + InternalExtensions.Shim(this.ModRegistry); + Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); + Mod.Shim(this.DeprecationManager); + PlayerEvents.Shim(this.DeprecationManager); + TimeEvents.Shim(this.DeprecationManager); +#pragma warning restore 618 + // redirect direct console output { Monitor monitor = this.GetSecondaryMonitor("Console.Out"); @@ -152,10 +152,6 @@ namespace StardewModdingAPI // print file paths this.Monitor.Log($"Mods go here: {this.ModPath}"); - // initialise legacy log - Log.Monitor = this.GetSecondaryMonitor("legacy mod"); - Log.ModRegistry = this.ModRegistry; - // hook into & launch the game try { -- cgit From a0c94752c02c185b9e75b61a6098aaefa4e24aac Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 16 Feb 2017 01:07:11 -0500 Subject: remove unneeded property for game exe path --- src/StardewModdingAPI/Program.cs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 41e12394..8fd9c8e1 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -35,9 +35,6 @@ namespace StardewModdingAPI Platform.Mono; #endif - /// The full path to the Stardew Valley executable. - private readonly string GameExecutablePath; - /// The full path to the folder containing mods. private readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); @@ -93,7 +90,6 @@ namespace StardewModdingAPI /// Construct an instance. internal Program(bool writeToConsole) { - this.GameExecutablePath = Path.Combine(Constants.ExecutionPath, this.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); } @@ -156,20 +152,22 @@ namespace StardewModdingAPI try { // verify version - if (String.Compare(Game1.version, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0) + if (string.Compare(Game1.version, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0) { this.Monitor.Log($"Oops! You're running Stardew Valley {Game1.version}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); return; } - // initialise + // initialise folders this.Monitor.Log("Loading SMAPI..."); - Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion}"; this.VerifyPath(this.ModPath); this.VerifyPath(Constants.LogDir); - if (!File.Exists(this.GameExecutablePath)) + + // get executable path + string executablePath = Path.Combine(Constants.ExecutionPath, this.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); + if (!File.Exists(executablePath)) { - this.Monitor.Log($"Couldn't find executable: {this.GameExecutablePath}", LogLevel.Error); + this.Monitor.Log($"Couldn't find executable: {executablePath}", LogLevel.Error); this.PressAnyKeyToExit(); return; } @@ -179,7 +177,7 @@ namespace StardewModdingAPI GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync(); // launch game - this.StartGame(); + this.StartGame(executablePath); } catch (Exception ex) { @@ -234,7 +232,8 @@ namespace StardewModdingAPI } /// Hook into Stardew Valley and launch the game. - private void StartGame() + /// The absolute path to the executable to launch. + private void StartGame(string executablePath) { try { @@ -249,7 +248,7 @@ namespace StardewModdingAPI { // load assembly this.Monitor.Log("Loading game..."); - Assembly gameAssembly = Assembly.UnsafeLoadFrom(this.GameExecutablePath); + Assembly gameAssembly = Assembly.UnsafeLoadFrom(executablePath); Type gameProgramType = gameAssembly.GetType("StardewValley.Program", true); // set Game1 instance @@ -536,7 +535,6 @@ namespace StardewModdingAPI catch (Exception ex) { this.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); - continue; } } -- cgit From f8866ac4a81ad02f6f06391372ad1b32e6cfdb68 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 16 Feb 2017 01:10:07 -0500 Subject: remove unneeded property for number of mods loaded --- src/StardewModdingAPI/Program.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 8fd9c8e1..ecc0360e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -63,9 +63,6 @@ namespace StardewModdingAPI /// The underlying game instance. internal SGame GameInstance; - /// The number of mods currently loaded by SMAPI. - internal int ModsLoaded; - /// Tracks the installed mods. internal readonly ModRegistry ModRegistry = new ModRegistry(); @@ -359,6 +356,7 @@ namespace StardewModdingAPI } // load mod assemblies + int modsLoaded = 0; List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list foreach (string directoryPath in Directory.GetDirectories(this.ModPath)) { @@ -529,7 +527,7 @@ namespace StardewModdingAPI // track mod this.ModRegistry.Add(mod); - this.ModsLoaded += 1; + modsLoaded += 1; this.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) @@ -563,8 +561,8 @@ namespace StardewModdingAPI } // print result - this.Monitor.Log($"Loaded {this.ModsLoaded} mods."); - Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {this.ModsLoaded}"; + this.Monitor.Log($"Loaded {modsLoaded} mods."); + Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {modsLoaded}"; } // ReSharper disable once FunctionNeverReturns -- cgit From 16c362f4c5949b463107e88b4b49e1e49cc395d7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 16 Feb 2017 13:59:13 -0500 Subject: increase all notice deprecations to info, tweak deprecation message format --- release-notes.md | 4 ++-- src/StardewModdingAPI/Command.cs | 6 +++--- src/StardewModdingAPI/Config.cs | 2 +- src/StardewModdingAPI/Events/PlayerEvents.cs | 4 ++-- src/StardewModdingAPI/Events/TimeEvents.cs | 2 +- src/StardewModdingAPI/Framework/DeprecationManager.cs | 2 +- src/StardewModdingAPI/Log.cs | 2 +- src/StardewModdingAPI/Mod.cs | 6 +++--- src/StardewModdingAPI/Program.cs | 2 +- 9 files changed, 15 insertions(+), 15 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index 98f527d2..8d571514 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,9 +14,9 @@ For mod developers: * Added `SaveEvents.AfterReturnToTitle` and `TimeEvents.AfterDayStarted` events. * Added a simpler API for console commands (see `helper.ConsoleCommands`). * Added `GetPrivateProperty` reflection helper. -* Many deprecated APIs have been removed; see the - [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod) for more information. * Log files now always use `\r\n` to simplify crossplatform viewing. +* Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)), + and all _notice_-level deprecations have been increased to _info_. For SMAPI developers: * Added support for debugging with Visual Studio for Mac. diff --git a/src/StardewModdingAPI/Command.cs b/src/StardewModdingAPI/Command.cs index 6b056ce7..e2d08538 100644 --- a/src/StardewModdingAPI/Command.cs +++ b/src/StardewModdingAPI/Command.cs @@ -94,7 +94,7 @@ namespace StardewModdingAPI /// Encapsulates monitoring and logging. public static void CallCommand(string input, IMonitor monitor) { - Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.Notice); + Command.DeprecationManager.Warn("Command.CallCommand", "1.9", DeprecationLevel.Info); Command.CommandManager.Trigger(input); } @@ -107,7 +107,7 @@ namespace StardewModdingAPI name = name?.Trim().ToLower(); // raise deprecation warning - Command.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Notice); + Command.DeprecationManager.Warn("Command.RegisterCommand", "1.9", DeprecationLevel.Info); // validate if (Command.LegacyCommands.ContainsKey(name)) @@ -130,7 +130,7 @@ namespace StardewModdingAPI /// The command name to find. public static Command FindCommand(string name) { - Command.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.Notice); + Command.DeprecationManager.Warn("Command.FindCommand", "1.9", DeprecationLevel.Info); if (name == null) return null; diff --git a/src/StardewModdingAPI/Config.cs b/src/StardewModdingAPI/Config.cs index f253930d..9f4bfad2 100644 --- a/src/StardewModdingAPI/Config.cs +++ b/src/StardewModdingAPI/Config.cs @@ -125,7 +125,7 @@ namespace StardewModdingAPI /// Construct an instance. protected Config() { - Config.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Notice); + Config.DeprecationManager.Warn("the Config class", "1.0", DeprecationLevel.Info); Config.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0"); // typically used to construct config, avoid redundant warnings } } diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs index 996077ab..b02ebfec 100644 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -56,7 +56,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}"; Delegate[] handlers = PlayerEvents.LoadedGame.GetInvocationList(); - PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info); monitor.SafelyRaiseGenericEvent(name, handlers, null, loaded); } @@ -72,7 +72,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}"; Delegate[] handlers = PlayerEvents.FarmerChanged.GetInvocationList(); - PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info); monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); } diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index 0f9257c1..3f06a46b 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -102,7 +102,7 @@ namespace StardewModdingAPI.Events string name = $"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}"; Delegate[] handlers = TimeEvents.OnNewDay.GetInvocationList(); - TimeEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Notice); + TimeEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info); monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); } } diff --git a/src/StardewModdingAPI/Framework/DeprecationManager.cs b/src/StardewModdingAPI/Framework/DeprecationManager.cs index 8c32ba6a..e44cd369 100644 --- a/src/StardewModdingAPI/Framework/DeprecationManager.cs +++ b/src/StardewModdingAPI/Framework/DeprecationManager.cs @@ -70,7 +70,7 @@ namespace StardewModdingAPI.Framework break; case DeprecationLevel.Info: - this.Monitor.Log(message, LogLevel.Info); + this.Monitor.Log(message, LogLevel.Warn); break; case DeprecationLevel.PendingRemoval: diff --git a/src/StardewModdingAPI/Log.cs b/src/StardewModdingAPI/Log.cs index a8d78e55..d58cebfe 100644 --- a/src/StardewModdingAPI/Log.cs +++ b/src/StardewModdingAPI/Log.cs @@ -306,7 +306,7 @@ namespace StardewModdingAPI /// Raise a deprecation warning. private static void WarnDeprecated() { - Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Notice); + Log.DeprecationManager.Warn($"the {nameof(Log)} class", "1.1", DeprecationLevel.Info); } /// Get the name of the mod logging a message from the stack. diff --git a/src/StardewModdingAPI/Mod.cs b/src/StardewModdingAPI/Mod.cs index d3fe882f..44cfd4b3 100644 --- a/src/StardewModdingAPI/Mod.cs +++ b/src/StardewModdingAPI/Mod.cs @@ -35,7 +35,7 @@ namespace StardewModdingAPI { get { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0", DeprecationLevel.Info); return this._pathOnDisk; } internal set { this._pathOnDisk = value; } @@ -47,7 +47,7 @@ namespace StardewModdingAPI { get { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.BaseConfigPath)}", "1.0", DeprecationLevel.Info); Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings return Path.Combine(this.PathOnDisk, "config.json"); } @@ -96,7 +96,7 @@ namespace StardewModdingAPI [Obsolete] private string GetPerSaveConfigFolder() { - Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Notice); + Mod.DeprecationManager.Warn($"{nameof(Mod)}.{nameof(Mod.PerSaveConfigFolder)}", "1.0", DeprecationLevel.Info); Mod.DeprecationManager.MarkWarned($"{nameof(Mod)}.{nameof(Mod.PathOnDisk)}", "1.0"); // avoid redundant warnings if (!((Manifest)this.ModManifest).PerSaveConfigs) diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ecc0360e..de07e8ad 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -552,7 +552,7 @@ namespace StardewModdingAPI // raise deprecation warning for old Entry() methods if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Notice); + this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info); } catch (Exception ex) { -- cgit From 7521570341b79ea78c590a202fbda55bd6fe06b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 19 Feb 2017 20:12:15 -0500 Subject: make mod-not-compatible messages shorter --- src/StardewModdingAPI/Program.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index de07e8ad..b14240ca 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -418,11 +418,14 @@ namespace StardewModdingAPI { if (!compatibility.IsCompatible(manifest.Version)) { - string reasonPhrase = compatibility.ReasonPhrase ?? "this version is not compatible with the latest version of the game"; - string warning = $"Skipped {compatibility.Name} {manifest.Version} because {reasonPhrase}. Please check for a newer version of the mod here:"; - if (!string.IsNullOrWhiteSpace(compatibility.UpdateUrl)) - warning += $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; - if (!string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl)) + bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); + bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); + + string reasonPhrase = compatibility.ReasonPhrase ?? "it isn't compatible with the latest version of the game"; + string warning = $"Skipped {compatibility.Name} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + if (hasOfficialUrl) + warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + if (hasUnofficialUrl) warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; this.Monitor.Log(warning, LogLevel.Error); -- cgit From 703f5f89a8d8eadede4d6fa4a9022283702608bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 21 Feb 2017 15:54:48 -0500 Subject: fix new error when entering an empty command in SMAPI console --- src/StardewModdingAPI/Framework/CommandManager.cs | 3 +++ src/StardewModdingAPI/Program.cs | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Framework/CommandManager.cs b/src/StardewModdingAPI/Framework/CommandManager.cs index 3aa4bf97..9af3d27a 100644 --- a/src/StardewModdingAPI/Framework/CommandManager.cs +++ b/src/StardewModdingAPI/Framework/CommandManager.cs @@ -70,6 +70,9 @@ namespace StardewModdingAPI.Framework /// Returns whether a matching command was triggered. public bool Trigger(string input) { + if (string.IsNullOrWhiteSpace(input)) + return false; + string[] args = input.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); string name = args[0]; args = args.Skip(1).ToArray(); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b14240ca..ea81d182 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -575,7 +575,7 @@ namespace StardewModdingAPI while (true) { string input = Console.ReadLine(); - if (!this.CommandManager.Trigger(input)) + if (!string.IsNullOrWhiteSpace(input) && !this.CommandManager.Trigger(input)) this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); } } @@ -587,7 +587,6 @@ namespace StardewModdingAPI { if (arguments.Any()) { - Framework.Command result = this.CommandManager.Get(arguments[0]); if (result == null) this.Monitor.Log("There's no command with that name.", LogLevel.Error); -- cgit From 96c7010c1b1757217a14b49a563707a972fd6107 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Feb 2017 19:48:25 -0500 Subject: update for Stardew Valley 1.9 (#231) --- src/StardewModdingAPI/Constants.cs | 2 +- src/StardewModdingAPI/Framework/SGame.cs | 24 ++++++++++++++--------- src/StardewModdingAPI/Program.cs | 6 +++--- src/StardewModdingAPI/StardewModdingAPI.data.json | 14 +++++++++++++ 4 files changed, 33 insertions(+), 13 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 262ba61d..76003b69 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion => new SemanticVersion(1, 8, 0, null); /// The minimum supported version of Stardew Valley. - public const string MinimumGameVersion = "1.2"; + public const string MinimumGameVersion = "1.2.9"; /// The directory path containing Stardew Valley's app data. public static string DataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index f64457f8..56631260 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -923,7 +923,7 @@ namespace StardewModdingAPI.Framework { SpriteBatch spriteBatch = Game1.spriteBatch; SpriteFont smallFont = Game1.smallFont; - object[] objArray = new object[8]; + object[] objArray = new object[10]; int index1 = 0; string str1; if (!Game1.panMode) @@ -938,20 +938,26 @@ namespace StardewModdingAPI.Framework float cursorTransparency = Game1.mouseCursorTransparency; objArray[index3] = (object)cursorTransparency; int index4 = 3; - string str3 = " wasMouseVisibleThisFrame: "; + string str3 = " mousePosition: "; objArray[index4] = (object)str3; int index5 = 4; - string str4 = Game1.wasMouseVisibleThisFrame.ToString(); - objArray[index5] = (object)str4; + int mouseX = Game1.getMouseX(); + objArray[index5] = (object)mouseX; int index6 = 5; - string newLine = Environment.NewLine; - objArray[index6] = (object)newLine; + string str4 = ","; + objArray[index6] = (object)str4; int index7 = 6; - string str5 = "debugOutput: "; - objArray[index7] = (object)str5; + int mouseY = Game1.getMouseY(); + objArray[index7] = (object)mouseY; int index8 = 7; + string newLine = Environment.NewLine; + objArray[index8] = (object)newLine; + int index9 = 8; + string str5 = "debugOutput: "; + objArray[index9] = (object)str5; + int index10 = 9; string debugOutput = Game1.debugOutput; - objArray[index8] = (object)debugOutput; + objArray[index10] = (object)debugOutput; string text = string.Concat(objArray); Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y); Color red = Color.Red; diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ea81d182..b8a16e79 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -97,6 +97,7 @@ namespace StardewModdingAPI // initialise logging Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Game1.version}"; // read settings { @@ -252,11 +253,10 @@ namespace StardewModdingAPI this.GameInstance = new SGame(this.Monitor); this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); - this.GameInstance.Window.Title = $"Stardew Valley - Version {Game1.version}"; + this.GameInstance.Window.Title = $"Stardew Valley {Game1.version}"; gameProgramType.GetField("gamePtr").SetValue(gameProgramType, this.GameInstance); // configure - Game1.version += $" | SMAPI {Constants.ApiVersion}"; Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; } @@ -565,7 +565,7 @@ namespace StardewModdingAPI // print result this.Monitor.Log($"Loaded {modsLoaded} mods."); - Console.Title = $"Stardew Modding API Console - Version {Constants.ApiVersion} - Mods Loaded: {modsLoaded}"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Game1.version} with {modsLoaded} mods"; } // ReSharper disable once FunctionNeverReturns diff --git a/src/StardewModdingAPI/StardewModdingAPI.data.json b/src/StardewModdingAPI/StardewModdingAPI.data.json index c4f708dc..a28dc3e8 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.data.json +++ b/src/StardewModdingAPI/StardewModdingAPI.data.json @@ -65,6 +65,13 @@ This file contains advanced metadata for SMAPI. You shouldn't change this file. "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'." }, + { + "Name": "Chests Anywhere", + "ID": "Pathoschild.ChestsAnywhere", + "UpperVersion": "1.9-beta", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", + "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'." + }, { "Name": "CJB Automation", "ID": "CJBAutomation", @@ -184,6 +191,13 @@ This file contains advanced metadata for SMAPI. You shouldn't change this file. "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683", "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'." }, + { + "Name": "Teleporter", + "ID": "Teleporter", + "UpperVersion": "1.0.2", + "UpdateUrl": "http://community.playstarbound.com/resources/4374", + "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'." + }, { "Name": "Zoryn's Better RNG", "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", -- cgit From 6a18dd6fadaea7d72ce9ef4e2752c669c4f6420b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Feb 2017 22:58:23 -0500 Subject: merge config files --- README.md | 2 - src/StardewModdingAPI/Constants.cs | 3 - src/StardewModdingAPI/Framework/Models/SConfig.cs | 18 ++ .../Framework/Models/UserSettings.cs | 15 -- src/StardewModdingAPI/Program.cs | 65 ++---- .../StardewModdingAPI.config.json | 241 ++++++++++++++++++++- src/StardewModdingAPI/StardewModdingAPI.csproj | 6 +- src/StardewModdingAPI/StardewModdingAPI.data.json | 236 -------------------- src/prepare-install-package.targets | 2 - 9 files changed, 280 insertions(+), 308 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Models/SConfig.cs delete mode 100644 src/StardewModdingAPI/Framework/Models/UserSettings.cs delete mode 100644 src/StardewModdingAPI/StardewModdingAPI.data.json (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/README.md b/README.md index 63adcf78..349916cc 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,6 @@ folder containing `src`). StardewModdingAPI StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json - StardewModdingAPI.data.json StardewModdingAPI.exe StardewModdingAPI.exe.mdb steam_appid.txt @@ -97,7 +96,6 @@ folder containing `src`). Newtonsoft.Json.dll StardewModdingAPI.AssemblyRewriters.dll StardewModdingAPI.config.json - StardewModdingAPI.data.json StardewModdingAPI.exe StardewModdingAPI.pdb StardewModdingAPI.xml diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 76003b69..8b085eac 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -62,9 +62,6 @@ namespace StardewModdingAPI /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); - /// The file path for the SMAPI data file containing metadata about known mods. - internal static string ApiModMetadataPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.data.json"); - /// The file path to the log where the latest output should be saved. internal static string LogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs new file mode 100644 index 00000000..558da82a --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -0,0 +1,18 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// The SMAPI configuration settings. + internal class SConfig + { + /******** + ** Accessors + ********/ + /// Whether to enable development features. + public bool DeveloperMode { get; set; } + + /// Whether to check if a newer version of SMAPI is available on startup. + public bool CheckForUpdates { get; set; } = true; + + /// A list of mod versions which should be considered incompatible. + public IncompatibleMod[] IncompatibleMods { get; set; } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/UserSettings.cs b/src/StardewModdingAPI/Framework/Models/UserSettings.cs deleted file mode 100644 index a0074f77..00000000 --- a/src/StardewModdingAPI/Framework/Models/UserSettings.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace StardewModdingAPI.Framework.Models -{ - /// Contains user settings from SMAPI's JSON configuration file. - internal class UserSettings - { - /********* - ** Accessors - *********/ - /// Whether to enable development features. - public bool DeveloperMode { get; set; } - - /// Whether to check if a newer version of SMAPI is available on startup. - public bool CheckForUpdates { get; set; } = true; - } -} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b8a16e79..eadbaaeb 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -47,8 +47,8 @@ namespace StardewModdingAPI /// The core logger for SMAPI. private readonly Monitor Monitor; - /// The user settings for SMAPI. - private UserSettings Settings; + /// The SMAPI configuration settings. + private readonly SConfig Settings; /// Tracks whether the game should exit immediately and any pending initialisation should be cancelled. private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); @@ -87,6 +87,10 @@ namespace StardewModdingAPI /// Construct an instance. internal Program(bool writeToConsole) { + // load settings + this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + + // initialise this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); } @@ -99,20 +103,6 @@ namespace StardewModdingAPI this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Game1.version}"; - // read settings - { - string settingsPath = Constants.ApiConfigPath; - if (File.Exists(settingsPath)) - { - string json = File.ReadAllText(settingsPath); - this.Settings = JsonConvert.DeserializeObject(json); - } - else - this.Settings = new UserSettings(); - - File.WriteAllText(settingsPath, JsonConvert.SerializeObject(this.Settings, Formatting.Indented)); - } - // inject compatibility shims #pragma warning disable 618 Command.Shim(this.CommandManager, this.DeprecationManager, this.ModRegistry); @@ -341,20 +331,6 @@ namespace StardewModdingAPI AssemblyLoader modAssemblyLoader = new AssemblyLoader(this.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); - // get known incompatible mods - IDictionary incompatibleMods; - try - { - incompatibleMods = File.Exists(Constants.ApiModMetadataPath) - ? JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiModMetadataPath)).ToDictionary(p => p.ID, p => p) - : new Dictionary(0); - } - catch (Exception ex) - { - incompatibleMods = new Dictionary(); - this.Monitor.Log($"Couldn't read metadata file at {Constants.ApiModMetadataPath}. SMAPI will still run, but some features may be disabled.\n{ex}", LogLevel.Warn); - } - // load mod assemblies int modsLoaded = 0; List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list @@ -413,23 +389,26 @@ namespace StardewModdingAPI } // validate known incompatible mods - IncompatibleMod compatibility; - if (incompatibleMods.TryGetValue(!string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll, out compatibility)) { - if (!compatibility.IsCompatible(manifest.Version)) + string modKey = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + IncompatibleMod compatibility = this.Settings.IncompatibleMods.FirstOrDefault(p => p.ID == modKey); + if(compatibility != null) { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); + if (!compatibility.IsCompatible(manifest.Version)) + { + bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); + bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); - string reasonPhrase = compatibility.ReasonPhrase ?? "it isn't compatible with the latest version of the game"; - string warning = $"Skipped {compatibility.Name} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; - if (hasOfficialUrl) - warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; - if (hasUnofficialUrl) - warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + string reasonPhrase = compatibility.ReasonPhrase ?? "it isn't compatible with the latest version of the game"; + string warning = $"Skipped {compatibility.Name} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + if (hasOfficialUrl) + warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + if (hasUnofficialUrl) + warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - this.Monitor.Log(warning, LogLevel.Error); - continue; + this.Monitor.Log(warning, LogLevel.Error); + continue; + } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 2abaf73a..e971c324 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -1,4 +1,241 @@ -{ +/* + + +This file contains advanced configuration for SMAPI. You +generally shouldn't change this file unless necessary. + + +*/ +{ "DeveloperMode": true, - "CheckForUpdates": true + "CheckForUpdates": true, + "IncompatibleMods": [ + /* versions which crash the game */ + { + "Name": "NPC Map Locations", + "ID": "NPCMapLocationsMod", + "LowerVersion": "1.42", + "UpperVersion": "1.43", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", + "ReasonPhrase": "this version has an update check error which crashes the game" + }, + + /* versions not compatible with Stardew Valley 1.1+ */ + { + "Name": "Chest Label System", + "ID": "SPDChestLabel", + "UpperVersion": "1.5", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "ForceCompatibleVersion": "^1.5-EntoPatch" + }, + + /* versions not compatible with Stardew Valley 1.2+ */ + { + "Name": "AccessChestAnywhere", + "ID": "AccessChestAnywhere", + "UpperVersion": "1.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257", + "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", + "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." + }, + { + "Name": "Almighty Tool", + "ID": "AlmightyTool.dll", + "UpperVersion": "1.1.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439", + "Notes": "Uses obsolete StardewModdingAPI.Extensions." + }, + { + "Name": "Better Sprinklers", + "ID": "SPDSprinklersMod", + "UpperVersion": "2.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", + "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "ForceCompatibleVersion": "^2.1-EntoPatch.7", + "Notes": "Uses obsolete StardewModdingAPI.Extensions." + }, + { + "Name": "Casks Anywhere", + "ID": "CasksAnywhere", + "UpperVersion": "1.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/878", + "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange." + }, + { + "Name": "Chests Anywhere", + "ID": "ChestsAnywhere", + "UpperVersion": "1.8.2", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", + "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'." + }, + { + "Name": "Chests Anywhere", + "ID": "Pathoschild.ChestsAnywhere", + "UpperVersion": "1.9-beta", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", + "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'." + }, + { + "Name": "CJB Automation", + "ID": "CJBAutomation", + "UpperVersion": "1.4", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211", + "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." + }, + { + "Name": "CJB Cheats Menu", + "ID": "CJBCheatsMenu", + "UpperVersion": "1.13", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", + "Notes": "Uses removed Game1.borderFont." + }, + { + "Name": "CJB Item Spawner", + "ID": "CJBItemSpawner", + "UpperVersion": "1.6", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", + "Notes": "Uses removed Game1.borderFont." + }, + { + "Name": "Cooking Skill", + "ID": "CookingSkill", + "UpperVersion": "1.0.3", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522", + "Notes": "Crashes with 'Method not found: Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'." + }, + { + "Name": "Enemy Health Bars", + "ID": "SPDHealthBar", + "UpperVersion": "1.7", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193", + "Notes": "Uses obsolete GraphicsEvents.DrawTick." + }, + { + "Name": "Entoarox Framework", + "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9", + "UpperVersion": "1.6.5", + "UpdateUrl": "http://community.playstarbound.com/resources/4228", + "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)')." + }, + { + "Name": "Extended Fridge", + "ID": "Mystra007ExtendedFridge", + "UpperVersion": "1.0", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485", + "Notes": "Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest. Crashes with 'Field not found: StardewValley.Game1.mouseCursorTransparency'." + }, + { + "Name": "Get Dressed", + "ID": "GetDressed.dll", + "UpperVersion": "3.2", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331", + "Notes": "Crashes with NullReferenceException in GameEvents.UpdateTick." + }, + { + "Name": "Lookup Anything", + "ID": "LookupAnything", + "UpperVersion": "1.10", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", + "Notes": "Crashes with FormatException when looking up NPCs." + }, + { + "Name": "Lookup Anything", + "ID": "Pathoschild.LookupAnything", + "UpperVersion": "1.10.1", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", + "Notes": "ID changed in 1.10.1. Crashes with FormatException when looking up NPCs." + }, + { + "Name": "Makeshift Multiplayer", + "ID": "StardewValleyMP", + "UpperVersion": "0.2.10", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501", + "Notes": "Uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck." + }, + { + "Name": "NoSoilDecay", + "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", + "UpperVersion": "0.5", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237", + "Notes": "Uses Assembly.GetExecutingAssembly().Location." + }, + { + "Name": "Point-and-Plant", + "ID": "PointAndPlant.dll", + "UpperVersion": "1.0.2", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572", + "Notes": "Uses obsolete StardewModdingAPI.Extensions." + }, + { + "Name": "Reusable Wallpapers", + "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", + "UpperVersion": "1.5", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/356", + "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange." + }, + { + "Name": "Save Anywhere", + "ID": "SaveAnywhere", + "UpperVersion": "2.0", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444", + "Notes": "Depends on StarDustCore." + }, + { + "Name": "StackSplitX", + "ID": "StackSplitX.dll", + "UpperVersion": "1.0", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798", + "Notes": "Uses SMAPI's internal SGame class." + }, + { + "Name": "StarDustCore", + "ID": "StarDustCore", + "UpperVersion": "1.0", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683", + "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'." + }, + { + "Name": "Teleporter", + "ID": "Teleporter", + "UpperVersion": "1.0.2", + "UpdateUrl": "http://community.playstarbound.com/resources/4374", + "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'." + }, + { + "Name": "Zoryn's Better RNG", + "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", + "UpperVersion": "1.5", + "UpdateUrl": "http://community.playstarbound.com/threads/108756", + "Notes": "Uses SMAPI's internal SGame class." + }, + { + "Name": "Zoryn's Calendar Anywhere", + "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a", + "UpperVersion": "1.5", + "UpdateUrl": "http://community.playstarbound.com/threads/108756", + "Notes": "Uses SMAPI's internal SGame class." + }, + { + "Name": "Zoryn's Health Bars", + "ID": "HealthBars.dll", + "UpperVersion": "1.5", + "UpdateUrl": "http://community.playstarbound.com/threads/108756", + "Notes": "Uses SMAPI's internal SGame class." + }, + { + "Name": "Zoryn's Movement Mod", + "ID": "8a632929-8335-484f-87dd-c29d2ba3215d", + "UpperVersion": "1.5", + "UpdateUrl": "http://community.playstarbound.com/threads/108756", + "Notes": "Uses SMAPI's internal SGame class." + }, + { + "Name": "Zoryn's Regen Mod", + "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", + "UpperVersion": "1.5", + "UpdateUrl": "http://community.playstarbound.com/threads/108756", + "Notes": "Uses SMAPI's internal SGame class." + } + ] } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 796980cb..72cc1ed2 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -150,6 +150,7 @@ + @@ -182,7 +183,6 @@ - @@ -209,9 +209,6 @@ Always - - Always - Always @@ -255,7 +252,6 @@ - diff --git a/src/StardewModdingAPI/StardewModdingAPI.data.json b/src/StardewModdingAPI/StardewModdingAPI.data.json deleted file mode 100644 index a28dc3e8..00000000 --- a/src/StardewModdingAPI/StardewModdingAPI.data.json +++ /dev/null @@ -1,236 +0,0 @@ -/* - - -This file contains advanced metadata for SMAPI. You shouldn't change this file. - - -*/ -[ - /* versions which crash the game */ - { - "Name": "NPC Map Locations", - "ID": "NPCMapLocationsMod", - "LowerVersion": "1.42", - "UpperVersion": "1.43", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", - "ReasonPhrase": "this version has an update check error which crashes the game" - }, - - /* versions not compatible with Stardew Valley 1.1+ */ - { - "Name": "Chest Label System", - "ID": "SPDChestLabel", - "UpperVersion": "1.5", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", - "ForceCompatibleVersion": "^1.5-EntoPatch" - }, - - /* versions not compatible with Stardew Valley 1.2+ */ - { - "Name": "AccessChestAnywhere", - "ID": "AccessChestAnywhere", - "UpperVersion": "1.1", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257", - "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." - }, - { - "Name": "Almighty Tool", - "ID": "AlmightyTool.dll", - "UpperVersion": "1.1.1", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." - }, - { - "Name": "Better Sprinklers", - "ID": "SPDSprinklersMod", - "UpperVersion": "2.1", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", - "ForceCompatibleVersion": "^2.1-EntoPatch.7", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." - }, - { - "Name": "Casks Anywhere", - "ID": "CasksAnywhere", - "UpperVersion": "1.1", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/878", - "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange." - }, - { - "Name": "Chests Anywhere", - "ID": "ChestsAnywhere", - "UpperVersion": "1.8.2", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'." - }, - { - "Name": "Chests Anywhere", - "ID": "Pathoschild.ChestsAnywhere", - "UpperVersion": "1.9-beta", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'." - }, - { - "Name": "CJB Automation", - "ID": "CJBAutomation", - "UpperVersion": "1.4", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." - }, - { - "Name": "CJB Cheats Menu", - "ID": "CJBCheatsMenu", - "UpperVersion": "1.13", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", - "Notes": "Uses removed Game1.borderFont." - }, - { - "Name": "CJB Item Spawner", - "ID": "CJBItemSpawner", - "UpperVersion": "1.6", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", - "Notes": "Uses removed Game1.borderFont." - }, - { - "Name": "Cooking Skill", - "ID": "CookingSkill", - "UpperVersion": "1.0.3", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522", - "Notes": "Crashes with 'Method not found: Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'." - }, - { - "Name": "Enemy Health Bars", - "ID": "SPDHealthBar", - "UpperVersion": "1.7", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193", - "Notes": "Uses obsolete GraphicsEvents.DrawTick." - }, - { - "Name": "Entoarox Framework", - "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9", - "UpperVersion": "1.6.5", - "UpdateUrl": "http://community.playstarbound.com/resources/4228", - "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)')." - }, - { - "Name": "Extended Fridge", - "ID": "Mystra007ExtendedFridge", - "UpperVersion": "1.0", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485", - "Notes": "Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest. Crashes with 'Field not found: StardewValley.Game1.mouseCursorTransparency'." - }, - { - "Name": "Get Dressed", - "ID": "GetDressed.dll", - "UpperVersion": "3.2", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331", - "Notes": "Crashes with NullReferenceException in GameEvents.UpdateTick." - }, - { - "Name": "Lookup Anything", - "ID": "LookupAnything", - "UpperVersion": "1.10", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "Crashes with FormatException when looking up NPCs." - }, - { - "Name": "Lookup Anything", - "ID": "Pathoschild.LookupAnything", - "UpperVersion": "1.10.1", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "ID changed in 1.10.1. Crashes with FormatException when looking up NPCs." - }, - { - "Name": "Makeshift Multiplayer", - "ID": "StardewValleyMP", - "UpperVersion": "0.2.10", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501", - "Notes": "Uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck." - }, - { - "Name": "NoSoilDecay", - "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", - "UpperVersion": "0.5", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237", - "Notes": "Uses Assembly.GetExecutingAssembly().Location." - }, - { - "Name": "Point-and-Plant", - "ID": "PointAndPlant.dll", - "UpperVersion": "1.0.2", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." - }, - { - "Name": "Reusable Wallpapers", - "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", - "UpperVersion": "1.5", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/356", - "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange." - }, - { - "Name": "Save Anywhere", - "ID": "SaveAnywhere", - "UpperVersion": "2.0", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444", - "Notes": "Depends on StarDustCore." - }, - { - "Name": "StackSplitX", - "ID": "StackSplitX.dll", - "UpperVersion": "1.0", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798", - "Notes": "Uses SMAPI's internal SGame class." - }, - { - "Name": "StarDustCore", - "ID": "StarDustCore", - "UpperVersion": "1.0", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683", - "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'." - }, - { - "Name": "Teleporter", - "ID": "Teleporter", - "UpperVersion": "1.0.2", - "UpdateUrl": "http://community.playstarbound.com/resources/4374", - "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'." - }, - { - "Name": "Zoryn's Better RNG", - "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", - "UpperVersion": "1.5", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." - }, - { - "Name": "Zoryn's Calendar Anywhere", - "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a", - "UpperVersion": "1.5", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." - }, - { - "Name": "Zoryn's Health Bars", - "ID": "HealthBars.dll", - "UpperVersion": "1.5", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." - }, - { - "Name": "Zoryn's Movement Mod", - "ID": "8a632929-8335-484f-87dd-c29d2ba3215d", - "UpperVersion": "1.5", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." - }, - { - "Name": "Zoryn's Regen Mod", - "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", - "UpperVersion": "1.5", - "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." - } -] diff --git a/src/prepare-install-package.targets b/src/prepare-install-package.targets index bd9287f1..709bd8d4 100644 --- a/src/prepare-install-package.targets +++ b/src/prepare-install-package.targets @@ -28,7 +28,6 @@ - @@ -43,7 +42,6 @@ - -- cgit From 3005270437f609bb14be9da67e3ffadabd8685a3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Feb 2017 23:05:46 -0500 Subject: shorten mod path in error messages --- src/StardewModdingAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index eadbaaeb..9a6f07ef 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -355,7 +355,7 @@ namespace StardewModdingAPI this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } - string errorPrefix = $"Couldn't load mod for manifest '{manifestPath}'"; + string errorPrefix = $"Couldn't load mod for '{manifestPath.Replace(this.ModPath, "").Trim('/', '\\')}'"; // read manifest Manifest manifest; -- cgit From 6b26eceb57b8c1bdf245ec02ff979504701ede92 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Feb 2017 23:36:14 -0500 Subject: move incompatible mod logic into mod registry --- src/StardewModdingAPI/Framework/ModRegistry.cs | 30 ++++++++++++++++ .../Framework/Models/IncompatibleMod.cs | 40 +++++++++++++--------- src/StardewModdingAPI/Program.cs | 35 ++++++++----------- src/StardewModdingAPI/SemanticVersion.cs | 4 +++ 4 files changed, 73 insertions(+), 36 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index 209f1928..233deb3c 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; +using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework { @@ -18,10 +20,21 @@ namespace StardewModdingAPI.Framework /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name). private readonly IDictionary ModNamesByAssembly = new Dictionary(); + /// The mod versions which should be disabled due to incompatibility. + private readonly IncompatibleMod[] IncompatibleMods; + /********* ** Public methods *********/ + /// Construct an instance. + /// The mod versions which should be disabled due to incompatibility. + public ModRegistry(IEnumerable incompatibleMods) + { + this.IncompatibleMods = incompatibleMods.ToArray(); + } + + /**** ** IModRegistry ****/ @@ -113,5 +126,22 @@ namespace StardewModdingAPI.Framework // no known assembly found return null; } + + /// Get a record indicating why a mod is incompatible (if applicable). + /// The mod manifest. + /// Returns the incompatibility record if applicable, else null. + internal IncompatibleMod GetIncompatibilityRecord(IManifest manifest) + { + string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; + return ( + from mod in this.IncompatibleMods + where + mod.ID == key + && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) + && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) + && (string.IsNullOrWhiteSpace(mod.ForceCompatibleVersion) || !Regex.IsMatch(manifest.Version.ToString(), mod.ForceCompatibleVersion, RegexOptions.IgnoreCase)) + select mod + ).FirstOrDefault(); + } } } \ No newline at end of file diff --git a/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs b/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs index bcf5639c..29e18ddb 100644 --- a/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs +++ b/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.Runtime.Serialization; +using Newtonsoft.Json; namespace StardewModdingAPI.Framework.Models { @@ -8,6 +9,9 @@ namespace StardewModdingAPI.Framework.Models /********* ** Accessors *********/ + /**** + ** From config + ****/ /// The unique mod ID. public string ID { get; set; } @@ -34,24 +38,28 @@ namespace StardewModdingAPI.Framework.Models public string ReasonPhrase { get; set; } + /**** + ** Injected + ****/ + /// The semantic version corresponding to . + [JsonIgnore] + public ISemanticVersion LowerSemanticVersion { get; set; } + + /// The semantic version corresponding to . + [JsonIgnore] + public ISemanticVersion UpperSemanticVersion { get; set; } + + /********* - ** Public methods + ** Private methods *********/ - /// Get whether the specified version is compatible according to this metadata. - /// The current version of the matching mod. - public bool IsCompatible(ISemanticVersion version) + /// The method called when the model finishes deserialising. + /// The deserialisation context. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) { - ISemanticVersion lowerVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null; - ISemanticVersion upperVersion = new SemanticVersion(this.UpperVersion); - - // ignore versions not in range - if (lowerVersion != null && version.IsOlderThan(lowerVersion)) - return true; - if (version.IsNewerThan(upperVersion)) - return true; - - // allow versions matching override - return !string.IsNullOrWhiteSpace(this.ForceCompatibleVersion) && Regex.IsMatch(version.ToString(), this.ForceCompatibleVersion, RegexOptions.IgnoreCase); + this.LowerSemanticVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null; + this.UpperSemanticVersion = this.UpperVersion != null ? new SemanticVersion(this.UpperVersion) : null; } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 9a6f07ef..3b6d1702 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -64,7 +64,7 @@ namespace StardewModdingAPI internal SGame GameInstance; /// Tracks the installed mods. - internal readonly ModRegistry ModRegistry = new ModRegistry(); + internal readonly ModRegistry ModRegistry; /// Manages deprecation warnings. internal readonly DeprecationManager DeprecationManager; @@ -92,6 +92,7 @@ namespace StardewModdingAPI // initialise this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; + this.ModRegistry = new ModRegistry(this.Settings.IncompatibleMods); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); } @@ -388,28 +389,22 @@ namespace StardewModdingAPI continue; } - // validate known incompatible mods + // validate compatibility + IncompatibleMod compatibility = this.ModRegistry.GetIncompatibilityRecord(manifest); + if (compatibility != null) { - string modKey = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; - IncompatibleMod compatibility = this.Settings.IncompatibleMods.FirstOrDefault(p => p.ID == modKey); - if(compatibility != null) - { - if (!compatibility.IsCompatible(manifest.Version)) - { - bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); - bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); + bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); + bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); - string reasonPhrase = compatibility.ReasonPhrase ?? "it isn't compatible with the latest version of the game"; - string warning = $"Skipped {compatibility.Name} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; - if (hasOfficialUrl) - warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; - if (hasUnofficialUrl) - warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; + string reasonPhrase = compatibility.ReasonPhrase ?? "it isn't compatible with the latest version of the game"; + string warning = $"Skipped {compatibility.Name} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + if (hasOfficialUrl) + warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; + if (hasUnofficialUrl) + warning += $"{Environment.NewLine}- unofficial update: {compatibility.UnofficialUpdateUrl}"; - this.Monitor.Log(warning, LogLevel.Error); - continue; - } - } + this.Monitor.Log(warning, LogLevel.Error); + continue; } // validate SMAPI version diff --git a/src/StardewModdingAPI/SemanticVersion.cs b/src/StardewModdingAPI/SemanticVersion.cs index 3cb592e2..9610562f 100644 --- a/src/StardewModdingAPI/SemanticVersion.cs +++ b/src/StardewModdingAPI/SemanticVersion.cs @@ -63,9 +63,13 @@ namespace StardewModdingAPI /// Get an integer indicating whether this version precedes (less than 0), supercedes (more than 0), or is equivalent to (0) the specified version. /// The version to compare with this instance. + /// The value is null. /// The implementation is defined by Semantic Version 2.0 (http://semver.org/). public int CompareTo(ISemanticVersion other) { + if(other == null) + throw new ArgumentNullException(nameof(other)); + const int same = 0; const int curNewer = 1; const int curOlder = -1; -- cgit From 9bbd0a44593e65f9e05b9c1161347173d43ff395 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Feb 2017 23:36:48 -0500 Subject: make skipped-mod messages more user-friendly --- release-notes.md | 1 + src/StardewModdingAPI/Program.cs | 38 ++++++++++++++++++++------------------ 2 files changed, 21 insertions(+), 18 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index b827f6bf..d6fb4b5b 100644 --- a/release-notes.md +++ b/release-notes.md @@ -6,6 +6,7 @@ See [log](https://github.com/Pathoschild/SMAPI/compare/1.8...1.9). For players: * Updated for Stardew Valley 1.2. Most mods are now rewritten for compatibility; some mods require an update from their authors. * Simplified log filename. +* Simplified error messages when a mod can't be loaded. * Simple nested mod folders are now recognised by SMAPI (e.g. `ModName-1.0\ModName\manifest.json`). * Fixed game's debug output being shown in the console for all users. * Fixed installer errors for some players when deleting files. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 3b6d1702..8a6392ad 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -356,7 +356,7 @@ namespace StardewModdingAPI this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } - string errorPrefix = $"Couldn't load mod for '{manifestPath.Replace(this.ModPath, "").Trim('/', '\\')}'"; + string skippedPrefix = $"Skipped {manifestPath.Replace(this.ModPath, "").Trim('/', '\\')}"; // read manifest Manifest manifest; @@ -366,7 +366,7 @@ namespace StardewModdingAPI string json = File.ReadAllText(manifestPath); if (string.IsNullOrEmpty(json)) { - this.Monitor.Log($"{errorPrefix}: manifest is empty.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because the manifest is empty.", LogLevel.Error); continue; } @@ -374,20 +374,22 @@ namespace StardewModdingAPI manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json"), null); if (manifest == null) { - this.Monitor.Log($"{errorPrefix}: the manifest file does not exist.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error); continue; } if (string.IsNullOrEmpty(manifest.EntryDll)) { - this.Monitor.Log($"{errorPrefix}: manifest doesn't specify an entry DLL.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its manifest doesn't specify an entry DLL.", LogLevel.Error); continue; } } catch (Exception ex) { - this.Monitor.Log($"{errorPrefix}: manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } + if(!string.IsNullOrWhiteSpace(manifest.Name)) + skippedPrefix = $"Skipped {manifest.Name}"; // validate compatibility IncompatibleMod compatibility = this.ModRegistry.GetIncompatibilityRecord(manifest); @@ -396,8 +398,8 @@ namespace StardewModdingAPI bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); - string reasonPhrase = compatibility.ReasonPhrase ?? "it isn't compatible with the latest version of the game"; - string warning = $"Skipped {compatibility.Name} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; + string reasonPhrase = compatibility.ReasonPhrase ?? "it's not compatible with the latest version of the game"; + string warning = $"{skippedPrefix} because {reasonPhrase}. Please check for a version newer than {compatibility.UpperVersion} here:"; if (hasOfficialUrl) warning += !hasUnofficialUrl ? $" {compatibility.UpdateUrl}" : $"{Environment.NewLine}- official mod: {compatibility.UpdateUrl}"; if (hasUnofficialUrl) @@ -415,13 +417,13 @@ namespace StardewModdingAPI ISemanticVersion minVersion = new SemanticVersion(manifest.MinimumApiVersion); if (minVersion.IsNewerThan(Constants.ApiVersion)) { - this.Monitor.Log($"{errorPrefix}: this mod requires SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because it needs SMAPI {minVersion} or later. Please update SMAPI to the latest version to use this mod.", LogLevel.Error); continue; } } catch (FormatException ex) when (ex.Message.Contains("not a valid semantic version")) { - this.Monitor.Log($"{errorPrefix}: the mod specified an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because it has an invalid minimum SMAPI version '{manifest.MinimumApiVersion}'. This should be a semantic version number like {Constants.ApiVersion}.", LogLevel.Error); continue; } } @@ -436,13 +438,13 @@ namespace StardewModdingAPI Directory.CreateDirectory(psDir); if (!Directory.Exists(psDir)) { - this.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod. The failure reason is unknown.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created for some reason.", LogLevel.Error); continue; } } catch (Exception ex) { - this.Monitor.Log($"{errorPrefix}: couldn't create the per-save configuration directory ('psconfigs') requested by this mod.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because it requires per-save configuration files ('psconfigs') which couldn't be created:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } } @@ -451,7 +453,7 @@ namespace StardewModdingAPI string assemblyPath = Path.Combine(directory.FullName, manifest.EntryDll); if (!File.Exists(assemblyPath)) { - this.Monitor.Log($"{errorPrefix}: the entry DLL '{manifest.EntryDll}' does not exist.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' doesn't exist.", LogLevel.Error); continue; } @@ -463,7 +465,7 @@ namespace StardewModdingAPI } catch (Exception ex) { - this.Monitor.Log($"{errorPrefix}: an error occurred while preprocessing '{manifest.EntryDll}'.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -472,13 +474,13 @@ namespace StardewModdingAPI { if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) { - this.Monitor.Log($"{errorPrefix}: the mod DLL does not contain an implementation of the 'Mod' class.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its DLL has no 'Mod' subclass.", LogLevel.Error); continue; } } catch (Exception ex) { - this.Monitor.Log($"{errorPrefix}: an error occurred while reading the mod DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } @@ -491,7 +493,7 @@ namespace StardewModdingAPI mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { - this.Monitor.Log($"{errorPrefix}: the mod's entry class could not be instantiated."); + this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated."); continue; } @@ -505,11 +507,11 @@ namespace StardewModdingAPI // track mod this.ModRegistry.Add(mod); modsLoaded += 1; - this.Monitor.Log($"Loaded mod: {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); + this.Monitor.Log($"Loaded {manifest.Name} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) { - this.Monitor.Log($"{errorPrefix}: an error occurred while loading the target DLL.\n{ex.GetLogSummary()}", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because initialisation failed:\n{ex.GetLogSummary()}", LogLevel.Error); } } -- cgit From 12cb2d272d54a850d3912e5fb39089c70cae5a9e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Feb 2017 23:51:52 -0500 Subject: minor cleanup --- src/StardewModdingAPI/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 8a6392ad..0394362e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -388,7 +389,7 @@ namespace StardewModdingAPI this.Monitor.Log($"{skippedPrefix} because manifest parsing failed.\n{ex.GetLogSummary()}", LogLevel.Error); continue; } - if(!string.IsNullOrWhiteSpace(manifest.Name)) + if (!string.IsNullOrWhiteSpace(manifest.Name)) skippedPrefix = $"Skipped {manifest.Name}"; // validate compatibility @@ -485,12 +486,11 @@ namespace StardewModdingAPI } // initialise mod - Mod mod; try { // get implementation TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); - mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); + Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { this.Monitor.Log($"{skippedPrefix} because its entry class couldn't be instantiated."); @@ -544,8 +544,8 @@ namespace StardewModdingAPI Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Game1.version} with {modsLoaded} mods"; } - // ReSharper disable once FunctionNeverReturns /// Run a loop handling console input. + [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] private void ConsoleInputLoop() { while (true) -- cgit From 2ed3b25b6b886092e16980288491b47d7b54a309 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Feb 2017 23:54:50 -0500 Subject: further group deprecation warnings during mod loading --- src/StardewModdingAPI/Program.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 0394362e..df64de3b 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -515,11 +515,6 @@ namespace StardewModdingAPI } } - // log deprecation warnings - foreach (Action warning in deprecationWarnings) - warning(); - deprecationWarnings = null; - // initialise mods foreach (Mod mod in this.ModRegistry.GetMods()) { @@ -531,7 +526,7 @@ namespace StardewModdingAPI // raise deprecation warning for old Entry() methods if (this.DeprecationManager.IsVirtualMethodImplemented(mod.GetType(), typeof(Mod), nameof(Mod.Entry), new[] { typeof(object[]) })) - this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info); + deprecationWarnings.Add(() => this.DeprecationManager.Warn(mod.ModManifest.Name, $"{nameof(Mod)}.{nameof(Mod.Entry)}(object[]) instead of {nameof(Mod)}.{nameof(Mod.Entry)}({nameof(IModHelper)})", "1.0", DeprecationLevel.Info)); } catch (Exception ex) { @@ -541,6 +536,8 @@ namespace StardewModdingAPI // print result this.Monitor.Log($"Loaded {modsLoaded} mods."); + foreach (Action warning in deprecationWarnings) + warning(); Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Game1.version} with {modsLoaded} mods"; } -- cgit From be0aa19f30ac1168214f0dcf39a37587e8f4abb3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Feb 2017 00:15:45 -0500 Subject: fix game version always being detected as 1.2.9 because Game1.version is a const now --- src/StardewModdingAPI/Constants.cs | 12 ++++++++++++ src/StardewModdingAPI/Program.cs | 12 ++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 8b085eac..69dd1fa8 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -71,6 +71,18 @@ namespace StardewModdingAPI /// Whether a player save has been loaded. internal static bool PlayerNull => !Game1.hasLoadedGame || Game1.player == null || string.IsNullOrEmpty(Game1.player.name); + /// The actual game version. + /// This is necessary because is a constant, so SMAPI's references to it are inlined at compile-time. + internal static string GameVersion + { + get + { + FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); + if (field == null) + throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); + return (string)field.GetValue(null); + } + } /********* ** Protected methods diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index df64de3b..3431e02f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -102,8 +102,8 @@ namespace StardewModdingAPI { // initialise logging Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Game1.version} on {Environment.OSVersion}", LogLevel.Info); - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Game1.version}"; + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {Environment.OSVersion}", LogLevel.Info); + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; // inject compatibility shims #pragma warning disable 618 @@ -142,9 +142,9 @@ namespace StardewModdingAPI try { // verify version - if (string.Compare(Game1.version, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0) + if (string.Compare(Constants.GameVersion, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Game1.version}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); return; } @@ -245,7 +245,7 @@ namespace StardewModdingAPI this.GameInstance = new SGame(this.Monitor); this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); - this.GameInstance.Window.Title = $"Stardew Valley {Game1.version}"; + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion}"; gameProgramType.GetField("gamePtr").SetValue(gameProgramType, this.GameInstance); // configure @@ -538,7 +538,7 @@ namespace StardewModdingAPI this.Monitor.Log($"Loaded {modsLoaded} mods."); foreach (Action warning in deprecationWarnings) warning(); - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Game1.version} with {modsLoaded} mods"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; } /// Run a loop handling console input. -- cgit From fd2d7d714d025f7f787e14e47645078f988ae8ce Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Feb 2017 15:04:28 -0500 Subject: fix game version checks not using semantic versioning This caused an issue where SMAPI didn't consider SDV 1.2.10 to pass the minimum game version of 1.2.9. This requires some workarounds for SDV 1.11's non-semantic version. --- src/StardewModdingAPI/Constants.cs | 37 ++++++++++++++++++++++++++++++------- src/StardewModdingAPI/Program.cs | 4 ++-- 2 files changed, 32 insertions(+), 9 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 3326e43f..99bf834c 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 8, 0); /// The minimum supported version of Stardew Valley. - public static string MinimumGameVersion { get; } = "1.2.9"; + public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.9"); /// The path to the game folder. public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -68,8 +68,12 @@ namespace StardewModdingAPI /// Whether a player save has been loaded. internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); - /// The current game version. - internal static string GameVersion { get; } = Constants.GetGameVersion(); + /// The game's current semantic version. + internal static ISemanticVersion GameVersion { get; } = Constants.GetGameVersion(); + + /// The game's current version as it should be displayed to players. + internal static ISemanticVersion GameDisplayVersion { get; } = Constants.GetGameDisplayVersion(Constants.GameVersion); + /********* ** Protected methods @@ -142,14 +146,33 @@ namespace StardewModdingAPI return $"{prefix}_{Game1.uniqueIDForThisGame}"; } - /// Get the actual game version. - /// This uses reflection because is a constant, so SMAPI's references to it are inlined at compile-time. - private static string GetGameVersion() + /// Get the game's current semantic version. + private static ISemanticVersion GetGameVersion() { + // get raw version + // we need reflection because it's a constant, so SMAPI's references to it are inlined at compile-time FieldInfo field = typeof(Game1).GetField(nameof(Game1.version), BindingFlags.Public | BindingFlags.Static); if (field == null) throw new InvalidOperationException($"The {nameof(Game1)}.{nameof(Game1.version)} field could not be found."); - return (string)field.GetValue(null); + string version = (string)field.GetValue(null); + + // get semantic version + if (version == "1.11") + version = "1.1.1"; // The 1.1 patch was released as 1.11, which means it's out of order for semantic version checks + return new SemanticVersion(version); + } + + /// Get game current version as it should be displayed to players. + /// The semantic game version. + private static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version) + { + switch (version.ToString()) + { + case "1.1.1": + return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11 + default: + return version; + } } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 3431e02f..8264d0ee 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -142,9 +142,9 @@ namespace StardewModdingAPI try { // verify version - if (string.Compare(Constants.GameVersion, Constants.MinimumGameVersion, StringComparison.InvariantCultureIgnoreCase) < 0) + if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameDisplayVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); return; } -- cgit From ba55ed34ca855de13e738e6bb97dc404419f5350 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Feb 2017 15:06:38 -0500 Subject: fix 'please update your game' error not pausing before exit --- src/StardewModdingAPI/Program.cs | 1 + 1 file changed, 1 insertion(+) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 8264d0ee..05d3e5e4 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -145,6 +145,7 @@ namespace StardewModdingAPI if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameDisplayVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); + this.PressAnyKeyToExit(); return; } -- cgit From 60f31b0fc66f2215ccb23552a2c9124f09fd8769 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Feb 2017 15:10:02 -0500 Subject: clean up program properties --- src/StardewModdingAPI/Constants.cs | 11 +++++++++++ src/StardewModdingAPI/Program.cs | 35 ++++++++++------------------------- 2 files changed, 21 insertions(+), 25 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 99bf834c..be44c664 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -65,6 +65,9 @@ namespace StardewModdingAPI /// The file path to the log where the latest output should be saved. internal static string LogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); + /// The full path to the folder containing mods. + internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); + /// Whether a player save has been loaded. internal static bool IsSaveLoaded => Game1.hasLoadedGame && !string.IsNullOrEmpty(Game1.player.name); @@ -74,6 +77,14 @@ namespace StardewModdingAPI /// The game's current version as it should be displayed to players. internal static ISemanticVersion GameDisplayVersion { get; } = Constants.GetGameDisplayVersion(Constants.GameVersion); + /// The target game platform. + internal static Platform TargetPlatform { get; } = +#if SMAPI_FOR_WINDOWS + Platform.Windows; +#else + Platform.Mono; +#endif + /********* ** Protected methods diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 05d3e5e4..b7947df1 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -28,17 +28,6 @@ namespace StardewModdingAPI /********* ** Properties *********/ - /// The target game platform. - private readonly Platform TargetPlatform = -#if SMAPI_FOR_WINDOWS - Platform.Windows; -#else - Platform.Mono; -#endif - - /// The full path to the folder containing mods. - private readonly string ModPath = Path.Combine(Constants.ExecutionPath, "Mods"); - /// The log file to which to write messages. private readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); @@ -57,21 +46,17 @@ namespace StardewModdingAPI /// Whether the game is currently running. private bool IsGameRunning; - - /********* - ** Accessors - *********/ /// The underlying game instance. - internal SGame GameInstance; + private SGame GameInstance; /// Tracks the installed mods. - internal readonly ModRegistry ModRegistry; + private readonly ModRegistry ModRegistry; /// Manages deprecation warnings. - internal readonly DeprecationManager DeprecationManager; + private readonly DeprecationManager DeprecationManager; /// Manages console commands. - internal readonly CommandManager CommandManager = new CommandManager(); + private readonly CommandManager CommandManager = new CommandManager(); /********* @@ -136,7 +121,7 @@ namespace StardewModdingAPI this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); // print file paths - this.Monitor.Log($"Mods go here: {this.ModPath}"); + this.Monitor.Log($"Mods go here: {Constants.ModPath}"); // hook into & launch the game try @@ -151,11 +136,11 @@ namespace StardewModdingAPI // initialise folders this.Monitor.Log("Loading SMAPI..."); - this.VerifyPath(this.ModPath); + this.VerifyPath(Constants.ModPath); this.VerifyPath(Constants.LogDir); // get executable path - string executablePath = Path.Combine(Constants.ExecutionPath, this.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); + string executablePath = Path.Combine(Constants.ExecutionPath, Constants.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); if (!File.Exists(executablePath)) { this.Monitor.Log($"Couldn't find executable: {executablePath}", LogLevel.Error); @@ -331,13 +316,13 @@ namespace StardewModdingAPI JsonHelper jsonHelper = new JsonHelper(); // get assembly loader - AssemblyLoader modAssemblyLoader = new AssemblyLoader(this.TargetPlatform, this.Monitor); + AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.TargetPlatform, this.Monitor); AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => modAssemblyLoader.ResolveAssembly(e.Name); // load mod assemblies int modsLoaded = 0; List deprecationWarnings = new List(); // queue up deprecation warnings to show after mod list - foreach (string directoryPath in Directory.GetDirectories(this.ModPath)) + foreach (string directoryPath in Directory.GetDirectories(Constants.ModPath)) { // passthrough empty directories DirectoryInfo directory = new DirectoryInfo(directoryPath); @@ -358,7 +343,7 @@ namespace StardewModdingAPI this.Monitor.Log($"Ignored folder \"{directory.Name}\" which doesn't have a manifest.json.", LogLevel.Warn); continue; } - string skippedPrefix = $"Skipped {manifestPath.Replace(this.ModPath, "").Trim('/', '\\')}"; + string skippedPrefix = $"Skipped {manifestPath.Replace(Constants.ModPath, "").Trim('/', '\\')}"; // read manifest Manifest manifest; -- cgit From 9c53a254d50718fee3b8043bb0b8bb840557e82f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Feb 2017 15:22:45 -0500 Subject: add prototype content event + helper to manipulate XNB data (#173) --- src/StardewModdingAPI/Events/ContentEvents.cs | 69 ++++++++++ .../Framework/ContentEventHelper.cs | 142 +++++++++++++++++++++ src/StardewModdingAPI/Framework/SContentManager.cs | 36 +++++- src/StardewModdingAPI/IContentEventHelper.cs | 53 ++++++++ src/StardewModdingAPI/Program.cs | 1 + src/StardewModdingAPI/StardewModdingAPI.csproj | 3 + 6 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 src/StardewModdingAPI/Events/ContentEvents.cs create mode 100644 src/StardewModdingAPI/Framework/ContentEventHelper.cs create mode 100644 src/StardewModdingAPI/IContentEventHelper.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs new file mode 100644 index 00000000..cc07f242 --- /dev/null +++ b/src/StardewModdingAPI/Events/ContentEvents.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using StardewModdingAPI.Framework; + +namespace StardewModdingAPI.Events +{ + /// Events raised when the game loads content. + [Obsolete("This is an undocumented experimental API and may change without warning.")] + public static class ContentEvents + { + /********* + ** Properties + *********/ + /// Tracks the installed mods. + private static ModRegistry ModRegistry; + + /// Encapsulates monitoring and logging. + private static IMonitor Monitor; + + /// The mods using the experimental API for which a warning has been raised. + private static readonly HashSet WarnedMods = new HashSet(); + + + /********* + ** Events + *********/ + /// Raised when an XNB file is being read into the cache. Mods can change the data here before it's cached. + public static event EventHandler AssetLoading; + + + /********* + ** Internal methods + *********/ + /// Injects types required for backwards compatibility. + /// Tracks the installed mods. + /// Encapsulates monitoring and logging. + internal static void Shim(ModRegistry modRegistry, IMonitor monitor) + { + ContentEvents.ModRegistry = modRegistry; + ContentEvents.Monitor = monitor; + } + + /// Raise an event. + /// Encapsulates monitoring and logging. + /// Encapsulates access and changes to content being read from a data file. + internal static void InvokeAssetLoading(IMonitor monitor, IContentEventHelper contentHelper) + { + // raise warning about experimental API + foreach (Delegate handler in ContentEvents.AssetLoading.GetInvocationList()) + { + string modName = ContentEvents.ModRegistry.GetModFrom(handler) ?? "An unknown mod"; + if (!ContentEvents.WarnedMods.Contains(modName)) + { + ContentEvents.WarnedMods.Add(modName); + ContentEvents.Monitor.Log($"{modName} used the undocumented and experimental content API, which may change or be removed without warning.", LogLevel.Warn); + } + } + + // raise event + monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AssetLoading)}", ContentEvents.AssetLoading?.GetInvocationList(), null, contentHelper); + } + + /// Get whether there are any listeners. + internal static bool HasAssetLoadingListeners() + { + return ContentEvents.AssetLoading != null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/ContentEventHelper.cs b/src/StardewModdingAPI/Framework/ContentEventHelper.cs new file mode 100644 index 00000000..d4a9bbb8 --- /dev/null +++ b/src/StardewModdingAPI/Framework/ContentEventHelper.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using Microsoft.Xna.Framework.Graphics; + +namespace StardewModdingAPI.Framework +{ + /// Encapsulates access and changes to content being read from a data file. + internal class ContentEventHelper : EventArgs, IContentEventHelper + { + /********* + ** Properties + *********/ + /// Normalises an asset key to match the cache key. + private readonly Func GetNormalisedPath; + + + /********* + ** Accessors + *********/ + /// The normalised asset path being read. The format may change between platforms; see to compare with a known path. + public string Path { get; } + + /// The content data being read. + public object Data { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The file path being read. + /// The content data being read. + /// Normalises an asset key to match the cache key. + public ContentEventHelper(string path, object data, Func getNormalisedPath) + { + this.Path = path; + this.Data = data; + this.GetNormalisedPath = getNormalisedPath; + } + + /// Get whether the asset path being loaded matches a given path after normalisation. + /// The expected asset path, relative to the game folder and without the .xnb extension (like 'Data\ObjectInformation'). + /// Whether to match a localised version of the asset file (like 'Data\ObjectInformation.ja-JP'). + public bool IsPath(string path, bool matchLocalisedVersion = true) + { + path = this.GetNormalisedPath(path); + + // equivalent + if (this.Path.Equals(path, StringComparison.InvariantCultureIgnoreCase)) + return true; + + // localised version + if (matchLocalisedVersion) + { + return + this.Path.StartsWith($"{path}.", StringComparison.InvariantCultureIgnoreCase) // starts with given path + && Regex.IsMatch(this.Path.Substring(path.Length + 1), "^[a-z]+-[A-Z]+$"); // ends with locale (e.g. pt-BR) + } + + // no match + return false; + } + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + public TData GetData() + { + if (!(this.Data is TData)) + throw new InvalidCastException($"The content data of type {this.Data.GetType().FullName} can't be converted to the requested {typeof(TData).FullName}."); + return (TData)this.Data; + } + + /// Add or replace an entry in the dictionary data. + /// The entry key type. + /// The entry value type. + /// The entry key. + /// The entry value. + /// The content being read isn't a dictionary. + public void SetDictionaryEntry(TKey key, TValue value) + { + IDictionary data = this.GetData>(); + data[key] = value; + } + + /// Add or replace an entry in the dictionary data. + /// The entry key type. + /// The entry value type. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + /// The content being read isn't a dictionary. + public void SetDictionaryEntry(TKey key, Func value) + { + IDictionary data = this.GetData>(); + data[key] = value(data[key]); + } + + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + public void ReplaceWith(object value) + { + if (value == null) + throw new ArgumentNullException(nameof(value), "Can't set a loaded asset to a null value."); + if (!this.Data.GetType().IsInstanceOfType(value)) + throw new InvalidCastException($"Can't replace loaded asset of type {this.GetFriendlyTypeName(this.Data.GetType())} with value of type {this.GetFriendlyTypeName(value.GetType())}. The new type must be compatible to prevent game errors."); + + this.Data = value; + } + + + /********* + ** Private methods + *********/ + /// Get a human-readable type name. + /// The type to name. + private string GetFriendlyTypeName(Type type) + { + // dictionary + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>)) + { + Type[] genericArgs = type.GetGenericArguments(); + return $"Dictionary<{this.GetFriendlyTypeName(genericArgs[0])}, {this.GetFriendlyTypeName(genericArgs[1])}>"; + } + + // texture + if (type == typeof(Texture2D)) + return type.Name; + + // native type + if (type == typeof(int)) + return "int"; + if (type == typeof(string)) + return "string"; + + // default + return type.FullName; + } + } +} diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 2a876d72..344d3ed9 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Threading; using Microsoft.Xna.Framework; using StardewModdingAPI.AssemblyRewriters; +using StardewModdingAPI.Events; using StardewModdingAPI.Framework.Reflection; using StardewValley; @@ -22,7 +23,7 @@ namespace StardewModdingAPI.Framework private readonly IDictionary Cache; /// Normalises an asset key to match the cache key. - private readonly IPrivateMethod NormaliseAssetKey; + private readonly Func NormaliseAssetKey; /********* @@ -44,15 +45,23 @@ namespace StardewModdingAPI.Framework public SContentManager(IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, string languageCodeOverride, IMonitor monitor) : base(serviceProvider, rootDirectory, currentCulture, languageCodeOverride) { + // initialise this.Monitor = monitor; - IReflectionHelper reflection = new ReflectionHelper(); + + // get underlying asset cache this.Cache = reflection .GetPrivateField>(this, "loadedAssets") .GetValue(); - this.NormaliseAssetKey = Constants.TargetPlatform == Platform.Windows - ? reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath") - : reflection.GetPrivateMethod(this, nameof(this.NormaliseKeyForMono)); + + // get asset key normalisation logic + if (Constants.TargetPlatform == Platform.Windows) + { + IPrivateMethod method = reflection.GetPrivateMethod(typeof(TitleContainer), "GetCleanPath"); + this.NormaliseAssetKey = path => method.Invoke(path); + } + else + this.NormaliseAssetKey = this.NormaliseKeyForMono; } /// Load an asset that has been processed by the content pipeline. @@ -60,8 +69,21 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the loader root directory, not including the .xnb extension. public override T Load(string assetName) { - assetName = this.NormaliseAssetKey.Invoke(assetName); - return base.Load(assetName); + // pass through if no event handlers + if (!ContentEvents.HasAssetLoadingListeners()) + return base.Load(assetName); + + // skip if already loaded + string key = this.NormaliseAssetKey(assetName); + if (this.Cache.ContainsKey(key)) + return base.Load(assetName); + + // intercept load + T data = base.Load(assetName); + IContentEventHelper helper = new ContentEventHelper(assetName, data, this.NormaliseAssetKey); + ContentEvents.InvokeAssetLoading(this.Monitor, helper); + this.Cache[key] = helper.Data; + return (T)helper.Data; } diff --git a/src/StardewModdingAPI/IContentEventHelper.cs b/src/StardewModdingAPI/IContentEventHelper.cs new file mode 100644 index 00000000..98d074d9 --- /dev/null +++ b/src/StardewModdingAPI/IContentEventHelper.cs @@ -0,0 +1,53 @@ +using System; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to content being read from a data file. + public interface IContentEventHelper + { + /********* + ** Accessors + *********/ + /// The normalised asset path being read. The format may change between platforms; see to compare with a known path. + string Path { get; } + + /// The content data being read. + object Data { get; } + + + /********* + ** Public methods + *********/ + /// Get whether the asset path being loaded matches a given path after normalisation. + /// The expected asset path, relative to the game folder and without the .xnb extension (like 'Data\ObjectInformation'). + /// Whether to match a localised version of the asset file (like 'Data\ObjectInformation.ja-JP'). + bool IsPath(string path, bool matchLocalisedVersion = true); + + /// Get the data as a given type. + /// The expected data type. + /// The data can't be converted to . + TData GetData(); + + /// Add or replace an entry in the dictionary data. + /// The entry key type. + /// The entry value type. + /// The entry key. + /// The entry value. + /// The content being read isn't a dictionary. + void SetDictionaryEntry(TKey key, TValue value); + + /// Add or replace an entry in the dictionary data. + /// The entry key type. + /// The entry value type. + /// The entry key. + /// A callback which accepts the current value and returns the new value. + /// The content being read isn't a dictionary. + void SetDictionaryEntry(TKey key, Func value); + + /// Replace the entire content value with the given value. This is generally not recommended, since it may break compatibility with other mods or different versions of the game. + /// The new content value. + /// The is null. + /// The 's type is not compatible with the loaded asset's type. + void ReplaceWith(object value); + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b7947df1..18a83e32 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -97,6 +97,7 @@ namespace StardewModdingAPI InternalExtensions.Shim(this.ModRegistry); Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); Mod.Shim(this.DeprecationManager); + ContentEvents.Shim(this.ModRegistry, this.Monitor); PlayerEvents.Shim(this.DeprecationManager); TimeEvents.Shim(this.DeprecationManager); #pragma warning restore 618 diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index ee37379d..9b86adac 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -117,6 +117,7 @@ + @@ -147,6 +148,7 @@ + @@ -158,6 +160,7 @@ + -- cgit From 6de4888a1b3e62f3b3976ba013521fbd79405288 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Mar 2017 19:17:34 -0500 Subject: improve TrainerMod feedback to user, standardise color/colour spelling --- release-notes.md | 1 + src/StardewModdingAPI/Program.cs | 5 +- src/TrainerMod/TrainerMod.cs | 260 +++++++++++++++++++++++---------------- 3 files changed, 160 insertions(+), 106 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index d157c06a..81d770f0 100644 --- a/release-notes.md +++ b/release-notes.md @@ -8,6 +8,7 @@ For players: * Simplified log filename. * Simplified error messages when a mod can't be loaded. * Simple nested mod folders are now recognised by SMAPI (e.g. `ModName-1.0\ModName\manifest.json`). +* Improved TrainerMod command handling & feedback. * Fixed game's debug output being shown in the console for all users. * Fixed the game-outdated error not pausing before exit. * Fixed installer errors for some players when deleting files. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 18a83e32..07857512 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -554,7 +554,10 @@ namespace StardewModdingAPI this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); } else - this.Monitor.Log("Commands: " + string.Join(", ", this.CommandManager.GetAll().Select(p => p.Name)), LogLevel.Info); + { + this.Monitor.Log("The following commands are registered: " + string.Join(", ", this.CommandManager.GetAll().Select(p => p.Name)) + ".", LogLevel.Info); + this.Monitor.Log("For more information about a command, type 'help command_name'.", LogLevel.Info); + } } /// Show a 'press any key to exit' message, and exit when they press a key. diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs index 1298c5cd..b89cbf5c 100644 --- a/src/TrainerMod/TrainerMod.cs +++ b/src/TrainerMod/TrainerMod.cs @@ -94,7 +94,7 @@ namespace TrainerMod .Add("player_setlevel", "Sets the player's specified skill to the specified value.\n\nUsage: player_setlevel \n- skill: the skill to set (one of 'luck', 'mining', 'combat', 'farming', 'fishing', or 'foraging').\n- value: the target level (a number from 1 to 10).", this.HandleCommand) .Add("player_setspeed", "Sets the player's speed to the specified value?\n\nUsage: player_setspeed \n- value: an integer amount (0 is normal).", this.HandleCommand) - .Add("player_changecolour", "Sets the colour of a player feature.\n\nUsage: player_changecolor \n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- colour: a colour value in RGB format, like (255,255,255).", this.HandleCommand) + .Add("player_changecolor", "Sets the color of a player feature.\n\nUsage: player_changecolor \n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- color: a color value in RGB format, like (255,255,255).", this.HandleCommand) .Add("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changecolor .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the integer style ID.", this.HandleCommand) .Add("player_additem", $"Gives the player an item.\n\nUsage: player_additem [count] [quality]\n- item: the item ID (use the 'list_items' command to see a list).\n- count (optional): how many of the item to give.\n- quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium).", this.HandleCommand) @@ -104,7 +104,7 @@ namespace TrainerMod .Add("list_items", "Lists and searches items in the game data.\n\nUsage: list_items [search]\n- search (optional): an arbitrary search string to filter by.", this.HandleCommand) .Add("world_settime", "Sets the time to the specified value.\n\nUsage: world_settime \n- value: the target time in military time (like 0600 for 6am and 1800 for 6pm)", this.HandleCommand) - .Add("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze) or blank (toggle).", this.HandleCommand) + .Add("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).", this.HandleCommand) .Add("world_setday", "Sets the day to the specified value.\n\nUsage: world_setday .\n- value: the target day (a number from 1 to 28).", this.HandleCommand) .Add("world_setseason", "Sets the season to the specified value.\n\nUsage: world_setseason \n- value: the target season (one of 'spring', 'summer', 'fall', 'winter').", this.HandleCommand) .Add("world_downminelevel", "Goes down one mine level?", this.HandleCommand) @@ -115,21 +115,23 @@ namespace TrainerMod } /// Handle a TrainerMod command. - /// The command name. + /// The command name. /// The command arguments. - private void HandleCommand(string name, string[] args) + private void HandleCommand(string command, string[] args) { - switch (name) + switch (command) { case "type": - this.Monitor.Log($"[Int32: {int.MinValue} - {int.MaxValue}], [Int64: {long.MinValue} - {long.MaxValue}], [String: \"raw text\"], [Colour: r,g,b (EG: 128, 32, 255)]", LogLevel.Info); + this.Monitor.Log($"[Int32: {int.MinValue} - {int.MaxValue}], [Int64: {long.MinValue} - {long.MaxValue}], [String: \"raw text\"], [Color: r,g,b (EG: 128, 32, 255)]", LogLevel.Info); break; case "save": + this.Monitor.Log("Saving the game...", LogLevel.Info); SaveGame.Save(); break; case "load": + this.Monitor.Log("Triggering load menu...", LogLevel.Info); Game1.hasLoadedGame = false; Game1.activeClickableMenu = new LoadGameMenu(); break; @@ -145,17 +147,19 @@ namespace TrainerMod { case "player": Game1.player.Name = args[1]; + this.Monitor.Log($"OK, your player's name is now {Game1.player.Name}.", LogLevel.Info); break; case "farm": Game1.player.farmName = args[1]; + this.Monitor.Log($"OK, your farm's name is now {Game1.player.Name}.", LogLevel.Info); break; } } else - this.LogObjectInvalid(); + this.LogArgumentsInvalid(command); } else - this.LogObjectValueNotSpecified(); + this.Monitor.Log($"Your name is currently '{Game1.player.Name}'. Type 'help player_setname' for usage.", LogLevel.Info); break; case "player_setmoney": @@ -163,7 +167,10 @@ namespace TrainerMod { string amountStr = args[0]; if (amountStr == "inf") + { this.InfiniteMoney = true; + this.Monitor.Log("OK, you now have infinite money.", LogLevel.Info); + } else { this.InfiniteMoney = false; @@ -171,14 +178,14 @@ namespace TrainerMod if (int.TryParse(amountStr, out amount)) { Game1.player.Money = amount; - this.Monitor.Log($"Set {Game1.player.Name}'s money to {Game1.player.Money}", LogLevel.Info); + this.Monitor.Log($"OK, you now have {Game1.player.Money} gold.", LogLevel.Info); } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } } else - this.LogValueNotSpecified(); + this.Monitor.Log($"You currently have {(this.InfiniteMoney ? "infinite" : Game1.player.Money.ToString())} gold. Specify a value to change it.", LogLevel.Info); break; case "player_setstamina": @@ -186,7 +193,10 @@ namespace TrainerMod { string amountStr = args[0]; if (amountStr == "inf") + { this.InfiniteStamina = true; + this.Monitor.Log("OK, you now have infinite stamina.", LogLevel.Info); + } else { this.InfiniteStamina = false; @@ -194,14 +204,14 @@ namespace TrainerMod if (int.TryParse(amountStr, out amount)) { Game1.player.Stamina = amount; - this.Monitor.Log($"Set {Game1.player.Name}'s stamina to {Game1.player.Stamina}", LogLevel.Info); + this.Monitor.Log($"OK, you now have {Game1.player.Stamina} stamina.", LogLevel.Info); } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } } else - this.Monitor.Log($"{Game1.player.Name}'s stamina is {Game1.player.Stamina}", LogLevel.Info); + this.Monitor.Log($"You currently have {(this.InfiniteStamina ? "infinite" : Game1.player.Stamina.ToString())} stamina. Specify a value to change it.", LogLevel.Info); break; case "player_setmaxstamina": @@ -211,13 +221,13 @@ namespace TrainerMod if (int.TryParse(args[0], out amount)) { Game1.player.MaxStamina = amount; - this.Monitor.Log($"Set {Game1.player.Name}'s max stamina to {Game1.player.MaxStamina}", LogLevel.Info); + this.Monitor.Log($"OK, you now have {Game1.player.MaxStamina} max stamina.", LogLevel.Info); } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.Monitor.Log($"{Game1.player.Name}'s maxstamina is {Game1.player.MaxStamina}", LogLevel.Info); + this.Monitor.Log($"You currently have {Game1.player.MaxStamina} max stamina. Specify a value to change it.", LogLevel.Info); break; case "player_setlevel": @@ -234,32 +244,38 @@ namespace TrainerMod { case "luck": Game1.player.LuckLevel = level; + this.Monitor.Log($"OK, your luck skill is now {Game1.player.LuckLevel}.", LogLevel.Info); break; case "mining": Game1.player.MiningLevel = level; + this.Monitor.Log($"OK, your mining skill is now {Game1.player.MiningLevel}.", LogLevel.Info); break; case "combat": Game1.player.CombatLevel = level; + this.Monitor.Log($"OK, your combat skill is now {Game1.player.CombatLevel}.", LogLevel.Info); break; case "farming": Game1.player.FarmingLevel = level; + this.Monitor.Log($"OK, your farming skill is now {Game1.player.FarmingLevel}.", LogLevel.Info); break; case "fishing": Game1.player.FishingLevel = level; + this.Monitor.Log($"OK, your fishing skill is now {Game1.player.FishingLevel}.", LogLevel.Info); break; case "foraging": Game1.player.ForagingLevel = level; + this.Monitor.Log($"OK, your foraging skill is now {Game1.player.ForagingLevel}.", LogLevel.Info); break; } } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.Monitor.Log(" is invalid", LogLevel.Error); + this.LogUsageError("That isn't a valid skill.", command); } else - this.Monitor.Log(" and must be specified", LogLevel.Error); + this.LogArgumentsInvalid(command); break; case "player_setspeed": @@ -269,16 +285,16 @@ namespace TrainerMod if (amountStr.IsInt()) { Game1.player.addedSpeed = amountStr.ToInt(); - this.Monitor.Log($"Set {Game1.player.Name}'s added speed to {Game1.player.addedSpeed}", LogLevel.Info); + this.Monitor.Log($"OK, your added speed is now {Game1.player.addedSpeed}.", LogLevel.Info); } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.LogValueNotSpecified(); + this.Monitor.Log($"You currently have {Game1.player.addedSpeed} added speed. Specify a value to change it.", LogLevel.Info); break; - case "player_changecolour": + case "player_changecolor": if (args.Length > 1) { string target = args[0]; @@ -293,23 +309,26 @@ namespace TrainerMod { case "hair": Game1.player.hairstyleColor = color; + this.Monitor.Log("OK, your hair color is updated.", LogLevel.Info); break; case "eyes": Game1.player.changeEyeColor(color); + this.Monitor.Log("OK, your eye color is updated.", LogLevel.Info); break; case "pants": Game1.player.pantsColor = color; + this.Monitor.Log("OK, your pants color is updated.", LogLevel.Info); break; } } else - this.Monitor.Log(" is invalid", LogLevel.Error); + this.LogUsageError("The color should be an RBG value like '255,150,0'.", command); } else - this.LogObjectInvalid(); + this.LogArgumentsInvalid(command); } else - this.Monitor.Log(" and must be specified", LogLevel.Error); + this.LogArgumentsInvalid(command); break; case "player_changestyle": @@ -326,45 +345,66 @@ namespace TrainerMod { case "hair": Game1.player.changeHairStyle(styleID); + this.Monitor.Log("OK, your hair style is updated.", LogLevel.Info); break; case "shirt": Game1.player.changeShirt(styleID); + this.Monitor.Log("OK, your shirt style is updated.", LogLevel.Info); break; case "acc": Game1.player.changeAccessory(styleID); + this.Monitor.Log("OK, your accessory style is updated.", LogLevel.Info); break; case "skin": Game1.player.changeSkinColor(styleID); + this.Monitor.Log("OK, your skin color is updated.", LogLevel.Info); break; case "shoe": Game1.player.changeShoeColor(styleID); + this.Monitor.Log("OK, your shoe style is updated.", LogLevel.Info); break; case "swim": - if (styleID == 0) - Game1.player.changeOutOfSwimSuit(); - else if (styleID == 1) - Game1.player.changeIntoSwimsuit(); - else - this.Monitor.Log(" must be 0 or 1 for this ", LogLevel.Error); + switch (styleID) + { + case 0: + Game1.player.changeOutOfSwimSuit(); + this.Monitor.Log("OK, you're no longer in your swimming suit.", LogLevel.Info); + break; + case 1: + Game1.player.changeIntoSwimsuit(); + this.Monitor.Log("OK, you're now in your swimming suit.", LogLevel.Info); + break; + default: + this.LogUsageError("The swim value should be 0 (no swimming suit) or 1 (swimming suit).", command); + break; + } break; case "gender": - if (styleID == 0) - Game1.player.changeGender(true); - else if (styleID == 1) - Game1.player.changeGender(false); - else - this.Monitor.Log(" must be 0 or 1 for this ", LogLevel.Error); + switch (styleID) + { + case 0: + Game1.player.changeGender(true); + this.Monitor.Log("OK, you're now male.", LogLevel.Info); + break; + case 1: + Game1.player.changeGender(false); + this.Monitor.Log("OK, you're now female.", LogLevel.Info); + break; + default: + this.LogUsageError("The gender value should be 0 (male) or 1 (female).", command); + break; + } break; } } else - this.LogValueInvalid(); + this.LogArgumentsInvalid(command); } else - this.LogObjectInvalid(); + this.LogArgumentsInvalid(command); } else - this.LogObjectValueNotSpecified(); + this.LogArgumentsInvalid(command); break; case "world_freezetime": @@ -378,27 +418,20 @@ namespace TrainerMod { this.FreezeTime = value == 1; this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0; - this.Monitor.Log("Time is now " + (this.FreezeTime ? "frozen" : "thawed"), LogLevel.Info); + this.Monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info); } else - this.Monitor.Log(" should be 0, 1, or empty", LogLevel.Error); + this.LogUsageError("The value should be 0 (not frozen), 1 (frozen), or empty (toggle).", command); } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - int valu = 1; - if (this.FreezeTime == false) - { - valu = 1; - } - else - { - valu = 0; - } - this.FreezeTime = valu == 1; + { + this.FreezeTime = !this.FreezeTime; this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0; - this.Monitor.Log("Time is now " + (this.FreezeTime ? "frozen" : "thawed"), LogLevel.Info); + this.Monitor.Log($"OK, time is now {(this.FreezeTime ? "frozen" : "resumed")}.", LogLevel.Info); + } break; case "world_settime": @@ -413,16 +446,16 @@ namespace TrainerMod { Game1.timeOfDay = args[0].ToInt(); this.FrozenTime = this.FreezeTime ? Game1.timeOfDay : 0; - this.Monitor.Log($"Time set to: {Game1.timeOfDay}", LogLevel.Info); + this.Monitor.Log($"OK, the time is now {Game1.timeOfDay.ToString().PadLeft(4, '0')}.", LogLevel.Info); } else - this.Monitor.Log(" should be between 600 and 2600 (06:00 AM - 02:00 AM [NEXT DAY])", LogLevel.Error); + this.LogUsageError("That isn't a valid time.", command); } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.LogValueNotSpecified(); + this.Monitor.Log($"The current time is {Game1.timeOfDay}. Specify a value to change it.", LogLevel.Info); break; case "world_setday": @@ -434,15 +467,18 @@ namespace TrainerMod { int day = dayStr.ToInt(); if (day <= 28 && day > 0) + { Game1.dayOfMonth = day; + this.Monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); + } else - this.Monitor.Log(" must be between 1 and 28", LogLevel.Error); + this.LogUsageError("That isn't a valid day.", command); } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.LogValueNotSpecified(); + this.Monitor.Log($"The current date is {Game1.currentSeason} {Game1.dayOfMonth}. Specify a value to change the day.", LogLevel.Info); break; case "world_setseason": @@ -451,12 +487,15 @@ namespace TrainerMod string season = args[0]; string[] validSeasons = { "winter", "spring", "summer", "fall" }; if (validSeasons.Contains(season)) + { Game1.currentSeason = season; + this.Monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); + } else - this.LogValueInvalid(); + this.LogUsageError("That isn't a valid season name.", command); } else - this.LogValueNotSpecified(); + this.Monitor.Log($"The current season is {Game1.currentSeason}. Specify a value to change it.", LogLevel.Info); break; case "player_sethealth": @@ -465,18 +504,24 @@ namespace TrainerMod string amountStr = args[0]; if (amountStr == "inf") + { this.InfiniteHealth = true; + this.Monitor.Log("OK, you now have infinite health.", LogLevel.Info); + } else { this.InfiniteHealth = false; if (amountStr.IsInt()) + { Game1.player.health = amountStr.ToInt(); + this.Monitor.Log($"OK, you now have {Game1.player.health} health.", LogLevel.Info); + } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } } else - this.Monitor.Log($"Health is: {Game1.player.health}", LogLevel.Info); + this.Monitor.Log($"You currently have {(this.InfiniteHealth ? "infinite" : Game1.player.health.ToString())} health. Specify a value to change it.", LogLevel.Info); break; case "player_setmaxhealth": @@ -484,12 +529,15 @@ namespace TrainerMod { string amountStr = args[0]; if (amountStr.IsInt()) + { Game1.player.maxHealth = amountStr.ToInt(); + this.Monitor.Log($"OK, you now have {Game1.player.maxHealth} max health.", LogLevel.Info); + } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.Monitor.Log($"MaxHealth is: {Game1.player.maxHealth}", LogLevel.Info); + this.Monitor.Log($"You currently have {Game1.player.maxHealth} max health. Specify a value to change it.", LogLevel.Info); break; case "player_setimmunity": @@ -497,12 +545,15 @@ namespace TrainerMod { string amountStr = args[0]; if (amountStr.IsInt()) + { Game1.player.immunity = amountStr.ToInt(); + this.Monitor.Log($"OK, you now have {Game1.player.immunity} immunity.", LogLevel.Info); + } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.Monitor.Log($"Immunity is: {Game1.player.immunity}", LogLevel.Info); + this.Monitor.Log($"You currently have {Game1.player.immunity} immunity. Specify a value to change it.", LogLevel.Info); break; case "player_additem": @@ -520,7 +571,7 @@ namespace TrainerMod count = args[1].ToInt(); else { - this.Monitor.Log("[count] is invalid", LogLevel.Error); + this.LogUsageError("The optional count is invalid.", command); return; } @@ -530,21 +581,21 @@ namespace TrainerMod quality = args[2].ToInt(); else { - this.Monitor.Log("[quality] is invalid", LogLevel.Error); + this.LogUsageError("The optional quality is invalid.", command); return; } } } var item = new Object(itemID, count) { quality = quality }; - Game1.player.addItemByMenuIfNecessary(item); + this.Monitor.Log($"OK, added {item.Name} to your inventory.", LogLevel.Info); } else - this.Monitor.Log(" is invalid", LogLevel.Error); + this.LogUsageError("The item ID must be an integer.", command); } else - this.LogObjectValueNotSpecified(); + this.LogArgumentsInvalid(command); break; case "player_addmelee": @@ -554,13 +605,13 @@ namespace TrainerMod { MeleeWeapon weapon = new MeleeWeapon(args[0].ToInt()); Game1.player.addItemByMenuIfNecessary(weapon); - this.Monitor.Log($"Gave {weapon.Name} to {Game1.player.Name}", LogLevel.Info); + this.Monitor.Log($"OK, added {weapon.Name} to your inventory.", LogLevel.Info); } else - this.Monitor.Log(" is invalid", LogLevel.Error); + this.LogUsageError("The weapon ID must be an integer.", command); } else - this.LogObjectValueNotSpecified(); + this.LogArgumentsInvalid(command); break; case "player_addring": @@ -570,13 +621,13 @@ namespace TrainerMod { Ring ring = new Ring(args[0].ToInt()); Game1.player.addItemByMenuIfNecessary(ring); - this.Monitor.Log($"Gave {ring.Name} to {Game1.player.Name}", LogLevel.Info); + this.Monitor.Log($"OK, added {ring.Name} to your inventory.", LogLevel.Info); } else this.Monitor.Log(" is invalid", LogLevel.Error); } else - this.LogObjectValueNotSpecified(); + this.LogArgumentsInvalid(command); break; case "list_items": @@ -594,30 +645,37 @@ namespace TrainerMod case "world_downminelevel": Game1.nextMineLevel(); + this.Monitor.Log("OK, warping you to the next mine level.", LogLevel.Info); break; case "world_setminelevel": if (args.Any()) { if (args[0].IsInt()) - Game1.enterMine(true, args[0].ToInt(), ""); + { + int level = args[0].ToInt(); + Game1.enterMine(true, level, ""); + this.Monitor.Log($"OK, warping you to mine level {level}.", LogLevel.Info); + } else - this.LogValueNotInt32(); + this.LogArgumentNotInt(command); } else - this.LogValueNotSpecified(); + this.LogArgumentsInvalid(command); break; case "show_game_files": Process.Start(Constants.ExecutionPath); + this.Monitor.Log($"OK, opening {Constants.ExecutionPath}.", LogLevel.Info); break; case "show_data_files": Process.Start(Constants.DataPath); + this.Monitor.Log($"OK, opening {Constants.DataPath}.", LogLevel.Info); break; default: - throw new NotImplementedException($"TrainerMod received unknown command '{name}'."); + throw new NotImplementedException($"TrainerMod received unknown command '{command}'."); } } @@ -706,34 +764,26 @@ namespace TrainerMod /**** ** Logging ****/ - /// Log an error indicating a value must be specified. - public void LogValueNotSpecified() + /// Log an error indicating incorrect usage. + /// A sentence explaining the problem. + /// The name of the command. + private void LogUsageError(string error, string command) { - this.Monitor.Log(" must be specified", LogLevel.Error); + this.Monitor.Log($"{error} Type 'help {command}' for usage.", LogLevel.Error); } - /// Log an error indicating a target and value must be specified. - public void LogObjectValueNotSpecified() + /// Log an error indicating a value must be an integer. + /// The name of the command. + private void LogArgumentNotInt(string command) { - this.Monitor.Log(" and must be specified", LogLevel.Error); + this.LogUsageError("The value must be a whole number.", command); } /// Log an error indicating a value is invalid. - public void LogValueInvalid() - { - this.Monitor.Log(" is invalid", LogLevel.Error); - } - - /// Log an error indicating a target is invalid. - public void LogObjectInvalid() - { - this.Monitor.Log(" is invalid", LogLevel.Error); - } - - /// Log an error indicating a value must be an integer. - public void LogValueNotInt32() + /// The name of the command. + private void LogArgumentsInvalid(string command) { - this.Monitor.Log(" must be a whole number (Int32)", LogLevel.Error); + this.LogUsageError("The arguments are invalid.", command); } } } -- cgit From 696bdab3cd3352b439207076d604f21bbcf1d922 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Mar 2017 19:18:21 -0500 Subject: fix errors in console command handlers crashing the game --- release-notes.md | 1 + src/StardewModdingAPI/Program.cs | 11 +++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index 81d770f0..797da19d 100644 --- a/release-notes.md +++ b/release-notes.md @@ -15,6 +15,7 @@ For players: * Fixed installer not ignoring potential game folders that don't contain a Stardew Valley exe. * Fixed installer not recognising Linux/Mac paths starting with `~/` or containing an escaped space. * Fixed rare issue where mod dependencies would override SMAPI dependencies and cause unpredictable bugs. +* Fixed errors in console command handlers causing the game to crash. For mod developers: * Added `SaveEvents.AfterReturnToTitle` and `TimeEvents.AfterDayStarted` events. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 07857512..ebeefe96 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -535,8 +535,15 @@ namespace StardewModdingAPI while (true) { string input = Console.ReadLine(); - if (!string.IsNullOrWhiteSpace(input) && !this.CommandManager.Trigger(input)) - this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + try + { + if (!string.IsNullOrWhiteSpace(input) && !this.CommandManager.Trigger(input)) + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + } + catch (Exception ex) + { + this.Monitor.Log($"The handler registered for that command failed:\n{ex.GetLogSummary()}", LogLevel.Error); + } } } -- cgit From 5cdf75b463cdae632ee441da312b763c899e9e72 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Mar 2017 19:32:14 -0500 Subject: show OS caption (like "Windows 10") instead of internal version when available --- release-notes.md | 3 ++- src/StardewModdingAPI/Program.cs | 19 ++++++++++++++++++- src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 21 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index 3dfa42ad..029246b0 100644 --- a/release-notes.md +++ b/release-notes.md @@ -23,7 +23,8 @@ For mod developers: * Added a simpler API for console commands (see `helper.ConsoleCommands`). * Added `GetPrivateProperty` reflection helper. * SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings, so mods no longer need to add a `StringEnumConverter` themselves for those. -* Log files now always use `\r\n` to simplify crossplatform viewing. +* Logs now show the OS caption (like "Windows 10") instead of its internal version when available. +* Logs now always use `\r\n` to simplify crossplatform viewing. * Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)), and all _notice_-level deprecations have been increased to _info_. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ebeefe96..360f75f0 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Management; using System.Reflection; using System.Threading; #if SMAPI_FOR_WINDOWS @@ -87,7 +88,7 @@ namespace StardewModdingAPI { // initialise logging Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {Environment.OSVersion}", LogLevel.Info); + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; // inject compatibility shims @@ -582,5 +583,21 @@ namespace StardewModdingAPI { return new Monitor(name, this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = this.Monitor.WriteToConsole, ShowTraceInConsole = this.Settings.DeveloperMode }; } + + /// Get a human-readable name for the current platform. + private string GetFriendlyPlatformName() + { + try + { + return ( + from entry in new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem").Get().Cast() + select entry.GetPropertyValue("Caption").ToString() + ).FirstOrDefault(); + } + catch + { + return Environment.OSVersion.ToString(); + } + } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 9b86adac..2340a431 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -97,6 +97,7 @@ + True -- cgit From ade1a692a38efe12cdb1889883ec6b1179a9df6b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Mar 2017 20:20:58 -0500 Subject: deprecate `IConfigFile` (#238) --- release-notes.md | 3 ++- src/StardewModdingAPI/Advanced/ConfigFile.cs | 4 +++- src/StardewModdingAPI/Advanced/IConfigFile.cs | 5 ++++- src/StardewModdingAPI/Framework/ModHelper.cs | 15 ++++++++++++++- src/StardewModdingAPI/Program.cs | 1 + 5 files changed, 24 insertions(+), 4 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index 7a936694..4f62182e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -23,11 +23,12 @@ For mod developers: * Added `SaveEvents.AfterReturnToTitle` and `TimeEvents.AfterDayStarted` events. * Added a simpler API for console commands (see `helper.ConsoleCommands`). * Added `GetPrivateProperty` reflection helper. -* SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings, so mods no longer need to add a `StringEnumConverter` themselves for those. +* SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings automatically, so mods no longer need to add a `StringEnumConverter` themselves for those. * Logs now show the OS caption (like "Windows 10") instead of its internal version when available. * Logs now always use `\r\n` to simplify crossplatform viewing. * Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)), and all _notice_-level deprecations have been increased to _info_. +* Deprecated the experimental `IConfigFile`. For SMAPI developers: * Added support for debugging with Visual Studio for Mac. diff --git a/src/StardewModdingAPI/Advanced/ConfigFile.cs b/src/StardewModdingAPI/Advanced/ConfigFile.cs index 1a2e6618..78cad26a 100644 --- a/src/StardewModdingAPI/Advanced/ConfigFile.cs +++ b/src/StardewModdingAPI/Advanced/ConfigFile.cs @@ -1,9 +1,11 @@ -using System.IO; +using System; +using System.IO; using Newtonsoft.Json; namespace StardewModdingAPI.Advanced { /// Wraps a configuration file with IO methods for convenience. + [Obsolete] public abstract class ConfigFile : IConfigFile { /********* diff --git a/src/StardewModdingAPI/Advanced/IConfigFile.cs b/src/StardewModdingAPI/Advanced/IConfigFile.cs index 5bc31a88..1b424ace 100644 --- a/src/StardewModdingAPI/Advanced/IConfigFile.cs +++ b/src/StardewModdingAPI/Advanced/IConfigFile.cs @@ -1,6 +1,9 @@ -namespace StardewModdingAPI.Advanced +using System; + +namespace StardewModdingAPI.Advanced { /// Wraps a configuration file with IO methods for convenience. + [Obsolete] public interface IConfigFile { /********* diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 04767de5..0d50201b 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using StardewModdingAPI.Advanced; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -14,6 +15,9 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; + /// Manages deprecation warnings. + private static DeprecationManager DeprecationManager; + /********* ** Accessors @@ -61,6 +65,13 @@ namespace StardewModdingAPI.Framework this.ConsoleCommands = new CommandHelper(modName, commandManager); } + /// Injects types required for backwards compatibility. + /// Manages deprecation warnings. + internal static void Shim(DeprecationManager deprecationManager) + { + ModHelper.DeprecationManager = deprecationManager; + } + /**** ** Mod config file ****/ @@ -69,7 +80,9 @@ namespace StardewModdingAPI.Framework public TConfig ReadConfig() where TConfig : class, new() { - var config = this.ReadJsonFile("config.json") ?? new TConfig(); + TConfig config = this.ReadJsonFile("config.json") ?? new TConfig(); + if (config is IConfigFile) + ModHelper.DeprecationManager.Warn($"{nameof(IConfigFile)}", "1.9", DeprecationLevel.Info); this.WriteConfig(config); // create file or fill in missing fields return config; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 360f75f0..6ebeccd2 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -98,6 +98,7 @@ namespace StardewModdingAPI InternalExtensions.Shim(this.ModRegistry); Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); Mod.Shim(this.DeprecationManager); + ModHelper.Shim(this.DeprecationManager); ContentEvents.Shim(this.ModRegistry, this.Monitor); PlayerEvents.Shim(this.DeprecationManager); TimeEvents.Shim(this.DeprecationManager); -- cgit From 6f07801b047a4574445266de582b23cef815fe0e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 2 Mar 2017 22:03:23 -0500 Subject: only use WMI on Windows --- src/StardewModdingAPI/Program.cs | 20 +++++++++++--------- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 +- 2 files changed, 12 insertions(+), 10 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 6ebeccd2..3e428eb0 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -4,10 +4,10 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; -using System.Management; using System.Reflection; using System.Threading; #if SMAPI_FOR_WINDOWS +using System.Management; using System.Windows.Forms; #endif using Microsoft.Xna.Framework.Graphics; @@ -586,19 +586,21 @@ namespace StardewModdingAPI } /// Get a human-readable name for the current platform. + [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] private string GetFriendlyPlatformName() { +#if SMAPI_FOR_WINDOWS try { - return ( - from entry in new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem").Get().Cast() - select entry.GetPropertyValue("Caption").ToString() - ).FirstOrDefault(); - } - catch - { - return Environment.OSVersion.ToString(); + return new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") + .Get() + .Cast() + .Select(entry => entry.GetPropertyValue("Caption").ToString()) + .FirstOrDefault(); } + catch { } +#endif + return Environment.OSVersion.ToString(); } } } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 2340a431..4d782f56 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -97,7 +97,7 @@ - + True -- cgit From dfaed472b0e7f1e35ad07eea427fe5034b40ddc7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 4 Mar 2017 14:43:23 -0500 Subject: fix game window no longer showing SMAPI version --- src/StardewModdingAPI/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 3e428eb0..95161538 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -234,7 +234,7 @@ namespace StardewModdingAPI this.GameInstance = new SGame(this.Monitor); this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion}"; + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} with SMAPI {Constants.ApiVersion}"; gameProgramType.GetField("gamePtr").SetValue(gameProgramType, this.GameInstance); // configure -- cgit From edbbb7cff41ba0656bd17774a3d5d99dd321dd9b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 8 Mar 2017 15:51:00 -0500 Subject: update old instructions about resetting config file --- src/StardewModdingAPI/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 95161538..bf3c86fb 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -116,10 +116,10 @@ namespace StardewModdingAPI if (this.Settings.DeveloperMode) { this.Monitor.ShowTraceInConsole = true; - this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); + this.Monitor.Log($"You configured SMAPI to run in developer mode. The console may be much more verbose. You can disable developer mode by installing the non-developer version of SMAPI, or by editing {Constants.ApiConfigPath}.", LogLevel.Warn); } if (!this.Settings.CheckForUpdates) - this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by editing or deleting {Constants.ApiConfigPath}.", LogLevel.Warn); + this.Monitor.Log($"You configured SMAPI to not check for updates. Running an old version of SMAPI is not recommended. You can enable update checks by reinstalling SMAPI or editing {Constants.ApiConfigPath}.", LogLevel.Warn); if (!this.Monitor.WriteToConsole) this.Monitor.Log("Writing to the terminal is disabled because the --no-terminal argument was received. This usually means launching the terminal failed.", LogLevel.Warn); -- cgit From 6d2d90b7681e4e274e92742e98905ec4000486ca Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Mar 2017 01:31:15 -0500 Subject: add logic to detect incompatible mod instructions & reject mod load (#247) --- src/StardewModdingAPI/Constants.cs | 8 +++++++ src/StardewModdingAPI/Framework/AssemblyLoader.cs | 18 +++++++++++++++ .../Framework/IncompatibleInstructionException.cs | 27 ++++++++++++++++++++++ src/StardewModdingAPI/Program.cs | 5 ++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 5 files changed, 59 insertions(+) create mode 100644 src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 66bf5842..d8149766 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -135,6 +135,14 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } + /// Get finders which match incompatible CIL instructions in mod assemblies. + internal static IEnumerable GetIncompatibilityFinders() + { + return new IInstructionFinder[] + { + }; + } + /// Get rewriters which fix incompatible CIL instructions in mod assemblies. internal static IEnumerable GetRewriters() { diff --git a/src/StardewModdingAPI/Framework/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/AssemblyLoader.cs index 8af67772..dfe0d03f 100644 --- a/src/StardewModdingAPI/Framework/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/AssemblyLoader.cs @@ -55,6 +55,7 @@ namespace StardewModdingAPI.Framework /// Preprocess and load an assembly. /// The assembly file path. /// Returns the rewrite metadata for the preprocessed assembly. + /// An incompatible CIL instruction was found while rewriting the assembly. public Assembly Load(string assemblyPath) { // get referenced local assemblies @@ -159,6 +160,7 @@ namespace StardewModdingAPI.Framework /// Rewrite the types referenced by an assembly. /// The assembly to rewrite. /// Returns whether the assembly was modified. + /// An incompatible CIL instruction was found while rewriting the assembly. private bool RewriteAssembly(AssemblyDefinition assembly) { ModuleDefinition module = assembly.MainModule; @@ -189,6 +191,22 @@ namespace StardewModdingAPI.Framework this.ChangeTypeScope(type); } + // throw exception if assembly contains incompatible instructions can't be rewritten + { + IInstructionFinder[] finders = Constants.GetIncompatibilityFinders().ToArray(); + foreach (MethodDefinition method in this.GetMethods(module)) + { + foreach (Instruction instruction in method.Body.Instructions) + { + foreach (IInstructionFinder finder in finders) + { + if (finder.IsMatch(instruction, platformChanged)) + throw new IncompatibleInstructionException(finder.NounPhrase, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}."); + } + } + } + } + // rewrite incompatible instructions bool anyRewritten = false; IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); diff --git a/src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs b/src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs new file mode 100644 index 00000000..affe2cb3 --- /dev/null +++ b/src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs @@ -0,0 +1,27 @@ +using System; + +namespace StardewModdingAPI.Framework +{ + /// An exception raised when an incompatible instruction is found while loading a mod assembly. + internal class IncompatibleInstructionException : Exception + { + /********* + ** Accessors + *********/ + /// A brief noun phrase which describes the incompatible instruction that was found. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A brief noun phrase which describes the incompatible instruction that was found. + /// A message which describes the error. + public IncompatibleInstructionException(string nounPhrase, string message) + : base(message) + { + this.NounPhrase = nounPhrase; + } + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index bf3c86fb..cb8cc2e5 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -453,6 +453,11 @@ namespace StardewModdingAPI { modAssembly = modAssemblyLoader.Load(assemblyPath); } + catch (IncompatibleInstructionException ex) + { + this.Monitor.Log($"{skippedPrefix} because it's not compatible with the latest version of the game (detected {ex.NounPhrase}). Please check for a newer version of the mod (you have v{manifest.Version}).", LogLevel.Error); + continue; + } catch (Exception ex) { this.Monitor.Log($"{skippedPrefix} because its DLL '{manifest.EntryDll}' couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index ab808948..92726ca0 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -154,6 +154,7 @@ + -- cgit From 003a9586b287f3bd831f9dfe7ca52b9e4e03c028 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Mar 2017 16:56:16 -0400 Subject: simplify access to game's Program class Stardew Valley 1.2.15 made the class public, so we no longer need reflection to access it. --- src/StardewModdingAPI/Program.cs | 43 ++++++++++++---------------------------- 1 file changed, 13 insertions(+), 30 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index cb8cc2e5..f0cc00c7 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -12,7 +12,6 @@ using System.Windows.Forms; #endif using Microsoft.Xna.Framework.Graphics; using Newtonsoft.Json; -using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; @@ -142,21 +141,12 @@ namespace StardewModdingAPI this.VerifyPath(Constants.ModPath); this.VerifyPath(Constants.LogDir); - // get executable path - string executablePath = Path.Combine(Constants.ExecutionPath, Constants.TargetPlatform == Platform.Windows ? "Stardew Valley.exe" : "StardewValley.exe"); - if (!File.Exists(executablePath)) - { - this.Monitor.Log($"Couldn't find executable: {executablePath}", LogLevel.Error); - this.PressAnyKeyToExit(); - return; - } - // check for update when game loads if (this.Settings.CheckForUpdates) GameEvents.GameLoaded += (sender, e) => this.CheckForUpdateAsync(); // launch game - this.StartGame(executablePath); + this.StartGame(); } catch (Exception ex) { @@ -211,11 +201,12 @@ namespace StardewModdingAPI } /// Hook into Stardew Valley and launch the game. - /// The absolute path to the executable to launch. - private void StartGame(string executablePath) + private void StartGame() { try { + this.Monitor.Log("Loading game..."); + // add error handlers #if SMAPI_FOR_WINDOWS Application.ThreadException += (sender, e) => this.Monitor.Log($"Critical thread exception: {e.Exception.GetLogSummary()}", LogLevel.Error); @@ -223,23 +214,15 @@ namespace StardewModdingAPI #endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - // initialise game - { - // load assembly - this.Monitor.Log("Loading game..."); - Assembly gameAssembly = Assembly.UnsafeLoadFrom(executablePath); - Type gameProgramType = gameAssembly.GetType("StardewValley.Program", true); - - // set Game1 instance - this.GameInstance = new SGame(this.Monitor); - this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; - this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); - this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} with SMAPI {Constants.ApiVersion}"; - gameProgramType.GetField("gamePtr").SetValue(gameProgramType, this.GameInstance); - - // configure - Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; - } + // override Game1 instance + this.GameInstance = new SGame(this.Monitor); + this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; + this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); + this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} with SMAPI {Constants.ApiVersion}"; + StardewValley.Program.gamePtr = this.GameInstance; + + // configure + Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; // load mods this.LoadMods(); -- cgit From 183fb9ff6e66e519ee9c0e3a3357504e8caf662a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Mar 2017 20:12:47 -0400 Subject: remove unused IConfigFile (#238) --- src/StardewModdingAPI/Advanced/ConfigFile.cs | 37 ---------------------- src/StardewModdingAPI/Advanced/IConfigFile.cs | 28 ---------------- src/StardewModdingAPI/Constants.cs | 2 ++ src/StardewModdingAPI/Framework/ModHelper.cs | 15 +-------- .../Framework/Serialisation/JsonHelper.cs | 14 ++------ src/StardewModdingAPI/Program.cs | 3 +- src/StardewModdingAPI/StardewModdingAPI.csproj | 2 -- 7 files changed, 6 insertions(+), 95 deletions(-) delete mode 100644 src/StardewModdingAPI/Advanced/ConfigFile.cs delete mode 100644 src/StardewModdingAPI/Advanced/IConfigFile.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Advanced/ConfigFile.cs b/src/StardewModdingAPI/Advanced/ConfigFile.cs deleted file mode 100644 index 78cad26a..00000000 --- a/src/StardewModdingAPI/Advanced/ConfigFile.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.IO; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Advanced -{ - /// Wraps a configuration file with IO methods for convenience. - [Obsolete] - public abstract class ConfigFile : IConfigFile - { - /********* - ** Accessors - *********/ - /// Provides simplified APIs for writing mods. - public IModHelper ModHelper { get; set; } - - /// The file path from which the model was loaded, relative to the mod directory. - public string FilePath { get; set; } - - - /********* - ** Public methods - *********/ - /// Reparse the underlying file and update this model. - public void Reload() - { - string json = File.ReadAllText(Path.Combine(this.ModHelper.DirectoryPath, this.FilePath)); - JsonConvert.PopulateObject(json, this); - } - - /// Save this model to the underlying file. - public void Save() - { - this.ModHelper.WriteJsonFile(this.FilePath, this); - } - } -} \ No newline at end of file diff --git a/src/StardewModdingAPI/Advanced/IConfigFile.cs b/src/StardewModdingAPI/Advanced/IConfigFile.cs deleted file mode 100644 index 1b424ace..00000000 --- a/src/StardewModdingAPI/Advanced/IConfigFile.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace StardewModdingAPI.Advanced -{ - /// Wraps a configuration file with IO methods for convenience. - [Obsolete] - public interface IConfigFile - { - /********* - ** Accessors - *********/ - /// Provides simplified APIs for writing mods. - IModHelper ModHelper { get; set; } - - /// The file path from which the model was loaded, relative to the mod directory. - string FilePath { get; set; } - - - /********* - ** Methods - *********/ - /// Reparse the underlying file and update this model. - void Reload(); - - /// Save this model to the underlying file. - void Save(); - } -} diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 8949bc55..ae2fbe87 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -147,6 +147,8 @@ namespace StardewModdingAPI new GenericFieldFinder("StardewValley.Item", "set_Name", isStatic: false), // APIs removed in SMAPI 1.9 + new GenericTypeFinder("StardewModdingAPI.Advanced.ConfigFile"), + new GenericTypeFinder("StardewModdingAPI.Advanced.IConfigFile"), new GenericTypeFinder("StardewModdingAPI.Entities.SPlayer"), new GenericTypeFinder("StardewModdingAPI.Extensions"), new GenericTypeFinder("StardewModdingAPI.Inheritance.ItemStackChange"), diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 0d50201b..c8c44dba 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using StardewModdingAPI.Advanced; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialisation; @@ -15,9 +14,6 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; - /// Manages deprecation warnings. - private static DeprecationManager DeprecationManager; - /********* ** Accessors @@ -65,13 +61,6 @@ namespace StardewModdingAPI.Framework this.ConsoleCommands = new CommandHelper(modName, commandManager); } - /// Injects types required for backwards compatibility. - /// Manages deprecation warnings. - internal static void Shim(DeprecationManager deprecationManager) - { - ModHelper.DeprecationManager = deprecationManager; - } - /**** ** Mod config file ****/ @@ -81,8 +70,6 @@ namespace StardewModdingAPI.Framework where TConfig : class, new() { TConfig config = this.ReadJsonFile("config.json") ?? new TConfig(); - if (config is IConfigFile) - ModHelper.DeprecationManager.Warn($"{nameof(IConfigFile)}", "1.9", DeprecationLevel.Info); this.WriteConfig(config); // create file or fill in missing fields return config; } @@ -107,7 +94,7 @@ namespace StardewModdingAPI.Framework where TModel : class { path = Path.Combine(this.DirectoryPath, path); - return this.JsonHelper.ReadJsonFile(path, this); + return this.JsonHelper.ReadJsonFile(path); } /// Save to a JSON file. diff --git a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs index d5f5bfd0..bd15c7bb 100644 --- a/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs +++ b/src/StardewModdingAPI/Framework/Serialisation/JsonHelper.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using Microsoft.Xna.Framework.Input; using Newtonsoft.Json; -using StardewModdingAPI.Advanced; namespace StardewModdingAPI.Framework.Serialisation { @@ -31,9 +30,8 @@ namespace StardewModdingAPI.Framework.Serialisation /// Read a JSON file. /// The model type. /// The absolete file path. - /// The mod helper to inject for instances. /// Returns the deserialised model, or null if the file doesn't exist or is empty. - public TModel ReadJsonFile(string fullPath, IModHelper modHelper) + public TModel ReadJsonFile(string fullPath) where TModel : class { // read file @@ -48,15 +46,7 @@ namespace StardewModdingAPI.Framework.Serialisation } // deserialise model - TModel model = JsonConvert.DeserializeObject(json, this.JsonSettings); - if (model is IConfigFile) - { - var wrapper = (IConfigFile)model; - wrapper.ModHelper = modHelper; - wrapper.FilePath = fullPath; - } - - return model; + return JsonConvert.DeserializeObject(json, this.JsonSettings); } /// Save to a JSON file. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index f0cc00c7..db7a3df6 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -97,7 +97,6 @@ namespace StardewModdingAPI InternalExtensions.Shim(this.ModRegistry); Log.Shim(this.DeprecationManager, this.GetSecondaryMonitor("legacy mod"), this.ModRegistry); Mod.Shim(this.DeprecationManager); - ModHelper.Shim(this.DeprecationManager); ContentEvents.Shim(this.ModRegistry, this.Monitor); PlayerEvents.Shim(this.DeprecationManager); TimeEvents.Shim(this.DeprecationManager); @@ -344,7 +343,7 @@ namespace StardewModdingAPI } // deserialise manifest - manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json"), null); + manifest = jsonHelper.ReadJsonFile(Path.Combine(directory.FullName, "manifest.json")); if (manifest == null) { this.Monitor.Log($"{skippedPrefix} because its manifest is invalid.", LogLevel.Error); diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 92726ca0..dceae74e 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -115,8 +115,6 @@ Properties\GlobalAssemblyInfo.cs - - -- cgit From da630efc1d5c95816493c2969736ae883e723757 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 14 Mar 2017 14:15:50 -0400 Subject: downgrade to .NET Framework 4.0 for better compatibility on Windows 7–8.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- release-notes.md | 1 + .../PlatformAssemblyMap.cs | 2 +- .../StardewModdingAPI.AssemblyRewriters.csproj | 14 +++++------ .../packages.config | 2 +- .../StardewModdingAPI.Installer.csproj | 2 +- src/StardewModdingAPI/App.config | 2 +- .../Events/ContentEventHandler.cs | 8 ++++++ src/StardewModdingAPI/Events/ContentEvents.cs | 2 +- .../Framework/InternalExtensions.cs | 29 ++++++++++++++++++++-- .../Framework/Reflection/PrivateProperty.cs | 4 +-- src/StardewModdingAPI/Framework/UpdateHelper.cs | 7 +++--- src/StardewModdingAPI/Program.cs | 24 ++++++------------ src/StardewModdingAPI/StardewModdingAPI.csproj | 20 ++++++++------- src/StardewModdingAPI/packages.config | 4 +-- src/TrainerMod/TrainerMod.csproj | 4 +-- src/TrainerMod/packages.config | 2 +- 16 files changed, 77 insertions(+), 50 deletions(-) create mode 100644 src/StardewModdingAPI/Events/ContentEventHandler.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index f91ef733..01d49dd7 100644 --- a/release-notes.md +++ b/release-notes.md @@ -20,6 +20,7 @@ For players: * Simplified error messages when a mod can't be loaded. * Simple nested mod folders are now recognised by SMAPI (e.g. `ModName-1.0\ModName\manifest.json`). * Improved TrainerMod command handling & feedback. +* Reduced minimum .NET Framework version to 4.0 for improved compatibility on Windows 7–8.1. * Fixed game's debug output being shown in the console for all users. * Fixed the game-outdated error not pausing before exit. * Fixed installer errors for some players when deleting files. diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs index fce2b187..3ca90149 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.AssemblyRewriters // cache assembly metadata this.Targets = targetAssemblies; this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); - this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); + this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.GetModules().Single().FullyQualifiedName)); } } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index a3322e67..15bb15ac 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -9,7 +9,7 @@ Properties StardewModdingAPI.AssemblyRewriters StardewModdingAPI.AssemblyRewriters - v4.5 + v4.0 512 @@ -49,16 +49,16 @@ - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll - True + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.dll - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll - True + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Mdb.dll - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll - True + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Pdb.dll + + + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Rocks.dll diff --git a/src/StardewModdingAPI.AssemblyRewriters/packages.config b/src/StardewModdingAPI.AssemblyRewriters/packages.config index 88fbc79d..70ba1aed 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/packages.config +++ b/src/StardewModdingAPI.AssemblyRewriters/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj index e31a1452..366e1c6e 100644 --- a/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj +++ b/src/StardewModdingAPI.Installer/StardewModdingAPI.Installer.csproj @@ -9,7 +9,7 @@ Properties StardewModdingAPI.Installer StardewModdingAPI.Installer - v4.5 + v4.0 512 true diff --git a/src/StardewModdingAPI/App.config b/src/StardewModdingAPI/App.config index 27cdf0f7..314845f7 100644 --- a/src/StardewModdingAPI/App.config +++ b/src/StardewModdingAPI/App.config @@ -1,7 +1,7 @@ - + diff --git a/src/StardewModdingAPI/Events/ContentEventHandler.cs b/src/StardewModdingAPI/Events/ContentEventHandler.cs new file mode 100644 index 00000000..2a7e75d1 --- /dev/null +++ b/src/StardewModdingAPI/Events/ContentEventHandler.cs @@ -0,0 +1,8 @@ +namespace StardewModdingAPI.Events +{ + /// Represents a method that will handle a content event. + /// The source of the event. + /// The event arguments. + /// This deviates from in allowing T to be an interface instead of a concrete class. While .NET Framework 4.5 allows that, the current .NET Framework 4.0 targeted by SMAPI to improve compatibility does not. + public delegate void ContentEventHandler(object sender, IContentEventHelper e); +} diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs index 9418673a..339e90fd 100644 --- a/src/StardewModdingAPI/Events/ContentEvents.cs +++ b/src/StardewModdingAPI/Events/ContentEvents.cs @@ -28,7 +28,7 @@ namespace StardewModdingAPI.Events public static event EventHandler> AfterLocaleChanged; /// Raised when an XNB file is being read into the cache. Mods can change the data here before it's cached. - internal static event EventHandler AfterAssetLoaded; + internal static event ContentEventHandler AfterAssetLoaded; /********* diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index 4ca79518..46c76656 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework { @@ -59,12 +60,36 @@ namespace StardewModdingAPI.Framework /// The event handlers. /// The event sender. /// The event arguments. - public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) + public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) where TEventArgs : EventArgs { if (handlers == null) return; - foreach (EventHandler handler in Enumerable.Cast>(handlers)) + foreach (EventHandler handler in handlers.Cast>()) + { + try + { + handler.Invoke(sender, args); + } + catch (Exception ex) + { + monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); + } + } + } + + /// Safely raise an event, and intercept any exceptions thrown by its handlers. + /// Encapsulates monitoring and logging. + /// The event name for error messages. + /// The event handlers. + /// The event sender. + /// The event arguments. + public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, IContentEventHelper args) + { + if (handlers == null) + return; + + foreach (ContentEventHandler handler in handlers.Cast()) { try { diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs index 08204b7e..d89e8e44 100644 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs @@ -60,7 +60,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - return (TValue)this.PropertyInfo.GetValue(this.Parent); + return (TValue)this.PropertyInfo.GetValue(this.Parent, null); } catch (InvalidCastException) { @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - this.PropertyInfo.SetValue(this.Parent, value); + this.PropertyInfo.SetValue(this.Parent, value, null); } catch (InvalidCastException) { diff --git a/src/StardewModdingAPI/Framework/UpdateHelper.cs b/src/StardewModdingAPI/Framework/UpdateHelper.cs index e01e55c8..342a08cf 100644 --- a/src/StardewModdingAPI/Framework/UpdateHelper.cs +++ b/src/StardewModdingAPI/Framework/UpdateHelper.cs @@ -1,7 +1,6 @@ using System.IO; using System.Net; using System.Reflection; -using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Framework.Models; @@ -15,17 +14,17 @@ namespace StardewModdingAPI.Framework *********/ /// Get the latest release from a GitHub repository. /// The name of the repository from which to fetch releases (like "cjsu/SMAPI"). - public static async Task GetLatestVersionAsync(string repository) + public static GitRelease GetLatestVersion(string repository) { // build request // (avoid HttpClient for Mac compatibility) - HttpWebRequest request = WebRequest.CreateHttp($"https://api.github.com/repos/{repository}/releases/latest"); + HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"https://api.github.com/repos/{repository}/releases/latest"); AssemblyName assembly = typeof(UpdateHelper).Assembly.GetName(); request.UserAgent = $"{assembly.Name}/{assembly.Version}"; request.Accept = "application/vnd.github.v3+json"; // fetch data - using (WebResponse response = await request.GetResponseAsync()) + using (WebResponse response = request.GetResponse()) using (Stream responseStream = response.GetResponseStream()) using (StreamReader reader = new StreamReader(responseStream)) { diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index db7a3df6..81e6518e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -187,7 +187,7 @@ namespace StardewModdingAPI { try { - GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; + GitRelease release = UpdateHelper.GetLatestVersion(Constants.GitHubRepository); ISemanticVersion latestVersion = new SemanticVersion(release.Tag); if (latestVersion.IsNewerThan(Constants.ApiVersion)) this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); @@ -446,26 +446,18 @@ namespace StardewModdingAPI continue; } - // validate assembly + // initialise mod try { - if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) + // get mod entry type + Type modEntryType = modAssembly.GetExportedTypes().FirstOrDefault(x => x.BaseType == typeof(Mod)); + if(modEntryType == null) { - this.Monitor.Log($"{skippedPrefix} because its DLL has no 'Mod' subclass.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its DLL has no {typeof(Mod).FullName} entry class.", LogLevel.Error); continue; } - } - catch (Exception ex) - { - this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - - // initialise mod - try - { - // get implementation - TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); + + // get mod class Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index dceae74e..3a2bb756 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -9,7 +9,7 @@ Properties StardewModdingAPI StardewModdingAPI - v4.5 + v4.0 512 false @@ -60,7 +60,7 @@ $(SolutionDir)\..\bin\Debug\SMAPI $(SolutionDir)\..\bin\Debug\SMAPI\StardewModdingAPI.xml true - 6 + 7 x86 @@ -79,19 +79,22 @@ - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.dll True - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Mdb.dll True - ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Pdb.dll True + + ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Rocks.dll + - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.8.0.3\lib\net40\Newtonsoft.Json.dll True @@ -116,6 +119,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -215,12 +219,10 @@ Designer - - Designer - Always + Always diff --git a/src/StardewModdingAPI/packages.config b/src/StardewModdingAPI/packages.config index e5fa3c3a..1dee2c2a 100644 --- a/src/StardewModdingAPI/packages.config +++ b/src/StardewModdingAPI/packages.config @@ -1,5 +1,5 @@  - - + + \ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index a6303767..7845bd8c 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -9,7 +9,7 @@ Properties TrainerMod TrainerMod - v4.5 + v4.0 512 @@ -39,7 +39,7 @@ - ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.8.0.3\lib\net40\Newtonsoft.Json.dll True diff --git a/src/TrainerMod/packages.config b/src/TrainerMod/packages.config index 75e68e71..2c6c3f12 100644 --- a/src/TrainerMod/packages.config +++ b/src/TrainerMod/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file -- cgit From 307304a03edb82f3a1f5cfa6c47cb17687560ccb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 14 Mar 2017 18:16:44 -0400 Subject: revert all projects except installer to .NET Framework 4.5 This caused obscure invalid-IL crashes when compiled through MonoDevelop on Linux. --- release-notes.md | 1 - .../PlatformAssemblyMap.cs | 2 +- .../StardewModdingAPI.AssemblyRewriters.csproj | 14 +++++------ .../packages.config | 2 +- src/StardewModdingAPI/App.config | 2 +- .../Events/ContentEventHandler.cs | 8 ------ src/StardewModdingAPI/Events/ContentEvents.cs | 2 +- .../Framework/InternalExtensions.cs | 29 ++-------------------- .../Framework/Reflection/PrivateProperty.cs | 4 +-- src/StardewModdingAPI/Framework/UpdateHelper.cs | 7 +++--- src/StardewModdingAPI/Program.cs | 24 ++++++++++++------ src/StardewModdingAPI/StardewModdingAPI.csproj | 20 +++++++-------- src/StardewModdingAPI/packages.config | 4 +-- src/TrainerMod/TrainerMod.csproj | 4 +-- src/TrainerMod/packages.config | 2 +- 15 files changed, 49 insertions(+), 76 deletions(-) delete mode 100644 src/StardewModdingAPI/Events/ContentEventHandler.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index 01d49dd7..f91ef733 100644 --- a/release-notes.md +++ b/release-notes.md @@ -20,7 +20,6 @@ For players: * Simplified error messages when a mod can't be loaded. * Simple nested mod folders are now recognised by SMAPI (e.g. `ModName-1.0\ModName\manifest.json`). * Improved TrainerMod command handling & feedback. -* Reduced minimum .NET Framework version to 4.0 for improved compatibility on Windows 7–8.1. * Fixed game's debug output being shown in the console for all users. * Fixed the game-outdated error not pausing before exit. * Fixed installer errors for some players when deleting files. diff --git a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs index 3ca90149..fce2b187 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/PlatformAssemblyMap.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.AssemblyRewriters // cache assembly metadata this.Targets = targetAssemblies; this.TargetReferences = this.Targets.ToDictionary(assembly => assembly, assembly => AssemblyNameReference.Parse(assembly.FullName)); - this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.GetModules().Single().FullyQualifiedName)); + this.TargetModules = this.Targets.ToDictionary(assembly => assembly, assembly => ModuleDefinition.ReadModule(assembly.Modules.Single().FullyQualifiedName)); } } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index 15bb15ac..a3322e67 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -9,7 +9,7 @@ Properties StardewModdingAPI.AssemblyRewriters StardewModdingAPI.AssemblyRewriters - v4.0 + v4.5 512 @@ -49,16 +49,16 @@ - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll + True - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Mdb.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll + True - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Pdb.dll - - - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Rocks.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll + True diff --git a/src/StardewModdingAPI.AssemblyRewriters/packages.config b/src/StardewModdingAPI.AssemblyRewriters/packages.config index 70ba1aed..88fbc79d 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/packages.config +++ b/src/StardewModdingAPI.AssemblyRewriters/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/src/StardewModdingAPI/App.config b/src/StardewModdingAPI/App.config index 314845f7..27cdf0f7 100644 --- a/src/StardewModdingAPI/App.config +++ b/src/StardewModdingAPI/App.config @@ -1,7 +1,7 @@ - + diff --git a/src/StardewModdingAPI/Events/ContentEventHandler.cs b/src/StardewModdingAPI/Events/ContentEventHandler.cs deleted file mode 100644 index 2a7e75d1..00000000 --- a/src/StardewModdingAPI/Events/ContentEventHandler.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace StardewModdingAPI.Events -{ - /// Represents a method that will handle a content event. - /// The source of the event. - /// The event arguments. - /// This deviates from in allowing T to be an interface instead of a concrete class. While .NET Framework 4.5 allows that, the current .NET Framework 4.0 targeted by SMAPI to improve compatibility does not. - public delegate void ContentEventHandler(object sender, IContentEventHelper e); -} diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs index 339e90fd..9418673a 100644 --- a/src/StardewModdingAPI/Events/ContentEvents.cs +++ b/src/StardewModdingAPI/Events/ContentEvents.cs @@ -28,7 +28,7 @@ namespace StardewModdingAPI.Events public static event EventHandler> AfterLocaleChanged; /// Raised when an XNB file is being read into the cache. Mods can change the data here before it's cached. - internal static event ContentEventHandler AfterAssetLoaded; + internal static event EventHandler AfterAssetLoaded; /********* diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index 46c76656..4ca79518 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using StardewModdingAPI.Events; namespace StardewModdingAPI.Framework { @@ -60,36 +59,12 @@ namespace StardewModdingAPI.Framework /// The event handlers. /// The event sender. /// The event arguments. - public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) where TEventArgs : EventArgs + public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, TEventArgs args) { if (handlers == null) return; - foreach (EventHandler handler in handlers.Cast>()) - { - try - { - handler.Invoke(sender, args); - } - catch (Exception ex) - { - monitor.Log($"A mod failed handling the {name} event:\n{ex.GetLogSummary()}", LogLevel.Error); - } - } - } - - /// Safely raise an event, and intercept any exceptions thrown by its handlers. - /// Encapsulates monitoring and logging. - /// The event name for error messages. - /// The event handlers. - /// The event sender. - /// The event arguments. - public static void SafelyRaiseGenericEvent(this IMonitor monitor, string name, IEnumerable handlers, object sender, IContentEventHelper args) - { - if (handlers == null) - return; - - foreach (ContentEventHandler handler in handlers.Cast()) + foreach (EventHandler handler in Enumerable.Cast>(handlers)) { try { diff --git a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs b/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs index d89e8e44..08204b7e 100644 --- a/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs +++ b/src/StardewModdingAPI/Framework/Reflection/PrivateProperty.cs @@ -60,7 +60,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - return (TValue)this.PropertyInfo.GetValue(this.Parent, null); + return (TValue)this.PropertyInfo.GetValue(this.Parent); } catch (InvalidCastException) { @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Framework.Reflection { try { - this.PropertyInfo.SetValue(this.Parent, value, null); + this.PropertyInfo.SetValue(this.Parent, value); } catch (InvalidCastException) { diff --git a/src/StardewModdingAPI/Framework/UpdateHelper.cs b/src/StardewModdingAPI/Framework/UpdateHelper.cs index 342a08cf..e01e55c8 100644 --- a/src/StardewModdingAPI/Framework/UpdateHelper.cs +++ b/src/StardewModdingAPI/Framework/UpdateHelper.cs @@ -1,6 +1,7 @@ using System.IO; using System.Net; using System.Reflection; +using System.Threading.Tasks; using Newtonsoft.Json; using StardewModdingAPI.Framework.Models; @@ -14,17 +15,17 @@ namespace StardewModdingAPI.Framework *********/ /// Get the latest release from a GitHub repository. /// The name of the repository from which to fetch releases (like "cjsu/SMAPI"). - public static GitRelease GetLatestVersion(string repository) + public static async Task GetLatestVersionAsync(string repository) { // build request // (avoid HttpClient for Mac compatibility) - HttpWebRequest request = (HttpWebRequest)WebRequest.Create($"https://api.github.com/repos/{repository}/releases/latest"); + HttpWebRequest request = WebRequest.CreateHttp($"https://api.github.com/repos/{repository}/releases/latest"); AssemblyName assembly = typeof(UpdateHelper).Assembly.GetName(); request.UserAgent = $"{assembly.Name}/{assembly.Version}"; request.Accept = "application/vnd.github.v3+json"; // fetch data - using (WebResponse response = request.GetResponse()) + using (WebResponse response = await request.GetResponseAsync()) using (Stream responseStream = response.GetResponseStream()) using (StreamReader reader = new StreamReader(responseStream)) { diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 81e6518e..db7a3df6 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -187,7 +187,7 @@ namespace StardewModdingAPI { try { - GitRelease release = UpdateHelper.GetLatestVersion(Constants.GitHubRepository); + GitRelease release = UpdateHelper.GetLatestVersionAsync(Constants.GitHubRepository).Result; ISemanticVersion latestVersion = new SemanticVersion(release.Tag); if (latestVersion.IsNewerThan(Constants.ApiVersion)) this.Monitor.Log($"You can update SMAPI from version {Constants.ApiVersion} to {latestVersion}", LogLevel.Alert); @@ -446,18 +446,26 @@ namespace StardewModdingAPI continue; } - // initialise mod + // validate assembly try { - // get mod entry type - Type modEntryType = modAssembly.GetExportedTypes().FirstOrDefault(x => x.BaseType == typeof(Mod)); - if(modEntryType == null) + if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) { - this.Monitor.Log($"{skippedPrefix} because its DLL has no {typeof(Mod).FullName} entry class.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its DLL has no 'Mod' subclass.", LogLevel.Error); continue; } - - // get mod class + } + catch (Exception ex) + { + this.Monitor.Log($"{skippedPrefix} because its DLL couldn't be loaded.\n{ex.GetLogSummary()}", LogLevel.Error); + continue; + } + + // initialise mod + try + { + // get implementation + TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index dcb299a2..99666f08 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -9,7 +9,7 @@ Properties StardewModdingAPI StardewModdingAPI - v4.0 + v4.5 512 false @@ -77,22 +77,19 @@ - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.dll True - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Mdb.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Mdb.dll True - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Pdb.dll + ..\packages\Mono.Cecil.0.9.6.4\lib\net45\Mono.Cecil.Pdb.dll True - - ..\packages\Mono.Cecil.0.9.6.4\lib\net40\Mono.Cecil.Rocks.dll - - ..\packages\Newtonsoft.Json.8.0.3\lib\net40\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll True @@ -117,7 +114,6 @@ Properties\GlobalAssemblyInfo.cs - @@ -217,10 +213,12 @@ Designer + + Designer + Always - Always @@ -282,4 +280,4 @@ - \ No newline at end of file + diff --git a/src/StardewModdingAPI/packages.config b/src/StardewModdingAPI/packages.config index 1dee2c2a..e5fa3c3a 100644 --- a/src/StardewModdingAPI/packages.config +++ b/src/StardewModdingAPI/packages.config @@ -1,5 +1,5 @@  - - + + \ No newline at end of file diff --git a/src/TrainerMod/TrainerMod.csproj b/src/TrainerMod/TrainerMod.csproj index c66f2ab8..0bd667d4 100644 --- a/src/TrainerMod/TrainerMod.csproj +++ b/src/TrainerMod/TrainerMod.csproj @@ -9,7 +9,7 @@ Properties TrainerMod TrainerMod - v4.0 + v4.5 512 @@ -37,7 +37,7 @@ - ..\packages\Newtonsoft.Json.8.0.3\lib\net40\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll True diff --git a/src/TrainerMod/packages.config b/src/TrainerMod/packages.config index 2c6c3f12..75e68e71 100644 --- a/src/TrainerMod/packages.config +++ b/src/TrainerMod/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file -- cgit From 104aa314127bd816d5dbcac8c57ecb84b12f20d1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 14 Mar 2017 20:48:02 -0400 Subject: let players override SMAPI incompatible-code detection if needed --- README.md | 2 +- src/StardewModdingAPI/Framework/AssemblyLoader.cs | 20 +++-- src/StardewModdingAPI/Framework/ModRegistry.cs | 20 ++--- .../Framework/Models/IncompatibleMod.cs | 65 --------------- .../Framework/Models/ModCompatibility.cs | 65 +++++++++++++++ .../Framework/Models/ModCompatibilityType.cs | 12 +++ src/StardewModdingAPI/Framework/Models/SConfig.cs | 4 +- src/StardewModdingAPI/Program.cs | 8 +- .../StardewModdingAPI.config.json | 95 ++++++++++++++-------- src/StardewModdingAPI/StardewModdingAPI.csproj | 3 +- 10 files changed, 171 insertions(+), 123 deletions(-) delete mode 100644 src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs create mode 100644 src/StardewModdingAPI/Framework/Models/ModCompatibility.cs create mode 100644 src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/README.md b/README.md index 87383ffb..c90e62cc 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ field | purpose ----- | ------- `DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers. Currently this only makes `TRACE`-level messages appear in the console. `CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`IncompatibleMods` | A list of mod versions SMAPI considers incompatible and will refuse to load. Changing this field is not recommended. +`ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. Changing this field is not recommended and may destabilise your game. ### Command-line arguments SMAPI recognises the following command-line arguments. These are intended for internal use and may diff --git a/src/StardewModdingAPI/Framework/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/AssemblyLoader.cs index bc56de01..c7ad3da4 100644 --- a/src/StardewModdingAPI/Framework/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/AssemblyLoader.cs @@ -54,9 +54,10 @@ namespace StardewModdingAPI.Framework /// Preprocess and load an assembly. /// The assembly file path. + /// Assume the mod is compatible, even if incompatible code is detected. /// Returns the rewrite metadata for the preprocessed assembly. /// An incompatible CIL instruction was found while rewriting the assembly. - public Assembly Load(string assemblyPath) + public Assembly Load(string assemblyPath, bool assumeCompatible) { // get referenced local assemblies AssemblyParseResult[] assemblies; @@ -73,7 +74,7 @@ namespace StardewModdingAPI.Framework Assembly lastAssembly = null; foreach (AssemblyParseResult assembly in assemblies) { - bool changed = this.RewriteAssembly(assembly.Definition); + bool changed = this.RewriteAssembly(assembly.Definition, assumeCompatible); if (changed) { this.Monitor.Log($"Loading {assembly.File.Name} (rewritten in memory)...", LogLevel.Trace); @@ -159,12 +160,13 @@ namespace StardewModdingAPI.Framework ****/ /// Rewrite the types referenced by an assembly. /// The assembly to rewrite. + /// Assume the mod is compatible, even if incompatible code is detected. /// Returns whether the assembly was modified. /// An incompatible CIL instruction was found while rewriting the assembly. - private bool RewriteAssembly(AssemblyDefinition assembly) + private bool RewriteAssembly(AssemblyDefinition assembly, bool assumeCompatible) { ModuleDefinition module = assembly.MainModule; - HashSet loggedRewrites = new HashSet(); + HashSet loggedMessages = new HashSet(); // swap assembly references if needed (e.g. XNA => MonoGame) bool platformChanged = false; @@ -173,7 +175,7 @@ namespace StardewModdingAPI.Framework // remove old assembly reference if (this.AssemblyMap.RemoveNames.Any(name => module.AssemblyReferences[i].Name == name)) { - this.LogOnce(this.Monitor, loggedRewrites, $"Rewriting {assembly.Name.Name} for OS..."); + this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} for OS..."); platformChanged = true; module.AssemblyReferences.RemoveAt(i); i--; @@ -203,13 +205,17 @@ namespace StardewModdingAPI.Framework // throw exception if instruction is incompatible but can't be rewritten IInstructionFinder finder = finders.FirstOrDefault(p => p.IsMatch(instruction, platformChanged)); if (finder != null) - throw new IncompatibleInstructionException(finder.NounPhrase, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}."); + { + if (!assumeCompatible) + throw new IncompatibleInstructionException(finder.NounPhrase, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}."); + this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + } // rewrite instruction if needed IInstructionRewriter rewriter = rewriters.FirstOrDefault(p => p.IsMatch(instruction, platformChanged)); if (rewriter != null) { - this.LogOnce(this.Monitor, loggedRewrites, $"Rewriting {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); rewriter.Rewrite(module, cil, instruction, this.AssemblyMap); anyRewritten = true; } diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index 233deb3c..f015b7ba 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Text.RegularExpressions; using StardewModdingAPI.Framework.Models; namespace StardewModdingAPI.Framework @@ -20,18 +19,18 @@ namespace StardewModdingAPI.Framework /// The friendly mod names treated as deprecation warning sources (assembly full name => mod name). private readonly IDictionary ModNamesByAssembly = new Dictionary(); - /// The mod versions which should be disabled due to incompatibility. - private readonly IncompatibleMod[] IncompatibleMods; + /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + private readonly ModCompatibility[] CompatibilityRecords; /********* ** Public methods *********/ /// Construct an instance. - /// The mod versions which should be disabled due to incompatibility. - public ModRegistry(IEnumerable incompatibleMods) + /// Metadata about mods that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + public ModRegistry(IEnumerable compatibilityRecords) { - this.IncompatibleMods = incompatibleMods.ToArray(); + this.CompatibilityRecords = compatibilityRecords.ToArray(); } @@ -127,21 +126,20 @@ namespace StardewModdingAPI.Framework return null; } - /// Get a record indicating why a mod is incompatible (if applicable). + /// Get metadata that indicates whether SMAPI should assume the mod is compatible or broken, regardless of whether it detects incompatible code. /// The mod manifest. /// Returns the incompatibility record if applicable, else null. - internal IncompatibleMod GetIncompatibilityRecord(IManifest manifest) + internal ModCompatibility GetCompatibilityRecord(IManifest manifest) { string key = !string.IsNullOrWhiteSpace(manifest.UniqueID) ? manifest.UniqueID : manifest.EntryDll; return ( - from mod in this.IncompatibleMods + from mod in this.CompatibilityRecords where mod.ID == key && (mod.LowerSemanticVersion == null || !manifest.Version.IsOlderThan(mod.LowerSemanticVersion)) && !manifest.Version.IsNewerThan(mod.UpperSemanticVersion) - && (string.IsNullOrWhiteSpace(mod.ForceCompatibleVersion) || !Regex.IsMatch(manifest.Version.ToString(), mod.ForceCompatibleVersion, RegexOptions.IgnoreCase)) select mod ).FirstOrDefault(); } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs b/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs deleted file mode 100644 index 29e18ddb..00000000 --- a/src/StardewModdingAPI/Framework/Models/IncompatibleMod.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Runtime.Serialization; -using Newtonsoft.Json; - -namespace StardewModdingAPI.Framework.Models -{ - /// Contains abstract metadata about an incompatible mod. - internal class IncompatibleMod - { - /********* - ** Accessors - *********/ - /**** - ** From config - ****/ - /// The unique mod ID. - public string ID { get; set; } - - /// The mod name. - public string Name { get; set; } - - /// The oldest incompatible mod version, or null for all past versions. - public string LowerVersion { get; set; } - - /// The most recent incompatible mod version. - public string UpperVersion { get; set; } - - /// The URL the user can check for an official updated version. - public string UpdateUrl { get; set; } - - /// The URL the user can check for an unofficial updated version. - public string UnofficialUpdateUrl { get; set; } - - /// A regular expression matching version strings to consider compatible, even if they technically precede . - public string ForceCompatibleVersion { get; set; } - - /// The reason phrase to show in the warning, or null to use the default value. - /// "this version is incompatible with the latest version of the game" - public string ReasonPhrase { get; set; } - - - /**** - ** Injected - ****/ - /// The semantic version corresponding to . - [JsonIgnore] - public ISemanticVersion LowerSemanticVersion { get; set; } - - /// The semantic version corresponding to . - [JsonIgnore] - public ISemanticVersion UpperSemanticVersion { get; set; } - - - /********* - ** Private methods - *********/ - /// The method called when the model finishes deserialising. - /// The deserialisation context. - [OnDeserialized] - private void OnDeserialized(StreamingContext context) - { - this.LowerSemanticVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null; - this.UpperSemanticVersion = this.UpperVersion != null ? new SemanticVersion(this.UpperVersion) : null; - } - } -} diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs new file mode 100644 index 00000000..1e71dae0 --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibility.cs @@ -0,0 +1,65 @@ +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace StardewModdingAPI.Framework.Models +{ + /// Metadata about a mod version that SMAPI should assume is compatible or broken, regardless of whether it detects incompatible code. + internal class ModCompatibility + { + /********* + ** Accessors + *********/ + /**** + ** From config + ****/ + /// The unique mod ID. + public string ID { get; set; } + + /// The mod name. + public string Name { get; set; } + + /// The oldest incompatible mod version, or null for all past versions. + public string LowerVersion { get; set; } + + /// The most recent incompatible mod version. + public string UpperVersion { get; set; } + + /// The URL the user can check for an official updated version. + public string UpdateUrl { get; set; } + + /// The URL the user can check for an unofficial updated version. + public string UnofficialUpdateUrl { get; set; } + + /// The reason phrase to show in the warning, or null to use the default value. + /// "this version is incompatible with the latest version of the game" + public string ReasonPhrase { get; set; } + + /// Indicates how SMAPI should consider the mod. + public ModCompatibilityType Compatibility { get; set; } + + + /**** + ** Injected + ****/ + /// The semantic version corresponding to . + [JsonIgnore] + public ISemanticVersion LowerSemanticVersion { get; set; } + + /// The semantic version corresponding to . + [JsonIgnore] + public ISemanticVersion UpperSemanticVersion { get; set; } + + + /********* + ** Private methods + *********/ + /// The method called when the model finishes deserialising. + /// The deserialisation context. + [OnDeserialized] + private void OnDeserialized(StreamingContext context) + { + this.LowerSemanticVersion = this.LowerVersion != null ? new SemanticVersion(this.LowerVersion) : null; + this.UpperSemanticVersion = this.UpperVersion != null ? new SemanticVersion(this.UpperVersion) : null; + } + } +} diff --git a/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs b/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs new file mode 100644 index 00000000..35edec5e --- /dev/null +++ b/src/StardewModdingAPI/Framework/Models/ModCompatibilityType.cs @@ -0,0 +1,12 @@ +namespace StardewModdingAPI.Framework.Models +{ + /// Indicates how SMAPI should consider a mod. + internal enum ModCompatibilityType + { + /// Assume the mod is not compatible, even if SMAPI doesn't detect any incompatible code. + AssumeBroken = 0, + + /// Assume the mod is compatible, even if SMAPI detects incompatible code. + AssumeCompatible = 1 + } +} diff --git a/src/StardewModdingAPI/Framework/Models/SConfig.cs b/src/StardewModdingAPI/Framework/Models/SConfig.cs index 558da82a..0de96297 100644 --- a/src/StardewModdingAPI/Framework/Models/SConfig.cs +++ b/src/StardewModdingAPI/Framework/Models/SConfig.cs @@ -12,7 +12,7 @@ /// Whether to check if a newer version of SMAPI is available on startup. public bool CheckForUpdates { get; set; } = true; - /// A list of mod versions which should be considered incompatible. - public IncompatibleMod[] IncompatibleMods { get; set; } + /// A list of mod versions which should be considered compatible or incompatible regardless of whether SMAPI detects incompatible code. + public ModCompatibility[] ModCompatibility { get; set; } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index db7a3df6..ac646b1f 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -78,7 +78,7 @@ namespace StardewModdingAPI // initialise this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; - this.ModRegistry = new ModRegistry(this.Settings.IncompatibleMods); + this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); } @@ -364,8 +364,8 @@ namespace StardewModdingAPI skippedPrefix = $"Skipped {manifest.Name}"; // validate compatibility - IncompatibleMod compatibility = this.ModRegistry.GetIncompatibilityRecord(manifest); - if (compatibility != null) + ModCompatibility compatibility = this.ModRegistry.GetCompatibilityRecord(manifest); + if (compatibility?.Compatibility == ModCompatibilityType.AssumeBroken) { bool hasOfficialUrl = !string.IsNullOrWhiteSpace(compatibility.UpdateUrl); bool hasUnofficialUrl = !string.IsNullOrWhiteSpace(compatibility.UnofficialUpdateUrl); @@ -433,7 +433,7 @@ namespace StardewModdingAPI Assembly modAssembly; try { - modAssembly = modAssemblyLoader.Load(assemblyPath); + modAssembly = modAssemblyLoader.Load(assemblyPath, assumeCompatible: compatibility?.Compatibility == ModCompatibilityType.AssumeCompatible); } catch (IncompatibleInstructionException ex) { diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 9ecf2912..31514a21 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -9,7 +9,7 @@ generally shouldn't change this file unless necessary. { "DeveloperMode": true, "CheckForUpdates": true, - "IncompatibleMods": [ + "ModCompatibility": [ /* versions which crash the game */ { "Name": "NPC Map Locations", @@ -17,7 +17,8 @@ generally shouldn't change this file unless necessary. "LowerVersion": "1.42", "UpperVersion": "1.43", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/239", - "ReasonPhrase": "this version has an update check error which crashes the game" + "ReasonPhrase": "this version has an update check error which crashes the game", + "Compatibility": "AssumeBroken" }, /* versions not compatible with Stardew Valley 1.1+ */ @@ -25,7 +26,8 @@ generally shouldn't change this file unless necessary. "Name": "Chest Label System", "ID": "SPDChestLabel", "UpperVersion": "1.5", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242" + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/242", + "Compatibility": "AssumeBroken" }, /* versions not compatible with Stardew Valley 1.2+ */ @@ -35,14 +37,16 @@ generally shouldn't change this file unless necessary. "UpperVersion": "1.1", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257", "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." + "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'.", + "Compatibility": "AssumeBroken" }, { "Name": "Almighty Tool", "ID": "AlmightyTool.dll", "UpperVersion": "1.1.1", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/439", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." + "Notes": "Uses obsolete StardewModdingAPI.Extensions.", + "Compatibility": "AssumeBroken" }, { "Name": "Better Sprinklers", @@ -50,189 +54,216 @@ generally shouldn't change this file unless necessary. "UpperVersion": "2.3", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." + "Notes": "Uses obsolete StardewModdingAPI.Extensions.", + "Compatibility": "AssumeBroken" }, { "Name": "Casks Anywhere", "ID": "CasksAnywhere", "UpperVersion": "1.1", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/878", - "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange." + "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange.", + "Compatibility": "AssumeBroken" }, { "Name": "Chests Anywhere", "ID": "ChestsAnywhere", "UpperVersion": "1.8.2", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'." + "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'.", + "Compatibility": "AssumeBroken" }, { "Name": "Chests Anywhere", "ID": "Pathoschild.ChestsAnywhere", "UpperVersion": "1.9-beta", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'." + "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'.", + "Compatibility": "AssumeBroken" }, { "Name": "CJB Automation", "ID": "CJBAutomation", "UpperVersion": "1.4", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." + "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'.", + "Compatibility": "AssumeBroken" }, { "Name": "CJB Cheats Menu", "ID": "CJBCheatsMenu", "UpperVersion": "1.13", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", - "Notes": "Uses removed Game1.borderFont." + "Notes": "Uses removed Game1.borderFont.", + "Compatibility": "AssumeBroken" }, { "Name": "CJB Item Spawner", "ID": "CJBItemSpawner", "UpperVersion": "1.6", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", - "Notes": "Uses removed Game1.borderFont." + "Notes": "Uses removed Game1.borderFont.", + "Compatibility": "AssumeBroken" }, { "Name": "Cooking Skill", "ID": "CookingSkill", "UpperVersion": "1.0.3", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522", - "Notes": "Crashes with 'Method not found: Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'." + "Notes": "Crashes with 'Method not found: Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'.", + "Compatibility": "AssumeBroken" }, { "Name": "Enemy Health Bars", "ID": "SPDHealthBar", "UpperVersion": "1.7", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/193", - "Notes": "Uses obsolete GraphicsEvents.DrawTick." + "Notes": "Uses obsolete GraphicsEvents.DrawTick.", + "Compatibility": "AssumeBroken" }, { "Name": "Entoarox Framework", "ID": "eacdb74b-4080-4452-b16b-93773cda5cf9", "UpperVersion": "1.6.5", "UpdateUrl": "http://community.playstarbound.com/resources/4228", - "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)')." + "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)').", + "Compatibility": "AssumeBroken" }, { "Name": "Extended Fridge", "ID": "Mystra007ExtendedFridge", "UpperVersion": "1.0", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485", - "Notes": "Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest. Crashes with 'Field not found: StardewValley.Game1.mouseCursorTransparency'." + "Notes": "Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest. Crashes with 'Field not found: StardewValley.Game1.mouseCursorTransparency'.", + "Compatibility": "AssumeBroken" }, { "Name": "Get Dressed", "ID": "GetDressed.dll", "UpperVersion": "3.2", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331", - "Notes": "Crashes with NullReferenceException in GameEvents.UpdateTick." + "Notes": "Crashes with NullReferenceException in GameEvents.UpdateTick.", + "Compatibility": "AssumeBroken" }, { "Name": "Lookup Anything", "ID": "LookupAnything", "UpperVersion": "1.10", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "Crashes with FormatException when looking up NPCs." + "Notes": "Crashes with FormatException when looking up NPCs.", + "Compatibility": "AssumeBroken" }, { "Name": "Lookup Anything", "ID": "Pathoschild.LookupAnything", "UpperVersion": "1.10.1", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "ID changed in 1.10.1. Crashes with FormatException when looking up NPCs." + "Notes": "ID changed in 1.10.1. Crashes with FormatException when looking up NPCs.", + "Compatibility": "AssumeBroken" }, { "Name": "Makeshift Multiplayer", "ID": "StardewValleyMP", "UpperVersion": "0.2.10", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/501", - "Notes": "Uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck." + "Notes": "Uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck.", + "Compatibility": "AssumeBroken" }, { "Name": "NoSoilDecay", "ID": "289dee03-5f38-4d8e-8ffc-e440198e8610", "UpperVersion": "0.5", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237", - "Notes": "Uses Assembly.GetExecutingAssembly().Location." + "Notes": "Uses Assembly.GetExecutingAssembly().Location.", + "Compatibility": "AssumeBroken" }, { "Name": "Point-and-Plant", "ID": "PointAndPlant.dll", "UpperVersion": "1.0.2", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/572", - "Notes": "Uses obsolete StardewModdingAPI.Extensions." + "Notes": "Uses obsolete StardewModdingAPI.Extensions.", + "Compatibility": "AssumeBroken" }, { "Name": "Reusable Wallpapers", "ID": "dae1b553-2e39-43e7-8400-c7c5c836134b", "UpperVersion": "1.5", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/356", - "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange." + "Notes": "Uses obsolete StardewModdingAPI.Inheritance.ItemStackChange.", + "Compatibility": "AssumeBroken" }, { "Name": "Save Anywhere", "ID": "SaveAnywhere", "UpperVersion": "2.0", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/444", - "Notes": "Depends on StarDustCore." + "Notes": "Depends on StarDustCore.", + "Compatibility": "AssumeBroken" }, { "Name": "StackSplitX", "ID": "StackSplitX.dll", "UpperVersion": "1.0", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/798", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Uses SMAPI's internal SGame class.", + "Compatibility": "AssumeBroken" }, { "Name": "StarDustCore", "ID": "StarDustCore", "UpperVersion": "1.0", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683", - "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'." + "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'.", + "Compatibility": "AssumeBroken" }, { "Name": "Teleporter", "ID": "Teleporter", "UpperVersion": "1.0.2", "UpdateUrl": "http://community.playstarbound.com/resources/4374", - "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'." + "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'.", + "Compatibility": "AssumeBroken" }, { "Name": "Zoryn's Better RNG", "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", "UpperVersion": "1.5", "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Uses SMAPI's internal SGame class.", + "Compatibility": "AssumeBroken" }, { "Name": "Zoryn's Calendar Anywhere", "ID": "a41c01cd-0437-43eb-944f-78cb5a53002a", "UpperVersion": "1.5", "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Uses SMAPI's internal SGame class.", + "Compatibility": "AssumeBroken" }, { "Name": "Zoryn's Health Bars", "ID": "HealthBars.dll", "UpperVersion": "1.5", "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Uses SMAPI's internal SGame class.", + "Compatibility": "AssumeBroken" }, { "Name": "Zoryn's Movement Mod", "ID": "8a632929-8335-484f-87dd-c29d2ba3215d", "UpperVersion": "1.5", "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Uses SMAPI's internal SGame class.", + "Compatibility": "AssumeBroken" }, { "Name": "Zoryn's Regen Mod", "ID": "dfac4383-1b6b-4f33-ae4e-37fc23e5252e", "UpperVersion": "1.5", "UpdateUrl": "http://community.playstarbound.com/threads/108756", - "Notes": "Uses SMAPI's internal SGame class." + "Notes": "Uses SMAPI's internal SGame class.", + "Compatibility": "AssumeBroken" } ] } diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 99666f08..091b3d90 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -154,6 +154,7 @@ + @@ -176,7 +177,7 @@ - + -- cgit From 85ed48809032fdbb8461ce4c34acfbe06f68652b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Mar 2017 19:01:35 -0400 Subject: merge CIL finders & rewriters into one interface (#254) --- .../Finders/EventFinder.cs | 22 ++++++++++++-- .../Finders/FieldFinder.cs | 22 ++++++++++++-- .../Finders/MethodFinder.cs | 22 ++++++++++++-- .../Finders/TypeFinder.cs | 26 ++++++++++++++-- .../IInstructionFinder.cs | 23 -------------- .../IInstructionRewriter.cs | 16 ++++++++-- .../IncompatibleInstructionException.cs | 35 ++++++++++++++++++++++ .../Rewriters/FieldReplaceRewriter.cs | 28 +++++++++-------- .../Rewriters/FieldToPropertyRewriter.cs | 11 +++++-- .../Rewriters/MethodParentRewriter.cs | 35 ++++++++++++++-------- .../StardewModdingAPI.AssemblyRewriters.csproj | 2 +- src/StardewModdingAPI/Constants.cs | 23 +++++++------- src/StardewModdingAPI/Framework/AssemblyLoader.cs | 33 ++++++++++---------- .../Framework/IncompatibleInstructionException.cs | 27 ----------------- src/StardewModdingAPI/Program.cs | 1 + src/StardewModdingAPI/StardewModdingAPI.csproj | 1 - 16 files changed, 203 insertions(+), 124 deletions(-) delete mode 100644 src/StardewModdingAPI.AssemblyRewriters/IInstructionFinder.cs create mode 100644 src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs delete mode 100644 src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs index 848e54ff..bcceee32 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/EventFinder.cs @@ -3,8 +3,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.AssemblyRewriters.Finders { - /// Finds CIL instructions that reference a given event. - public sealed class EventFinder : IInstructionFinder + /// Finds incompatible CIL instructions that reference a given event and throws an . + public class EventFinder : IInstructionRewriter { /********* ** Properties @@ -37,6 +37,22 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{eventName} event"; } + /// Rewrite a CIL instruction for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction, platformChanged)) + return false; + + throw new IncompatibleInstructionException(this.NounPhrase); + } + /********* ** Protected methods @@ -44,7 +60,7 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// Get whether a CIL instruction matches. /// The IL instruction. /// Whether the mod was compiled on a different platform. - public bool IsMatch(Instruction instruction, bool platformChanged) + protected bool IsMatch(Instruction instruction, bool platformChanged) { MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); return diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs index 068119b8..cdfc3bd5 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/FieldFinder.cs @@ -3,8 +3,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.AssemblyRewriters.Finders { - /// Finds CIL instructions that reference a given field. - public class FieldFinder : IInstructionFinder + /// Finds incompatible CIL instructions that reference a given field and throws an . + public class FieldFinder : IInstructionRewriter { /********* ** Properties @@ -37,6 +37,22 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{fieldName} field"; } + /// Rewrite a CIL instruction for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction, platformChanged)) + return false; + + throw new IncompatibleInstructionException(this.NounPhrase); + } + /********* ** Protected methods @@ -44,7 +60,7 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// Get whether a CIL instruction matches. /// The IL instruction. /// Whether the mod was compiled on a different platform. - public bool IsMatch(Instruction instruction, bool platformChanged) + protected bool IsMatch(Instruction instruction, bool platformChanged) { FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction); return diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs index d174bacd..2efcbb0f 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/MethodFinder.cs @@ -3,8 +3,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.AssemblyRewriters.Finders { - /// Finds CIL instructions that reference a given method. - public class MethodFinder : IInstructionFinder + /// Finds incompatible CIL instructions that reference a given method and throws an . + public class MethodFinder : IInstructionRewriter { /********* ** Properties @@ -37,6 +37,22 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders this.NounPhrase = nounPhrase ?? $"{fullTypeName}.{methodName} method"; } + /// Rewrite a CIL instruction for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction, platformChanged)) + return false; + + throw new IncompatibleInstructionException(this.NounPhrase); + } + /********* ** Protected methods @@ -44,7 +60,7 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders /// Get whether a CIL instruction matches. /// The IL instruction. /// Whether the mod was compiled on a different platform. - public bool IsMatch(Instruction instruction, bool platformChanged) + protected bool IsMatch(Instruction instruction, bool platformChanged) { MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); return diff --git a/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs index 8f492d5f..96cbb229 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/Finders/TypeFinder.cs @@ -4,8 +4,8 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.AssemblyRewriters.Finders { - /// Finds CIL instructions that reference a given type. - public class TypeFinder : IInstructionFinder + /// Finds incompatible CIL instructions that reference a given type and throws an . + public class TypeFinder : IInstructionRewriter { /********* ** Accessors @@ -33,10 +33,30 @@ namespace StardewModdingAPI.AssemblyRewriters.Finders this.NounPhrase = nounPhrase ?? $"{fullTypeName} type"; } + /// Rewrite a CIL instruction for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public virtual bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction, platformChanged)) + return false; + + throw new IncompatibleInstructionException(this.NounPhrase); + } + + + /********* + ** Protected methods + *********/ /// Get whether a CIL instruction matches. /// The IL instruction. /// Whether the mod was compiled on a different platform. - public bool IsMatch(Instruction instruction, bool platformChanged) + protected bool IsMatch(Instruction instruction, bool platformChanged) { string fullName = this.FullTypeName; diff --git a/src/StardewModdingAPI.AssemblyRewriters/IInstructionFinder.cs b/src/StardewModdingAPI.AssemblyRewriters/IInstructionFinder.cs deleted file mode 100644 index cc3006b9..00000000 --- a/src/StardewModdingAPI.AssemblyRewriters/IInstructionFinder.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Mono.Cecil.Cil; - -namespace StardewModdingAPI.AssemblyRewriters -{ - /// Finds CIL instructions considered incompatible. - public interface IInstructionFinder - { - /********* - ** Accessors - *********/ - /// A brief noun phrase indicating what the instruction finder matches. - string NounPhrase { get; } - - - /********* - ** Methods - *********/ - /// Get whether a CIL instruction matches. - /// The IL instruction. - /// Whether the mod was compiled on a different platform. - bool IsMatch(Instruction instruction, bool platformChanged); - } -} diff --git a/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs index b230f227..3a7b1365 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/IInstructionRewriter.cs @@ -3,9 +3,16 @@ using Mono.Cecil.Cil; namespace StardewModdingAPI.AssemblyRewriters { - /// Rewrites a CIL instruction for compatibility. - public interface IInstructionRewriter : IInstructionFinder + /// Rewrites CIL instructions for compatibility. + public interface IInstructionRewriter { + /********* + ** Accessors + *********/ + /// A brief noun phrase indicating what the rewriter matches. + string NounPhrase { get; } + + /********* ** Methods *********/ @@ -14,6 +21,9 @@ namespace StardewModdingAPI.AssemblyRewriters /// The CIL rewriter. /// The instruction to rewrite. /// Metadata for mapping assemblies to the current platform. - void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap); + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged); } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs b/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs new file mode 100644 index 00000000..f7e6bd8f --- /dev/null +++ b/src/StardewModdingAPI.AssemblyRewriters/IncompatibleInstructionException.cs @@ -0,0 +1,35 @@ +using System; + +namespace StardewModdingAPI.AssemblyRewriters +{ + /// An exception raised when an incompatible instruction is found while loading a mod assembly. + public class IncompatibleInstructionException : Exception + { + /********* + ** Accessors + *********/ + /// A brief noun phrase which describes the incompatible instruction that was found. + public string NounPhrase { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// A brief noun phrase which describes the incompatible instruction that was found. + public IncompatibleInstructionException(string nounPhrase) + : base($"Found an incompatible CIL instruction ({nounPhrase}).") + { + this.NounPhrase = nounPhrase; + } + + /// Construct an instance. + /// A brief noun phrase which describes the incompatible instruction that was found. + /// A message which describes the error. + public IncompatibleInstructionException(string nounPhrase, string message) + : base(message) + { + this.NounPhrase = nounPhrase; + } + } +} diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs index ffd22e7c..95663c49 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldReplaceRewriter.cs @@ -7,16 +7,13 @@ using StardewModdingAPI.AssemblyRewriters.Finders; namespace StardewModdingAPI.AssemblyRewriters.Rewriters { /// Rewrites references to one field with another. - public class FieldReplaceRewriter : FieldFinder, IInstructionRewriter + public class FieldReplaceRewriter : FieldFinder { /********* ** Properties *********/ - /// The type whose field to which references should be rewritten. - private readonly Type Type; - - /// The new field name to reference. - private readonly string ToFieldName; + /// The new field to reference. + private readonly FieldInfo ToField; /********* @@ -30,8 +27,9 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName, string nounPhrase = null) : base(type.FullName, fromFieldName, nounPhrase) { - this.Type = type; - this.ToFieldName = toFieldName; + this.ToField = type.GetField(toFieldName); + if (this.ToField == null) + throw new InvalidOperationException($"The {type.FullName} class doesn't have a {toFieldName} field."); } /// Rewrite a CIL instruction for compatibility. @@ -39,13 +37,17 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// The CIL rewriter. /// The instruction to rewrite. /// Metadata for mapping assemblies to the current platform. - public void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap) + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { - FieldInfo field = this.Type.GetField(this.ToFieldName); - if (field == null) - throw new InvalidOperationException($"The {this.Type.FullName} class doesn't have a {this.ToFieldName} field."); - FieldReference newRef = module.Import(field); + if (!this.IsMatch(instruction, platformChanged)) + return false; + + FieldReference newRef = module.Import(this.ToField); cil.Replace(instruction, cil.Create(instruction.OpCode, newRef)); + return true; } } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs index f2f99cc1..a25f3fef 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/FieldToPropertyRewriter.cs @@ -6,7 +6,7 @@ using StardewModdingAPI.AssemblyRewriters.Finders; namespace StardewModdingAPI.AssemblyRewriters.Rewriters { /// Rewrites field references into property references. - public class FieldToPropertyRewriter : FieldFinder, IInstructionRewriter + public class FieldToPropertyRewriter : FieldFinder { /********* ** Properties @@ -37,11 +37,18 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters /// The CIL rewriter. /// The instruction to rewrite. /// Metadata for mapping assemblies to the current platform. - public void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap) + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public override bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) { + if (!this.IsMatch(instruction, platformChanged)) + return false; + string methodPrefix = instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Ldfld ? "get" : "set"; MethodReference propertyRef = module.Import(this.Type.GetMethod($"{methodPrefix}_{this.FieldName}")); cil.Replace(instruction, cil.Create(OpCodes.Call, propertyRef)); + return true; } } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs index 24d4dff9..3ec8c704 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs +++ b/src/StardewModdingAPI.AssemblyRewriters/Rewriters/MethodParentRewriter.cs @@ -43,10 +43,32 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters this.OnlyIfPlatformChanged = onlyIfPlatformChanged; } + /// Rewrite a CIL instruction for compatibility. + /// The module being rewritten. + /// The CIL rewriter. + /// The instruction to rewrite. + /// Metadata for mapping assemblies to the current platform. + /// Whether the mod was compiled on a different platform. + /// Returns whether the instruction was rewritten. + /// The CIL instruction is not compatible, and can't be rewritten. + public bool Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap, bool platformChanged) + { + if (!this.IsMatch(instruction, platformChanged)) + return false; + + MethodReference methodRef = (MethodReference)instruction.Operand; + methodRef.DeclaringType = module.Import(this.ToType); + return true; + } + + + /********* + ** Protected methods + *********/ /// Get whether a CIL instruction matches. /// The IL instruction. /// Whether the mod was compiled on a different platform. - public bool IsMatch(Instruction instruction, bool platformChanged) + protected bool IsMatch(Instruction instruction, bool platformChanged) { MethodReference methodRef = RewriteHelper.AsMethodReference(instruction); return @@ -55,16 +77,5 @@ namespace StardewModdingAPI.AssemblyRewriters.Rewriters && methodRef.DeclaringType.FullName == this.FromType.FullName && RewriteHelper.HasMatchingSignature(this.ToType, methodRef); } - - /// Rewrite a CIL instruction for compatibility. - /// The module being rewritten. - /// The CIL rewriter. - /// The instruction to rewrite. - /// Metadata for mapping assemblies to the current platform. - public void Rewrite(ModuleDefinition module, ILProcessor cil, Instruction instruction, PlatformAssemblyMap assemblyMap) - { - MethodReference methodRef = (MethodReference)instruction.Operand; - methodRef.DeclaringType = module.Import(this.ToType); - } } } diff --git a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj index 09fd79ed..3c3acde3 100644 --- a/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj +++ b/src/StardewModdingAPI.AssemblyRewriters/StardewModdingAPI.AssemblyRewriters.csproj @@ -70,8 +70,8 @@ + - diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 52be6c05..de0eab57 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -137,12 +137,15 @@ namespace StardewModdingAPI return new PlatformAssemblyMap(targetPlatform, removeAssemblyReferences, targetAssemblies); } - /// Get finders which match incompatible CIL instructions in mod assemblies. - internal static IEnumerable GetIncompatibilityFinders() + /// Get rewriters which detect or fix incompatible CIL instructions in mod assemblies. + internal static IEnumerable GetRewriters() { - return new IInstructionFinder[] + return new IInstructionRewriter[] { - // changes in Stardew Valley 1.2 (that don't have rewriters) + /**** + ** Finders throw an exception when incompatible code is found. + ****/ + // changes in Stardew Valley 1.2 (with no rewriters) new FieldFinder("StardewValley.Item", "set_Name"), // APIs removed in SMAPI 1.9 @@ -161,15 +164,11 @@ namespace StardewModdingAPI new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderHudEventNoCheck"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPostRenderGuiEventNoCheck"), new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderHudEventNoCheck"), - new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck") - }; - } + new EventFinder("StardewModdingAPI.Events.GraphicsEvents", "OnPreRenderGuiEventNoCheck"), - /// Get rewriters which fix incompatible CIL instructions in mod assemblies. - internal static IEnumerable GetRewriters() - { - return new IInstructionRewriter[] - { + /**** + ** Rewriters change CIL as needed to fix incompatible code + ****/ // crossplatform new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true), diff --git a/src/StardewModdingAPI/Framework/AssemblyLoader.cs b/src/StardewModdingAPI/Framework/AssemblyLoader.cs index aee0bbb3..5d00c525 100644 --- a/src/StardewModdingAPI/Framework/AssemblyLoader.cs +++ b/src/StardewModdingAPI/Framework/AssemblyLoader.cs @@ -193,33 +193,30 @@ namespace StardewModdingAPI.Framework this.ChangeTypeScope(type); } - // find incompatible instructions + // find (and optionally rewrite) incompatible instructions bool anyRewritten = false; - IInstructionFinder[] finders = Constants.GetIncompatibilityFinders().ToArray(); IInstructionRewriter[] rewriters = Constants.GetRewriters().ToArray(); foreach (MethodDefinition method in this.GetMethods(module)) { ILProcessor cil = method.Body.GetILProcessor(); foreach (Instruction instruction in cil.Body.Instructions.ToArray()) { - // throw exception if instruction is incompatible but can't be rewritten - IInstructionFinder finder = finders.FirstOrDefault(p => p.IsMatch(instruction, platformChanged)); - if (finder != null) - { - if (!assumeCompatible) - throw new IncompatibleInstructionException(finder.NounPhrase, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}."); - this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({finder.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); - } - - // rewrite instruction if needed foreach (IInstructionRewriter rewriter in rewriters) { - if (!rewriter.IsMatch(instruction, platformChanged)) - continue; - - this.LogOnce(this.Monitor, loggedMessages, $"Rewriting {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); - rewriter.Rewrite(module, cil, instruction, this.AssemblyMap); - anyRewritten = true; + try + { + if (rewriter.Rewrite(module, cil, instruction, this.AssemblyMap, platformChanged)) + { + this.LogOnce(this.Monitor, loggedMessages, $"Rewrote {assembly.Name.Name} to fix {rewriter.NounPhrase}..."); + anyRewritten = true; + } + } + catch (IncompatibleInstructionException) + { + if (!assumeCompatible) + throw new IncompatibleInstructionException(rewriter.NounPhrase, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}."); + this.LogOnce(this.Monitor, loggedMessages, $"Found an incompatible CIL instruction ({rewriter.NounPhrase}) while loading assembly {assembly.Name.Name}, but SMAPI is configured to allow it anyway. The mod may crash or behave unexpectedly.", LogLevel.Warn); + } } } } diff --git a/src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs b/src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs deleted file mode 100644 index affe2cb3..00000000 --- a/src/StardewModdingAPI/Framework/IncompatibleInstructionException.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework -{ - /// An exception raised when an incompatible instruction is found while loading a mod assembly. - internal class IncompatibleInstructionException : Exception - { - /********* - ** Accessors - *********/ - /// A brief noun phrase which describes the incompatible instruction that was found. - public string NounPhrase { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// A brief noun phrase which describes the incompatible instruction that was found. - /// A message which describes the error. - public IncompatibleInstructionException(string nounPhrase, string message) - : base(message) - { - this.NounPhrase = nounPhrase; - } - } -} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ac646b1f..25605148 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -12,6 +12,7 @@ using System.Windows.Forms; #endif using Microsoft.Xna.Framework.Graphics; using Newtonsoft.Json; +using StardewModdingAPI.AssemblyRewriters; using StardewModdingAPI.Events; using StardewModdingAPI.Framework; using StardewModdingAPI.Framework.Logging; diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 091b3d90..bcd0c390 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -150,7 +150,6 @@ - -- cgit From 04cae4ef4608352329c746e2163b2a9356b063f6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Mar 2017 20:15:48 -0400 Subject: fix SMAPI not recognising Mod instances that don't subclass Mod directly (#252) --- release-notes.md | 1 + src/StardewModdingAPI/Program.cs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index 67286308..3b654ee3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -44,6 +44,7 @@ For mod developers: * The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available. * The SMAPI log now always uses `\r\n` line endings to simplify crossplatform viewing. * Fixed `SaveEvents.AfterLoad` being raised during the new-game intro before the player is initialised. +* Fixed SMAPI not recognising `Mod` instances that don't subclass `Mod` directly. * Several obsolete APIs have been removed (see [deprecation guide](http://canimod.com/guides/updating-a-smapi-mod)), and all _notice_-level deprecations have been increased to _info_. * Removed the experimental `IConfigFile`. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 25605148..276b66ce 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -450,9 +450,15 @@ namespace StardewModdingAPI // validate assembly try { - if (modAssembly.DefinedTypes.Count(x => x.BaseType == typeof(Mod)) == 0) + int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); + if(modEntries == 0) { - this.Monitor.Log($"{skippedPrefix} because its DLL has no 'Mod' subclass.", LogLevel.Error); + this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error); + continue; + } + if (modEntries > 1) + { + this.Monitor.Log($"{skippedPrefix} because its DLL contains multiple '{nameof(Mod)}' subclasses.", LogLevel.Error); continue; } } @@ -466,7 +472,7 @@ namespace StardewModdingAPI try { // get implementation - TypeInfo modEntryType = modAssembly.DefinedTypes.First(x => x.BaseType == typeof(Mod)); + TypeInfo modEntryType = modAssembly.DefinedTypes.First(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); Mod mod = (Mod)modAssembly.CreateInstance(modEntryType.ToString()); if (mod == null) { -- cgit From df1e748629f7ed5f150daba6fd83f5cf576a97b7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 30 Mar 2017 18:59:01 -0400 Subject: backport SMAPI 1.9 to Stardew Valley 1.11 (#258) --- release-notes.md | 15 +- src/StardewModdingAPI/Constants.cs | 14 +- src/StardewModdingAPI/Framework/SGame.cs | 1042 +++++++------------- src/StardewModdingAPI/Program.cs | 5 +- .../StardewModdingAPI.config.json | 100 +- 5 files changed, 418 insertions(+), 758 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/release-notes.md b/release-notes.md index 3b654ee3..55740ef1 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,20 +2,23 @@ ## 1.9 See [log](https://github.com/Pathoschild/SMAPI/compare/1.8...1.9). For players: -* Updated for Stardew Valley 1.2. -* SMAPI now rewrites many mods for compatibility with game updates, but some mods will need an update. * SMAPI now detects incompatible mods and disables them before they cause problems. * SMAPI now allows mods nested into an otherwise empty parent folder (like `Mods\ModName-1.0\ModName\manifest.json`), since that's a common default behaviour when unpacking mods. * The installer now detects if you need to update .NET Framework before installing SMAPI. @@ -35,10 +38,10 @@ For players: For mod developers: * Added a simpler API for console commands (see `helper.ConsoleCommands`). -* Added `SaveEvents.AfterReturnToTitle` event triggered when the player returns to the title screen. * Added `TimeEvents.AfterDayStarted` event triggered when a day starts. This happens no matter how the day started (including new game, loaded save, or player went to bed). -* Added `ContentEvents.AfterLocaleChanged` event triggered when the player changes the content language. -* Added `GetPrivateProperty` to the reflection helper. +* Added `ContentEvents.AfterLocaleChanged` event triggered when the player changes the content language (for the upcoming Stardew Valley 1.2). +* Added `SaveEvents.AfterReturnToTitle` event triggered when the player returns to the title screen (for the upcoming Stardew Valley 1.2). +* Added `helper.Reflection.GetPrivateProperty` method. * SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings automatically, so mods no longer need to add a `StringEnumConverter` themselves for those. * The SMAPI log now has a simpler filename. * The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available. diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 1f1b6a65..3762269d 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -33,10 +33,10 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 8, 0); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 9, 0); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.15"); + public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.1.1"); /// The path to the game folder. public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); @@ -146,9 +146,6 @@ namespace StardewModdingAPI /**** ** Finders throw an exception when incompatible code is found. ****/ - // changes in Stardew Valley 1.2 (with no rewriters) - new FieldFinder("StardewValley.Item", "set_Name"), - // APIs removed in SMAPI 1.9 new TypeFinder("StardewModdingAPI.Advanced.ConfigFile"), new TypeFinder("StardewModdingAPI.Advanced.IConfigFile"), @@ -172,12 +169,7 @@ namespace StardewModdingAPI // crossplatform new MethodParentRewriter(typeof(SpriteBatch), typeof(SpriteBatchWrapper), onlyIfPlatformChanged: true), - // Stardew Valley 1.2 - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.activeClickableMenu)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.gameMode)), - new FieldToPropertyRewriter(typeof(Game1), nameof(Game1.player)), - new FieldReplaceRewriter(typeof(Game1), "borderFont", nameof(Game1.smallFont)), - new FieldReplaceRewriter(typeof(Game1), "smoothFont", nameof(Game1.smallFont)), + // SMAPI 1.9 new TypeReferenceRewriter("StardewModdingAPI.Inheritance.ItemStackChange", typeof(ItemStackChange)) }; } diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 33d823ef..5f265139 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -1,10 +1,8 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Threading.Tasks; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; @@ -16,7 +14,7 @@ using StardewValley.Locations; using StardewValley.Menus; using StardewValley.Tools; using xTile.Dimensions; -using xTile.Layers; +using Rectangle = Microsoft.Xna.Framework.Rectangle; using SFarmer = StardewValley.Farmer; namespace StardewModdingAPI.Framework @@ -136,9 +134,6 @@ namespace StardewModdingAPI.Framework /// The player character at last check. private SFarmer PreviousFarmer; - /// The previous content locale. - private LocalizedContentManager.LanguageCode? PreviousLocale; - /// An index incremented on every tick and reset every 60th tick (0–59). private int CurrentUpdateTick; @@ -154,20 +149,12 @@ namespace StardewModdingAPI.Framework // ReSharper disable ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming /// Used to access private fields and methods. private static readonly IReflectionHelper Reflection = new ReflectionHelper(); - private static List _fpsList => SGame.Reflection.GetPrivateField>(typeof(Game1), nameof(_fpsList)).GetValue(); - private static Stopwatch _fpsStopwatch => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(SGame._fpsStopwatch)).GetValue(); - private static float _fps - { - set { SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_fps)).SetValue(value); } - } - private static Task _newDayTask => SGame.Reflection.GetPrivateField(typeof(Game1), nameof(_newDayTask)).GetValue(); private Color bgColor => SGame.Reflection.GetPrivateField(this, nameof(bgColor)).GetValue(); - public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateProperty(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop + public RenderTarget2D screenWrapper => SGame.Reflection.GetPrivateField(this, "screen").GetValue(); // deliberately renamed to avoid an infinite loop public BlendState lightingBlend => SGame.Reflection.GetPrivateField(this, nameof(lightingBlend)).GetValue(); private readonly Action drawFarmBuildings = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawFarmBuildings)).Invoke(new object[0]); private readonly Action drawHUD = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawHUD)).Invoke(new object[0]); private readonly Action drawDialogueBox = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(drawDialogueBox)).Invoke(new object[0]); - private readonly Action renderScreenBuffer = () => SGame.Reflection.GetPrivateMethod(SGame.Instance, nameof(renderScreenBuffer)).Invoke(new object[0]); // ReSharper restore ArrangeStaticMemberQualifier, ArrangeThisQualifier, InconsistentNaming @@ -197,14 +184,6 @@ namespace StardewModdingAPI.Framework GameEvents.InvokeInitialize(this.Monitor); } - /// Constructor a content manager to read XNB files. - /// The service provider to use to locate services. - /// The root directory to search for content. - protected override LocalizedContentManager CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) - { - return new SContentManager(this.Content.ServiceProvider, this.Content.RootDirectory, this.Monitor); - } - /// The method called before XNA or MonoGame loads or reloads graphics resources. protected override void LoadContent() { @@ -216,22 +195,6 @@ namespace StardewModdingAPI.Framework /// A snapshot of the game timing state. protected override void Update(GameTime gameTime) { - // While a background new-day task is in progress, the game skips its own update logic - // and defers to the XNA Update method. Running mod code in parallel to the background - // update is risky, because data changes can conflict (e.g. collection changed during - // enumeration errors) and data may change unexpectedly from one mod instruction to the - // next. - // - // Therefore we can just run Game1.Update here without raising any SMAPI events. There's - // a small chance that the task will finish after we defer but before the game checks, - // which means technically events should be raised, but the effects of missing one - // update tick are neglible and not worth the complications of bypassing Game1.Update. - if (SGame._newDayTask != null) - { - base.Update(gameTime); - return; - } - // raise game loaded if (this.FirstUpdate) GameEvents.InvokeGameLoaded(this.Monitor); @@ -274,8 +237,7 @@ namespace StardewModdingAPI.Framework this.CurrentUpdateTick = 0; // track keyboard state - if (this.KStatePrior != this.KStateNow) - this.KStatePrior = this.KStateNow; + this.KStatePrior = this.KStateNow; // track controller button state for (var i = PlayerIndex.One; i <= PlayerIndex.Four; i++) @@ -296,649 +258,412 @@ namespace StardewModdingAPI.Framework { try { - if (Game1.debugMode) + if (!this.ZoomLevelIsOne) + this.GraphicsDevice.SetRenderTarget(this.screenWrapper); + + this.GraphicsDevice.Clear(this.bgColor); + if (Game1.options.showMenuBackground && Game1.activeClickableMenu != null && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) { - if (SGame._fpsStopwatch.IsRunning) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + try { - float totalSeconds = (float)SGame._fpsStopwatch.Elapsed.TotalSeconds; - SGame._fpsList.Add(totalSeconds); - while (SGame._fpsList.Count >= 120) - SGame._fpsList.RemoveAt(0); - float num = 0.0f; - foreach (float fps in SGame._fpsList) - num += fps; - SGame._fps = (float)(1.0 / ((double)num / (double)SGame._fpsList.Count)); + Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); } - SGame._fpsStopwatch.Restart(); + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing its background. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + try + { + Game1.activeClickableMenu.draw(Game1.spriteBatch); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + Game1.spriteBatch.End(); + if (!this.ZoomLevelIsOne) + { + this.GraphicsDevice.SetRenderTarget(null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + return; } - else + if (Game1.gameMode == 11) { - if (SGame._fpsStopwatch.IsRunning) - SGame._fpsStopwatch.Reset(); - SGame._fps = 0.0f; - SGame._fpsList.Clear(); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.DrawString(Game1.smoothFont, "Stardew Valley has crashed...", new Vector2(16f, 16f), Color.HotPink); + Game1.spriteBatch.DrawString(Game1.smoothFont, "Please send the error report or a screenshot of this message to @ConcernedApe. (http://stardewvalley.net/contact/)", new Vector2(16f, 32f), new Color(0, 255, 0)); + Game1.spriteBatch.DrawString(Game1.smoothFont, Game1.parseText(Game1.errorMessage, Game1.smoothFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); + Game1.spriteBatch.End(); + return; } - if (SGame._newDayTask != null) + if (Game1.currentMinigame != null) { - this.GraphicsDevice.Clear(this.bgColor); - //base.Draw(gameTime); + Game1.currentMinigame.draw(Game1.spriteBatch); + if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); + Game1.spriteBatch.End(); + } + if (!this.ZoomLevelIsOne) + { + this.GraphicsDevice.SetRenderTarget(null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + return; } - else + if (Game1.showingEndOfNightStuff) { - if ((double)Game1.options.zoomLevel != 1.0) - this.GraphicsDevice.SetRenderTarget(this.screenWrapper); - if (this.IsSaving) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + try { + Game1.activeClickableMenu?.draw(Game1.spriteBatch); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); + } + Game1.spriteBatch.End(); + if (!this.ZoomLevelIsOne) + { + this.GraphicsDevice.SetRenderTarget(null); this.GraphicsDevice.Clear(this.bgColor); - IClickableMenu activeClickableMenu = Game1.activeClickableMenu; - if (activeClickableMenu != null) + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + return; + } + if (Game1.gameMode == 6) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + string text = ""; + int num = 0; + while (num < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0) + { + text += "."; + num++; + } + SpriteText.drawString(Game1.spriteBatch, "Loading" + text, 64, Game1.graphics.GraphicsDevice.Viewport.Height - 64, 999, -1, 999, 1f, 1f, false, 0, "Loading..."); + Game1.spriteBatch.End(); + if (!this.ZoomLevelIsOne) + { + this.GraphicsDevice.SetRenderTarget(null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); + } + return; + } + if (Game1.gameMode == 0) + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + else + { + if (Game1.drawLighting) + { + this.GraphicsDevice.SetRenderTarget(Game1.lightmap); + this.GraphicsDevice.Clear(Color.White * 0f); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null); + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : ((!Game1.ambientLight.Equals(Color.White) && (!Game1.isRaining || !Game1.currentLocation.isOutdoors)) ? Game1.ambientLight : Game1.outdoorLight)); + for (int i = 0; i < Game1.currentLightSources.Count; i++) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {activeClickableMenu.GetType().FullName} menu crashed while drawing itself during save. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - activeClickableMenu.exitThisMenu(); - } - Game1.spriteBatch.End(); + if (Utility.isOnScreen(Game1.currentLightSources.ElementAt(i).position, (int)(Game1.currentLightSources.ElementAt(i).radius * Game1.tileSize * 4f))) + Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(i).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(i).position) / Game1.options.lightingQuality, Game1.currentLightSources.ElementAt(i).lightTexture.Bounds, Game1.currentLightSources.ElementAt(i).color, 0f, new Vector2(Game1.currentLightSources.ElementAt(i).lightTexture.Bounds.Center.X, Game1.currentLightSources.ElementAt(i).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(i).radius / Game1.options.lightingQuality, SpriteEffects.None, 0.9f); } - //base.Draw(gameTime); - this.renderScreenBuffer(); + Game1.spriteBatch.End(); + this.GraphicsDevice.SetRenderTarget(this.ZoomLevelIsOne ? null : this.screenWrapper); } - else + if (Game1.bloomDay) + Game1.bloom?.BeginDraw(); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); + Game1.background?.draw(Game1.spriteBatch); + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.currentLocation.drawWater(Game1.spriteBatch); + if (Game1.CurrentEvent == null) { - this.GraphicsDevice.Clear(this.bgColor); - if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet()) + using (List.Enumerator enumerator = Game1.currentLocation.characters.GetEnumerator()) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - try - { - Game1.activeClickableMenu.drawBackground(Game1.spriteBatch); - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) + while (enumerator.MoveNext()) { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); + NPC current = enumerator.Current; + if (current != null && !current.swimming && !current.hideShadow && !current.IsMonster && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(current.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, current.position + new Vector2(current.sprite.spriteWidth * Game1.pixelZoom / 2f, current.GetBoundingBox().Height + (current.IsMonster ? 0 : (Game1.pixelZoom * 3)))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (Game1.pixelZoom + current.yJumpOffset / 40f) * current.scale, SpriteEffects.None, Math.Max(0f, current.getStandingY() / 10000f) - 1E-06f); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } - else if ((int)Game1.gameMode == 11) - { - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3685"), new Vector2(16f, 16f), Color.HotPink); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3686"), new Vector2(16f, 32f), new Color(0, (int)byte.MaxValue, 0)); - Game1.spriteBatch.DrawString(Game1.dialogueFont, Game1.parseText(Game1.errorMessage, Game1.dialogueFont, Game1.graphics.GraphicsDevice.Viewport.Width), new Vector2(16f, 48f), Color.White); - Game1.spriteBatch.End(); + goto IL_B30; } - else if (Game1.currentMinigame != null) + } + foreach (NPC current2 in Game1.CurrentEvent.actors) + { + if (!current2.swimming && !current2.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(current2.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, current2.position + new Vector2(current2.sprite.spriteWidth * Game1.pixelZoom / 2f, current2.GetBoundingBox().Height + (current2.IsMonster ? 0 : (Game1.pixelZoom * 3)))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (Game1.pixelZoom + current2.yJumpOffset / 40f) * current2.scale, SpriteEffects.None, Math.Max(0f, current2.getStandingY() / 10000f) - 1E-06f); + } + IL_B30: + if (!Game1.player.swimming && !Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((Game1.player.running || Game1.player.usingTool) && Game1.player.FarmerSprite.indexInCurrentAnimation > 1) ? (Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, 0f); + Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (Game1.CurrentEvent == null) + { + using (List.Enumerator enumerator3 = Game1.currentLocation.characters.GetEnumerator()) { - Game1.currentMinigame.draw(Game1.spriteBatch); - if (Game1.globalFade && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha)); - Game1.spriteBatch.End(); - } - if ((double)Game1.options.zoomLevel != 1.0) + while (enumerator3.MoveNext()) { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); + NPC current3 = enumerator3.Current; + if (current3 != null && !current3.swimming && !current3.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(current3.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, current3.position + new Vector2(current3.sprite.spriteWidth * Game1.pixelZoom / 2f, current3.GetBoundingBox().Height + (current3.IsMonster ? 0 : (Game1.pixelZoom * 3)))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (Game1.pixelZoom + current3.yJumpOffset / 40f) * current3.scale, SpriteEffects.None, Math.Max(0f, current3.getStandingY() / 10000f) - 1E-06f); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + goto IL_F5F; } - else if (Game1.showingEndOfNightStuff) + } + foreach (NPC current4 in Game1.CurrentEvent.actors) + { + if (!current4.swimming && !current4.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(current4.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, current4.position + new Vector2(current4.sprite.spriteWidth * Game1.pixelZoom / 2f, current4.GetBoundingBox().Height + (current4.IsMonster ? 0 : (Game1.pixelZoom * 3)))), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), (Game1.pixelZoom + current4.yJumpOffset / 40f) * current4.scale, SpriteEffects.None, Math.Max(0f, current4.getStandingY() / 10000f) - 1E-06f); + } + IL_F5F: + if (!Game1.player.swimming && !Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation())) + Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)), Game1.shadowTexture.Bounds, Color.White, 0f, new Vector2(Game1.shadowTexture.Bounds.Center.X, Game1.shadowTexture.Bounds.Center.Y), 4f - (((Game1.player.running || Game1.player.usingTool) && Game1.player.FarmerSprite.indexInCurrentAnimation > 1) ? (Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5f) : 0f), SpriteEffects.None, Math.Max(0.0001f, Game1.player.getStandingY() / 10000f + 0.00011f) - 0.0001f); + if (Game1.displayFarmer) + Game1.player.draw(Game1.spriteBatch); + if ((Game1.eventUp || Game1.killScreen) && !Game1.killScreen) + Game1.currentLocation.currentEvent?.draw(Game1.spriteBatch); + if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) + Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), Game1.player.currentUpgrade.getSourceRectangle(), Color.White, 0f, Vector2.Zero, 1f, SpriteEffects.None, (Game1.player.currentUpgrade.positionOfCarpenter.Y + Game1.tileSize * 3 / 4) / 10000f); + Game1.currentLocation.draw(Game1.spriteBatch); + if (Game1.eventUp && Game1.currentLocation.currentEvent?.messageToScreen != null) + Game1.drawWithBorder(Game1.currentLocation.currentEvent.messageToScreen, Color.Black, Color.White, new Vector2(Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width / 2 - Game1.borderFont.MeasureString(Game1.currentLocation.currentEvent.messageToScreen).X / 2f, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Height - Game1.tileSize), 0f, 1f, 0.999f); + if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool)) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Name.Equals("Farm")) + this.drawFarmBuildings(); + if (Game1.tvStation >= 0) + Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2(6 * Game1.tileSize + Game1.tileSize / 4, 2 * Game1.tileSize + Game1.tileSize / 2)), new Rectangle(Game1.tvStation * 24, 0, 24, 15), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); + if (Game1.panMode) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Rectangle((int)Math.Floor((Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f); + foreach (Warp current5 in Game1.currentLocation.warps) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Rectangle(current5.X * Game1.tileSize - Game1.viewport.X, current5.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f); + } + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); + Game1.spriteBatch.End(); + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200u) + { + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2(3 * Game1.tileSize + Game1.tileSize / 4, Game1.tileSize + Game1.tileSize / 3)), Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2(4 * Game1.tileSize + Game1.tileSize, 2 * Game1.tileSize + Game1.tileSize)), Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2(5 * Game1.tileSize, 2 * Game1.tileSize)), Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2(3 * Game1.tileSize + Game1.tileSize / 2, 3 * Game1.tileSize)), Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2(5 * Game1.tileSize - Game1.tileSize / 4, Game1.tileSize)), Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2(4 * Game1.tileSize, 3 * Game1.tileSize + Game1.tileSize / 6)), Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16), Color.White); + Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2(4 * Game1.tileSize + Game1.tileSize / 5, 2 * Game1.tileSize + Game1.tileSize / 3)), Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16), Color.White); + } + if (Game1.displayFarmer && Game1.player.ActiveObject != null && Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer() && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + Game1.drawPlayerHeldObject(Game1.player); + else if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) || (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")))) + Game1.drawPlayerHeldObject(Game1.player); + if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) + Game1.drawTool(Game1.player); + if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) + { + Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); + Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); + Game1.mapDisplayDevice.EndScene(); + } + if (Game1.toolHold > 400f && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) + { + Color color = Color.White; + switch ((int)(Game1.toolHold / 600f) + 2) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.activeClickableMenu != null) - { - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself during end-of-night-stuff. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) - { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); - } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); + case 1: + color = Tool.copperColor; + break; + case 2: + color = Tool.steelColor; + break; + case 3: + color = Tool.goldColor; + break; + case 4: + color = Tool.iridiumColor; + break; } - else if ((int)Game1.gameMode == 6) + Game1.spriteBatch.Draw(Game1.littleEffect, new Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)(Game1.toolHold % 600f * 0.08f) + 4, Game1.tileSize / 8 + 4), Color.Black); + Game1.spriteBatch.Draw(Game1.littleEffect, new Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)(Game1.toolHold % 600f * 0.08f), Game1.tileSize / 8), color); + } + if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && !Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert") && Game1.viewport.X > -10) + { + foreach (WeatherDebris current6 in Game1.debrisWeather) + current6.draw(Game1.spriteBatch); + } + Game1.farmEvent?.draw(Game1.spriteBatch); + if (Game1.currentLocation.LightLevel > 0f && Game1.timeOfDay < 2000) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); + if (Game1.screenGlow) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); + Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); + if (Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0f || (Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)) + Game1.player.CurrentTool.draw(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation.IsOutdoors && !Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2(Game1.viewport.X / Game1.tileSize, Game1.viewport.Y / Game1.tileSize)))) + { + for (int j = 0; j < Game1.rainDrops.Length; j++) + Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[j].position, Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[j].frame), Color.White); + } + + Game1.spriteBatch.End(); + + //base.Draw(gameTime); + + Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (Game1.eventUp && Game1.currentLocation.currentEvent != null) + { + foreach (NPC current7 in Game1.currentLocation.currentEvent.actors) { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - string str1 = ""; - for (int index = 0; (double)index < gameTime.TotalGameTime.TotalMilliseconds % 999.0 / 333.0; ++index) - str1 += "."; - string str2 = Game1.content.LoadString("Strings\\StringsFromCSFiles:Game1.cs.3688"); - string str3 = str1; - string s = str2 + str3; - string str4 = "..."; - string str5 = str2 + str4; - int widthOfString = SpriteText.getWidthOfString(str5); - int height = 64; - int x = 64; - int y = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - height; - SpriteText.drawString(Game1.spriteBatch, s, x, y, 999999, widthOfString, height, 1f, 0.88f, false, 0, str5, -1); - Game1.spriteBatch.End(); - if ((double)Game1.options.zoomLevel != 1.0) + if (current7.isEmoting) { - this.GraphicsDevice.SetRenderTarget((RenderTarget2D)null); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.AlphaBlend, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); - Game1.spriteBatch.Draw((Texture2D)this.screenWrapper, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(this.screenWrapper.Bounds), Color.White, 0.0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); - Game1.spriteBatch.End(); + Vector2 localPosition = current7.getLocalPosition(Game1.viewport); + localPosition.Y -= Game1.tileSize * 2 + Game1.pixelZoom * 3; + if (current7.age == 2) + localPosition.Y += Game1.tileSize / 2; + else if (current7.gender == 1) + localPosition.Y += Game1.tileSize / 6; + Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Rectangle(current7.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, current7.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4), Color.White, 0f, Vector2.Zero, 4f, SpriteEffects.None, current7.getStandingY() / 10000f); } - if (Game1.overlayMenu == null) - return; - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); } - else + } + Game1.spriteBatch.End(); + if (Game1.drawLighting) + { + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, new BlendState { - Microsoft.Xna.Framework.Rectangle rectangle; - if ((int)Game1.gameMode == 0) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - } - else - { - if (Game1.drawLighting) - { - this.GraphicsDevice.SetRenderTarget(Game1.lightmap); - this.GraphicsDevice.Clear(Color.White * 0.0f); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, Game1.currentLocation.name.Equals("UndergroundMine") ? Game1.mine.getLightingColor(gameTime) : (Game1.ambientLight.Equals(Color.White) || Game1.isRaining && Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight)); - for (int index = 0; index < Game1.currentLightSources.Count; ++index) - { - if (Utility.isOnScreen(Game1.currentLightSources.ElementAt(index).position, (int)((double)Game1.currentLightSources.ElementAt(index).radius * (double)Game1.tileSize * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, Game1.currentLightSources.ElementAt(index).position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), Game1.currentLightSources.ElementAt(index).color, 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), Game1.currentLightSources.ElementAt(index).radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); - } - Game1.spriteBatch.End(); - this.GraphicsDevice.SetRenderTarget((double)Game1.options.zoomLevel == 1.0 ? (RenderTarget2D)null : this.screenWrapper); - } - if (Game1.bloomDay && Game1.bloom != null) - Game1.bloom.BeginDraw(); - this.GraphicsDevice.Clear(this.bgColor); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - GraphicsEvents.InvokeOnPreRenderEvent(this.Monitor); - if (Game1.background != null) - Game1.background.draw(Game1.spriteBatch); - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Back").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.currentLocation.drawWater(Game1.spriteBatch); - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!character.swimming && !character.hideShadow && (!character.isInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)character.yJumpOffset / 40f) * character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!actor.swimming && !actor.hideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.sprite.spriteHeight <= 16 ? -Game1.pixelZoom : Game1.pixelZoom * 3))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), ((float)Game1.pixelZoom + (float)actor.yJumpOffset / 40f) * actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); - } - } - Microsoft.Xna.Framework.Rectangle bounds; - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = 0.0; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - Game1.currentLocation.Map.GetLayer("Buildings").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.CurrentEvent == null) - { - foreach (NPC character in Game1.currentLocation.characters) - { - if (!character.swimming && !character.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, character.position + new Vector2((float)(character.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)character.yJumpOffset / 40.0) * (double)character.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - } - } - else - { - foreach (NPC actor in Game1.CurrentEvent.actors) - { - if (!actor.swimming && !actor.hideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.viewport, actor.position + new Vector2((float)(actor.sprite.spriteWidth * Game1.pixelZoom) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : Game1.pixelZoom * 3)))); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - bounds = Game1.shadowTexture.Bounds; - double x = (double)bounds.Center.X; - bounds = Game1.shadowTexture.Bounds; - double y = (double)bounds.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = ((double)Game1.pixelZoom + (double)actor.yJumpOffset / 40.0) * (double)actor.scale; - int num3 = 0; - double num4 = (double)Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 9.99999997475243E-07; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - } - } - if (Game1.displayFarmer && !Game1.player.swimming && (!Game1.player.isRidingHorse() && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(Game1.player.getTileLocation()))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D shadowTexture = Game1.shadowTexture; - Vector2 local = Game1.GlobalToLocal(Game1.player.position + new Vector2(32f, 24f)); - Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds); - Color white = Color.White; - double num1 = 0.0; - double x = (double)Game1.shadowTexture.Bounds.Center.X; - rectangle = Game1.shadowTexture.Bounds; - double y = (double)rectangle.Center.Y; - Vector2 origin = new Vector2((float)x, (float)y); - double num2 = 4.0 - (!Game1.player.running && !Game1.player.usingTool || Game1.player.FarmerSprite.indexInCurrentAnimation <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[Game1.player.FarmerSprite.CurrentFrame]) * 0.5); - int num3 = 0; - double num4 = (double)Math.Max(0.0001f, (float)((double)Game1.player.getStandingY() / 10000.0 + 0.000110000000859145)) - 9.99999974737875E-05; - spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, (float)num1, origin, (float)num2, (SpriteEffects)num3, (float)num4); - } - if (Game1.displayFarmer) - Game1.player.draw(Game1.spriteBatch); - if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null)) - Game1.currentLocation.currentEvent.draw(Game1.spriteBatch); - if (Game1.player.currentUpgrade != null && Game1.player.currentUpgrade.daysLeftTillUpgradeDone <= 3 && Game1.currentLocation.Name.Equals("Farm")) - Game1.spriteBatch.Draw(Game1.player.currentUpgrade.workerTexture, Game1.GlobalToLocal(Game1.viewport, Game1.player.currentUpgrade.positionOfCarpenter), new Microsoft.Xna.Framework.Rectangle?(Game1.player.currentUpgrade.getSourceRectangle()), Color.White, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, (float)(((double)Game1.player.currentUpgrade.positionOfCarpenter.Y + (double)(Game1.tileSize * 3 / 4)) / 10000.0)); - Game1.currentLocation.draw(Game1.spriteBatch); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - string messageToScreen = Game1.currentLocation.currentEvent.messageToScreen; - } - if (Game1.player.ActiveObject == null && (Game1.player.UsingTool || Game1.pickingTool) && (Game1.player.CurrentTool != null && (!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Name.Equals("Farm")) - this.drawFarmBuildings(); - if (Game1.tvStation >= 0) - Game1.spriteBatch.Draw(Game1.tvStationTexture, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(6 * Game1.tileSize + Game1.tileSize / 4), (float)(2 * Game1.tileSize + Game1.tileSize / 2))), new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(Game1.tvStation * 24, 0, 24, 15)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, 1E-08f); - if (Game1.panMode) - { - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((int)Math.Floor((double)(Game1.getOldMouseX() + Game1.viewport.X) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.X, (int)Math.Floor((double)(Game1.getOldMouseY() + Game1.viewport.Y) / (double)Game1.tileSize) * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Lime * 0.75f); - foreach (Warp warp in Game1.currentLocation.warps) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle(warp.X * Game1.tileSize - Game1.viewport.X, warp.Y * Game1.tileSize - Game1.viewport.Y, Game1.tileSize, Game1.tileSize), Color.Red * 0.75f); - } - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("Front").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); - Game1.spriteBatch.End(); - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.currentLocation.Name.Equals("Farm") && Game1.stats.SeedsSown >= 200U) - { - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 4), (float)(Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize), (float)(2 * Game1.tileSize + Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize), (float)(2 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(3 * Game1.tileSize + Game1.tileSize / 2), (float)(3 * Game1.tileSize))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(5 * Game1.tileSize - Game1.tileSize / 4), (float)Game1.tileSize)), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize), (float)(3 * Game1.tileSize + Game1.tileSize / 6))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - Game1.spriteBatch.Draw(Game1.debrisSpriteSheet, Game1.GlobalToLocal(Game1.viewport, new Vector2((float)(4 * Game1.tileSize + Game1.tileSize / 5), (float)(2 * Game1.tileSize + Game1.tileSize / 3))), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.debrisSpriteSheet, 16, -1, -1)), Color.White); - } - if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null) - Game1.drawPlayerHeldObject(Game1.player); - else if (Game1.displayFarmer && Game1.player.ActiveObject != null) - { - if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.position.X, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")) - { - Layer layer1 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size1 = Game1.viewport.Size; - if (layer1.PickTile(mapDisplayLocation1, size1) != null) - { - Layer layer2 = Game1.currentLocation.Map.GetLayer("Front"); - rectangle = Game1.player.GetBoundingBox(); - Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.position.Y - Game1.tileSize * 3 / 5); - Size size2 = Game1.viewport.Size; - if (layer2.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways")) - goto label_127; - } - else - goto label_127; - } - Game1.drawPlayerHeldObject(Game1.player); - } - label_127: - if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.position.Y - Game1.tileSize * 3 / 5), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null))) - Game1.drawTool(Game1.player); - if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) - { - Game1.mapDisplayDevice.BeginScene(Game1.spriteBatch); - Game1.currentLocation.Map.GetLayer("AlwaysFront").Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, Game1.pixelZoom); - Game1.mapDisplayDevice.EndScene(); - } - if ((double)Game1.toolHold > 400.0 && Game1.player.CurrentTool.UpgradeLevel >= 1 && Game1.player.canReleaseTool) - { - Color color = Color.White; - switch ((int)((double)Game1.toolHold / 600.0) + 2) - { - case 1: - color = Tool.copperColor; - break; - case 2: - color = Tool.steelColor; - break; - case 3: - color = Tool.goldColor; - break; - case 4: - color = Tool.iridiumColor; - break; - } - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X - 2, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize) - 2, (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607) + 4, Game1.tileSize / 8 + 4), Color.Black); - Game1.spriteBatch.Draw(Game1.littleEffect, new Microsoft.Xna.Framework.Rectangle((int)Game1.player.getLocalPosition(Game1.viewport).X, (int)Game1.player.getLocalPosition(Game1.viewport).Y - (Game1.player.CurrentTool.Name.Equals("Watering Can") ? 0 : Game1.tileSize), (int)((double)Game1.toolHold % 600.0 * 0.0799999982118607), Game1.tileSize / 8), color); - } - if (Game1.isDebrisWeather && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.ignoreDebrisWeather && !Game1.currentLocation.Name.Equals("Desert")) && Game1.viewport.X > -10) - { - foreach (WeatherDebris weatherDebris in Game1.debrisWeather) - weatherDebris.draw(Game1.spriteBatch); - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * Game1.currentLocation.LightLevel); - if (Game1.screenGlow) - Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha); - Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); - if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure))) - Game1.player.CurrentTool.draw(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation.IsOutdoors && (!Game1.currentLocation.Name.Equals("Desert") && !(Game1.currentLocation is Summit)) && (!Game1.eventUp || Game1.currentLocation.isTileOnMap(new Vector2((float)(Game1.viewport.X / Game1.tileSize), (float)(Game1.viewport.Y / Game1.tileSize))))) - { - for (int index = 0; index < Game1.rainDrops.Length; ++index) - Game1.spriteBatch.Draw(Game1.rainTexture, Game1.rainDrops[index].position, new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.rainTexture, Game1.rainDrops[index].frame, -1, -1)), Color.White); - } - Game1.spriteBatch.End(); - //base.Draw(gameTime); - Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.eventUp && Game1.currentLocation.currentEvent != null) - { - foreach (NPC actor in Game1.currentLocation.currentEvent.actors) - { - if (actor.isEmoting) - { - Vector2 localPosition = actor.getLocalPosition(Game1.viewport); - localPosition.Y -= (float)(Game1.tileSize * 2 + Game1.pixelZoom * 3); - if (actor.age == 2) - localPosition.Y += (float)(Game1.tileSize / 2); - else if (actor.gender == 1) - localPosition.Y += (float)(Game1.tileSize / 6); - Game1.spriteBatch.Draw(Game1.emoteSpriteSheet, localPosition, new Microsoft.Xna.Framework.Rectangle?(new Microsoft.Xna.Framework.Rectangle(actor.CurrentEmoteIndex * (Game1.tileSize / 4) % Game1.emoteSpriteSheet.Width, actor.CurrentEmoteIndex * (Game1.tileSize / 4) / Game1.emoteSpriteSheet.Width * (Game1.tileSize / 4), Game1.tileSize / 4, Game1.tileSize / 4)), Color.White, 0.0f, Vector2.Zero, 4f, SpriteEffects.None, (float)actor.getStandingY() / 10000f); - } - } - } - Game1.spriteBatch.End(); - if (Game1.drawLighting) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f); - if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) - Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); - Game1.spriteBatch.End(); - } - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - if (Game1.drawGrid) - { - int x1 = -Game1.viewport.X % Game1.tileSize; - float num1 = (float)(-Game1.viewport.Y % Game1.tileSize); - int x2 = x1; - while (x2 < Game1.graphics.GraphicsDevice.Viewport.Width) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x2, (int)num1, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); - x2 += Game1.tileSize; - } - float num2 = num1; - while ((double)num2 < (double)Game1.graphics.GraphicsDevice.Viewport.Height) - { - Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle(x1, (int)num2, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); - num2 += (float)Game1.tileSize; - } - } - if (Game1.currentBillboard != 0) - this.drawBillboard(); - if ((Game1.displayHUD || Game1.eventUp) && (Game1.currentBillboard == 0 && (int)Game1.gameMode == 3) && (!Game1.freezeControls && !Game1.panMode)) - { - GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); - this.drawHUD(); - GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); - } - else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) - Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2((float)Game1.getOldMouseX(), (float)Game1.getOldMouseY()), new Microsoft.Xna.Framework.Rectangle?(Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16)), Color.White, 0.0f, Vector2.Zero, (float)(4.0 + (double)Game1.dialogueButtonScale / 150.0), SpriteEffects.None, 1f); - if (Game1.hudMessages.Count > 0 && (!Game1.eventUp || Game1.isFestival())) - { - for (int i = Game1.hudMessages.Count - 1; i >= 0; --i) - Game1.hudMessages[i].draw(Game1.spriteBatch, i); - } - } - if (Game1.farmEvent != null) - Game1.farmEvent.draw(Game1.spriteBatch); - if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && (Game1.activeClickableMenu == null || !(Game1.activeClickableMenu is DialogueBox))) - this.drawDialogueBox(); - Viewport viewport; - if (Game1.progressBar) - { - SpriteBatch spriteBatch1 = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - int x1 = (Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - rectangle = Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea; - int y1 = rectangle.Bottom - Game1.tileSize * 2; - int dialogueWidth = Game1.dialogueWidth; - int height1 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, height1); - Color lightGray = Color.LightGray; - spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray); - SpriteBatch spriteBatch2 = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - int x2 = (viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2; - viewport = Game1.graphics.GraphicsDevice.Viewport; - rectangle = viewport.TitleSafeArea; - int y2 = rectangle.Bottom - Game1.tileSize * 2; - int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth); - int height2 = Game1.tileSize / 2; - Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, height2); - Color dimGray = Color.DimGray; - spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray); - } - if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null) - Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); - if (Game1.isRaining && Game1.currentLocation != null && (Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D staminaRect = Game1.staminaRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Blue * 0.2f; - spriteBatch.Draw(staminaRect, bounds, color); - } - if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.Black * ((int)Game1.gameMode == 0 ? 1f - Game1.fadeToBlackAlpha : Game1.fadeToBlackAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - else if ((double)Game1.flashAlpha > 0.0) - { - if (Game1.options.screenFlash) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - Texture2D fadeToBlackRect = Game1.fadeToBlackRect; - viewport = Game1.graphics.GraphicsDevice.Viewport; - Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds; - Color color = Color.White * Math.Min(1f, Game1.flashAlpha); - spriteBatch.Draw(fadeToBlackRect, bounds, color); - } - Game1.flashAlpha -= 0.1f; - } - if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) - this.drawDialogueBox(); - foreach (TemporaryAnimatedSprite overlayTempSprite in Game1.screenOverlayTempSprites) - overlayTempSprite.draw(Game1.spriteBatch, true, 0, 0); - if (Game1.debugMode) - { - SpriteBatch spriteBatch = Game1.spriteBatch; - SpriteFont smallFont = Game1.smallFont; - object[] objArray = new object[10]; - int index1 = 0; - string str1; - if (!Game1.panMode) - str1 = "player: " + (object)(Game1.player.getStandingX() / Game1.tileSize) + ", " + (object)(Game1.player.getStandingY() / Game1.tileSize); - else - str1 = ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize).ToString() + "," + (object)((Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize); - objArray[index1] = (object)str1; - int index2 = 1; - string str2 = " mouseTransparency: "; - objArray[index2] = (object)str2; - int index3 = 2; - float cursorTransparency = Game1.mouseCursorTransparency; - objArray[index3] = (object)cursorTransparency; - int index4 = 3; - string str3 = " mousePosition: "; - objArray[index4] = (object)str3; - int index5 = 4; - int mouseX = Game1.getMouseX(); - objArray[index5] = (object)mouseX; - int index6 = 5; - string str4 = ","; - objArray[index6] = (object)str4; - int index7 = 6; - int mouseY = Game1.getMouseY(); - objArray[index7] = (object)mouseY; - int index8 = 7; - string newLine = Environment.NewLine; - objArray[index8] = (object)newLine; - int index9 = 8; - string str5 = "debugOutput: "; - objArray[index9] = (object)str5; - int index10 = 9; - string debugOutput = Game1.debugOutput; - objArray[index10] = (object)debugOutput; - string text = string.Concat(objArray); - Vector2 position = new Vector2((float)this.GraphicsDevice.Viewport.TitleSafeArea.X, (float)this.GraphicsDevice.Viewport.TitleSafeArea.Y); - Color red = Color.Red; - double num1 = 0.0; - Vector2 zero = Vector2.Zero; - double num2 = 1.0; - int num3 = 0; - double num4 = 0.99999988079071; - spriteBatch.DrawString(smallFont, text, position, red, (float)num1, zero, (float)num2, (SpriteEffects)num3, (float)num4); - } - if (Game1.showKeyHelp) - Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2((float)Game1.tileSize, (float)(Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? Game1.tileSize * 3 + (Game1.isQuestion ? Game1.questionChoices.Count * Game1.tileSize : 0) : 0)) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0.0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); - if (Game1.activeClickableMenu != null) - { - try - { - GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); - Game1.activeClickableMenu.draw(Game1.spriteBatch); - GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); - } - catch (Exception ex) - { - this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); - Game1.activeClickableMenu.exitThisMenu(); - } - } - else if (Game1.farmEvent != null) - Game1.farmEvent.drawAboveEverything(Game1.spriteBatch); - Game1.spriteBatch.End(); - if (Game1.overlayMenu != null) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); - Game1.overlayMenu.draw(Game1.spriteBatch); - Game1.spriteBatch.End(); - } + ColorBlendFunction = BlendFunction.ReverseSubtract, + ColorDestinationBlend = Blend.One, + ColorSourceBlend = Blend.SourceColor + }, SamplerState.LinearClamp, null, null); + Game1.spriteBatch.Draw(Game1.lightmap, Vector2.Zero, Game1.lightmap.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.lightingQuality, SpriteEffects.None, 1f); + if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + { + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.OrangeRed * 0.45f); + } + Game1.spriteBatch.End(); + } + Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); + if (Game1.drawGrid) + { + int num2 = -Game1.viewport.X % Game1.tileSize; + float num3 = -(float)Game1.viewport.Y % Game1.tileSize; + for (int k = num2; k < Game1.graphics.GraphicsDevice.Viewport.Width; k += Game1.tileSize) + Game1.spriteBatch.Draw(Game1.staminaRect, new Rectangle(k, (int)num3, 1, Game1.graphics.GraphicsDevice.Viewport.Height), Color.Red * 0.5f); + for (float num4 = num3; num4 < (float)Game1.graphics.GraphicsDevice.Viewport.Height; num4 += (float)Game1.tileSize) + Game1.spriteBatch.Draw(Game1.staminaRect, new Rectangle(num2, (int)num4, Game1.graphics.GraphicsDevice.Viewport.Width, 1), Color.Red * 0.5f); + } + if (Game1.currentBillboard != 0) + this.drawBillboard(); - if (GraphicsEvents.HasPostRenderListeners()) - { - Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, null, null); - GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); - Game1.spriteBatch.End(); - } + if ((Game1.displayHUD || Game1.eventUp) && Game1.currentBillboard == 0 && Game1.gameMode == 3 && !Game1.freezeControls && !Game1.panMode) + { + GraphicsEvents.InvokeOnPreRenderHudEvent(this.Monitor); + this.drawHUD(); + GraphicsEvents.InvokeOnPostRenderHudEvent(this.Monitor); + } + else if (Game1.activeClickableMenu == null && Game1.farmEvent == null) + Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2(Game1.getOldMouseX(), Game1.getOldMouseY()), Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, 0, 16, 16), Color.White, 0f, Vector2.Zero, 4f + Game1.dialogueButtonScale / 150f, SpriteEffects.None, 1f); - this.renderScreenBuffer(); - } + if (Game1.hudMessages.Any() && (!Game1.eventUp || Game1.isFestival())) + { + for (int l = Game1.hudMessages.Count - 1; l >= 0; l--) + Game1.hudMessages[l].draw(Game1.spriteBatch, l); + } + } + Game1.farmEvent?.draw(Game1.spriteBatch); + if (Game1.dialogueUp && !Game1.nameSelectUp && !Game1.messagePause && !(Game1.activeClickableMenu is DialogueBox)) + this.drawDialogueBox(); + if (Game1.progressBar) + { + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Rectangle((Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - Game1.tileSize * 2, Game1.dialogueWidth, Game1.tileSize / 2), Color.LightGray); + Game1.spriteBatch.Draw(Game1.staminaRect, new Rectangle((Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.TitleSafeArea.Bottom - Game1.tileSize * 2, (int)(Game1.pauseAccumulator / Game1.pauseTime * Game1.dialogueWidth), Game1.tileSize / 2), Color.DimGray); + } + if (Game1.eventUp) + Game1.currentLocation.currentEvent?.drawAfterMap(Game1.spriteBatch); + if (Game1.isRaining && Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)) + Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Blue * 0.2f); + if ((Game1.fadeToBlack || Game1.globalFade) && !Game1.menuUp && (!Game1.nameSelectUp || Game1.messagePause)) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.Black * ((Game1.gameMode == 0) ? (1f - Game1.fadeToBlackAlpha) : Game1.fadeToBlackAlpha)); + else if (Game1.flashAlpha > 0f) + { + if (Game1.options.screenFlash) + Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Color.White * Math.Min(1f, Game1.flashAlpha)); + Game1.flashAlpha -= 0.1f; + } + if ((Game1.messagePause || Game1.globalFade) && Game1.dialogueUp) + this.drawDialogueBox(); + foreach (TemporaryAnimatedSprite current8 in Game1.screenOverlayTempSprites) + current8.draw(Game1.spriteBatch, true); + if (Game1.debugMode) + { + Game1.spriteBatch.DrawString(Game1.smallFont, string.Concat(new object[] + { + Game1.panMode ? ((Game1.getOldMouseX() + Game1.viewport.X) / Game1.tileSize + "," + (Game1.getOldMouseY() + Game1.viewport.Y) / Game1.tileSize) : string.Concat("aplayer: ", Game1.player.getStandingX() / Game1.tileSize, ", ", Game1.player.getStandingY() / Game1.tileSize), + Environment.NewLine, + "debugOutput: ", + Game1.debugOutput + }), new Vector2(this.GraphicsDevice.Viewport.TitleSafeArea.X, this.GraphicsDevice.Viewport.TitleSafeArea.Y), Color.Red, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + } + /*if (inputMode) + { + spriteBatch.DrawString(smallFont, "Input: " + debugInput, new Vector2(tileSize, tileSize * 3), Color.Purple); + }*/ + if (Game1.showKeyHelp) + Game1.spriteBatch.DrawString(Game1.smallFont, Game1.keyHelpString, new Vector2(Game1.tileSize, Game1.viewport.Height - Game1.tileSize - (Game1.dialogueUp ? (Game1.tileSize * 3 + (Game1.isQuestion ? (Game1.questionChoices.Count * Game1.tileSize) : 0)) : 0) - Game1.smallFont.MeasureString(Game1.keyHelpString).Y), Color.LightGray, 0f, Vector2.Zero, 1f, SpriteEffects.None, 0.9999999f); + + if (Game1.activeClickableMenu != null) + { + GraphicsEvents.InvokeOnPreRenderGuiEvent(this.Monitor); + try + { + Game1.activeClickableMenu.draw(Game1.spriteBatch); + } + catch (Exception ex) + { + this.Monitor.Log($"The {Game1.activeClickableMenu.GetType().FullName} menu crashed while drawing itself. SMAPI will force it to exit to avoid crashing the game.\n{ex.GetLogSummary()}", LogLevel.Error); + Game1.activeClickableMenu.exitThisMenu(); } + GraphicsEvents.InvokeOnPostRenderGuiEvent(this.Monitor); + } + else + Game1.farmEvent?.drawAboveEverything(Game1.spriteBatch); + + GraphicsEvents.InvokeOnPostRenderEvent(this.Monitor); + Game1.spriteBatch.End(); + + if (!this.ZoomLevelIsOne) + { + this.GraphicsDevice.SetRenderTarget(null); + this.GraphicsDevice.Clear(this.bgColor); + Game1.spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.LinearClamp, DepthStencilState.Default, RasterizerState.CullNone); + Game1.spriteBatch.Draw(this.screenWrapper, Vector2.Zero, this.screenWrapper.Bounds, Color.White, 0f, Vector2.Zero, Game1.options.zoomLevel, SpriteEffects.None, 1f); + Game1.spriteBatch.End(); } } catch (Exception ex) @@ -1076,19 +801,8 @@ namespace StardewModdingAPI.Framework /// Detect changes since the last update ticket and trigger mod events. private void UpdateEventCalls() { - // content locale changed event - if (this.PreviousLocale != LocalizedContentManager.CurrentLanguageCode) - { - var oldValue = this.PreviousLocale; - var newValue = LocalizedContentManager.CurrentLanguageCode; - - if (oldValue != null) - ContentEvents.InvokeAfterLocaleChanged(this.Monitor, oldValue.ToString(), newValue.ToString()); - this.PreviousLocale = newValue; - } - // save loaded event - if (Constants.IsSaveLoaded && !SaveGame.IsProcessing/*still loading save*/ && this.AfterLoadTimer >= 0) + if (Constants.IsSaveLoaded && this.AfterLoadTimer >= 0) { if (this.AfterLoadTimer == 0) { @@ -1346,4 +1060,4 @@ namespace StardewModdingAPI.Framework return hash; } } -} +} \ No newline at end of file diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 276b66ce..e11f411e 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -219,7 +219,10 @@ namespace StardewModdingAPI this.GameInstance.Exiting += (sender, e) => this.IsGameRunning = false; this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); this.GameInstance.Window.Title = $"Stardew Valley {Constants.GameVersion} with SMAPI {Constants.ApiVersion}"; - StardewValley.Program.gamePtr = this.GameInstance; + { + Type type = typeof(Game1).Assembly.GetType("StardewValley.Program", true); + type.GetField("gamePtr").SetValue(null, this.GameInstance); + } // configure Game1.graphics.GraphicsProfile = GraphicsProfile.HiDef; diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index a254fdd0..0b6f3a37 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -27,15 +27,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha * Changing this field is not recommended and may destabilise your game. */ "ModCompatibility": [ - { - "Name": "AccessChestAnywhere", - "ID": "AccessChestAnywhere", - "UpperVersion": "1.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/257", - "UnofficialUpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." - }, { "Name": "Almighty Tool", "ID": "AlmightyTool.dll", @@ -50,7 +41,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpperVersion": "2.3", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", - "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/125031", + "Notes": "Uses obsolete StardewModdingAPI.Extensions." + }, + { + "Name": "Better Sprinklers", + "ID": "Speeder.BetterSprinklers", + "UpperVersion": "2.3", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", "Notes": "Uses obsolete StardewModdingAPI.Extensions." }, { @@ -62,36 +60,26 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Notes": "Not compatible with Stardew Valley 1.1+" }, { - "Name": "Chests Anywhere", - "ID": "ChestsAnywhere", - "UpperVersion": "1.8.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "Crashes with 'Method not found: Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'." - }, - { - "Name": "Chests Anywhere", - "ID": "Pathoschild.ChestsAnywhere", - "UpperVersion": "1.9-beta", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/518", - "Notes": "ID changed in 1.9. Crashes with InvalidOperationException: 'The menu doesn't seem to have a player inventory'." + "Name": "CJB Cheats Menu", + "ID": "CJBCheatsMenu", + "UpperVersion": "1.12", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/4", + "Notes": "Not compatible with Stardew Valley 1.1+" }, { - "Name": "CJB Automation", - "ID": "CJBAutomation", - "UpperVersion": "1.4", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/211", - "Notes": "Crashes with 'Method not found: Void StardewValley.Item.set_Name(System.String)'." + "Name": "CJB Item Spawner", + "ID": "CJBItemSpawner", + "UpperVersion": "1.5", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", + "Notes": "Not compatible with Stardew Valley 1.1+" }, { - "Name": "Cooking Skill", - "ID": "CookingSkill", - "UpperVersion": "1.0.3", + "Name": "CJB Show Item Sell Price", + "ID": "CJBShowItemSellPrice", + "UpperVersion": "1.6", "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/522", - "Notes": "Crashes with 'Method not found: Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'." + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/93", + "Notes": "Uses SMAPI's internal SGame class." }, { "Name": "Enemy Health Bars", @@ -109,38 +97,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrl": "http://community.playstarbound.com/resources/4228", "Notes": "Uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ('Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)')." }, - { - "Name": "Extended Fridge", - "ID": "Mystra007ExtendedFridge", - "UpperVersion": "1.0", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/485", - "Notes": "Actual upper version is 0.94, but mod incorrectly sets it to 1.0 in the manifest. Crashes with 'Field not found: StardewValley.Game1.mouseCursorTransparency'." - }, - { - "Name": "Get Dressed", - "ID": "GetDressed.dll", - "UpperVersion": "3.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/331", - "Notes": "Crashes with NullReferenceException in GameEvents.UpdateTick." - }, - { - "Name": "Lookup Anything", - "ID": "LookupAnything", - "UpperVersion": "1.10", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "Crashes with FormatException when looking up NPCs." - }, - { - "Name": "Lookup Anything", - "ID": "Pathoschild.LookupAnything", - "UpperVersion": "1.10.1", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/541", - "Notes": "ID changed in 1.10.1. Crashes with FormatException when looking up NPCs." - }, { "Name": "Makeshift Multiplayer", "ID": "StardewValleyMP", @@ -155,7 +111,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpperVersion": "0.5", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/237", - "Notes": "Uses Assembly.GetExecutingAssembly().Location." + "Notes": "Uses obsolete StardewModdingAPI.Extensions." }, { "Name": "NPC Map Locations", @@ -198,14 +154,6 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/683", "Notes": "Crashes with 'Method not found: Void StardewModdingAPI.Command.CallCommand(System.String)'." }, - { - "Name": "Teleporter", - "ID": "Teleporter", - "UpperVersion": "1.0.2", - "Compatibility": "AssumeBroken", - "UpdateUrl": "http://community.playstarbound.com/resources/4374", - "Notes": "Crashes with 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field'." - }, { "Name": "Zoryn's Better RNG", "ID": "76b6d1e1-f7ba-4d72-8c32-5a1e6d2716f6", -- cgit From 7175e9f8ee24a88a41f453fad500a19b9bbebeba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 30 Mar 2017 19:43:10 -0400 Subject: add upper version check (#258) --- src/StardewModdingAPI/Constants.cs | 5 ++++- src/StardewModdingAPI/Program.cs | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 3762269d..37fa4d31 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -36,7 +36,10 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 9, 0); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.1.1"); + public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.1"); + + /// The maximum supported version of Stardew Valley. + public static ISemanticVersion MaximumGameVersion { get; } = new SemanticVersion("1.1.1"); /// The path to the game folder. public static string ExecutionPath { get; } = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index e11f411e..90839216 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -131,7 +131,13 @@ namespace StardewModdingAPI // verify version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameDisplayVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you're on the Steam beta channel, note that the beta channel may not receive the latest updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameDisplayVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error); + this.PressAnyKeyToExit(); + return; + } + if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) + { + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameDisplayVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI.", LogLevel.Error); this.PressAnyKeyToExit(); return; } -- cgit From c023118356d9d7877dbdc9e88c4512fcdf33c256 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 30 Mar 2017 20:21:15 -0400 Subject: always show friendly game version --- src/StardewModdingAPI/Constants.cs | 29 +++++++++++++---------------- src/StardewModdingAPI/Program.cs | 10 +++++----- 2 files changed, 18 insertions(+), 21 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 37fa4d31..e28b33d7 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -80,9 +80,6 @@ namespace StardewModdingAPI /// The game's current semantic version. internal static ISemanticVersion GameVersion { get; } = Constants.GetGameVersion(); - /// The game's current version as it should be displayed to players. - internal static ISemanticVersion GameDisplayVersion { get; } = Constants.GetGameDisplayVersion(Constants.GameVersion); - /// The target game platform. internal static Platform TargetPlatform { get; } = #if SMAPI_FOR_WINDOWS @@ -177,6 +174,19 @@ namespace StardewModdingAPI }; } + /// Get game current version as it should be displayed to players. + /// The semantic game version. + internal static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version) + { + switch (version.ToString()) + { + case "1.1.1": + return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11 + default: + return version; + } + } + /// Get the name of a save directory for the current player. private static string GetSaveFolderName() { @@ -199,18 +209,5 @@ namespace StardewModdingAPI version = "1.1.1"; // The 1.1 patch was released as 1.11, which means it's out of order for semantic version checks return new SemanticVersion(version); } - - /// Get game current version as it should be displayed to players. - /// The semantic game version. - private static ISemanticVersion GetGameDisplayVersion(ISemanticVersion version) - { - switch (version.ToString()) - { - case "1.1.1": - return new SemanticVersion(1, 11, 0); // The 1.1 patch was released as 1.11 - default: - return version; - } - } } } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 90839216..ac98f2e4 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -88,8 +88,8 @@ namespace StardewModdingAPI { // initialise logging Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB"); // for consistent log formatting - this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {this.GetFriendlyPlatformName()}", LogLevel.Info); - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion}"; + this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} on {this.GetFriendlyPlatformName()}", LogLevel.Info); + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}"; // inject compatibility shims #pragma warning disable 618 @@ -131,13 +131,13 @@ namespace StardewModdingAPI // verify version if (Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameDisplayVersion}, but the oldest supported version is {Constants.MinimumGameVersion}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but the oldest supported version is {Constants.GetGameDisplayVersion(Constants.MinimumGameVersion)}. Please update your game before using SMAPI. If you have the beta version on Steam, you may need to opt out to get the latest non-beta updates.", LogLevel.Error); this.PressAnyKeyToExit(); return; } if (Constants.MaximumGameVersion != null && Constants.GameVersion.IsNewerThan(Constants.MaximumGameVersion)) { - this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GameDisplayVersion}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.MaximumGameVersion}. Please check for a newer version of SMAPI.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)}, but this version of SMAPI is only compatible up to Stardew Valley {Constants.GetGameDisplayVersion(Constants.MaximumGameVersion)}. Please check for a newer version of SMAPI.", LogLevel.Error); this.PressAnyKeyToExit(); return; } @@ -530,7 +530,7 @@ namespace StardewModdingAPI this.Monitor.Log($"Loaded {modsLoaded} mods."); foreach (Action warning in deprecationWarnings) warning(); - Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GameVersion} with {modsLoaded} mods"; + Console.Title = $"SMAPI {Constants.ApiVersion} - running Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} with {modsLoaded} mods"; } /// Run a loop handling console input. -- cgit From 4675da0600edf6781cd740549ad0a175b606fc1e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Apr 2017 16:08:31 -0400 Subject: add --log-path argument to specify SMAPI log path during testing --- README.md | 5 +++-- release-notes.md | 1 + src/StardewModdingAPI/Constants.cs | 2 +- src/StardewModdingAPI/Program.cs | 29 +++++++++++++++++++++++++---- 4 files changed, 30 insertions(+), 7 deletions(-) (limited to 'src/StardewModdingAPI/Program.cs') diff --git a/README.md b/README.md index c90e62cc..74388144 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,10 @@ field | purpose `ModCompatibility` | A list of mod versions SMAPI should consider compatible or broken regardless of whether it detects incompatible code. Each record can be set to `AssumeCompatible` or `AssumeBroken`. Changing this field is not recommended and may destabilise your game. ### Command-line arguments -SMAPI recognises the following command-line arguments. These are intended for internal use and may -change without warning. +SMAPI recognises the following command-line arguments. These are intended for internal use or +testing and may change without warning. argument | purpose -------- | ------- +`--log path "path"` | The relative or absolute path of the log file SMAPI should write. `--no-terminal` | SMAPI won't write anything to the console window. (Messages will still be written to the log file.) diff --git a/release-notes.md b/release-notes.md index bde3e4b0..a5ef4a0b 100644 --- a/release-notes.md +++ b/release-notes.md @@ -43,6 +43,7 @@ For mod developers: * Added `ContentEvents.AfterLocaleChanged` event triggered when the player changes the content language (for the upcoming Stardew Valley 1.2). * Added `SaveEvents.AfterReturnToTitle` event triggered when the player returns to the title screen (for the upcoming Stardew Valley 1.2). * Added `helper.Reflection.GetPrivateProperty` method. +* Added a `--log-path` argument to specify the SMAPI log path during testing. * SMAPI now writes XNA input enums (`Buttons` and `Keys`) to JSON as strings automatically, so mods no longer need to add a `StringEnumConverter` themselves for those. * The SMAPI log now has a simpler filename. * The SMAPI log now shows the OS caption (like "Windows 10") instead of its internal version when available. diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index e28b33d7..4a036cd0 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -69,7 +69,7 @@ namespace StardewModdingAPI internal static string ApiConfigPath => Path.Combine(Constants.ExecutionPath, $"{typeof(Program).Assembly.GetName().Name}.config.json"); /// The file path to the log where the latest output should be saved. - internal static string LogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); + internal static string DefaultLogPath => Path.Combine(Constants.LogDir, "SMAPI-latest.txt"); /// The full path to the folder containing mods. internal static string ModPath { get; } = Path.Combine(Constants.ExecutionPath, "Mods"); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index ac98f2e4..58850dc3 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -30,7 +30,7 @@ namespace StardewModdingAPI ** Properties *********/ /// The log file to which to write messages. - private readonly LogFileManager LogFile = new LogFileManager(Constants.LogPath); + private readonly LogFileManager LogFile; /// Manages console output interception. private readonly ConsoleInterceptionManager ConsoleManager = new ConsoleInterceptionManager(); @@ -67,17 +67,38 @@ namespace StardewModdingAPI /// The command-line arguments. private static void Main(string[] args) { - new Program(writeToConsole: !args.Contains("--no-terminal")) + // get flags from arguments + bool writeToConsole = !args.Contains("--no-terminal"); + + // get log path from arguments + string logPath = null; + { + int pathIndex = Array.LastIndexOf(args, "--log-path") + 1; + if (pathIndex >= 1 && args.Length >= pathIndex) + { + logPath = args[pathIndex]; + if (!Path.IsPathRooted(logPath)) + logPath = Path.Combine(Constants.LogDir, logPath); + } + } + if (string.IsNullOrWhiteSpace(logPath)) + logPath = Constants.DefaultLogPath; + + // load SMAPI + new Program(writeToConsole, logPath) .LaunchInteractively(); } /// Construct an instance. - internal Program(bool writeToConsole) + /// Whether to output log messages to the console. + /// The full file path to which to write log messages. + internal Program(bool writeToConsole, string logPath) { // load settings this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); // initialise + this.LogFile = new LogFileManager(logPath); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.ExitGameImmediately) { WriteToConsole = writeToConsole }; this.ModRegistry = new ModRegistry(this.Settings.ModCompatibility); this.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -460,7 +481,7 @@ namespace StardewModdingAPI try { int modEntries = modAssembly.DefinedTypes.Count(type => typeof(Mod).IsAssignableFrom(type) && !type.IsAbstract); - if(modEntries == 0) + if (modEntries == 0) { this.Monitor.Log($"{skippedPrefix} because its DLL has no '{nameof(Mod)}' subclass.", LogLevel.Error); continue; -- cgit