From d47e55d0405de145ea18f37eba078608f6deac9f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 May 2017 15:11:36 -0400 Subject: show friendly errors when the game is missing or pre-1.2 --- src/StardewModdingAPI/Program.cs | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) (limited to 'src') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 06523144..21717cc3 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -78,6 +78,8 @@ namespace StardewModdingAPI /// The command-line arguments. public static void Main(string[] args) { + Program.AssertMinimumCompatibility(); + // get flags from arguments bool writeToConsole = !args.Contains("--no-terminal"); @@ -261,6 +263,49 @@ namespace StardewModdingAPI /********* ** Private methods *********/ + /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. + /// Returns whether the minimum conditions are met. + private static void AssertMinimumCompatibility() + { + void PrintErrorAndExit(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine(message); + Console.ResetColor(); + Program.PressAnyKeyToExit(showMessage: true); + } + + // get game assembly name + const string gameAssemblyName = +#if SMAPI_FOR_WINDOWS + "Stardew Valley"; +#else + "StardewValley"; +#endif + + // game not present + if (Type.GetType($"StardewValley.Game1, {gameAssemblyName}", throwOnError: false) == null) + { + PrintErrorAndExit( + "Oops! SMAPI can't find the game. " + + (Assembly.GetCallingAssembly().Location?.Contains(Path.Combine("internal", "Windows")) == true || Assembly.GetCallingAssembly().Location?.Contains(Path.Combine("internal", "Mono")) == true + ? "It looks like you're running SMAPI from the download package, but you need to run the installed version instead. " + : "Make sure you're running StardewModdingAPI.exe in your game folder. " + ) + + "See the readme.txt file for details." + ); + } + + // Stardew Valley 1.2 types not present + if (Type.GetType($"StardewValley.LocalizedContentManager+LanguageCode, {gameAssemblyName}", throwOnError: false) == null) + { + PrintErrorAndExit(Constants.GameVersion.IsOlderThan(Constants.MinimumGameVersion) + ? $"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." + : "Oops! SMAPI doesn't seem to be compatible with your game. Make sure you're running the latest version of Stardew Valley and SMAPI." + ); + } + } + /// Initialise SMAPI and mods after the game starts. private void InitialiseAfterGameStart() { @@ -655,6 +700,15 @@ namespace StardewModdingAPI private void PressAnyKeyToExit() { this.Monitor.Log("Game has ended. Press any key to exit.", LogLevel.Info); + Program.PressAnyKeyToExit(showMessage: false); + } + + /// Show a 'press any key to exit' message, and exit when they press a key. + /// Whether to print a 'press any key to exit' message to the console. + private static void PressAnyKeyToExit(bool showMessage) + { + if (showMessage) + Console.WriteLine("Game has ended. Press any key to exit."); Thread.Sleep(100); Console.ReadKey(); Environment.Exit(0); -- cgit From c4eb21bd3104c90c4295bd05ec8bf8dc7611f3b1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 20 May 2017 15:11:51 -0400 Subject: update installer readme --- src/StardewModdingAPI.Installer/readme.txt | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Installer/readme.txt b/src/StardewModdingAPI.Installer/readme.txt index 4756099e..cf6090c4 100644 --- a/src/StardewModdingAPI.Installer/readme.txt +++ b/src/StardewModdingAPI.Installer/readme.txt @@ -13,13 +13,7 @@ SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. - -To install: - - Windows: double-click install.exe. - - Linux or Mac: open a terminal and run `mono install.exe`. - - Need help? See: - - Install guide: http://canimod.com/guides/using-mods#installing-smapi - - Troubleshooting: http://canimod.com/guides/smapi-faq#troubleshooting + - Install guide: http://canimod.com/for-players/install-smapi + - Troubleshooting: http://canimod.com/for-players/faqs#troubleshooting - Ask for help: https://discord.gg/kH55QXP -- cgit From 9e7c77f1f4bb9bc244a8227d932a28999b022471 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 May 2017 17:29:40 -0400 Subject: enable mod dependencies (#285) --- release-notes.md | 8 ++++++-- src/StardewModdingAPI.Tests/ModResolverTests.cs | 8 -------- src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 4 ---- src/StardewModdingAPI/Framework/Models/Manifest.cs | 2 -- src/StardewModdingAPI/IManifest.cs | 2 -- src/StardewModdingAPI/Program.cs | 2 -- 6 files changed, 6 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 7fee542c..ec05d4c3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,8 +14,12 @@ For mod developers: See [log](https://github.com/Pathoschild/SMAPI/compare/1.13.1...1.14). For players: -* SMAPI now shows a friendly message when it can't detect the game. -* SMAPI now shows a friendly message when you have Stardew Valley 1.11 or earlier (which aren't compatible). +* SMAPI now shows a friendly error when it can't detect the game. +* SMAPI now shows a friendly error when you have Stardew Valley 1.11 or earlier (which aren't compatible). +* SMAPI now shows a friendly error if a mod dependency is missing (if it's listed in the mod's manifest). + +For modders: +* You can now list mod dependencies in the `manifest.json`. SMAPI will make sure your dependencies are loaded before your mod, and will show a friendly error if a dependency is missing. ## 1.13.1 See [log](https://github.com/Pathoschild/SMAPI/compare/1.13...1.13.1). diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index 8cf5a29e..c0545cd6 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -71,9 +71,7 @@ namespace StardewModdingAPI.Tests [nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}", [nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll", [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}-{Sample.String()}", -#if EXPERIMENTAL [nameof(IManifest.Dependencies)] = new[] { originalDependency }, -#endif ["ExtraString"] = Sample.String(), ["ExtraInt"] = Sample.Int() }; @@ -110,11 +108,9 @@ namespace StardewModdingAPI.Tests Assert.AreEqual(original["ExtraString"], mod.Manifest.ExtraFields["ExtraString"], "The manifest's extra fields should contain an 'ExtraString' value."); Assert.AreEqual(original["ExtraInt"], mod.Manifest.ExtraFields["ExtraInt"], "The manifest's extra fields should contain an 'ExtraInt' value."); -#if EXPERIMENTAL Assert.IsNotNull(mod.Manifest.Dependencies, "The dependencies field should not be null."); Assert.AreEqual(1, mod.Manifest.Dependencies.Length, "The dependencies field should contain one value."); Assert.AreEqual(originalDependency[nameof(IManifestDependency.UniqueID)], mod.Manifest.Dependencies[0].UniqueID, "The first dependency's unique ID doesn't match."); -#endif } /**** @@ -216,7 +212,6 @@ namespace StardewModdingAPI.Tests // if Moq doesn't throw a method-not-setup exception, the validation didn't override the status. } -#if EXPERIMENTAL /**** ** ProcessDependencies ****/ @@ -345,7 +340,6 @@ namespace StardewModdingAPI.Tests modD.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod D was expected to fail since it's part of a dependency loop."); modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); } -#endif /********* @@ -368,7 +362,6 @@ namespace StardewModdingAPI.Tests return manifest; } -#if EXPERIMENTAL /// Get a randomised basic manifest. /// The mod's name and unique ID. /// The dependencies this mod requires. @@ -395,6 +388,5 @@ namespace StardewModdingAPI.Tests } return mod; } -#endif } } diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 2c68a639..21aebeb1 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -126,7 +126,6 @@ namespace StardewModdingAPI.Framework.ModLoading } } -#if EXPERIMENTAL /// Sort the given mods by the order they should be loaded. /// The mods to process. public IEnumerable ProcessDependencies(IEnumerable mods) @@ -149,13 +148,11 @@ namespace StardewModdingAPI.Framework.ModLoading return sortedMods.Reverse(); } -#endif /********* ** Private methods *********/ -#if EXPERIMENTAL /// Sort a mod's dependencies by the order they should be loaded, and remove any mods that can't be loaded due to missing or conflicting dependencies. /// The full list of mods being validated. /// The mod whose dependencies to process. @@ -270,7 +267,6 @@ namespace StardewModdingAPI.Framework.ModLoading return states[mod] = ModDependencyStatus.Sorted; } } -#endif /// Get all mod folders in a root folder, passing through empty folders as needed. /// The root folder path to search. diff --git a/src/StardewModdingAPI/Framework/Models/Manifest.cs b/src/StardewModdingAPI/Framework/Models/Manifest.cs index 53384852..be781585 100644 --- a/src/StardewModdingAPI/Framework/Models/Manifest.cs +++ b/src/StardewModdingAPI/Framework/Models/Manifest.cs @@ -30,11 +30,9 @@ namespace StardewModdingAPI.Framework.Models /// The name of the DLL in the directory that has the method. public string EntryDll { get; set; } -#if EXPERIMENTAL /// The other mods that must be loaded before this mod. [JsonConverter(typeof(ManifestFieldConverter))] public IManifestDependency[] Dependencies { get; set; } -#endif /// The unique mod ID. public string UniqueID { get; set; } diff --git a/src/StardewModdingAPI/IManifest.cs b/src/StardewModdingAPI/IManifest.cs index c036fdd3..9533aadb 100644 --- a/src/StardewModdingAPI/IManifest.cs +++ b/src/StardewModdingAPI/IManifest.cs @@ -29,10 +29,8 @@ namespace StardewModdingAPI /// The name of the DLL in the directory that has the method. string EntryDll { get; } -#if EXPERIMENTAL /// The other mods that must be loaded before this mod. IManifestDependency[] Dependencies { get; } -#endif /// Any manifest fields which didn't match a valid field. IDictionary ExtraFields { get; } diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 21717cc3..4df63456 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -402,10 +402,8 @@ namespace StardewModdingAPI } } -#if EXPERIMENTAL // process dependencies mods = resolver.ProcessDependencies(mods).ToArray(); -#endif // load mods modsLoaded = this.LoadMods(mods, new JsonHelper(), (SContentManager)Game1.content, deprecationWarnings); -- cgit From bf3ed26a8b6480a12c7e62f483234d8c616fae28 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 May 2017 17:58:17 -0400 Subject: fix smapi-crash.txt being copied from default log even if --log-path is specified --- release-notes.md | 1 + src/StardewModdingAPI/Framework/Logging/LogFileManager.cs | 11 ++++++++++- src/StardewModdingAPI/Program.cs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index ec05d4c3..679699a3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -20,6 +20,7 @@ For players: For modders: * You can now list mod dependencies in the `manifest.json`. SMAPI will make sure your dependencies are loaded before your mod, and will show a friendly error if a dependency is missing. +* Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. ## 1.13.1 See [log](https://github.com/Pathoschild/SMAPI/compare/1.13...1.13.1). diff --git a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs index 1f6ade1d..8cfe0527 100644 --- a/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs +++ b/src/StardewModdingAPI/Framework/Logging/LogFileManager.cs @@ -13,6 +13,13 @@ namespace StardewModdingAPI.Framework.Logging private readonly StreamWriter Stream; + /********* + ** Accessors + *********/ + /// The full path to the log file being written. + public string Path { get; } + + /********* ** Public methods *********/ @@ -20,8 +27,10 @@ namespace StardewModdingAPI.Framework.Logging /// The log file to write. public LogFileManager(string path) { + this.Path = path; + // create log directory if needed - string logDir = Path.GetDirectoryName(path); + string logDir = System.IO.Path.GetDirectoryName(path); if (logDir == null) throw new ArgumentException($"The log path '{path}' is not valid."); Directory.CreateDirectory(logDir); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 4df63456..4a4b2ca7 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -160,7 +160,7 @@ namespace StardewModdingAPI try { File.WriteAllText(Constants.FatalCrashMarker, string.Empty); - File.Copy(Constants.DefaultLogPath, Constants.FatalCrashLog, overwrite: true); + File.Copy(this.LogFile.Path, Constants.FatalCrashLog, overwrite: true); } catch (Exception ex) { -- cgit From ec19c85d66a10bbd41c7fabfd9ea858fe659e747 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 21 May 2017 18:22:21 -0400 Subject: reimplement event deprecation warnings to fix "unknown mod" warnings --- release-notes.md | 1 + src/StardewModdingAPI/Events/ContentEvents.cs | 37 ++++---- src/StardewModdingAPI/Events/GameEvents.cs | 98 +++++++++++++--------- src/StardewModdingAPI/Events/PlayerEvents.cs | 48 +++++++---- src/StardewModdingAPI/Events/TimeEvents.cs | 25 ++++-- .../Framework/InternalExtensions.cs | 38 --------- src/StardewModdingAPI/Framework/ModRegistry.cs | 10 --- src/StardewModdingAPI/Program.cs | 1 - 8 files changed, 125 insertions(+), 133 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 679699a3..b9aabbf5 100644 --- a/release-notes.md +++ b/release-notes.md @@ -17,6 +17,7 @@ For players: * SMAPI now shows a friendly error when it can't detect the game. * SMAPI now shows a friendly error when you have Stardew Valley 1.11 or earlier (which aren't compatible). * SMAPI now shows a friendly error if a mod dependency is missing (if it's listed in the mod's manifest). +* Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. For modders: * You can now list mod dependencies in the `manifest.json`. SMAPI will make sure your dependencies are loaded before your mod, and will show a friendly error if a dependency is missing. diff --git a/src/StardewModdingAPI/Events/ContentEvents.cs b/src/StardewModdingAPI/Events/ContentEvents.cs index 0dcd2cc6..8fa9ae3c 100644 --- a/src/StardewModdingAPI/Events/ContentEvents.cs +++ b/src/StardewModdingAPI/Events/ContentEvents.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework; namespace StardewModdingAPI.Events @@ -19,6 +20,9 @@ namespace StardewModdingAPI.Events /// The mods using the experimental API for which a warning has been raised. private static readonly HashSet WarnedMods = new HashSet(); + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _AfterAssetLoaded; /********* ** Events @@ -32,7 +36,15 @@ namespace StardewModdingAPI.Events #else internal #endif - static event EventHandler AfterAssetLoaded; + static event EventHandler AfterAssetLoaded + { + add + { + ContentEvents.RaiseContentExperimentalWarning(); + ContentEvents._AfterAssetLoaded += value; + } + remove => ContentEvents._AfterAssetLoaded -= value; + } /********* @@ -61,30 +73,21 @@ namespace StardewModdingAPI.Events /// Encapsulates access and changes to content being read from a data file. internal static void InvokeAfterAssetLoaded(IMonitor monitor, IContentEventHelper contentHelper) { - if (ContentEvents.AfterAssetLoaded != null) - { - Delegate[] handlers = ContentEvents.AfterAssetLoaded.GetInvocationList(); - ContentEvents.RaiseDeprecationWarning(handlers); - monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterAssetLoaded)}", handlers, null, contentHelper); - } + monitor.SafelyRaiseGenericEvent($"{nameof(ContentEvents)}.{nameof(ContentEvents.AfterAssetLoaded)}", ContentEvents._AfterAssetLoaded?.GetInvocationList(), null, contentHelper); } /********* ** Private methods *********/ - /// Raise a 'experimental API' warning for each mod using the content API. - /// The event handlers. - private static void RaiseDeprecationWarning(Delegate[] handlers) + /// Raise an 'experimental API' warning for a mod using the content API. + private static void RaiseContentExperimentalWarning() { - foreach (Delegate handler in handlers) + string modName = ContentEvents.ModRegistry.GetModFromStack() ?? "An unknown mod"; + if (!ContentEvents.WarnedMods.Contains(modName)) { - 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); - } + 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); } } } diff --git a/src/StardewModdingAPI/Events/GameEvents.cs b/src/StardewModdingAPI/Events/GameEvents.cs index 4f9ce7a7..8e3cf662 100644 --- a/src/StardewModdingAPI/Events/GameEvents.cs +++ b/src/StardewModdingAPI/Events/GameEvents.cs @@ -1,6 +1,8 @@ using System; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework; +#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. namespace StardewModdingAPI.Events { /// Events raised when the game changes state. @@ -12,6 +14,22 @@ namespace StardewModdingAPI.Events /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _Initialize; + + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _LoadContent; + + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _GameLoaded; + + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _FirstUpdateTick; + /********* ** Events @@ -24,19 +42,51 @@ namespace StardewModdingAPI.Events /// Raised during launch after configuring XNA or MonoGame. The game window hasn't been opened by this point. Called after . [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.Initialize) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler Initialize; + public static event EventHandler Initialize + { + add + { + GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", "1.10", DeprecationLevel.Info); + GameEvents._Initialize += value; + } + remove => GameEvents._Initialize -= value; + } /// Raised before XNA loads or reloads graphics resources. Called during . [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the " + nameof(GameEvents.LoadContent) + " event, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler LoadContent; + public static event EventHandler LoadContent + { + add + { + GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", "1.10", DeprecationLevel.Info); + GameEvents._LoadContent += value; + } + remove => GameEvents._LoadContent -= value; + } /// Raised during launch after configuring Stardew Valley, loading it into memory, and opening the game window. The window is still blank by this point. [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler GameLoaded; + public static event EventHandler GameLoaded + { + add + { + GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", "1.12", DeprecationLevel.Info); + GameEvents._GameLoaded += value; + } + remove => GameEvents._GameLoaded -= value; + } /// Raised during the first game update tick. [Obsolete("The " + nameof(Mod) + "." + nameof(Mod.Entry) + " method is now called after the game loads, so any contained logic can be done directly in " + nameof(Mod.Entry) + ".")] - public static event EventHandler FirstUpdateTick; + public static event EventHandler FirstUpdateTick + { + add + { + GameEvents.DeprecationManager.Warn($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", "1.12", DeprecationLevel.Info); + GameEvents._FirstUpdateTick += value; + } + remove => GameEvents._FirstUpdateTick -= value; + } /// Raised when the game updates its state (≈60 times per second). public static event EventHandler UpdateTick; @@ -74,62 +124,30 @@ namespace StardewModdingAPI.Events /// Encapsulates logging and monitoring. internal static void InvokeInitialize(IMonitor monitor) { - // notify SMAPI monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.InitializeInternal)}", GameEvents.InitializeInternal?.GetInvocationList()); - - // notify mods - if (GameEvents.Initialize == null) - return; - string name = $"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}"; - Delegate[] handlers = GameEvents.Initialize.GetInvocationList(); - GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.10", DeprecationLevel.Info); - monitor.SafelyRaisePlainEvent(name, handlers); + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.Initialize)}", GameEvents._Initialize?.GetInvocationList()); } /// Raise a event. /// Encapsulates logging and monitoring. internal static void InvokeLoadContent(IMonitor monitor) { - if (GameEvents.LoadContent == null) - return; - - string name = $"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}"; - Delegate[] handlers = GameEvents.LoadContent.GetInvocationList(); - - GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.10", DeprecationLevel.Info); - monitor.SafelyRaisePlainEvent(name, handlers); + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.LoadContent)}", GameEvents._LoadContent?.GetInvocationList()); } /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeGameLoaded(IMonitor monitor) { - // notify SMAPI monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoadedInternal)}", GameEvents.GameLoadedInternal?.GetInvocationList()); - - // notify mods - if (GameEvents.GameLoaded == null) - return; - - string name = $"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}"; - Delegate[] handlers = GameEvents.GameLoaded.GetInvocationList(); - - GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.12", DeprecationLevel.Info); - monitor.SafelyRaisePlainEvent(name, handlers); + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.GameLoaded)}", GameEvents._GameLoaded?.GetInvocationList()); } /// Raise a event. /// Encapsulates monitoring and logging. internal static void InvokeFirstUpdateTick(IMonitor monitor) { - if (GameEvents.FirstUpdateTick == null) - return; - - string name = $"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}"; - Delegate[] handlers = GameEvents.FirstUpdateTick.GetInvocationList(); - - GameEvents.DeprecationManager.WarnForEvent(handlers, name, "1.12", DeprecationLevel.Info); - monitor.SafelyRaisePlainEvent(name, handlers); + monitor.SafelyRaisePlainEvent($"{nameof(GameEvents)}.{nameof(GameEvents.FirstUpdateTick)}", GameEvents._FirstUpdateTick?.GetInvocationList()); } /// Raise an event. diff --git a/src/StardewModdingAPI/Events/PlayerEvents.cs b/src/StardewModdingAPI/Events/PlayerEvents.cs index b02ebfec..37649fee 100644 --- a/src/StardewModdingAPI/Events/PlayerEvents.cs +++ b/src/StardewModdingAPI/Events/PlayerEvents.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using StardewModdingAPI.Framework; using StardewValley; using SFarmer = StardewValley.Farmer; +#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. namespace StardewModdingAPI.Events { /// Events raised when the player data changes. @@ -16,17 +18,41 @@ namespace StardewModdingAPI.Events /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _LoadedGame; + + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _FarmerChanged; + /********* ** Events *********/ /// Raised after the player loads a saved game. [Obsolete("Use " + nameof(SaveEvents) + "." + nameof(SaveEvents.AfterLoad) + " instead")] - public static event EventHandler LoadedGame; + public static event EventHandler LoadedGame + { + add + { + PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", "1.6", DeprecationLevel.Info); + PlayerEvents._LoadedGame += value; + } + remove => PlayerEvents._LoadedGame -= value; + } /// Raised after the game assigns a new player character. This happens just before ; it's unclear how this would happen any other time. [Obsolete("should no longer be used")] - public static event EventHandler FarmerChanged; + public static event EventHandler FarmerChanged + { + add + { + PlayerEvents.DeprecationManager.Warn($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", "1.6", DeprecationLevel.Info); + PlayerEvents._FarmerChanged += value; + } + remove => PlayerEvents._FarmerChanged -= value; + } /// Raised after the player's inventory changes in any way (added or removed item, sorted, etc). public static event EventHandler InventoryChanged; @@ -50,14 +76,7 @@ namespace StardewModdingAPI.Events /// Whether the save has been loaded. This is always true. internal static void InvokeLoadedGame(IMonitor monitor, EventArgsLoadedGameChanged loaded) { - if (PlayerEvents.LoadedGame == null) - return; - - string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}"; - Delegate[] handlers = PlayerEvents.LoadedGame.GetInvocationList(); - - PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info); - monitor.SafelyRaiseGenericEvent(name, handlers, null, loaded); + monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.LoadedGame)}", PlayerEvents._LoadedGame?.GetInvocationList(), null, loaded); } /// Raise a event. @@ -66,14 +85,7 @@ namespace StardewModdingAPI.Events /// The new player character. internal static void InvokeFarmerChanged(IMonitor monitor, SFarmer priorFarmer, SFarmer newFarmer) { - if (PlayerEvents.FarmerChanged == null) - return; - - string name = $"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}"; - Delegate[] handlers = PlayerEvents.FarmerChanged.GetInvocationList(); - - PlayerEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info); - monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); + monitor.SafelyRaiseGenericEvent($"{nameof(PlayerEvents)}.{nameof(PlayerEvents.FarmerChanged)}", PlayerEvents._FarmerChanged?.GetInvocationList(), null, new EventArgsFarmerChanged(priorFarmer, newFarmer)); } /// Raise an event. diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index 3f06a46b..572898c7 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -1,6 +1,8 @@ using System; +using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Framework; +#pragma warning disable 618 // Suppress obsolete-symbol errors in this file. Since several events are marked obsolete, this produces unnecessary warnings. namespace StardewModdingAPI.Events { /// Events raised when the in-game date or time changes. @@ -12,6 +14,10 @@ namespace StardewModdingAPI.Events /// Manages deprecation warnings. private static DeprecationManager DeprecationManager; + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _OnNewDay; + /********* ** Events @@ -33,7 +39,15 @@ namespace StardewModdingAPI.Events /// Raised when the player is transitioning to a new day and the game is performing its day update logic. This event is triggered twice: once after the game starts transitioning, and again after it finishes. [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] - public static event EventHandler OnNewDay; + public static event EventHandler OnNewDay + { + add + { + TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", "1.6", DeprecationLevel.Info); + TimeEvents._OnNewDay += value; + } + remove => TimeEvents._OnNewDay -= value; + } /********* @@ -96,14 +110,7 @@ namespace StardewModdingAPI.Events /// Whether the game just started the transition (true) or finished it (false). internal static void InvokeOnNewDay(IMonitor monitor, int priorDay, int newDay, bool isTransitioning) { - if (TimeEvents.OnNewDay == null) - return; - - string name = $"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}"; - Delegate[] handlers = TimeEvents.OnNewDay.GetInvocationList(); - - TimeEvents.DeprecationManager.WarnForEvent(handlers, name, "1.6", DeprecationLevel.Info); - monitor.SafelyRaiseGenericEvent(name, handlers, null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); + monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.OnNewDay)}", TimeEvents._OnNewDay?.GetInvocationList(), null, new EventArgsNewDay(priorDay, newDay, isTransitioning)); } } } diff --git a/src/StardewModdingAPI/Framework/InternalExtensions.cs b/src/StardewModdingAPI/Framework/InternalExtensions.cs index cadf6598..b99d3798 100644 --- a/src/StardewModdingAPI/Framework/InternalExtensions.cs +++ b/src/StardewModdingAPI/Framework/InternalExtensions.cs @@ -10,23 +10,6 @@ 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 ****/ @@ -110,27 +93,6 @@ namespace StardewModdingAPI.Framework } } - /**** - ** Deprecation - ****/ - /// Log a deprecation warning for mods using an event. - /// The deprecation manager to extend. - /// The event handlers. - /// A noun phrase describing what is deprecated. - /// The SMAPI version which deprecated it. - /// How deprecated the code is. - public static void WarnForEvent(this DeprecationManager deprecationManager, Delegate[] handlers, string nounPhrase, string version, DeprecationLevel severity) - { - if (handlers == null || !handlers.Any()) - return; - - foreach (Delegate handler in handlers) - { - string modName = InternalExtensions.ModRegistry.GetModFrom(handler) ?? "an unknown mod"; // suppress stack trace for unknown mods, not helpful here - deprecationManager.Warn(modName, nounPhrase, version, severity); - } - } - /**** ** Sprite batch ****/ diff --git a/src/StardewModdingAPI/Framework/ModRegistry.cs b/src/StardewModdingAPI/Framework/ModRegistry.cs index 62063fbd..f9d3cfbf 100644 --- a/src/StardewModdingAPI/Framework/ModRegistry.cs +++ b/src/StardewModdingAPI/Framework/ModRegistry.cs @@ -63,16 +63,6 @@ namespace StardewModdingAPI.Framework return (from mod in this.Mods select mod); } - /// Get the friendly mod name which handles a delegate. - /// The delegate to follow. - /// Returns the mod name, or null if the delegate isn't implemented by a known mod. - public string GetModFrom(Delegate @delegate) - { - return @delegate?.Target != null - ? this.GetModFrom(@delegate.Target.GetType()) - : null; - } - /// Get the friendly mod name which defines a type. /// The type to check. /// Returns the mod name, or null if the type isn't part of a known mod. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 4a4b2ca7..b92108c3 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -322,7 +322,6 @@ namespace StardewModdingAPI #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); ContentEvents.Shim(this.ModRegistry, this.Monitor); -- cgit From 51d56f1eecdc171052fea425f92562f695c068b0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 22 May 2017 16:41:50 -0400 Subject: add Context.IsInDrawLoop for specialised mods --- release-notes.md | 1 + src/StardewModdingAPI/Context.cs | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index b9aabbf5..3bf68a1e 100644 --- a/release-notes.md +++ b/release-notes.md @@ -20,6 +20,7 @@ For players: * Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. For modders: +* Added `Context.IsInDrawLoop` for specialised mods. * You can now list mod dependencies in the `manifest.json`. SMAPI will make sure your dependencies are loaded before your mod, and will show a friendly error if a dependency is missing. * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs index 6bc5ae56..6ceabd5a 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/StardewModdingAPI/Context.cs @@ -1,4 +1,5 @@ -using StardewValley; +using StardewModdingAPI.Events; +using StardewValley; using StardewValley.Menus; namespace StardewModdingAPI @@ -15,6 +16,9 @@ namespace StardewModdingAPI /// Whether the player has loaded a save and the world has finished initialising. public static bool IsWorldReady { get; internal set; } + /// Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use to draw to the screen. + public static bool IsInDrawLoop { get; set; } + /**** ** Internal ****/ @@ -23,8 +27,5 @@ namespace StardewModdingAPI /// Whether the game is currently writing to the save file. internal static bool IsSaving => Game1.activeClickableMenu is SaveGameMenu || Game1.activeClickableMenu is ShippingMenu; // saving is performed by SaveGameMenu, but it's wrapped by ShippingMenu on days when the player shipping something - - /// Whether the game is currently running the draw loop. - internal static bool IsInDrawLoop { get; set; } } } -- cgit From 5fa13459d367bc5e3248a8b1447f4f56354e1fae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 22 May 2017 18:55:09 -0400 Subject: show friendly error when running install.exe from within zip file --- release-notes.md | 8 +++++--- src/StardewModdingAPI.Installer/InteractiveInstaller.cs | 5 ++++- 2 files changed, 9 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 3bf68a1e..c654e408 100644 --- a/release-notes.md +++ b/release-notes.md @@ -14,9 +14,11 @@ For mod developers: See [log](https://github.com/Pathoschild/SMAPI/compare/1.13.1...1.14). For players: -* SMAPI now shows a friendly error when it can't detect the game. -* SMAPI now shows a friendly error when you have Stardew Valley 1.11 or earlier (which aren't compatible). -* SMAPI now shows a friendly error if a mod dependency is missing (if it's listed in the mod's manifest). +* SMAPI now shows friendly errors when... + * it can't detect the game; + * a mod dependency is missing (if it's listed in the mod manifest); + * you have Stardew Valley 1.11 or earlier (which aren't compatible); + * you run `install.exe` from within the downloaded zip file. * Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. For modders: diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 01f7a01f..43cb7d70 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -158,7 +158,10 @@ namespace StardewModdingApi.Installer ****/ if (!packageDir.Exists) { - this.PrintError($"The 'internal/{platform}' package folder is missing (should be at {packageDir})."); + this.PrintError(platform == Platform.Windows && packageDir.FullName.Contains(Path.GetTempPath()) && packageDir.FullName.Contains(".zip") + ? "The installer is missing some files. It looks like you're running the installer from inside the downloaded zip; make sure you unzip the downloaded file first, then run the installer from the unzipped folder." + : $"The 'internal/{platform}' package folder is missing (should be at {packageDir})." + ); Console.ReadLine(); return; } -- cgit From 8f1379e27323f00361c1cef107cf4ab269827b16 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 23 May 2017 16:51:03 -0400 Subject: fix rare crash when window loses focus for some players --- release-notes.md | 1 + src/StardewModdingAPI/Framework/SGame.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index c654e408..9506de9c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -20,6 +20,7 @@ For players: * you have Stardew Valley 1.11 or earlier (which aren't compatible); * you run `install.exe` from within the downloaded zip file. * Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. +* Fixed rare crash when window loses focuses for a few players. For modders: * Added `Context.IsInDrawLoop` for specialised mods. diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 3d421a37..05ef9626 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -350,8 +350,9 @@ namespace StardewModdingAPI.Framework } /********* - ** Input events + ** Input events (if window has focus) *********/ + if (Game1.game1.IsActive) { // get latest state this.KStateNow = Keyboard.GetState(); -- cgit From 56919271fc60f85a752b0967fe27e69c1f3770c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 23 May 2017 23:31:34 -0400 Subject: fix Context.IsInDrawLoop having a public setter --- release-notes.md | 2 +- src/StardewModdingAPI/Context.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 9506de9c..1a4bf67c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -20,7 +20,7 @@ For players: * you have Stardew Valley 1.11 or earlier (which aren't compatible); * you run `install.exe` from within the downloaded zip file. * Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. -* Fixed rare crash when window loses focuses for a few players. +* Fixed rare crash when window loses focus for a few players. For modders: * Added `Context.IsInDrawLoop` for specialised mods. diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs index 6ceabd5a..3fe70c68 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/StardewModdingAPI/Context.cs @@ -17,7 +17,7 @@ namespace StardewModdingAPI public static bool IsWorldReady { get; internal set; } /// Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use to draw to the screen. - public static bool IsInDrawLoop { get; set; } + public static bool IsInDrawLoop { get; internal set; } /**** ** Internal -- cgit From 79dabe26717654364d50c927678f52caed1ab93c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 May 2017 13:48:17 -0400 Subject: add translation API (#296) --- src/StardewModdingAPI/Framework/CommandHelper.cs | 2 +- src/StardewModdingAPI/Framework/ModHelper.cs | 19 ++- src/StardewModdingAPI/Framework/SContentManager.cs | 6 + .../Framework/TranslationHelper.cs | 117 +++++++++++++++++ src/StardewModdingAPI/IModHelper.cs | 10 ++ src/StardewModdingAPI/ITranslationHelper.cs | 29 +++++ src/StardewModdingAPI/Program.cs | 49 +++++++- src/StardewModdingAPI/StardewModdingAPI.csproj | 3 + src/StardewModdingAPI/Translation.cs | 139 +++++++++++++++++++++ 9 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/TranslationHelper.cs create mode 100644 src/StardewModdingAPI/ITranslationHelper.cs create mode 100644 src/StardewModdingAPI/Translation.cs (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/CommandHelper.cs b/src/StardewModdingAPI/Framework/CommandHelper.cs index 2e9dea8e..86734fc5 100644 --- a/src/StardewModdingAPI/Framework/CommandHelper.cs +++ b/src/StardewModdingAPI/Framework/CommandHelper.cs @@ -50,4 +50,4 @@ namespace StardewModdingAPI.Framework return this.CommandManager.Trigger(name, arguments); } } -} \ No newline at end of file +} diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index f939b83c..8c578dbe 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -32,22 +32,25 @@ namespace StardewModdingAPI.Framework /// An API for managing console commands. public ICommandHelper ConsoleCommands { get; } + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + public ITranslationHelper Translation { get; } + /********* ** Public methods *********/ /// Construct an instance. /// The mod's display name. - /// The manifest for the associated mod. /// The full path to the mod's folder. /// Encapsulate SMAPI's JSON parsing. /// Metadata about loaded mods. /// Manages console commands. /// The content manager which loads content assets. /// Simplifies access to private game code. + /// Provides translations stored in the mod folder. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string displayName, IManifest manifest, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) + public ModHelper(string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection, ITranslationHelper translations) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -66,6 +69,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.ConsoleCommands = new CommandHelper(displayName, commandManager); this.Reflection = reflection; + this.Translation = translations; } /**** @@ -115,6 +119,17 @@ namespace StardewModdingAPI.Framework this.JsonHelper.WriteJsonFile(path, model); } + + /**** + ** Translation + ****/ + /// Get a translation for the current locale. This is a convenience shortcut for . + /// The translation key. + public Translation Translate(string key) + { + return this.Translation.Translate(key); + } + /**** ** Disposal ****/ diff --git a/src/StardewModdingAPI/Framework/SContentManager.cs b/src/StardewModdingAPI/Framework/SContentManager.cs index 54349a91..acd3e108 100644 --- a/src/StardewModdingAPI/Framework/SContentManager.cs +++ b/src/StardewModdingAPI/Framework/SContentManager.cs @@ -145,6 +145,12 @@ namespace StardewModdingAPI.Framework this.Cache[assetName] = value; } + /// Get the current content locale. + public string GetLocale() + { + return this.GetKeyLocale.Invoke(); + } + /********* ** Private methods *********/ diff --git a/src/StardewModdingAPI/Framework/TranslationHelper.cs b/src/StardewModdingAPI/Framework/TranslationHelper.cs new file mode 100644 index 00000000..dece6214 --- /dev/null +++ b/src/StardewModdingAPI/Framework/TranslationHelper.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + internal class TranslationHelper : ITranslationHelper + { + /********* + ** Properties + *********/ + /// The name of the relevant mod for error messages. + private readonly string ModName; + + /// The translations for each locale. + private readonly IDictionary> All = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + + /// The translations for the current locale, with locale fallback taken into account. + private IDictionary ForLocale; + + + /********* + ** Accessors + *********/ + /// The current locale. + public string Locale { get; private set; } + + /// The game's current language code. + public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The name of the relevant mod for error messages. + /// The initial locale. + /// The game's current language code. + /// The translations for each locale. + public TranslationHelper(string modName, string locale, LocalizedContentManager.LanguageCode languageCode, IDictionary> translations) + { + // save data + this.ModName = modName; + foreach (var pair in translations) + this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); + + // set locale + this.SetLocale(locale, languageCode); + } + + /// Get all translations for the current locale. + public IDictionary GetTranslations() + { + return new Dictionary(this.ForLocale, StringComparer.InvariantCultureIgnoreCase); + } + + /// Get a translation for the current locale. + /// The translation key. + public Translation Translate(string key) + { + this.ForLocale.TryGetValue(key, out string text); + return new Translation(this.ModName, this.Locale, key, text); + } + + /// Set the current locale and precache translations. + /// The current locale. + /// The game's current language code. + internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) + { + this.Locale = locale.ToLower().Trim(); + this.LocaleEnum = localeEnum; + + this.ForLocale = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + foreach (string next in this.GetRelevantLocales(this.Locale)) + { + // skip if locale not defined + if (!this.All.TryGetValue(next, out IDictionary translations)) + continue; + + // add missing translations + foreach (var pair in translations) + { + if (!this.ForLocale.ContainsKey(pair.Key)) + this.ForLocale.Add(pair); + } + } + } + + + /********* + ** Private methods + *********/ + /// Get the locales which can provide translations for the given locale, in precedence order. + /// The locale for which to find valid locales. + private IEnumerable GetRelevantLocales(string locale) + { + // given locale + yield return locale; + + // broader locales (like pt-BR => pt) + while (true) + { + int dashIndex = locale.LastIndexOf('-'); + if (dashIndex <= 0) + break; + + locale = locale.Substring(0, dashIndex); + yield return locale; + } + + // default + if (locale != "default") + yield return "default"; + } + } +} diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs index cdff6ac8..38bfd366 100644 --- a/src/StardewModdingAPI/IModHelper.cs +++ b/src/StardewModdingAPI/IModHelper.cs @@ -21,6 +21,9 @@ /// An API for managing console commands. ICommandHelper ConsoleCommands { get; } + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + ITranslationHelper Translation { get; } + /********* ** Public methods @@ -51,5 +54,12 @@ /// The file path relative to the mod directory. /// The model to save. void WriteJsonFile(string path, TModel model) where TModel : class; + + /**** + ** Translations + ****/ + /// Get a translation for the current locale. This is a convenience shortcut for . + /// The translation key. + Translation Translate(string key); } } \ No newline at end of file diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/StardewModdingAPI/ITranslationHelper.cs new file mode 100644 index 00000000..84571d0e --- /dev/null +++ b/src/StardewModdingAPI/ITranslationHelper.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI +{ + /// Provides translations stored in the mod's i18n folder, with one file per locale (like en.json) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like pt-BR.json < pt.json < default.json). + public interface ITranslationHelper + { + /********* + ** Accessors + *********/ + /// The current locale. + string Locale { get; } + + /// The game's current language code. + LocalizedContentManager.LanguageCode LocaleEnum { get; } + + + /********* + ** Public methods + *********/ + /// Get all translations for the current locale. + IDictionary GetTranslations(); + + /// Get a translation for the current locale. + /// The translation key. + Translation Translate(string key); + } +} diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b92108c3..b1cfb32d 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -48,6 +48,9 @@ namespace StardewModdingAPI /// The underlying game instance. private SGame GameInstance; + /// The underlying content manager. + private SContentManager ContentManager => (SContentManager)this.GameInstance.Content; + /// The SMAPI configuration settings. /// This is initialised after the game starts. private SConfig Settings; @@ -179,6 +182,7 @@ namespace StardewModdingAPI this.GameInstance.Window.ClientSizeChanged += (sender, e) => GraphicsEvents.InvokeResize(this.Monitor, sender, e); GameEvents.InitializeInternal += (sender, e) => this.InitialiseAfterGameStart(); GameEvents.GameLoadedInternal += (sender, e) => this.CheckForUpdateAsync(); + ContentEvents.AfterLocaleChanged += (sender, e) => this.OnLocaleChanged(); // set window titles this.GameInstance.Window.Title = $"Stardew Valley {Constants.GetGameDisplayVersion(Constants.GameVersion)} - running SMAPI {Constants.ApiVersion}"; @@ -405,7 +409,7 @@ namespace StardewModdingAPI mods = resolver.ProcessDependencies(mods).ToArray(); // load mods - modsLoaded = this.LoadMods(mods, new JsonHelper(), (SContentManager)Game1.content, deprecationWarnings); + modsLoaded = this.LoadMods(mods, new JsonHelper(), this.ContentManager, deprecationWarnings); foreach (Action warning in deprecationWarnings) warning(); } @@ -423,6 +427,18 @@ namespace StardewModdingAPI new Thread(this.RunConsoleLoop).Start(); } + /// Handle the game changing locale. + private void OnLocaleChanged() + { + // get locale + string locale = this.ContentManager.GetLocale(); + LocalizedContentManager.LanguageCode languageCode = this.ContentManager.GetCurrentLanguage(); + + // update mod translation helpers + foreach (IModMetadata mod in this.ModRegistry.GetMods()) + (mod.Mod.Helper.Translation as TranslationHelper)?.SetLocale(locale, languageCode); + } + /// Run a loop handling console input. [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] private void RunConsoleLoop() @@ -620,16 +636,43 @@ namespace StardewModdingAPI continue; } + // get translations + TranslationHelper translations; + { + IDictionary> translationValues = new Dictionary>(); + + // read translation files + DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); + if (translationsDir.Exists) + { + foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) + { + string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); + try + { + translationValues[locale] = jsonHelper.ReadJsonFile>(file.FullName); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't read {metadata.DisplayName}'s i18n/{locale}.json file: {ex.GetLogSummary()}"); + } + } + } + + // create translation helper + translations = new TranslationHelper(metadata.DisplayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage(), translationValues); + } + // inject data mod.ModManifest = manifest; - mod.Helper = new ModHelper(metadata.DisplayName, manifest, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); + mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection, translations); mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); mod.PathOnDisk = metadata.DirectoryPath; // track mod metadata.SetMod(mod); this.ModRegistry.Add(metadata); - modsLoaded += 1; + modsLoaded++; this.Monitor.Log($"Loaded {metadata.DisplayName} by {manifest.Author}, v{manifest.Version} | {manifest.Description}", LogLevel.Info); } catch (Exception ex) diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index d8bfd473..31b2c1cf 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -149,6 +149,7 @@ + @@ -178,6 +179,7 @@ + @@ -198,6 +200,7 @@ + diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs new file mode 100644 index 00000000..ae4b833c --- /dev/null +++ b/src/StardewModdingAPI/Translation.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace StardewModdingAPI +{ + /// A translation string with a fluent API to customise it. + public class Translation + { + /********* + ** Properties + *********/ + /// The name of the relevant mod for error messages. + private readonly string ModName; + + /// The locale for which the translation was fetched. + private readonly string Locale; + + /// The translation key. + private readonly string Key; + + /// The underlying translation text. + private readonly string Text; + + /// The value to return if the translations is undefined. + private readonly string Placeholder; + + + /********* + ** Public methods + *********/ + /// Construct an isntance. + /// The name of the relevant mod for error messages. + /// The locale for which the translation was fetched. + /// The translation key. + /// The underlying translation text. + internal Translation(string modName, string locale, string key, string text) + : this(modName, locale, key, text, $"(no translation:{key})") { } + + /// Construct an isntance. + /// The name of the relevant mod for error messages. + /// The locale for which the translation was fetched. + /// The translation key. + /// The underlying translation text. + /// The value to return if the translations is undefined. + internal Translation(string modName, string locale, string key, string text, string placeholder) + { + this.ModName = modName; + this.Locale = locale; + this.Key = key; + this.Text = text; + this.Placeholder = placeholder; + } + + /// Throw an exception if the translation text is null or empty. + /// There's no available translation matching the requested key and locale. + public Translation Assert() + { + if (!this.HasValue()) + throw new KeyNotFoundException($"The '{this.ModName}' mod doesn't have a translation with key '{this.Key}' for the '{this.Locale}' locale or its fallbacks."); + return this; + } + + /// Replace the text if it's null or empty. If you set a null or empty value, the translation will show the fallback "no translation" placeholder (see if you want to disable that). Returns a new instance if changed. + /// The default value. + public Translation Default(string @default) + { + return this.HasValue() + ? this + : new Translation(this.ModName, this.Locale, this.Key, @default); + } + + /// Whether to return a "no translation" placeholder if the translation is null or empty. Returns a new instance. + /// Whether to return a placeholder. + public Translation UsePlaceholder(bool use) + { + return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? $"(no translation:{this.Key})" : null); + } + + /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. + /// An anonymous object containing token key/value pairs, like new { value = 42, name = "Cranberries" }. + /// The argument is null. + public Translation Tokens(object tokens) + { + if (tokens == null) + throw new ArgumentNullException(nameof(tokens)); + + IDictionary dictionary = tokens + .GetType() + .GetProperties() + .ToDictionary( + p => p.Name, + p => p.GetValue(tokens) + ); + return this.Tokens(dictionary); + } + + /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. + /// A dictionary containing token key/value pairs. + /// The argument is null. + public Translation Tokens(IDictionary tokens) + { + if (tokens == null) + throw new ArgumentNullException(nameof(tokens)); + + tokens = tokens.ToDictionary(p => p.Key.Trim(), p => p.Value, StringComparer.InvariantCultureIgnoreCase); + string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match => + { + string key = match.Groups[1].Value.Trim(); + return tokens.TryGetValue(key, out object value) + ? value?.ToString() + : match.Value; + }); + return new Translation(this.ModName, this.Locale, this.Key, text); + } + + /// Get whether the translation has a defined value. + public bool HasValue() + { + return !string.IsNullOrEmpty(this.Text); + } + + /// Get the translation text. Calling this method isn't strictly necessary, since you can assign a value directly to a string. + public override string ToString() + { + return this.Placeholder != null && !this.HasValue() + ? this.Placeholder + : this.Text; + } + + /// Get a string representation of the given translation. + /// The translation key. + public static implicit operator string(Translation translation) + { + return translation?.ToString(); + } + } +} -- cgit From f5063cf81e68d142a5af6209ef80fd29fd80fce2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 24 May 2017 17:00:23 -0400 Subject: add translation unit tests (#296) --- src/StardewModdingAPI.Tests/ModResolverTests.cs | 1 + .../StardewModdingAPI.Tests.csproj | 2 + src/StardewModdingAPI.Tests/TranslationTests.cs | 333 +++++++++++++++++++++ src/StardewModdingAPI/Translation.cs | 7 +- src/crossplatform.targets | 1 - 5 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 src/StardewModdingAPI.Tests/TranslationTests.cs (limited to 'src') diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index c0545cd6..46c1c0cf 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -13,6 +13,7 @@ using StardewModdingAPI.Tests.Framework; namespace StardewModdingAPI.Tests { + /// Unit tests for . [TestFixture] public class ModResolverTests { diff --git a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj index c84adbd7..3818ec9c 100644 --- a/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj +++ b/src/StardewModdingAPI.Tests/StardewModdingAPI.Tests.csproj @@ -48,6 +48,7 @@ Properties\GlobalAssemblyInfo.cs + @@ -61,5 +62,6 @@ StardewModdingAPI + \ No newline at end of file diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs new file mode 100644 index 00000000..3e6e6499 --- /dev/null +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using StardewModdingAPI.Framework; +using StardewValley; + +namespace StardewModdingAPI.Tests +{ + /// Unit tests for and . + [TestFixture] + public class TranslationTests + { + /********* + ** Data + *********/ + /// Sample translation text for unit tests. + public static string[] Samples = { null, "", " ", "boop", " boop " }; + + /// A token structure type. + public enum TokenType + { + /// The tokens are passed in a dictionary. + Dictionary, + + /// The tokens are passed in an anonymous object. + AnonymousObject + } + + + /********* + ** Unit tests + *********/ + /**** + ** Translation helper + ****/ + [Test(Description = "Assert that the translation helper correctly handles no translations.")] + public void Helper_HandlesNoTranslations() + { + // arrange + var data = new Dictionary>(); + + // act + ITranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + Translation translation = helper.Translate("key"); + + // assert + Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(LocalizedContentManager.LanguageCode.en, helper.LocaleEnum, "The locale enum doesn't match the input value."); + Assert.IsNotNull(helper.GetTranslations(), "The full list of translations is unexpectedly null."); + Assert.AreEqual(0, helper.GetTranslations().Count, "The full list of translations is unexpectedly not empty."); + + Assert.IsNotNull(translation, "The translation helper unexpectedly returned a null translation."); + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value."); + } + + [Test(Description = "Assert that the translation helper returns the expected translations correctly.")] + public void Helper_GetTranslations_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary>(); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + actual[locale] = helper.GetTranslations(); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]), $"The translations for {locale} don't match the expected values."); + } + } + + [Test(Description = "Assert that the translations returned by the helper has the expected text.")] + public void Helper_Translate_ReturnsExpectedText() + { + // arrange + var data = this.GetSampleData(); + var expected = this.GetExpectedTranslations(); + + // act + var actual = new Dictionary>(); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + foreach (string locale in expected.Keys) + { + this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); + actual[locale] = new Dictionary(); + foreach (string key in expected[locale].Keys) + actual[locale][key] = helper.Translate(key); + } + + // assert + foreach (string locale in expected.Keys) + { + Assert.IsNotNull(actual[locale], $"The translations for {locale} is unexpectedly null."); + Assert.That(actual[locale], Is.EquivalentTo(expected[locale]), $"The translations for {locale} don't match the expected values."); + } + } + + /**** + ** Translation + ****/ + [Test(Description = "Assert that HasValue returns the expected result for various inputs.")] + [TestCase(null, ExpectedResult = false)] + [TestCase("", ExpectedResult = false)] + [TestCase(" ", ExpectedResult = true)] + [TestCase("boop", ExpectedResult = true)] + [TestCase(" boop ", ExpectedResult = true)] + public bool Translation_HasValue(string text) + { + return new Translation("ModName", "pt-BR", "key", text).HasValue(); + } + + [Test(Description = "Assert that the translation's ToString method returns the expected text for various inputs.")] + public void Translation_ToString([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation's implicit string conversion returns the expected text for various inputs.")] + public void Translation_ImplicitStringConversion([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, (string)translation, "The translation returned an unexpected value given a valid input."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), (string)translation, "The translation returned an unexpected value given a null or empty input."); + } + + [Test(Description = "Assert that the translation returns the expected text given a use-placeholder setting.")] + public void Translation_UsePlaceholder([Values(true, false)] bool value, [ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).UsePlaceholder(value); + + // assert + if (translation.HasValue()) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid input."); + else if (!value) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder disabled."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty input with the placeholder enabled."); + } + + [Test(Description = "Assert that the translation's Assert method throws the expected exception.")] + public void Translation_Assert([ValueSource(nameof(TranslationTests.Samples))] string text) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text); + + // assert + if (translation.HasValue()) + Assert.That(() => translation.Assert(), Throws.Nothing, "The assert unexpected threw an exception for a valid input."); + else + Assert.That(() => translation.Assert(), Throws.Exception.TypeOf(), "The assert didn't throw an exception for invalid input."); + } + + [Test(Description = "Assert that the translation returns the expected text after setting the default.")] + public void Translation_Default([ValueSource(nameof(TranslationTests.Samples))] string text, [ValueSource(nameof(TranslationTests.Samples))] string @default) + { + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Default(@default); + + // assert + if (!string.IsNullOrEmpty(text)) + Assert.AreEqual(text, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + else if (!string.IsNullOrEmpty(@default)) + Assert.AreEqual(@default, translation.ToString(), "The translation returned an unexpected value given a null or empty base text, but valid default."); + else + Assert.AreEqual(this.GetPlaceholderText("key"), translation.ToString(), "The translation returned an unexpected value given a null or empty base and default text."); + } + + /**** + ** Translation tokens + ****/ + [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] + public void Translation_Tokens([Values(TokenType.AnonymousObject, TokenType.Dictionary)] TokenType tokenType) + { + // arrange + string start = Guid.NewGuid().ToString("N"); + string middle = Guid.NewGuid().ToString("N"); + string end = Guid.NewGuid().ToString("N"); + const string input = "{{start}} tokens are properly replaced (including {{middle}} {{ MIDdlE}}) {{end}}"; + string expected = $"{start} tokens are properly replaced (including {middle} {middle}) {end}"; + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", input); + switch (tokenType) + { + case TokenType.AnonymousObject: + translation = translation.Tokens(new { start, middle, end }); + break; + + case TokenType.Dictionary: + translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); + break; + + default: + throw new NotSupportedException($"Unknown token type {tokenType}."); + } + + // assert + Assert.AreEqual(expected, translation.ToString(), "The translation returned an unexpected text."); + } + + [Test(Description = "Assert that the translation can replace tokens in all valid formats.")] + [TestCase("{{value}}", "value")] + [TestCase("{{ value }}", "value")] + [TestCase("{{value }}", "value")] + [TestCase("{{ the_value }}", "the_value")] + [TestCase("{{ the.value_here }}", "the.value_here")] + [TestCase("{{ the_value-here.... }}", "the_value-here....")] + [TestCase("{{ tHe_vALuE-HEre.... }}", "tHe_vALuE-HEre....")] + public void Translation_Tokens_ValidFormats(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + [Test(Description = "Assert that translation tokens are case-insensitive and surrounding-whitespace-insensitive.")] + [TestCase("{{value}}", "value")] + [TestCase("{{VaLuE}}", "vAlUe")] + [TestCase("{{VaLuE }}", " vAlUe")] + public void Translation_Tokens_KeysAreNormalised(string text, string key) + { + // arrange + string value = Guid.NewGuid().ToString("N"); + + // act + Translation translation = new Translation("ModName", "pt-BR", "key", text).Tokens(new Dictionary { [key] = value }); + + // assert + Assert.AreEqual(value, translation.ToString(), "The translation returned an unexpected value given a valid base text."); + } + + + /********* + ** Private methods + *********/ + /// Set a translation helper's locale and assert that it was set correctly. + /// The translation helper to change. + /// The expected locale. + /// The expected game language code. + private void AssertSetLocale(TranslationHelper helper, string locale, LocalizedContentManager.LanguageCode localeEnum) + { + helper.SetLocale(locale, localeEnum); + Assert.AreEqual(locale, helper.Locale, "The locale doesn't match the input value."); + Assert.AreEqual(localeEnum, helper.LocaleEnum, "The locale enum doesn't match the input value."); + } + + /// Get sample raw translations to input. + private IDictionary> GetSampleData() + { + return new Dictionary> + { + ["default"] = new Dictionary + { + ["key A"] = "default A", + ["key C"] = "default C" + }, + ["en"] = new Dictionary + { + ["key A"] = "en A", + ["key B"] = "en B" + }, + ["en-US"] = new Dictionary(), + ["zzz"] = new Dictionary + { + ["key A"] = "zzz A" + } + }; + } + + /// Get the expected translation output given , based on the expected locale fallback. + private IDictionary> GetExpectedTranslations() + { + return new Dictionary> + { + ["default"] = new Dictionary + { + ["key A"] = "default A", + ["key C"] = "default C" + }, + ["en"] = new Dictionary + { + ["key A"] = "en A", + ["key B"] = "en B", + ["key C"] = "default C" + }, + ["en-us"] = new Dictionary + { + ["key A"] = "en A", + ["key B"] = "en B", + ["key C"] = "default C" + }, + ["zzz"] = new Dictionary + { + ["key A"] = "zzz A", + ["key C"] = "default C" + } + }; + } + + /// Get the default placeholder text when a translation is missing. + /// The translation key. + private string GetPlaceholderText(string key) + { + return string.Format(Translation.PlaceholderText, key); + } + } +} diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs index ae4b833c..868ee4fa 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/StardewModdingAPI/Translation.cs @@ -11,6 +11,9 @@ namespace StardewModdingAPI /********* ** Properties *********/ + /// The placeholder text when the translation is null or empty, where {0} is the translation key. + internal const string PlaceholderText = "(no translation:{0})"; + /// The name of the relevant mod for error messages. private readonly string ModName; @@ -36,7 +39,7 @@ namespace StardewModdingAPI /// The translation key. /// The underlying translation text. internal Translation(string modName, string locale, string key, string text) - : this(modName, locale, key, text, $"(no translation:{key})") { } + : this(modName, locale, key, text, string.Format(Translation.PlaceholderText, key)) { } /// Construct an isntance. /// The name of the relevant mod for error messages. @@ -75,7 +78,7 @@ namespace StardewModdingAPI /// Whether to return a placeholder. public Translation UsePlaceholder(bool use) { - return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? $"(no translation:{this.Key})" : null); + return new Translation(this.ModName, this.Locale, this.Key, this.Text, use ? string.Format(Translation.PlaceholderText, this.Key) : null); } /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. diff --git a/src/crossplatform.targets b/src/crossplatform.targets index 00b731eb..31d4722d 100644 --- a/src/crossplatform.targets +++ b/src/crossplatform.targets @@ -33,7 +33,6 @@ $(GamePath)\Stardew Valley.exe - False $(GamePath)\xTile.dll -- cgit From a91e111247cbe5d9b92953a7f449a12c6fa7b4cf Mon Sep 17 00:00:00 2001 From: Platonymous Date: Mon, 15 May 2017 06:56:51 +0200 Subject: Added experimental .tbin support --- src/StardewModdingAPI/Framework/ContentHelper.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 893fa2c8..5a2a8b9b 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -6,6 +6,9 @@ using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using StardewValley; +using xTile; +using xTile.Format; +using xTile.Tiles; namespace StardewModdingAPI.Framework { @@ -74,6 +77,23 @@ namespace StardewModdingAPI.Framework case ".xnb": return this.ContentManager.Load(assetPath); + case ".tbin": + // validate + if (typeof(T) != typeof(Map)) + throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + + // try cache + if (this.ContentManager.IsLoaded(assetPath)) + return this.ContentManager.Load(assetPath); + + // fetch & cache + FormatManager formatManager = FormatManager.Instance; + Map map = formatManager.LoadMap(file.FullName); + foreach (TileSheet t in map.TileSheets) + t.ImageSource = t.ImageSource.Replace(".png", ""); + this.ContentManager.Inject(assetPath, map); + return (T)(object)map; + case ".png": // validate if (typeof(T) != typeof(Texture2D)) @@ -91,6 +111,10 @@ namespace StardewModdingAPI.Framework this.ContentManager.Inject(assetPath, texture); return (T)(object)texture; } + + + + default: throw new ContentLoadException($"Unknown file extension '{file.Extension}'; must be '.xnb' or '.png'."); -- cgit From 24e214b6012de06246deadfd3d63f4a5e25a71ba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 25 May 2017 20:55:08 -0400 Subject: minor cleanup --- src/StardewModdingAPI/Framework/ContentHelper.cs | 25 ++++++++++-------------- src/StardewModdingAPI/Program.cs | 1 - 2 files changed, 10 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 5a2a8b9b..10240cd1 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -71,38 +71,37 @@ namespace StardewModdingAPI.Framework // get asset path string assetPath = this.GetModAssetPath(key, file.FullName); + // try cache + if (this.ContentManager.IsLoaded(assetPath)) + return this.ContentManager.Load(assetPath); + // load content switch (file.Extension.ToLower()) { + // XNB file case ".xnb": return this.ContentManager.Load(assetPath); + // unpacked map case ".tbin": // validate if (typeof(T) != typeof(Map)) throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); - - // try cache - if (this.ContentManager.IsLoaded(assetPath)) - return this.ContentManager.Load(assetPath); // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - foreach (TileSheet t in map.TileSheets) - t.ImageSource = t.ImageSource.Replace(".png", ""); + foreach (TileSheet tilesheet in map.TileSheets) + tilesheet.ImageSource = tilesheet.ImageSource.Replace(".png", ""); this.ContentManager.Inject(assetPath, map); return (T)(object)map; - + + // unpacked image case ".png": // validate if (typeof(T) != typeof(Texture2D)) throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); - // try cache - if (this.ContentManager.IsLoaded(assetPath)) - return this.ContentManager.Load(assetPath); - // fetch & cache using (FileStream stream = File.OpenRead(file.FullName)) { @@ -111,10 +110,6 @@ namespace StardewModdingAPI.Framework this.ContentManager.Inject(assetPath, texture); return (T)(object)texture; } - - - - default: throw new ContentLoadException($"Unknown file extension '{file.Extension}'; must be '.xnb' or '.png'."); diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index b1cfb32d..a22e16a5 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -268,7 +268,6 @@ namespace StardewModdingAPI ** Private methods *********/ /// Assert that the minimum conditions are present to initialise SMAPI without type load exceptions. - /// Returns whether the minimum conditions are met. private static void AssertMinimumCompatibility() { void PrintErrorAndExit(string message) -- cgit From a47ca7e3910d532a3468f2ff222c9c1ed28514c4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 25 May 2017 21:02:05 -0400 Subject: expand .tbin loading to support custom tilesheets from the mod folder --- src/StardewModdingAPI/Framework/ContentHelper.cs | 43 ++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 10240cd1..e15f0dcf 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -91,8 +91,47 @@ namespace StardewModdingAPI.Framework // fetch & cache FormatManager formatManager = FormatManager.Instance; Map map = formatManager.LoadMap(file.FullName); - foreach (TileSheet tilesheet in map.TileSheets) - tilesheet.ImageSource = tilesheet.ImageSource.Replace(".png", ""); + if (map.TileSheets.Any()) + { + string relativeMapFolder = Path.GetDirectoryName(key) ?? ""; // folder path containing the map, relative to the mod folder + foreach (TileSheet tilesheet in map.TileSheets) + { + // check for tilesheet relative to map + { + string localKey = Path.Combine(relativeMapFolder, tilesheet.ImageSource); + FileInfo localFile = this.GetModFile(localKey); + if (localFile.Exists) + { + try + { + this.Load(localKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"{this.ModName} failed loading map '{key}' from {source} because the local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); + } + tilesheet.ImageSource = this.GetActualAssetKey(localKey); + continue; + } + } + + // fallback to game content + string contentKey = tilesheet.ImageSource; + if (contentKey.EndsWith(".png")) + contentKey = contentKey.Substring(0, contentKey.Length - 4); + try + { + this.ContentManager.Load(contentKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"{this.ModName} failed loading map '{key}' from {source} because the '{tilesheet.ImageSource}' tilesheet couldn't be found relative to either map file or the game's content folder.", ex); + } + tilesheet.ImageSource = contentKey; + } + } + + // inject map this.ContentManager.Inject(assetPath, map); return (T)(object)map; -- cgit From 7f210cd7b0717cd0ca242ceb730846847a356796 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 25 May 2017 21:35:43 -0400 Subject: fix tilesheets for local XNB maps too --- src/StardewModdingAPI/Framework/ContentHelper.cs | 116 +++++++++++++---------- 1 file changed, 67 insertions(+), 49 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index e15f0dcf..2a590609 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -80,61 +80,30 @@ namespace StardewModdingAPI.Framework { // XNB file case ".xnb": - return this.ContentManager.Load(assetPath); + { + T asset = this.ContentManager.Load(assetPath); + if (asset is Map) + this.FixLocalMapTilesheets(asset as Map, key); + return asset; + } // unpacked map case ".tbin": - // validate - if (typeof(T) != typeof(Map)) - throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); - - // fetch & cache - FormatManager formatManager = FormatManager.Instance; - Map map = formatManager.LoadMap(file.FullName); - if (map.TileSheets.Any()) { - string relativeMapFolder = Path.GetDirectoryName(key) ?? ""; // folder path containing the map, relative to the mod folder - foreach (TileSheet tilesheet in map.TileSheets) - { - // check for tilesheet relative to map - { - string localKey = Path.Combine(relativeMapFolder, tilesheet.ImageSource); - FileInfo localFile = this.GetModFile(localKey); - if (localFile.Exists) - { - try - { - this.Load(localKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"{this.ModName} failed loading map '{key}' from {source} because the local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); - } - tilesheet.ImageSource = this.GetActualAssetKey(localKey); - continue; - } - } - - // fallback to game content - string contentKey = tilesheet.ImageSource; - if (contentKey.EndsWith(".png")) - contentKey = contentKey.Substring(0, contentKey.Length - 4); - try - { - this.ContentManager.Load(contentKey); - } - catch (Exception ex) - { - throw new ContentLoadException($"{this.ModName} failed loading map '{key}' from {source} because the '{tilesheet.ImageSource}' tilesheet couldn't be found relative to either map file or the game's content folder.", ex); - } - tilesheet.ImageSource = contentKey; - } + // validate + if (typeof(T) != typeof(Map)) + throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + + // fetch & cache + FormatManager formatManager = FormatManager.Instance; + Map map = formatManager.LoadMap(file.FullName); + this.FixLocalMapTilesheets(map, key); + + // inject map + this.ContentManager.Inject(assetPath, map); + return (T)(object)map; } - // inject map - this.ContentManager.Inject(assetPath, map); - return (T)(object)map; - // unpacked image case ".png": // validate @@ -188,6 +157,55 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// Fix the tilesheets for a map loaded from the mod folder. + /// The map whose tilesheets to fix. + /// The map asset key within the mod folder. + /// The map tilesheets could not be loaded. + private void FixLocalMapTilesheets(Map map, string mapKey) + { + if (!map.TileSheets.Any()) + return; + + string relativeMapFolder = Path.GetDirectoryName(mapKey) ?? ""; // folder path containing the map, relative to the mod folder + foreach (TileSheet tilesheet in map.TileSheets) + { + // check for tilesheet relative to map + { + string localKey = Path.Combine(relativeMapFolder, tilesheet.ImageSource); + FileInfo localFile = this.GetModFile(localKey); + if (localFile.Exists) + { + try + { + this.Load(localKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"{this.ModName} failed loading map '{mapKey}' from {ContentSource.ModFolder} because the local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); + } + tilesheet.ImageSource = this.GetActualAssetKey(localKey); + continue; + } + } + + // fallback to game content + { + string contentKey = tilesheet.ImageSource; + if (contentKey.EndsWith(".png")) + contentKey = contentKey.Substring(0, contentKey.Length - 4); + try + { + this.ContentManager.Load(contentKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"{this.ModName} failed loading map '{mapKey}' from {ContentSource.ModFolder} because the '{tilesheet.ImageSource}' tilesheet couldn't be found relative to either map file or the game's content folder.", ex); + } + tilesheet.ImageSource = contentKey; + } + } + } + /// Assert that the given key has a valid format. /// The asset key to check. /// The asset key is empty or contains invalid characters. -- cgit From 569ae2b87b10bcef1088b51a33a3f3d5734d60df Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 25 May 2017 21:52:15 -0400 Subject: reduce ContentLoadException nesting --- src/StardewModdingAPI/Framework/ContentHelper.cs | 21 ++++++++++++--------- .../Framework/Exceptions/SContentLoadException.cs | 18 ++++++++++++++++++ src/StardewModdingAPI/StardewModdingAPI.csproj | 1 + 3 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 2a590609..14b6aa8f 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Exceptions; using StardewValley; using xTile; using xTile.Format; @@ -54,6 +55,8 @@ namespace StardewModdingAPI.Framework /// The content asset couldn't be loaded (e.g. because it doesn't exist). public T Load(string key, ContentSource source = ContentSource.ModFolder) { + SContentLoadException GetContentError(string reasonPhrase) => new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}: {reasonPhrase}."); + this.AssertValidAssetKeyFormat(key); try { @@ -66,7 +69,7 @@ namespace StardewModdingAPI.Framework // get file FileInfo file = this.GetModFile(key); if (!file.Exists) - throw new ContentLoadException($"There is no file at path '{file.FullName}'."); + throw GetContentError($"there's no matching file at path '{file.FullName}'."); // get asset path string assetPath = this.GetModAssetPath(key, file.FullName); @@ -92,7 +95,7 @@ namespace StardewModdingAPI.Framework { // validate if (typeof(T) != typeof(Map)) - throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Map)}'."); // fetch & cache FormatManager formatManager = FormatManager.Instance; @@ -108,7 +111,7 @@ namespace StardewModdingAPI.Framework case ".png": // validate if (typeof(T) != typeof(Texture2D)) - throw new ContentLoadException($"Can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); + throw GetContentError($"can't read file with extension '{file.Extension}' as type '{typeof(T)}'; must be type '{typeof(Texture2D)}'."); // fetch & cache using (FileStream stream = File.OpenRead(file.FullName)) @@ -120,16 +123,16 @@ namespace StardewModdingAPI.Framework } default: - throw new ContentLoadException($"Unknown file extension '{file.Extension}'; must be '.xnb' or '.png'."); + throw GetContentError($"unknown file extension '{file.Extension}'; must be one of '.png', '.tbin', or '.xnb'."); } default: - throw new NotSupportedException($"Unknown content source '{source}'."); + throw GetContentError($"unknown content source '{source}'."); } } - catch (Exception ex) + catch (Exception ex) when (!(ex is SContentLoadException)) { - throw new ContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); + throw new SContentLoadException($"{this.ModName} failed loading content asset '{key}' from {source}.", ex); } } @@ -181,7 +184,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - throw new ContentLoadException($"{this.ModName} failed loading map '{mapKey}' from {ContentSource.ModFolder} because the local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); + throw new ContentLoadException($"The local '{tilesheet.ImageSource}' tilesheet couldn't be loaded.", ex); } tilesheet.ImageSource = this.GetActualAssetKey(localKey); continue; @@ -199,7 +202,7 @@ namespace StardewModdingAPI.Framework } catch (Exception ex) { - throw new ContentLoadException($"{this.ModName} failed loading map '{mapKey}' from {ContentSource.ModFolder} because the '{tilesheet.ImageSource}' tilesheet couldn't be found relative to either map file or the game's content folder.", ex); + throw new ContentLoadException($"The '{tilesheet.ImageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex); } tilesheet.ImageSource = contentKey; } diff --git a/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs b/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs new file mode 100644 index 00000000..85d85e3d --- /dev/null +++ b/src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs @@ -0,0 +1,18 @@ +using System; +using Microsoft.Xna.Framework.Content; + +namespace StardewModdingAPI.Framework.Exceptions +{ + /// An implementation of used by SMAPI to detect whether it was thrown by SMAPI or the underlying framework. + internal class SContentLoadException : ContentLoadException + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The error message. + /// The underlying exception, if any. + public SContentLoadException(string message, Exception ex = null) + : base(message, ex) { } + } +} diff --git a/src/StardewModdingAPI/StardewModdingAPI.csproj b/src/StardewModdingAPI/StardewModdingAPI.csproj index 31b2c1cf..ae454a35 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.csproj +++ b/src/StardewModdingAPI/StardewModdingAPI.csproj @@ -94,6 +94,7 @@ + -- cgit From 395925ad2b82e3c73dc3ecbb6b5533f46ec76ab8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 May 2017 00:33:32 -0400 Subject: fix errors handling dependencies if some mods have no manifest --- src/StardewModdingAPI.Tests/ModResolverTests.cs | 36 ++++++++++++++++++++++ .../Framework/ModLoading/ModResolver.cs | 6 ++-- 2 files changed, 39 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Tests/ModResolverTests.cs b/src/StardewModdingAPI.Tests/ModResolverTests.cs index 46c1c0cf..23aeba64 100644 --- a/src/StardewModdingAPI.Tests/ModResolverTests.cs +++ b/src/StardewModdingAPI.Tests/ModResolverTests.cs @@ -245,6 +245,20 @@ namespace StardewModdingAPI.Tests Assert.AreSame(modC.Object, mods[2], "The load order unexpectedly changed with no dependencies."); } + [Test(Description = "Assert that processing dependencies skips mods that have already failed without calling any other properties.")] + public void ProcessDependencies_Skips_Failed() + { + // arrange + Mock mock = new Mock(MockBehavior.Strict); + mock.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); + + // act + new ModResolver().ProcessDependencies(new[] { mock.Object }); + + // assert + mock.VerifyGet(p => p.Status, Times.Once, "The validation did not check the manifest status."); + } + [Test(Description = "Assert that simple dependencies are reordered correctly.")] public void ProcessDependencies_Reorders_SimpleDependencies() { @@ -342,6 +356,28 @@ namespace StardewModdingAPI.Tests modE.Verify(p => p.SetStatus(ModMetadataStatus.Failed, It.IsAny()), Times.Once, "Mod E was expected to fail since it's part of a dependency loop."); } + [Test(Description = "Assert that dependencies are sorted correctly even if some of the mods failed during metadata loading.")] + public void ProcessDependencies_WithSomeFailedMods_Succeeds() + { + // arrange + // A ◀── B ◀── C D (failed) + Mock modA = this.GetMetadataForDependencyTest("Mod A"); + Mock modB = this.GetMetadataForDependencyTest("Mod B", dependencies: new[] { "Mod A" }); + Mock modC = this.GetMetadataForDependencyTest("Mod C", dependencies: new[] { "Mod B" }, allowStatusChange: true); + Mock modD = new Mock(MockBehavior.Strict); + modD.Setup(p => p.Manifest).Returns(null); + modD.Setup(p => p.Status).Returns(ModMetadataStatus.Failed); + + // act + IModMetadata[] mods = new ModResolver().ProcessDependencies(new[] { modC.Object, modA.Object, modB.Object, modD.Object }).ToArray(); + + // assert + Assert.AreEqual(4, mods.Length, 0, "Expected to get the same number of mods input."); + Assert.AreSame(modD.Object, mods[0], "The load order is incorrect: mod D should be first since it was already failed."); + Assert.AreSame(modA.Object, mods[1], "The load order is incorrect: mod A should be second since it's needed by mod B."); + Assert.AreSame(modB.Object, mods[2], "The load order is incorrect: mod B should be third since it needs mod A, and is needed by mod C."); + Assert.AreSame(modC.Object, mods[3], "The load order is incorrect: mod C should be fourth since it needs mod B, and is needed by mod D."); + } /********* ** Private methods diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 21aebeb1..f5139ce5 100644 --- a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs +++ b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs @@ -141,7 +141,7 @@ namespace StardewModdingAPI.Framework.ModLoading states[mod] = ModDependencyStatus.Failed; sortedMods.Push(mod); } - + // sort mods foreach (IModMetadata mod in mods) this.ProcessDependencies(mods.ToArray(), mod, states, sortedMods, new List()); @@ -198,7 +198,7 @@ namespace StardewModdingAPI.Framework.ModLoading string[] missingModIDs = ( from dependency in mod.Manifest.Dependencies - where mods.All(m => m.Manifest.UniqueID != dependency.UniqueID) + where mods.All(m => m.Manifest?.UniqueID != dependency.UniqueID) orderby dependency.UniqueID select dependency.UniqueID ) @@ -219,7 +219,7 @@ namespace StardewModdingAPI.Framework.ModLoading IModMetadata[] modsToLoadFirst = ( from other in mods - where mod.Manifest.Dependencies.Any(required => required.UniqueID == other.Manifest.UniqueID) + where mod.Manifest.Dependencies.Any(required => required.UniqueID == other.Manifest?.UniqueID) select other ) .ToArray(); -- cgit From 3324628e3b8fa5d85102a57e273595b783f86318 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 May 2017 23:55:57 -0400 Subject: standardise indentation in unix launcher --- src/StardewModdingAPI/unix-launcher.sh | 108 ++++++++++++++++----------------- 1 file changed, 54 insertions(+), 54 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/StardewModdingAPI/unix-launcher.sh index bf0e9d5e..eb2f42af 100644 --- a/src/StardewModdingAPI/unix-launcher.sh +++ b/src/StardewModdingAPI/unix-launcher.sh @@ -12,64 +12,64 @@ ARCH=`uname -m` # MonoKickstart picks the right libfolder, so just execute the right binary. if [ "$UNAME" == "Darwin" ]; then - # ... Except on OSX. - export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ + # ... Except on OSX. + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ - # El Capitan is a total idiot and wipes this variable out, making the - # Steam overlay disappear. This sidesteps "System Integrity Protection" - # and resets the variable with Valve's own variable (they provided this - # fix by the way, thanks Valve!). Note that you will need to update your - # launch configuration to the script location, NOT just the app location - # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). - # -flibit - if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then - export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" - fi + # El Capitan is a total idiot and wipes this variable out, making the + # Steam overlay disappear. This sidesteps "System Integrity Protection" + # and resets the variable with Valve's own variable (they provided this + # fix by the way, thanks Valve!). Note that you will need to update your + # launch configuration to the script location, NOT just the app location + # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). + # -flibit + if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then + export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" + fi - ln -sf mcs.bin.osx mcs - cp StardewValley.bin.osx StardewModdingAPI.bin.osx - open -a Terminal ./StardewModdingAPI.bin.osx $@ + ln -sf mcs.bin.osx mcs + cp StardewValley.bin.osx StardewModdingAPI.bin.osx + open -a Terminal ./StardewModdingAPI.bin.osx $@ else - # choose launcher - LAUNCHER="" - if [ "$ARCH" == "x86_64" ]; then - ln -sf mcs.bin.x86_64 mcs - cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" - else - ln -sf mcs.bin.x86 mcs - cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCHER="./StardewModdingAPI.bin.x86 $@" - fi + # choose launcher + LAUNCHER="" + if [ "$ARCH" == "x86_64" ]; then + ln -sf mcs.bin.x86_64 mcs + cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 + LAUNCHER="./StardewModdingAPI.bin.x86_64 $@" + else + ln -sf mcs.bin.x86 mcs + cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 + LAUNCHER="./StardewModdingAPI.bin.x86 $@" + fi - # get cross-distro version of POSIX command - COMMAND="" - if command -v command 2>/dev/null; then - COMMAND="command -v" - elif type type 2>/dev/null; then - COMMAND="type" - fi + # get cross-distro version of POSIX command + COMMAND="" + if command -v command 2>/dev/null; then + COMMAND="command -v" + elif type type 2>/dev/null; then + COMMAND="type" + fi - # open SMAPI in terminal - if $COMMAND x-terminal-emulator 2>/dev/null; then - x-terminal-emulator -e "$LAUNCHER" - elif $COMMAND gnome-terminal 2>/dev/null; then - gnome-terminal -e "$LAUNCHER" - elif $COMMAND xterm 2>/dev/null; then - xterm -e "$LAUNCHER" - elif $COMMAND konsole 2>/dev/null; then - konsole -e "$LAUNCHER" - elif $COMMAND terminal 2>/dev/null; then - terminal -e "$LAUNCHER" - else - $LAUNCHER - fi + # open SMAPI in terminal + if $COMMAND x-terminal-emulator 2>/dev/null; then + x-terminal-emulator -e "$LAUNCHER" + elif $COMMAND gnome-terminal 2>/dev/null; then + gnome-terminal -e "$LAUNCHER" + elif $COMMAND xterm 2>/dev/null; then + xterm -e "$LAUNCHER" + elif $COMMAND konsole 2>/dev/null; then + konsole -e "$LAUNCHER" + elif $COMMAND terminal 2>/dev/null; then + terminal -e "$LAUNCHER" + else + $LAUNCHER + fi - # some Linux users get error 127 (command not found) from the above block, even though - # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when - # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal - # (which can be slow if there is none). - if [ $? -eq 127 ]; then - $LAUNCHER --no-terminal - fi + # some Linux users get error 127 (command not found) from the above block, even though + # `command -v` indicates the command is valid. As a fallback, launch SMAPI without a terminal when + # that happens and pass in an argument indicating SMAPI shouldn't try writing to the terminal + # (which can be slow if there is none). + if [ $? -eq 127 ]; then + $LAUNCHER --no-terminal + fi fi -- cgit From e92dbc41df1548b72be4ca5c0a6c6fe17235d950 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 May 2017 00:21:48 -0400 Subject: improve libgdiplus.dylib fix for Mono players --- lib/libgdiplus.dylib | Bin 1202796 -> 0 bytes release-notes.md | 3 ++- .../InteractiveInstaller.cs | 1 + src/StardewModdingAPI/unix-launcher.sh | 11 +++++++++++ src/prepare-install-package.targets | 3 --- 5 files changed, 14 insertions(+), 4 deletions(-) delete mode 100644 lib/libgdiplus.dylib (limited to 'src') diff --git a/lib/libgdiplus.dylib b/lib/libgdiplus.dylib deleted file mode 100644 index 8a9676c8..00000000 Binary files a/lib/libgdiplus.dylib and /dev/null differ diff --git a/release-notes.md b/release-notes.md index fd69e1c7..deab5cfe 100644 --- a/release-notes.md +++ b/release-notes.md @@ -20,6 +20,7 @@ For players: * you have Stardew Valley 1.11 or earlier (which aren't compatible); * you run `install.exe` from within the downloaded zip file. * Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. +* Fixed `libgdiplus.dylib` errors for some players on Mac. * Fixed rare crash when window loses focus for a few players. For modders: @@ -68,7 +69,7 @@ For players: * The installer now lets you choose the install path if you have multiple copies of the game, instead of using the first path found. * Fixed mod draw errors breaking the game. * Fixed mods on Linux/Mac no longer working after the game saves. -* Fixed libgdiplus DLL-not-found errors on Linux/Mac when mods read PNG files. +* Fixed `libgdiplus.dylib` errors on Mac when mods read PNG files. * Adopted pufferchick. For mod developers: diff --git a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs index 43cb7d70..efad0a3e 100644 --- a/src/StardewModdingAPI.Installer/InteractiveInstaller.cs +++ b/src/StardewModdingAPI.Installer/InteractiveInstaller.cs @@ -85,6 +85,7 @@ namespace StardewModdingApi.Installer yield return GetInstallPath("steam_appid.txt"); // Linux/Mac only + yield return GetInstallPath("libgdiplus.dylib"); yield return GetInstallPath("StardewModdingAPI"); yield return GetInstallPath("StardewModdingAPI.exe.mdb"); yield return GetInstallPath("System.Numerics.dll"); diff --git a/src/StardewModdingAPI/unix-launcher.sh b/src/StardewModdingAPI/unix-launcher.sh index eb2f42af..39fd4f29 100644 --- a/src/StardewModdingAPI/unix-launcher.sh +++ b/src/StardewModdingAPI/unix-launcher.sh @@ -26,7 +26,18 @@ if [ "$UNAME" == "Darwin" ]; then export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" fi + # this was here before ln -sf mcs.bin.osx mcs + + # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI + if [ -f libgdiplus.dylib ]; then + rm libgdiplus.dylib + fi + if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then + ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib + fi + + # launch SMAPI cp StardewValley.bin.osx StardewModdingAPI.bin.osx open -a Terminal ./StardewModdingAPI.bin.osx $@ else diff --git a/src/prepare-install-package.targets b/src/prepare-install-package.targets index 9a514abd..f2a2b23c 100644 --- a/src/prepare-install-package.targets +++ b/src/prepare-install-package.targets @@ -35,9 +35,6 @@ - - - -- cgit From 55fa8198ffc140237e1041056f3a4d8f4e7469c8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 May 2017 01:01:45 -0400 Subject: fix content API not matching XNB files with two dots (like 'a.b.xnb') if extension isn't specified --- release-notes.md | 1 + src/StardewModdingAPI/Framework/ContentHelper.cs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index deab5cfe..622fcd49 100644 --- a/release-notes.md +++ b/release-notes.md @@ -31,6 +31,7 @@ For modders: _When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._ * Added `Context.IsInDrawLoop` for specialised mods. * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. +* Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. ## 1.13.1 See [log](https://github.com/Pathoschild/SMAPI/compare/1.13...1.13.1). diff --git a/src/StardewModdingAPI/Framework/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 14b6aa8f..7fd5e803 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -225,10 +225,18 @@ namespace StardewModdingAPI.Framework /// The asset path relative to the mod folder. private FileInfo GetModFile(string path) { + // try exact match path = Path.Combine(this.ModFolderPath, this.ContentManager.NormalisePathSeparators(path)); FileInfo file = new FileInfo(path); - if (!file.Exists && file.Extension == "") - file = new FileInfo(Path.Combine(this.ModFolderPath, path + ".xnb")); + + // try with default extension + if (!file.Exists && file.Extension.ToLower() != ".xnb") + { + FileInfo result = new FileInfo(path + ".xnb"); + if (result.Exists) + file = result; + } + return file; } -- cgit From 173dd8cd030992983ddea36c944f22f7ad6ed9bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 May 2017 23:48:16 -0400 Subject: add Context.IsPlayerFree --- release-notes.md | 1 + src/StardewModdingAPI/Context.cs | 3 +++ 2 files changed, 4 insertions(+) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 622fcd49..ce4ec082 100644 --- a/release-notes.md +++ b/release-notes.md @@ -29,6 +29,7 @@ For modders: * You can now load unpacked `.tbin` files from your mod folder through the content API. * SMAPI now automatically fixes tilesheet references for maps loaded from the mod folder. _When loading a map from the mod folder, SMAPI will automatically use tilesheets relative to the map file if they exists. Otherwise it will default to tilesheets in the game content._ +* Added `Context.IsPlayerFree` for mods that need to check if the player can act (i.e. save is loaded, no menu is displayed, no cutscene is in progress, etc). * Added `Context.IsInDrawLoop` for specialised mods. * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. * Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. diff --git a/src/StardewModdingAPI/Context.cs b/src/StardewModdingAPI/Context.cs index 3fe70c68..6c5ae40e 100644 --- a/src/StardewModdingAPI/Context.cs +++ b/src/StardewModdingAPI/Context.cs @@ -16,6 +16,9 @@ namespace StardewModdingAPI /// Whether the player has loaded a save and the world has finished initialising. public static bool IsWorldReady { get; internal set; } + /// Whether the player is free to move around (e.g. save is loaded, no menu is displayed, no cutscene is in progress, etc). + public static bool IsPlayerFree => Context.IsWorldReady && Game1.activeClickableMenu == null && Game1.player.CanMove && !Game1.dialogueUp && !Game1.eventUp; + /// Whether the game is currently running the draw loop. This isn't relevant to most mods, since you should use to draw to the screen. public static bool IsInDrawLoop { get; internal set; } -- cgit From 586f27c4c265e06d0138e419f762e326bbe3ca46 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 00:13:43 -0400 Subject: remove unneeded SGame fields --- src/StardewModdingAPI/Framework/SGame.cs | 73 ++++++++++++-------------------- 1 file changed, 27 insertions(+), 46 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 05ef9626..7e222a2c 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -55,37 +55,16 @@ namespace StardewModdingAPI.Framework ** Game state ****/ /// Arrays of pressed controller buttons indexed by . - private Buttons[] PreviouslyPressedButtons = new Buttons[0]; - - /// A record of the keyboard state (i.e. the up/down state for each button) as of the latest tick. - private KeyboardState KStateNow; + private Buttons[] PreviousPressedButtons = new Buttons[0]; /// A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick. - private KeyboardState KStatePrior; - - /// A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the latest tick. - private MouseState MStateNow; + private KeyboardState PreviousKeyState; /// A record of the mouse state (i.e. the cursor position, scroll amount, and the up/down state for each button) as of the previous tick. - private MouseState MStatePrior; - - /// The current mouse position on the screen adjusted for the zoom level. - private Point MPositionNow; + private MouseState PreviousMouseState; /// The previous mouse position on the screen adjusted for the zoom level. - private Point MPositionPrior; - - /// The keys that were pressed as of the latest tick. - private Keys[] CurrentlyPressedKeys => this.KStateNow.GetPressedKeys(); - - /// The keys that were pressed as of the previous tick. - private Keys[] PreviouslyPressedKeys => this.KStatePrior.GetPressedKeys(); - - /// The keys that just entered the down state. - private Keys[] FramePressedKeys => this.CurrentlyPressedKeys.Except(this.PreviouslyPressedKeys).ToArray(); - - /// The keys that just entered the up state. - private Keys[] FrameReleasedKeys => this.PreviouslyPressedKeys.Except(this.CurrentlyPressedKeys).ToArray(); + private Point PreviousMousePosition; /// The previous save ID at last check. private ulong PreviousSaveID; @@ -355,16 +334,22 @@ namespace StardewModdingAPI.Framework if (Game1.game1.IsActive) { // get latest state - this.KStateNow = Keyboard.GetState(); - this.MStateNow = Mouse.GetState(); - this.MPositionNow = new Point(Game1.getMouseX(), Game1.getMouseY()); + KeyboardState keyState = Keyboard.GetState(); + MouseState mouseState = Mouse.GetState(); + Point mousePosition = new Point(Game1.getMouseX(), Game1.getMouseY()); + + // analyse state + Keys[] currentlyPressedKeys = keyState.GetPressedKeys(); + Keys[] previousPressedKeys = this.PreviousKeyState.GetPressedKeys(); + Keys[] framePressedKeys = currentlyPressedKeys.Except(previousPressedKeys).ToArray(); + Keys[] frameReleasedKeys = previousPressedKeys.Except(currentlyPressedKeys).ToArray(); // raise key pressed - foreach (Keys key in this.FramePressedKeys) + foreach (Keys key in framePressedKeys) ControlEvents.InvokeKeyPressed(this.Monitor, key); // raise key released - foreach (Keys key in this.FrameReleasedKeys) + foreach (Keys key in frameReleasedKeys) ControlEvents.InvokeKeyReleased(this.Monitor, key); // raise controller button pressed @@ -392,16 +377,18 @@ namespace StardewModdingAPI.Framework } // raise keyboard state changed - if (this.KStateNow != this.KStatePrior) - ControlEvents.InvokeKeyboardChanged(this.Monitor, this.KStatePrior, this.KStateNow); + if (keyState != this.PreviousKeyState) + ControlEvents.InvokeKeyboardChanged(this.Monitor, this.PreviousKeyState, keyState); // raise mouse state changed - if (this.MStateNow != this.MStatePrior) - { - ControlEvents.InvokeMouseChanged(this.Monitor, this.MStatePrior, this.MStateNow, this.MPositionPrior, this.MPositionNow); - this.MStatePrior = this.MStateNow; - this.MPositionPrior = this.MPositionNow; - } + if (mouseState != this.PreviousMouseState) + ControlEvents.InvokeMouseChanged(this.Monitor, this.PreviousMouseState, mouseState, this.PreviousMousePosition, mousePosition); + + // track state + this.PreviousMouseState = mouseState; + this.PreviousMousePosition = mousePosition; + this.PreviousKeyState = keyState; + this.PreviousPressedButtons = this.GetButtonsDown(); } /********* @@ -562,12 +549,6 @@ namespace StardewModdingAPI.Framework if (this.CurrentUpdateTick >= 60) this.CurrentUpdateTick = 0; - /********* - ** Update input state - *********/ - this.KStatePrior = this.KStateNow; - this.PreviouslyPressedButtons = this.GetButtonsDown(); - this.UpdateCrashTimer.Reset(); } catch (Exception ex) @@ -1385,7 +1366,7 @@ namespace StardewModdingAPI.Framework /// The last known state. private bool WasButtonJustPressed(Buttons button, ButtonState buttonState) { - return buttonState == ButtonState.Pressed && !this.PreviouslyPressedButtons.Contains(button); + return buttonState == ButtonState.Pressed && !this.PreviousPressedButtons.Contains(button); } /// Get whether a controller button was released since the last check. @@ -1393,7 +1374,7 @@ namespace StardewModdingAPI.Framework /// The last known state. private bool WasButtonJustReleased(Buttons button, ButtonState buttonState) { - return buttonState == ButtonState.Released && this.PreviouslyPressedButtons.Contains(button); + return buttonState == ButtonState.Released && this.PreviousPressedButtons.Contains(button); } /// Get whether an analogue controller button was pressed since the last check. -- cgit From 698930820cc59c5e7193283afb7e9d535391afdb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 00:39:01 -0400 Subject: log debug command output (#297) --- release-notes.md | 1 + src/TrainerMod/TrainerMod.cs | 8 ++++++++ 2 files changed, 9 insertions(+) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index ce4ec082..f202f4d4 100644 --- a/release-notes.md +++ b/release-notes.md @@ -33,6 +33,7 @@ For modders: * Added `Context.IsInDrawLoop` for specialised mods. * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. * Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. +* Fixed `debug` command output not printed to console. ## 1.13.1 See [log](https://github.com/Pathoschild/SMAPI/compare/1.13...1.13.1). diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs index 95c7cbaf..815cfb17 100644 --- a/src/TrainerMod/TrainerMod.cs +++ b/src/TrainerMod/TrainerMod.cs @@ -124,9 +124,17 @@ namespace TrainerMod switch (command) { case "debug": + // build debug string string debugCommand = string.Join(" ", args); this.Monitor.Log($"Sending debug command to the game: {debugCommand}...", LogLevel.Info); + + // submit command + string oldOutput = Game1.debugOutput; Game1.game1.parseDebugInput(debugCommand); + + // log command output (if any) + if (Game1.debugOutput != oldOutput) + this.Monitor.Log($"> {Game1.debugOutput}", LogLevel.Info); break; case "save": -- cgit From 0c992e562ad1d3aafd12d0820bbd8ce0bb580c5d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 00:46:25 -0400 Subject: simplify debug command output (#297) --- src/TrainerMod/TrainerMod.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/src/TrainerMod/TrainerMod.cs b/src/TrainerMod/TrainerMod.cs index 815cfb17..9a3a8d0b 100644 --- a/src/TrainerMod/TrainerMod.cs +++ b/src/TrainerMod/TrainerMod.cs @@ -124,17 +124,15 @@ namespace TrainerMod switch (command) { case "debug": - // build debug string - string debugCommand = string.Join(" ", args); - this.Monitor.Log($"Sending debug command to the game: {debugCommand}...", LogLevel.Info); - // submit command + string debugCommand = string.Join(" ", args); string oldOutput = Game1.debugOutput; Game1.game1.parseDebugInput(debugCommand); - // log command output (if any) - if (Game1.debugOutput != oldOutput) - this.Monitor.Log($"> {Game1.debugOutput}", LogLevel.Info); + // show result + this.Monitor.Log(Game1.debugOutput != oldOutput + ? $"> {Game1.debugOutput}" + : "Sent debug command to the game, but there was no output.", LogLevel.Info); break; case "save": -- cgit From 01197b0eb1845251b12322ba1b9cf130cd2a9dc1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 01:05:41 -0400 Subject: minor cleanup --- src/StardewModdingAPI/Program.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index a22e16a5..47ba7c30 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -452,10 +452,15 @@ namespace StardewModdingAPI { while (true) { + // get input string input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) + continue; + + // parse input try { - if (!string.IsNullOrWhiteSpace(input) && !this.CommandManager.Trigger(input)) + if (!this.CommandManager.Trigger(input)) this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); } catch (Exception ex) -- cgit From 12ffd9c334906e2256a1c32976b6a07f75027f25 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 01:06:35 -0400 Subject: add shortcut indexer to translation API (#296) --- src/StardewModdingAPI/Framework/TranslationHelper.cs | 4 ++++ src/StardewModdingAPI/ITranslationHelper.cs | 4 ++++ 2 files changed, 8 insertions(+) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/TranslationHelper.cs b/src/StardewModdingAPI/Framework/TranslationHelper.cs index dece6214..c3104d1b 100644 --- a/src/StardewModdingAPI/Framework/TranslationHelper.cs +++ b/src/StardewModdingAPI/Framework/TranslationHelper.cs @@ -29,6 +29,10 @@ namespace StardewModdingAPI.Framework /// The game's current language code. public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + /// Get a translation for the current locale. This is a convenience shortcut for . + /// The translation key. + public Translation this[string key] => this.Translate(key); + /********* ** Public methods diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/StardewModdingAPI/ITranslationHelper.cs index 84571d0e..297a0d5a 100644 --- a/src/StardewModdingAPI/ITranslationHelper.cs +++ b/src/StardewModdingAPI/ITranslationHelper.cs @@ -15,6 +15,10 @@ namespace StardewModdingAPI /// The game's current language code. LocalizedContentManager.LanguageCode LocaleEnum { get; } + /// Get a translation for the current locale. This is a convenience shortcut for . + /// The translation key. + Translation this[string key] { get; } + /********* ** Public methods -- cgit From 5af58c7b18a120ce47f230ede7f116678d97e038 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 01:49:21 -0400 Subject: refactor translation init for reuse (#296) --- src/StardewModdingAPI.Tests/TranslationTests.cs | 6 +-- src/StardewModdingAPI/Framework/ModHelper.cs | 5 +- .../Framework/TranslationHelper.cs | 20 +++++-- src/StardewModdingAPI/Program.cs | 63 ++++++++++++---------- 4 files changed, 56 insertions(+), 38 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs index 3e6e6499..76da8706 100644 --- a/src/StardewModdingAPI.Tests/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -40,7 +40,7 @@ namespace StardewModdingAPI.Tests var data = new Dictionary>(); // act - ITranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + ITranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); Translation translation = helper.Translate("key"); // assert @@ -62,7 +62,7 @@ namespace StardewModdingAPI.Tests // act var actual = new Dictionary>(); - TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); foreach (string locale in expected.Keys) { this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); @@ -86,7 +86,7 @@ namespace StardewModdingAPI.Tests // act var actual = new Dictionary>(); - TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en, data); + TranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); foreach (string locale in expected.Keys) { this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 8c578dbe..947b1ae8 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -47,10 +47,9 @@ namespace StardewModdingAPI.Framework /// Manages console commands. /// The content manager which loads content assets. /// Simplifies access to private game code. - /// Provides translations stored in the mod folder. /// An argument is null or empty. /// The path does not exist on disk. - public ModHelper(string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection, ITranslationHelper translations) + public ModHelper(string displayName, string modDirectory, JsonHelper jsonHelper, IModRegistry modRegistry, CommandManager commandManager, SContentManager contentManager, IReflectionHelper reflection) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -69,7 +68,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.ConsoleCommands = new CommandHelper(displayName, commandManager); this.Reflection = reflection; - this.Translation = translations; + this.Translation = new TranslationHelper(displayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); } /**** diff --git a/src/StardewModdingAPI/Framework/TranslationHelper.cs b/src/StardewModdingAPI/Framework/TranslationHelper.cs index c3104d1b..ebcd69b1 100644 --- a/src/StardewModdingAPI/Framework/TranslationHelper.cs +++ b/src/StardewModdingAPI/Framework/TranslationHelper.cs @@ -41,13 +41,10 @@ namespace StardewModdingAPI.Framework /// The name of the relevant mod for error messages. /// The initial locale. /// The game's current language code. - /// The translations for each locale. - public TranslationHelper(string modName, string locale, LocalizedContentManager.LanguageCode languageCode, IDictionary> translations) + public TranslationHelper(string modName, string locale, LocalizedContentManager.LanguageCode languageCode) { // save data this.ModName = modName; - foreach (var pair in translations) - this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); // set locale this.SetLocale(locale, languageCode); @@ -67,6 +64,21 @@ namespace StardewModdingAPI.Framework return new Translation(this.ModName, this.Locale, key, text); } + /// Set the translations to use. + /// The translations to use. + internal TranslationHelper SetTranslations(IDictionary> translations) + { + // reset translations + this.All.Clear(); + foreach (var pair in translations) + this.All[pair.Key] = new Dictionary(pair.Value, StringComparer.InvariantCultureIgnoreCase); + + // rebuild cache + this.SetLocale(this.Locale, this.LocaleEnum); + + return this; + } + /// Set the current locale and precache translations. /// The current locale. /// The game's current language code. diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 47ba7c30..6939a03d 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -640,36 +640,9 @@ namespace StardewModdingAPI continue; } - // get translations - TranslationHelper translations; - { - IDictionary> translationValues = new Dictionary>(); - - // read translation files - DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); - if (translationsDir.Exists) - { - foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) - { - string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); - try - { - translationValues[locale] = jsonHelper.ReadJsonFile>(file.FullName); - } - catch (Exception ex) - { - this.Monitor.Log($"Couldn't read {metadata.DisplayName}'s i18n/{locale}.json file: {ex.GetLogSummary()}"); - } - } - } - - // create translation helper - translations = new TranslationHelper(metadata.DisplayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage(), translationValues); - } - // inject data mod.ModManifest = manifest; - mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection, translations); + mod.Helper = new ModHelper(metadata.DisplayName, metadata.DirectoryPath, jsonHelper, this.ModRegistry, this.CommandManager, contentManager, this.Reflection); mod.Monitor = this.GetSecondaryMonitor(metadata.DisplayName); mod.PathOnDisk = metadata.DirectoryPath; @@ -685,6 +658,9 @@ namespace StardewModdingAPI } } + // initialise translations + this.ReloadTranslations(); + // initialise loaded mods foreach (IModMetadata metadata in this.ModRegistry.GetMods()) { @@ -711,6 +687,37 @@ namespace StardewModdingAPI return modsLoaded; } + /// Reload translations for all mods. + private void ReloadTranslations() + { + JsonHelper jsonHelper = new JsonHelper(); + foreach (IModMetadata metadata in this.ModRegistry.GetMods()) + { + // read translation files + IDictionary> translations = new Dictionary>(); + DirectoryInfo translationsDir = new DirectoryInfo(Path.Combine(metadata.DirectoryPath, "i18n")); + if (translationsDir.Exists) + { + foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) + { + string locale = Path.GetFileNameWithoutExtension(file.Name.ToLower().Trim()); + try + { + translations[locale] = jsonHelper.ReadJsonFile>(file.FullName); + } + catch (Exception ex) + { + this.Monitor.Log($"Couldn't read {metadata.DisplayName}'s i18n/{locale}.json file: {ex.GetLogSummary()}"); + } + } + } + + // update translation + TranslationHelper translationHelper = (TranslationHelper)metadata.Mod.Helper.Translation; + translationHelper.SetTranslations(translations); + } + } + /// The method called when the user submits the help command in the console. /// The command name. /// The command arguments. -- cgit From 2109229660f1d8bce001c25fc6e83c0450ffc8c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 01:58:50 -0400 Subject: add 'reload_i18n' console command to reload translation files (#296) --- src/StardewModdingAPI/Program.cs | 44 ++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Program.cs b/src/StardewModdingAPI/Program.cs index 6939a03d..d75d5193 100644 --- a/src/StardewModdingAPI/Program.cs +++ b/src/StardewModdingAPI/Program.cs @@ -442,10 +442,11 @@ namespace StardewModdingAPI [SuppressMessage("ReSharper", "FunctionNeverReturns", Justification = "The thread is aborted when the game exits.")] private void RunConsoleLoop() { - // prepare help command + // prepare console this.Monitor.Log("Starting console..."); this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); - this.CommandManager.Add("SMAPI", "help", "Lists all commands | 'help ' returns command description", this.HandleHelpCommand); + this.CommandManager.Add("SMAPI", "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); + this.CommandManager.Add("SMAPI", "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); // start handling command line input Thread inputThread = new Thread(() => @@ -718,23 +719,36 @@ namespace StardewModdingAPI } } - /// The method called when the user submits the help command in the console. + /// The method called when the user submits a core SMAPI command in the console. /// The command name. /// The command arguments. - private void HandleHelpCommand(string name, string[] arguments) + private void HandleCommand(string name, string[] arguments) { - if (arguments.Any()) + switch (name) { - Framework.Command result = this.CommandManager.Get(arguments[0]); - if (result == null) - this.Monitor.Log("There's no command with that name.", LogLevel.Error); - else - this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); - } - else - { - 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); + case "help": + 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); + else + this.Monitor.Log($"{result.Name}: {result.Documentation}\n(Added by {result.ModName}.)", LogLevel.Info); + } + else + { + 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); + } + break; + + case "reload_i18n": + this.ReloadTranslations(); + this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); + break; + + default: + throw new NotSupportedException($"Unrecognise core SMAPI command '{name}'."); } } -- cgit From 2958381b54d1f46ae97ae196d3d046fee9264a0e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 10:51:58 -0400 Subject: shorten translation method name, remove helper method (#296) --- src/StardewModdingAPI.Tests/TranslationTests.cs | 6 +++--- src/StardewModdingAPI/Framework/ModHelper.cs | 10 ---------- src/StardewModdingAPI/Framework/TranslationHelper.cs | 6 +----- src/StardewModdingAPI/IModHelper.cs | 7 ------- src/StardewModdingAPI/ITranslationHelper.cs | 6 +----- 5 files changed, 5 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs index 76da8706..1778ed6a 100644 --- a/src/StardewModdingAPI.Tests/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -41,7 +41,7 @@ namespace StardewModdingAPI.Tests // act ITranslationHelper helper = new TranslationHelper("ModName", "en", LocalizedContentManager.LanguageCode.en).SetTranslations(data); - Translation translation = helper.Translate("key"); + Translation translation = helper.Get("key"); // assert Assert.AreEqual("en", helper.Locale, "The locale doesn't match the input value."); @@ -78,7 +78,7 @@ namespace StardewModdingAPI.Tests } [Test(Description = "Assert that the translations returned by the helper has the expected text.")] - public void Helper_Translate_ReturnsExpectedText() + public void Helper_Get_ReturnsExpectedText() { // arrange var data = this.GetSampleData(); @@ -92,7 +92,7 @@ namespace StardewModdingAPI.Tests this.AssertSetLocale(helper, locale, LocalizedContentManager.LanguageCode.en); actual[locale] = new Dictionary(); foreach (string key in expected[locale].Keys) - actual[locale][key] = helper.Translate(key); + actual[locale][key] = helper.Get(key); } // assert diff --git a/src/StardewModdingAPI/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index 947b1ae8..5a8ce459 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -119,16 +119,6 @@ namespace StardewModdingAPI.Framework } - /**** - ** Translation - ****/ - /// Get a translation for the current locale. This is a convenience shortcut for . - /// The translation key. - public Translation Translate(string key) - { - return this.Translation.Translate(key); - } - /**** ** Disposal ****/ diff --git a/src/StardewModdingAPI/Framework/TranslationHelper.cs b/src/StardewModdingAPI/Framework/TranslationHelper.cs index ebcd69b1..e4a178e2 100644 --- a/src/StardewModdingAPI/Framework/TranslationHelper.cs +++ b/src/StardewModdingAPI/Framework/TranslationHelper.cs @@ -29,10 +29,6 @@ namespace StardewModdingAPI.Framework /// The game's current language code. public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } - /// Get a translation for the current locale. This is a convenience shortcut for . - /// The translation key. - public Translation this[string key] => this.Translate(key); - /********* ** Public methods @@ -58,7 +54,7 @@ namespace StardewModdingAPI.Framework /// Get a translation for the current locale. /// The translation key. - public Translation Translate(string key) + public Translation Get(string key) { this.ForLocale.TryGetValue(key, out string text); return new Translation(this.ModName, this.Locale, key, text); diff --git a/src/StardewModdingAPI/IModHelper.cs b/src/StardewModdingAPI/IModHelper.cs index 38bfd366..116e8508 100644 --- a/src/StardewModdingAPI/IModHelper.cs +++ b/src/StardewModdingAPI/IModHelper.cs @@ -54,12 +54,5 @@ /// The file path relative to the mod directory. /// The model to save. void WriteJsonFile(string path, TModel model) where TModel : class; - - /**** - ** Translations - ****/ - /// Get a translation for the current locale. This is a convenience shortcut for . - /// The translation key. - Translation Translate(string key); } } \ No newline at end of file diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/StardewModdingAPI/ITranslationHelper.cs index 297a0d5a..23980a1b 100644 --- a/src/StardewModdingAPI/ITranslationHelper.cs +++ b/src/StardewModdingAPI/ITranslationHelper.cs @@ -15,10 +15,6 @@ namespace StardewModdingAPI /// The game's current language code. LocalizedContentManager.LanguageCode LocaleEnum { get; } - /// Get a translation for the current locale. This is a convenience shortcut for . - /// The translation key. - Translation this[string key] { get; } - /********* ** Public methods @@ -28,6 +24,6 @@ namespace StardewModdingAPI /// Get a translation for the current locale. /// The translation key. - Translation Translate(string key); + Translation Get(string key); } } -- cgit From af28b87660675139964dff597a73fc4a3eea4ccc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 11:21:49 -0400 Subject: make unit test easier to extend (#296) --- src/StardewModdingAPI.Tests/TranslationTests.cs | 15 ++++++-- src/StardewModdingAPI/Translation.cs | 51 ++++++++++++++----------- 2 files changed, 39 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs index 1778ed6a..09f0ce32 100644 --- a/src/StardewModdingAPI.Tests/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -19,8 +19,11 @@ namespace StardewModdingAPI.Tests /// A token structure type. public enum TokenType { - /// The tokens are passed in a dictionary. - Dictionary, + /// The tokens are passed in a string/object dictionary. + DictionaryStringObject, + + /// The tokens are passed in a string/string dictionary. + DictionaryStringString, /// The tokens are passed in an anonymous object. AnonymousObject @@ -190,7 +193,7 @@ namespace StardewModdingAPI.Tests ** Translation tokens ****/ [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] - public void Translation_Tokens([Values(TokenType.AnonymousObject, TokenType.Dictionary)] TokenType tokenType) + public void Translation_Tokens([Values(TokenType.AnonymousObject, TokenType.DictionaryStringObject, TokenType.DictionaryStringString)] TokenType tokenType) { // arrange string start = Guid.NewGuid().ToString("N"); @@ -207,10 +210,14 @@ namespace StardewModdingAPI.Tests translation = translation.Tokens(new { start, middle, end }); break; - case TokenType.Dictionary: + case TokenType.DictionaryStringObject: translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); break; + case TokenType.DictionaryStringString: + translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); + break; + default: throw new NotSupportedException($"Unknown token type {tokenType}."); } diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs index 868ee4fa..8dcaee0f 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/StardewModdingAPI/Translation.cs @@ -1,6 +1,7 @@ using System; +using System.Collections; using System.Collections.Generic; -using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; namespace StardewModdingAPI @@ -82,37 +83,41 @@ namespace StardewModdingAPI } /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. - /// An anonymous object containing token key/value pairs, like new { value = 42, name = "Cranberries" }. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }) or a dictionary of token values. /// The argument is null. public Translation Tokens(object tokens) { if (tokens == null) throw new ArgumentNullException(nameof(tokens)); - IDictionary dictionary = tokens - .GetType() - .GetProperties() - .ToDictionary( - p => p.Name, - p => p.GetValue(tokens) - ); - return this.Tokens(dictionary); - } - - /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. - /// A dictionary containing token key/value pairs. - /// The argument is null. - public Translation Tokens(IDictionary tokens) - { - if (tokens == null) - throw new ArgumentNullException(nameof(tokens)); - - tokens = tokens.ToDictionary(p => p.Key.Trim(), p => p.Value, StringComparer.InvariantCultureIgnoreCase); + // get dictionary of tokens + IDictionary tokenLookup = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + { + // from dictionary + if (tokens is IDictionary inputLookup) + { + foreach (DictionaryEntry entry in inputLookup) + { + string key = entry.Key?.ToString().Trim(); + if (key != null) + tokenLookup[key] = entry.Value?.ToString(); + } + } + + // from object properties + else + { + foreach (PropertyInfo prop in tokens.GetType().GetProperties()) + tokenLookup[prop.Name] = prop.GetValue(tokens)?.ToString(); + } + } + + // format translation string text = Regex.Replace(this.Text, @"{{([ \w\.\-]+)}}", match => { string key = match.Groups[1].Value.Trim(); - return tokens.TryGetValue(key, out object value) - ? value?.ToString() + return tokenLookup.TryGetValue(key, out string value) + ? value : match.Value; }); return new Translation(this.ModName, this.Locale, this.Key, text); -- cgit From 423a2f501249e237fe907cbe5e8568852b0710c5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 11:23:42 -0400 Subject: combine Translation::Tokens overloads, support dictionaries with any generic types (#296) --- src/StardewModdingAPI.Tests/TranslationTests.cs | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs index 09f0ce32..a2fef2b7 100644 --- a/src/StardewModdingAPI.Tests/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -16,19 +16,6 @@ namespace StardewModdingAPI.Tests /// Sample translation text for unit tests. public static string[] Samples = { null, "", " ", "boop", " boop " }; - /// A token structure type. - public enum TokenType - { - /// The tokens are passed in a string/object dictionary. - DictionaryStringObject, - - /// The tokens are passed in a string/string dictionary. - DictionaryStringString, - - /// The tokens are passed in an anonymous object. - AnonymousObject - } - /********* ** Unit tests @@ -193,7 +180,7 @@ namespace StardewModdingAPI.Tests ** Translation tokens ****/ [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] - public void Translation_Tokens([Values(TokenType.AnonymousObject, TokenType.DictionaryStringObject, TokenType.DictionaryStringString)] TokenType tokenType) + public void Translation_Tokens([Values("anonymous object", "IDictionary", "IDictionary")] string structure) { // arrange string start = Guid.NewGuid().ToString("N"); @@ -204,22 +191,22 @@ namespace StardewModdingAPI.Tests // act Translation translation = new Translation("ModName", "pt-BR", "key", input); - switch (tokenType) + switch (structure) { - case TokenType.AnonymousObject: + case "anonymous object": translation = translation.Tokens(new { start, middle, end }); break; - case TokenType.DictionaryStringObject: + case "IDictionary": translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); break; - case TokenType.DictionaryStringString: + case "IDictionary": translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); break; default: - throw new NotSupportedException($"Unknown token type {tokenType}."); + throw new NotSupportedException($"Unknown structure '{structure}'."); } // assert -- cgit From e20db6e8e4279dc95371e6f98c0e73dd19020c0f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 17:34:53 -0400 Subject: add token support for instance fields, expand unit test (#296) --- src/StardewModdingAPI.Tests/TranslationTests.cs | 23 ++++++++++++++++++++++- src/StardewModdingAPI/Translation.cs | 5 ++++- 2 files changed, 26 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI.Tests/TranslationTests.cs b/src/StardewModdingAPI.Tests/TranslationTests.cs index a2fef2b7..6a430aa7 100644 --- a/src/StardewModdingAPI.Tests/TranslationTests.cs +++ b/src/StardewModdingAPI.Tests/TranslationTests.cs @@ -180,7 +180,7 @@ namespace StardewModdingAPI.Tests ** Translation tokens ****/ [Test(Description = "Assert that multiple translation tokens are replaced correctly regardless of the token structure.")] - public void Translation_Tokens([Values("anonymous object", "IDictionary", "IDictionary")] string structure) + public void Translation_Tokens([Values("anonymous object", "class", "IDictionary", "IDictionary")] string structure) { // arrange string start = Guid.NewGuid().ToString("N"); @@ -197,6 +197,10 @@ namespace StardewModdingAPI.Tests translation = translation.Tokens(new { start, middle, end }); break; + case "class": + translation = translation.Tokens(new TokenModel { Start = start, Middle = middle, End = end }); + break; + case "IDictionary": translation = translation.Tokens(new Dictionary { ["start"] = start, ["middle"] = middle, ["end"] = end }); break; @@ -323,5 +327,22 @@ namespace StardewModdingAPI.Tests { return string.Format(Translation.PlaceholderText, key); } + + + /********* + ** Test models + *********/ + /// A model used to test token support. + private class TokenModel + { + /// A sample token property. + public string Start { get; set; } + + /// A sample token property. + public string Middle { get; set; } + + /// A sample token field. + public string End; + } } } diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs index 8dcaee0f..31a1b7e1 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/StardewModdingAPI/Translation.cs @@ -107,8 +107,11 @@ namespace StardewModdingAPI // from object properties else { - foreach (PropertyInfo prop in tokens.GetType().GetProperties()) + Type type = tokens.GetType(); + foreach (PropertyInfo prop in type.GetProperties()) tokenLookup[prop.Name] = prop.GetValue(tokens)?.ToString(); + foreach (FieldInfo field in type.GetFields()) + tokenLookup[field.Name] = field.GetValue(tokens)?.ToString(); } } -- cgit From 90275f1d56a6d74f93021d9b15a742f381ec4bfd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 17:53:44 -0400 Subject: add shortcut for translation with tokens (#296) --- src/StardewModdingAPI/Framework/TranslationHelper.cs | 8 ++++++++ src/StardewModdingAPI/ITranslationHelper.cs | 5 +++++ src/StardewModdingAPI/Translation.cs | 4 ++-- 3 files changed, 15 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/TranslationHelper.cs b/src/StardewModdingAPI/Framework/TranslationHelper.cs index e4a178e2..1e73c425 100644 --- a/src/StardewModdingAPI/Framework/TranslationHelper.cs +++ b/src/StardewModdingAPI/Framework/TranslationHelper.cs @@ -60,6 +60,14 @@ namespace StardewModdingAPI.Framework return new Translation(this.ModName, this.Locale, key, text); } + /// Get a translation for the current locale. + /// The translation key. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + public Translation Get(string key, object tokens) + { + return this.Get(key).Tokens(tokens); + } + /// Set the translations to use. /// The translations to use. internal TranslationHelper SetTranslations(IDictionary> translations) diff --git a/src/StardewModdingAPI/ITranslationHelper.cs b/src/StardewModdingAPI/ITranslationHelper.cs index 23980a1b..15f6b3c8 100644 --- a/src/StardewModdingAPI/ITranslationHelper.cs +++ b/src/StardewModdingAPI/ITranslationHelper.cs @@ -25,5 +25,10 @@ namespace StardewModdingAPI /// Get a translation for the current locale. /// The translation key. Translation Get(string key); + + /// Get a translation for the current locale. + /// The translation key. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. + Translation Get(string key, object tokens); } } diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs index 31a1b7e1..b86efc10 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/StardewModdingAPI/Translation.cs @@ -83,12 +83,12 @@ namespace StardewModdingAPI } /// Replace tokens in the text like {{value}} with the given values. Returns a new instance. - /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }) or a dictionary of token values. + /// An object containing token key/value pairs. This can be an anonymous object (like new { value = 42, name = "Cranberries" }), a dictionary, or a class instance. /// The argument is null. public Translation Tokens(object tokens) { if (tokens == null) - throw new ArgumentNullException(nameof(tokens)); + return this; // get dictionary of tokens IDictionary tokenLookup = new Dictionary(StringComparer.InvariantCultureIgnoreCase); -- cgit From bbfd42eeb3b71a773389b0df84d1b267ed0d606b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 20:31:50 -0400 Subject: bump minimum game version to 1.2.30 --- release-notes.md | 1 + src/StardewModdingAPI/Constants.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index f202f4d4..34c3e141 100644 --- a/release-notes.md +++ b/release-notes.md @@ -22,6 +22,7 @@ For players: * Fixed "unknown mod" deprecation warnings by improving how SMAPI detects the mod using the event. * Fixed `libgdiplus.dylib` errors for some players on Mac. * Fixed rare crash when window loses focus for a few players. +* Bumped minimum game version to 1.2.30. For modders: * You can now add dependencies to `manifest.json`. diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index 1c60da82..ef131575 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -36,7 +36,7 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 13, 1); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.26"); + public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From 9d9383e235dd732a5b90145dcf404a2daa5b7532 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 May 2017 20:58:31 -0400 Subject: drop special case for SDV 1.2.29 bug (min version is now 1.2.30) --- src/StardewModdingAPI/Framework/SGame.cs | 7 ------- 1 file changed, 7 deletions(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 7e222a2c..602a522b 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -584,13 +584,6 @@ namespace StardewModdingAPI.Framework return; } - // abort in known unrecoverable cases - if (Game1.toolSpriteSheet?.IsDisposed == true) - { - this.Monitor.ExitGameImmediately("the game unexpectedly disposed the tool spritesheet, so it crashed trying to draw a tool. This is a known bug in Stardew Valley 1.2.29, and there's no way to recover from it."); - return; - } - // recover sprite batch try { -- cgit From 6f69a1aca9481ed47bb3549312483e12a3bc1e9c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 29 May 2017 15:29:22 -0400 Subject: fix translation error when passing tokens for a null translation (#296) --- src/StardewModdingAPI/Translation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/StardewModdingAPI/Translation.cs b/src/StardewModdingAPI/Translation.cs index b86efc10..764145ea 100644 --- a/src/StardewModdingAPI/Translation.cs +++ b/src/StardewModdingAPI/Translation.cs @@ -87,7 +87,7 @@ namespace StardewModdingAPI /// The argument is null. public Translation Tokens(object tokens) { - if (tokens == null) + if (string.IsNullOrWhiteSpace(this.Text) || tokens == null) return this; // get dictionary of tokens -- cgit From 798439ea34daf4999685ec8886cb295f1c4d43ba Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 29 May 2017 23:52:33 -0400 Subject: deprecate TimeEvents.DayOfMonthChanged, SeasonOfYearChanged, and YearOfGameChanged --- release-notes.md | 1 + src/StardewModdingAPI/Events/TimeEvents.cs | 50 ++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index 34c3e141..f7b594e1 100644 --- a/release-notes.md +++ b/release-notes.md @@ -35,6 +35,7 @@ For modders: * Fixed `smapi-crash.txt` being copied from the default log even if a different path is specified with `--log-path`. * Fixed the content API not matching XNB filenames with two dots (like `a.b.xnb`) if you don't specify the `.xnb` extension. * Fixed `debug` command output not printed to console. +* Deprecated `TimeEvents.DayOfMonthChanged`, `SeasonOfYearChanged`, and `YearOfGameChanged`. These don't do what most modders think they do and aren't very reliable, since they depend on the SMAPI/game lifecycle which can change. You should use `TimeEvents.AfterDayStarted` or `SaveEvents.BeforeSave` instead. ## 1.13.1 See [log](https://github.com/Pathoschild/SMAPI/compare/1.13...1.13.1). diff --git a/src/StardewModdingAPI/Events/TimeEvents.cs b/src/StardewModdingAPI/Events/TimeEvents.cs index 572898c7..5dadf567 100644 --- a/src/StardewModdingAPI/Events/TimeEvents.cs +++ b/src/StardewModdingAPI/Events/TimeEvents.cs @@ -18,6 +18,17 @@ namespace StardewModdingAPI.Events [SuppressMessage("ReSharper", "InconsistentNaming")] private static event EventHandler _OnNewDay; + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _DayOfMonthChanged; + + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _SeasonOfYearChanged; + + /// The backing field for . + [SuppressMessage("ReSharper", "InconsistentNaming")] + private static event EventHandler _YearOfGameChanged; /********* ** Events @@ -29,13 +40,40 @@ namespace StardewModdingAPI.Events public static event EventHandler TimeOfDayChanged; /// Raised after the day-of-month value changes, including when loading a save. This may happen before save; in most cases you should use instead. - public static event EventHandler DayOfMonthChanged; + [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] + public static event EventHandler DayOfMonthChanged + { + add + { + TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", "1.14", DeprecationLevel.Info); + TimeEvents._DayOfMonthChanged += value; + } + remove => TimeEvents._DayOfMonthChanged -= value; + } /// Raised after the year value changes. - public static event EventHandler YearOfGameChanged; + [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] + public static event EventHandler YearOfGameChanged + { + add + { + TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", "1.14", DeprecationLevel.Info); + TimeEvents._YearOfGameChanged += value; + } + remove => TimeEvents._YearOfGameChanged -= value; + } /// Raised after the season value changes. - public static event EventHandler SeasonOfYearChanged; + [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] + public static event EventHandler SeasonOfYearChanged + { + add + { + TimeEvents.DeprecationManager.Warn($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", "1.14", DeprecationLevel.Info); + TimeEvents._SeasonOfYearChanged += value; + } + remove => TimeEvents._SeasonOfYearChanged -= value; + } /// Raised when the player is transitioning to a new day and the game is performing its day update logic. This event is triggered twice: once after the game starts transitioning, and again after it finishes. [Obsolete("Use " + nameof(TimeEvents) + "." + nameof(TimeEvents.AfterDayStarted) + " or " + nameof(SaveEvents) + " instead")] @@ -82,7 +120,7 @@ namespace StardewModdingAPI.Events /// The current day value. internal static void InvokeDayOfMonthChanged(IMonitor monitor, int priorDay, int newDay) { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", TimeEvents.DayOfMonthChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorDay, newDay)); + monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.DayOfMonthChanged)}", TimeEvents._DayOfMonthChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorDay, newDay)); } /// Raise a event. @@ -91,7 +129,7 @@ namespace StardewModdingAPI.Events /// The current year value. internal static void InvokeYearOfGameChanged(IMonitor monitor, int priorYear, int newYear) { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", TimeEvents.YearOfGameChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorYear, newYear)); + monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.YearOfGameChanged)}", TimeEvents._YearOfGameChanged?.GetInvocationList(), null, new EventArgsIntChanged(priorYear, newYear)); } /// Raise a event. @@ -100,7 +138,7 @@ namespace StardewModdingAPI.Events /// The current season name. internal static void InvokeSeasonOfYearChanged(IMonitor monitor, string priorSeason, string newSeason) { - monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", TimeEvents.SeasonOfYearChanged?.GetInvocationList(), null, new EventArgsStringChanged(priorSeason, newSeason)); + monitor.SafelyRaiseGenericEvent($"{nameof(TimeEvents)}.{nameof(TimeEvents.SeasonOfYearChanged)}", TimeEvents._SeasonOfYearChanged?.GetInvocationList(), null, new EventArgsStringChanged(priorSeason, newSeason)); } /// Raise a event. -- cgit From 40dc986ff24d342e79acd944d4615fde6915ecc7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 2 Jun 2017 01:59:37 -0400 Subject: bump incompatible version of Better Sprinklers --- src/StardewModdingAPI/StardewModdingAPI.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 08bd3cff..1cf575e9 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -52,7 +52,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha { "Name": "Better Sprinklers", "ID": [ "SPDSprinklersMod", /*since 2.3*/ "Speeder.BetterSprinklers" ], - "UpperVersion": "2.3", + "UpperVersion": "2.3.1-pathoschild-update", "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", -- cgit From 9610ccb51970c1c982433bcacb274844ef14b071 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 2 Jun 2017 18:00:57 -0400 Subject: add TimeSpeed <2.2.1 to incompatibility list --- release-notes.md | 1 + src/StardewModdingAPI/StardewModdingAPI.config.json | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/release-notes.md b/release-notes.md index f7b594e1..39d2fed5 100644 --- a/release-notes.md +++ b/release-notes.md @@ -23,6 +23,7 @@ For players: * Fixed `libgdiplus.dylib` errors for some players on Mac. * Fixed rare crash when window loses focus for a few players. * Bumped minimum game version to 1.2.30. +* Updated mod compatibility list. For modders: * You can now add dependencies to `manifest.json`. diff --git a/src/StardewModdingAPI/StardewModdingAPI.config.json b/src/StardewModdingAPI/StardewModdingAPI.config.json index 1cf575e9..f62db90c 100644 --- a/src/StardewModdingAPI/StardewModdingAPI.config.json +++ b/src/StardewModdingAPI/StardewModdingAPI.config.json @@ -56,7 +56,7 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "Compatibility": "AssumeBroken", "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/41", "UnofficialUpdateUrl": "http://community.playstarbound.com/threads/132096", - "Notes": "Needs update for SDV 1.2." + "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." }, { "Name": "Birthday Mail", @@ -331,6 +331,14 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha "UpdateUrl": "http://community.playstarbound.com/resources/4374", "Notes": "Needs update for SDV 1.2." }, + { + "Name": "TimeSpeed", + "ID": [ "TimeSpeed.dll", /* since 2.0.3 */ "4108e859-333c-4fec-a1a7-d2e18c1019fe", /*since 2.1*/ "community.TimeSpeed" ], + "UpperVersion": "2.2", + "Compatibility": "AssumeBroken", + "UpdateUrl": "http://www.nexusmods.com/stardewvalley/mods/169", + "Notes": "Needs update for SDV 1.2 and to migrate broken TimeEvents.AfterDayOfMonthChanged." + }, { "Name": "UiModSuite", "ID": [ "Demiacle.UiModSuite" ], -- cgit From 3a8e77a3098572fa413a27f41f832563daec3453 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 2 Jun 2017 18:25:11 -0400 Subject: update for release --- src/GlobalAssemblyInfo.cs | 4 ++-- src/StardewModdingAPI/Constants.cs | 2 +- src/TrainerMod/manifest.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/GlobalAssemblyInfo.cs b/src/GlobalAssemblyInfo.cs index 93ff68ec..4344dbce 100644 --- a/src/GlobalAssemblyInfo.cs +++ b/src/GlobalAssemblyInfo.cs @@ -2,5 +2,5 @@ using System.Runtime.InteropServices; [assembly: ComVisible(false)] -[assembly: AssemblyVersion("1.13.1.0")] -[assembly: AssemblyFileVersion("1.13.1.0")] \ No newline at end of file +[assembly: AssemblyVersion("1.14.0.0")] +[assembly: AssemblyFileVersion("1.14.0.0")] \ No newline at end of file diff --git a/src/StardewModdingAPI/Constants.cs b/src/StardewModdingAPI/Constants.cs index ef131575..4e53387e 100644 --- a/src/StardewModdingAPI/Constants.cs +++ b/src/StardewModdingAPI/Constants.cs @@ -33,7 +33,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 13, 1); + public static ISemanticVersion ApiVersion { get; } = new SemanticVersion(1, 14, 0); // alpha-{DateTime.UtcNow:yyyyMMddHHmm} /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new SemanticVersion("1.2.30"); diff --git a/src/TrainerMod/manifest.json b/src/TrainerMod/manifest.json index 8bddf02d..1232a365 100644 --- a/src/TrainerMod/manifest.json +++ b/src/TrainerMod/manifest.json @@ -3,7 +3,7 @@ "Author": "SMAPI", "Version": { "MajorVersion": 1, - "MinorVersion": 9, + "MinorVersion": 14, "PatchVersion": 0, "Build": null }, -- cgit