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) --- 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 -- 5 files changed, 18 deletions(-) (limited to 'src') 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 --- src/StardewModdingAPI/Framework/Logging/LogFileManager.cs | 11 ++++++++++- src/StardewModdingAPI/Program.cs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) (limited to 'src') 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 --- 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 - 7 files changed, 124 insertions(+), 133 deletions(-) (limited to 'src') 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 --- src/StardewModdingAPI/Context.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src') 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 --- src/StardewModdingAPI.Installer/InteractiveInstaller.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') 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 --- src/StardewModdingAPI/Framework/SGame.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') 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 --- src/StardewModdingAPI/Context.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') 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 @@ +