add LocaleChanged content event (#766)

This commit is contained in:
Jesse Plamondon-Willard 2022-03-26 19:08:25 -04:00
parent eebd8d54dc
commit 03efea2667
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
5 changed files with 100 additions and 22 deletions

View File

@ -19,5 +19,9 @@ namespace StardewModdingAPI.Events
/// <summary>Raised after an asset is loaded by the content pipeline, after all mod edits specified via <see cref="AssetRequested"/> have been applied.</summary>
/// <remarks>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.</remarks>
event EventHandler<AssetReadyEventArgs> AssetReady;
/// <summary>Raised after the game language changes.</summary>
/// <remarks>For non-English players, this may be raised during startup when the game switches to the previously selected language.</remarks>
event EventHandler<LocaleChangedEventArgs> LocaleChanged;
}
}

View File

@ -0,0 +1,45 @@
using System;
using LanguageCode = StardewValley.LocalizedContentManager.LanguageCode;
namespace StardewModdingAPI.Events
{
/// <summary>Event arguments for an <see cref="IContentEvents.LocaleChanged"/> event.</summary>
public class LocaleChangedEventArgs : EventArgs
{
/*********
** Accessors
*********/
/// <summary>The previous language enum value.</summary>
/// <remarks>For a custom language, this is always <see cref="LanguageCode.mod"/>.</remarks>
public LanguageCode OldLanguage { get; }
/// <summary>The previous locale code.</summary>
/// <remarks>This is the locale code as it appears in asset names, like <c>fr-FR</c> in <c>Maps/springobjects.fr-FR</c>. The locale code for English is an empty string.</remarks>
public string OldLocale { get; }
/// <summary>The new language enum value.</summary>
/// <remarks><inheritdoc cref="OldLanguage" select="remarks" /></remarks>
public LanguageCode NewLanguage { get; }
/// <summary>The new locale code.</summary>
/// <remarks><inheritdoc cref="OldLocale" select="remarks" /></remarks>
public string NewLocale { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="oldLanguage">The previous language enum value.</param>
/// <param name="oldLocale">The previous locale code.</param>
/// <param name="newLanguage">The new language enum value.</param>
/// <param name="newLocale">The new locale code.</param>
internal LocaleChangedEventArgs(LanguageCode oldLanguage, string oldLocale, LanguageCode newLanguage, string newLocale)
{
this.OldLanguage = oldLanguage;
this.OldLocale = oldLocale;
this.NewLanguage = newLanguage;
this.NewLocale = newLocale;
}
}
}

View File

@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.Events
/// <inheritdoc cref="IContentEvents.AssetReady" />
public readonly ManagedEvent<AssetReadyEventArgs> AssetReady;
/// <inheritdoc cref="IContentEvents.LocaleChanged" />
public readonly ManagedEvent<LocaleChangedEventArgs> LocaleChanged;
/****
** Display
@ -204,6 +207,7 @@ namespace StardewModdingAPI.Framework.Events
this.AssetRequested = ManageEventOf<AssetRequestedEventArgs>(nameof(IModEvents.Content), nameof(IContentEvents.AssetRequested));
this.AssetsInvalidated = ManageEventOf<AssetsInvalidatedEventArgs>(nameof(IModEvents.Content), nameof(IContentEvents.AssetsInvalidated));
this.AssetReady = ManageEventOf<AssetReadyEventArgs>(nameof(IModEvents.Content), nameof(IContentEvents.AssetReady));
this.LocaleChanged = ManageEventOf<LocaleChangedEventArgs>(nameof(IModEvents.Content), nameof(IContentEvents.LocaleChanged));
this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged));
this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true);

View File

@ -30,6 +30,13 @@ namespace StardewModdingAPI.Framework.Events
remove => this.EventManager.AssetReady.Remove(value);
}
/// <inheritdoc />
public event EventHandler<LocaleChangedEventArgs> LocaleChanged
{
add => this.EventManager.LocaleChanged.Add(value, this.Mod);
remove => this.EventManager.LocaleChanged.Remove(value);
}
/*********
** Public methods

View File

@ -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
****/
/// <summary>Tracks whether the game should exit immediately and any pending initialization should be cancelled.</summary>
private readonly CancellationTokenSource CancellationToken = new CancellationTokenSource();
private readonly CancellationTokenSource CancellationToken = new();
/// <summary>Manages the SMAPI console window and log file.</summary>
private readonly LogManager LogManager;
@ -71,16 +72,16 @@ namespace StardewModdingAPI.Framework
private Monitor Monitor => this.LogManager.Monitor;
/// <summary>Simplifies access to private game code.</summary>
private readonly Reflector Reflection = new Reflector();
private readonly Reflector Reflection = new();
/// <summary>Encapsulates access to SMAPI core translations.</summary>
private readonly Translator Translator = new Translator();
private readonly Translator Translator = new();
/// <summary>The SMAPI configuration settings.</summary>
private readonly SConfig Settings;
/// <summary>The mod toolkit used for generic mod interactions.</summary>
private readonly ModToolkit Toolkit = new ModToolkit();
private readonly ModToolkit Toolkit = new();
/****
** Higher-level components
@ -99,7 +100,7 @@ namespace StardewModdingAPI.Framework
/// <summary>Tracks the installed mods.</summary>
/// <remarks>This is initialized after the game starts.</remarks>
private readonly ModRegistry ModRegistry = new ModRegistry();
private readonly ModRegistry ModRegistry = new();
/// <summary>Manages SMAPI events for mods.</summary>
private readonly EventManager EventManager;
@ -129,18 +130,21 @@ namespace StardewModdingAPI.Framework
/// <summary>Whether the player just returned to the title screen.</summary>
public bool JustReturnedToTitle { get; set; }
/// <summary>The last language set by the game.</summary>
private (string Locale, LanguageCode Code) LastLanguage { get; set; } = ("", LanguageCode.en);
/// <summary>The maximum number of consecutive attempts SMAPI should make to recover from an update error.</summary>
private readonly Countdown UpdateCrashTimer = new Countdown(60); // 60 ticks = roughly one second
private readonly Countdown UpdateCrashTimer = new(60); // 60 ticks = roughly one second
/// <summary>Asset interceptors added or removed since the last tick.</summary>
private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new List<AssetInterceptorChange>();
private readonly List<AssetInterceptorChange> ReloadAssetInterceptorsQueue = new();
/// <summary>A list of queued commands to parse and execute.</summary>
/// <remarks>This property must be thread-safe, since it's accessed from a separate console input thread.</remarks>
private readonly ConcurrentQueue<string> RawCommandQueue = new ConcurrentQueue<string>();
private readonly ConcurrentQueue<string> RawCommandQueue = new();
/// <summary>A list of commands to execute on each screen.</summary>
private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new PerScreen<List<Tuple<Command, string, string[]>>>(() => new List<Tuple<Command, string, string[]>>());
private readonly PerScreen<List<Tuple<Command, string, string[]>>> ScreenCommandQueue = new(() => new List<Tuple<Command, string, string[]>>());
/*********
@ -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);
}
/// <summary>Raised when the low-level stage while loading a save changes.</summary>
@ -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<IModMetadata> skippedMods = new List<IModMetadata>();
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<string> suppressUpdateChecks = new HashSet<string>(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>(contentPack));
return contentPack;
@ -1982,7 +2000,7 @@ namespace StardewModdingAPI.Framework
// read translation files
var translations = new Dictionary<string, IDictionary<string, string>>();
errors = new List<string>();
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
/// <summary>Delete normal (non-crash) log files created by SMAPI.</summary>
private void PurgeNormalLogs()
{
DirectoryInfo logsDir = new DirectoryInfo(Constants.LogDir);
DirectoryInfo logsDir = new(Constants.LogDir);
if (!logsDir.Exists)
return;