diff --git a/src/SMAPI/Events/IContentEvents.cs b/src/SMAPI/Events/IContentEvents.cs index abbaaf33..d537db70 100644 --- a/src/SMAPI/Events/IContentEvents.cs +++ b/src/SMAPI/Events/IContentEvents.cs @@ -19,5 +19,9 @@ namespace StardewModdingAPI.Events /// Raised after an asset is loaded by the content pipeline, after all mod edits specified via have been applied. /// This event is only raised if something requested the asset from the content pipeline. Invalidating an asset from the content cache won't necessarily reload it automatically. event EventHandler AssetReady; + + /// Raised after the game language changes. + /// For non-English players, this may be raised during startup when the game switches to the previously selected language. + event EventHandler LocaleChanged; } } diff --git a/src/SMAPI/Events/LocaleChangedEventArgs.cs b/src/SMAPI/Events/LocaleChangedEventArgs.cs new file mode 100644 index 00000000..09d3f6e5 --- /dev/null +++ b/src/SMAPI/Events/LocaleChangedEventArgs.cs @@ -0,0 +1,45 @@ +using System; +using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode; + +namespace StardewModdingAPI.Events +{ + /// Event arguments for an event. + public class LocaleChangedEventArgs : EventArgs + { + /********* + ** Accessors + *********/ + /// The previous language enum value. + /// For a custom language, this is always . + public LanguageCode OldLanguage { get; } + + /// The previous locale code. + /// This is the locale code as it appears in asset names, like fr-FR in Maps/springobjects.fr-FR. The locale code for English is an empty string. + public string OldLocale { get; } + + /// The new language enum value. + /// + public LanguageCode NewLanguage { get; } + + /// The new locale code. + /// + public string NewLocale { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The previous language enum value. + /// The previous locale code. + /// The new language enum value. + /// The new locale code. + internal LocaleChangedEventArgs(LanguageCode oldLanguage, string oldLocale, LanguageCode newLanguage, string newLocale) + { + this.OldLanguage = oldLanguage; + this.OldLocale = oldLocale; + this.NewLanguage = newLanguage; + this.NewLocale = newLocale; + } + } +} diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index bcfd7dd7..41540047 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.Events /// public readonly ManagedEvent AssetReady; + /// + public readonly ManagedEvent LocaleChanged; + /**** ** Display @@ -204,6 +207,7 @@ namespace StardewModdingAPI.Framework.Events this.AssetRequested = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.AssetRequested)); this.AssetsInvalidated = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.AssetsInvalidated)); this.AssetReady = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.AssetReady)); + this.LocaleChanged = ManageEventOf(nameof(IModEvents.Content), nameof(IContentEvents.LocaleChanged)); this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true); diff --git a/src/SMAPI/Framework/Events/ModContentEvents.cs b/src/SMAPI/Framework/Events/ModContentEvents.cs index cb242e99..beb96031 100644 --- a/src/SMAPI/Framework/Events/ModContentEvents.cs +++ b/src/SMAPI/Framework/Events/ModContentEvents.cs @@ -30,6 +30,13 @@ namespace StardewModdingAPI.Framework.Events remove => this.EventManager.AssetReady.Remove(value); } + /// + public event EventHandler LocaleChanged + { + add => this.EventManager.LocaleChanged.Add(value, this.Mod); + remove => this.EventManager.LocaleChanged.Remove(value); + } + /********* ** Public methods diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index eab977ac..5deb177c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -46,6 +46,7 @@ using StardewModdingAPI.Utilities; using StardewValley; using StardewValley.Menus; using xTile.Display; +using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode; using MiniMonoModHotfix = MonoMod.Utils.MiniMonoModHotfix; using PathUtilities = StardewModdingAPI.Toolkit.Utilities.PathUtilities; using SObject = StardewValley.Object; @@ -62,7 +63,7 @@ namespace StardewModdingAPI.Framework ** Low-level components ****/ /// Tracks whether the game should exit immediately and any pending initialization should be cancelled. - private readonly CancellationTokenSource CancellationToken = new CancellationTokenSource(); + private readonly CancellationTokenSource CancellationToken = new(); /// Manages the SMAPI console window and log file. private readonly LogManager LogManager; @@ -71,16 +72,16 @@ namespace StardewModdingAPI.Framework private Monitor Monitor => this.LogManager.Monitor; /// Simplifies access to private game code. - private readonly Reflector Reflection = new Reflector(); + private readonly Reflector Reflection = new(); /// Encapsulates access to SMAPI core translations. - private readonly Translator Translator = new Translator(); + private readonly Translator Translator = new(); /// The SMAPI configuration settings. private readonly SConfig Settings; /// The mod toolkit used for generic mod interactions. - private readonly ModToolkit Toolkit = new ModToolkit(); + private readonly ModToolkit Toolkit = new(); /**** ** Higher-level components @@ -99,7 +100,7 @@ namespace StardewModdingAPI.Framework /// Tracks the installed mods. /// This is initialized after the game starts. - private readonly ModRegistry ModRegistry = new ModRegistry(); + private readonly ModRegistry ModRegistry = new(); /// Manages SMAPI events for mods. private readonly EventManager EventManager; @@ -129,18 +130,21 @@ namespace StardewModdingAPI.Framework /// Whether the player just returned to the title screen. public bool JustReturnedToTitle { get; set; } + /// The last language set by the game. + private (string Locale, LanguageCode Code) LastLanguage { get; set; } = ("", LanguageCode.en); + /// The maximum number of consecutive attempts SMAPI should make to recover from an update error. - private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second + private readonly Countdown UpdateCrashTimer = new(60); // 60 ticks = roughly one second /// Asset interceptors added or removed since the last tick. - private readonly List ReloadAssetInterceptorsQueue = new List(); + private readonly List ReloadAssetInterceptorsQueue = new(); /// A list of queued commands to parse and execute. /// This property must be thread-safe, since it's accessed from a separate console input thread. - private readonly ConcurrentQueue RawCommandQueue = new ConcurrentQueue(); + private readonly ConcurrentQueue RawCommandQueue = new(); /// A list of commands to execute on each screen. - private readonly PerScreen>> ScreenCommandQueue = new PerScreen>>(() => new List>()); + private readonly PerScreen>> ScreenCommandQueue = new(() => new List>()); /********* @@ -369,13 +373,13 @@ namespace StardewModdingAPI.Framework xTile.Format.FormatManager.Instance.RegisterMapFormat(new TMXTile.TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom)); // load mod data - ModToolkit toolkit = new ModToolkit(); + ModToolkit toolkit = new(); ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); // load mods { this.Monitor.Log("Loading mod metadata...", LogLevel.Debug); - ModResolver resolver = new ModResolver(); + ModResolver resolver = new(); // log loose files { @@ -1048,7 +1052,7 @@ namespace StardewModdingAPI.Framework // get locale string locale = this.ContentCore.GetLocale(); - LocalizedContentManager.LanguageCode languageCode = this.ContentCore.Language; + LanguageCode languageCode = this.ContentCore.Language; // update core translations this.Translator.SetLocale(locale, languageCode); @@ -1061,6 +1065,20 @@ namespace StardewModdingAPI.Framework foreach (ContentPack contentPack in mod.GetFakeContentPacks()) contentPack.TranslationImpl.SetLocale(locale, languageCode); } + + // raise event + if (this.EventManager.LocaleChanged.HasListeners()) + { + this.EventManager.LocaleChanged.Raise( + new LocaleChangedEventArgs( + oldLanguage: this.LastLanguage.Code, + oldLocale: this.LastLanguage.Locale, + newLanguage: languageCode, + newLocale: locale + ) + ); + } + this.LastLanguage = (locale, languageCode); } /// Raised when the low-level stage while loading a save changes. @@ -1385,7 +1403,7 @@ namespace StardewModdingAPI.Framework { // create client string url = this.Settings.WebApiBaseUrl; - WebApiClient client = new WebApiClient(url, Constants.ApiVersion); + WebApiClient client = new(url, Constants.ApiVersion); this.Monitor.Log("Checking for updates..."); // check SMAPI version @@ -1569,7 +1587,7 @@ namespace StardewModdingAPI.Framework // load mods IList skippedMods = new List(); - using (AssemblyLoader modAssemblyLoader = new AssemblyLoader(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods)) + using (AssemblyLoader modAssemblyLoader = new(Constants.Platform, this.Monitor, this.Settings.ParanoidWarnings, this.Settings.RewriteMods)) { // init HashSet suppressUpdateChecks = new HashSet(this.Settings.SuppressUpdateChecks, StringComparer.OrdinalIgnoreCase); @@ -1758,7 +1776,7 @@ namespace StardewModdingAPI.Framework IManifest manifest = mod.Manifest; IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); IContentHelper contentHelper = new ContentHelper(this.ContentCore, mod.DirectoryPath, manifest.UniqueID, mod.DisplayName, monitor); - TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); + TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); IContentPack contentPack = new ContentPack(mod.DirectoryPath, manifest, contentHelper, translationHelper, jsonHelper); mod.SetMod(contentPack, monitor, translationHelper); this.ModRegistry.Add(mod); @@ -1831,16 +1849,16 @@ namespace StardewModdingAPI.Framework // init mod helpers IMonitor monitor = this.LogManager.GetMonitor(mod.DisplayName); - TranslationHelper translationHelper = new TranslationHelper(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); + TranslationHelper translationHelper = new(manifest.UniqueID, contentCore.GetLocale(), contentCore.Language); IModHelper modHelper; { IContentPack CreateFakeContentPack(string packDirPath, IManifest packManifest) { IMonitor packMonitor = this.LogManager.GetMonitor(packManifest.Name); IContentHelper packContentHelper = new ContentHelper(contentCore, packDirPath, packManifest.UniqueID, packManifest.Name, packMonitor); - TranslationHelper packTranslationHelper = new TranslationHelper(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); + TranslationHelper packTranslationHelper = new(packManifest.UniqueID, contentCore.GetLocale(), contentCore.Language); - ContentPack contentPack = new ContentPack(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); + ContentPack contentPack = new(packDirPath, packManifest, packContentHelper, packTranslationHelper, this.Toolkit.JsonHelper); this.ReloadTranslationsForTemporaryContentPack(mod, contentPack); mod.FakeContentPacks.Add(new WeakReference(contentPack)); return contentPack; @@ -1982,7 +2000,7 @@ namespace StardewModdingAPI.Framework // read translation files var translations = new Dictionary>(); errors = new List(); - DirectoryInfo translationsDir = new DirectoryInfo(folderPath); + DirectoryInfo translationsDir = new(folderPath); if (translationsDir.Exists) { foreach (FileInfo file in translationsDir.EnumerateFiles("*.json")) @@ -2038,7 +2056,7 @@ namespace StardewModdingAPI.Framework { // default path { - FileInfo defaultFile = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}")); + FileInfo defaultFile = new(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.{Constants.LogExtension}")); if (!defaultFile.Exists) return defaultFile.FullName; } @@ -2046,7 +2064,7 @@ namespace StardewModdingAPI.Framework // get first disambiguated path for (int i = 2; i < int.MaxValue; i++) { - FileInfo file = new FileInfo(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}")); + FileInfo file = new(Path.Combine(Constants.LogDir, $"{Constants.LogFilename}.player-{i}.{Constants.LogExtension}")); if (!file.Exists) return file.FullName; } @@ -2058,7 +2076,7 @@ namespace StardewModdingAPI.Framework /// Delete normal (non-crash) log files created by SMAPI. private void PurgeNormalLogs() { - DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir); + DirectoryInfo logsDir = new(Constants.LogDir); if (!logsDir.Exists) return;