diff options
Diffstat (limited to 'src/StardewModdingAPI/Framework')
-rw-r--r-- | src/StardewModdingAPI/Framework/CommandHelper.cs | 2 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/ContentHelper.cs | 113 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs | 18 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/InternalExtensions.cs | 38 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Logging/LogFileManager.cs | 11 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/ModHelper.cs | 8 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs | 10 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/ModRegistry.cs | 10 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/Models/Manifest.cs | 2 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/SContentManager.cs | 6 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/SGame.cs | 83 | ||||
-rw-r--r-- | src/StardewModdingAPI/Framework/TranslationHelper.cs | 137 |
12 files changed, 310 insertions, 128 deletions
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/ContentHelper.cs b/src/StardewModdingAPI/Framework/ContentHelper.cs index 893fa2c8..7fd5e803 100644 --- a/src/StardewModdingAPI/Framework/ContentHelper.cs +++ b/src/StardewModdingAPI/Framework/ContentHelper.cs @@ -5,7 +5,11 @@ 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; +using xTile.Tiles; namespace StardewModdingAPI.Framework { @@ -51,6 +55,8 @@ namespace StardewModdingAPI.Framework /// <exception cref="ContentLoadException">The content asset couldn't be loaded (e.g. because it doesn't exist).</exception> public T Load<T>(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 { @@ -63,25 +69,49 @@ 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); + // try cache + if (this.ContentManager.IsLoaded(assetPath)) + return this.ContentManager.Load<T>(assetPath); + // load content switch (file.Extension.ToLower()) { + // XNB file case ".xnb": - return this.ContentManager.Load<T>(assetPath); + { + T asset = this.ContentManager.Load<T>(assetPath); + if (asset is Map) + this.FixLocalMapTilesheets(asset as Map, key); + return asset; + } + + // unpacked map + case ".tbin": + { + // validate + if (typeof(T) != 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; + Map map = formatManager.LoadMap(file.FullName); + this.FixLocalMapTilesheets(map, key); + + // inject map + 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<T>(assetPath); + 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)) @@ -93,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); } } @@ -130,6 +160,55 @@ namespace StardewModdingAPI.Framework /********* ** Private methods *********/ + /// <summary>Fix the tilesheets for a map loaded from the mod folder.</summary> + /// <param name="map">The map whose tilesheets to fix.</param> + /// <param name="mapKey">The map asset key within the mod folder.</param> + /// <exception cref="ContentLoadException">The map tilesheets could not be loaded.</exception> + 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<Texture2D>(localKey); + } + catch (Exception ex) + { + throw new ContentLoadException($"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<Texture2D>(contentKey); + } + catch (Exception 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; + } + } + } + /// <summary>Assert that the given key has a valid format.</summary> /// <param name="key">The asset key to check.</param> /// <exception cref="ArgumentException">The asset key is empty or contains invalid characters.</exception> @@ -146,10 +225,18 @@ namespace StardewModdingAPI.Framework /// <param name="path">The asset path relative to the mod folder.</param> 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; } 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 +{ + /// <summary>An implementation of <see cref="ContentLoadException"/> used by SMAPI to detect whether it was thrown by SMAPI or the underlying framework.</summary> + internal class SContentLoadException : ContentLoadException + { + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="message">The error message.</param> + /// <param name="ex">The underlying exception, if any.</param> + public SContentLoadException(string message, Exception ex = null) + : base(message, ex) { } + } +} 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 /// <summary>Provides extension methods for SMAPI's internal use.</summary> internal static class InternalExtensions { - /********* - ** Properties - *********/ - /// <summary>Tracks the installed mods.</summary> - private static ModRegistry ModRegistry; - - - /********* - ** Public methods - *********/ - /// <summary>Injects types required for backwards compatibility.</summary> - /// <param name="modRegistry">Tracks the installed mods.</param> - internal static void Shim(ModRegistry modRegistry) - { - InternalExtensions.ModRegistry = modRegistry; - } - /**** ** IMonitor ****/ @@ -111,27 +94,6 @@ namespace StardewModdingAPI.Framework } /**** - ** Deprecation - ****/ - /// <summary>Log a deprecation warning for mods using an event.</summary> - /// <param name="deprecationManager">The deprecation manager to extend.</param> - /// <param name="handlers">The event handlers.</param> - /// <param name="nounPhrase">A noun phrase describing what is deprecated.</param> - /// <param name="version">The SMAPI version which deprecated it.</param> - /// <param name="severity">How deprecated the code is.</param> - 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 ****/ /// <summary>Get whether the sprite batch is between a begin and end pair.</summary> 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 @@ -14,14 +14,23 @@ namespace StardewModdingAPI.Framework.Logging /********* + ** Accessors + *********/ + /// <summary>The full path to the log file being written.</summary> + public string Path { get; } + + + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="path">The log file to write.</param> 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/Framework/ModHelper.cs b/src/StardewModdingAPI/Framework/ModHelper.cs index f939b83c..5a8ce459 100644 --- a/src/StardewModdingAPI/Framework/ModHelper.cs +++ b/src/StardewModdingAPI/Framework/ModHelper.cs @@ -32,13 +32,15 @@ namespace StardewModdingAPI.Framework /// <summary>An API for managing console commands.</summary> public ICommandHelper ConsoleCommands { get; } + /// <summary>Provides translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary> + public ITranslationHelper Translation { get; } + /********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="displayName">The mod's display name.</param> - /// <param name="manifest">The manifest for the associated mod.</param> /// <param name="modDirectory">The full path to the mod's folder.</param> /// <param name="jsonHelper">Encapsulate SMAPI's JSON parsing.</param> /// <param name="modRegistry">Metadata about loaded mods.</param> @@ -47,7 +49,7 @@ namespace StardewModdingAPI.Framework /// <param name="reflection">Simplifies access to private game code.</param> /// <exception cref="ArgumentNullException">An argument is null or empty.</exception> /// <exception cref="InvalidOperationException">The <paramref name="modDirectory"/> path does not exist on disk.</exception> - 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) { // validate if (string.IsNullOrWhiteSpace(modDirectory)) @@ -66,6 +68,7 @@ namespace StardewModdingAPI.Framework this.ModRegistry = modRegistry; this.ConsoleCommands = new CommandHelper(displayName, commandManager); this.Reflection = reflection; + this.Translation = new TranslationHelper(displayName, contentManager.GetLocale(), contentManager.GetCurrentLanguage()); } /**** @@ -115,6 +118,7 @@ namespace StardewModdingAPI.Framework this.JsonHelper.WriteJsonFile(path, model); } + /**** ** Disposal ****/ diff --git a/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs b/src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs index 2c68a639..f5139ce5 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 /// <summary>Sort the given mods by the order they should be loaded.</summary> /// <param name="mods">The mods to process.</param> public IEnumerable<IModMetadata> ProcessDependencies(IEnumerable<IModMetadata> mods) @@ -142,20 +141,18 @@ 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<IModMetadata>()); return sortedMods.Reverse(); } -#endif /********* ** Private methods *********/ -#if EXPERIMENTAL /// <summary>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.</summary> /// <param name="mods">The full list of mods being validated.</param> /// <param name="mod">The mod whose dependencies to process.</param> @@ -201,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 ) @@ -222,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(); @@ -270,7 +267,6 @@ namespace StardewModdingAPI.Framework.ModLoading return states[mod] = ModDependencyStatus.Sorted; } } -#endif /// <summary>Get all mod folders in a root folder, passing through empty folders as needed.</summary> /// <param name="rootPath">The root folder path to search.</param> 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); } - /// <summary>Get the friendly mod name which handles a delegate.</summary> - /// <param name="delegate">The delegate to follow.</param> - /// <returns>Returns the mod name, or <c>null</c> if the delegate isn't implemented by a known mod.</returns> - public string GetModFrom(Delegate @delegate) - { - return @delegate?.Target != null - ? this.GetModFrom(@delegate.Target.GetType()) - : null; - } - /// <summary>Get the friendly mod name which defines a type.</summary> /// <param name="type">The type to check.</param> /// <returns>Returns the mod name, or <c>null</c> if the type isn't part of a known mod.</returns> 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 /// <summary>The name of the DLL in the directory that has the <see cref="Mod.Entry"/> method.</summary> public string EntryDll { get; set; } -#if EXPERIMENTAL /// <summary>The other mods that must be loaded before this mod.</summary> [JsonConverter(typeof(ManifestFieldConverter))] public IManifestDependency[] Dependencies { get; set; } -#endif /// <summary>The unique mod ID.</summary> public string UniqueID { get; set; } 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; } + /// <summary>Get the current content locale.</summary> + public string GetLocale() + { + return this.GetKeyLocale.Invoke<string>(); + } + /********* ** Private methods *********/ diff --git a/src/StardewModdingAPI/Framework/SGame.cs b/src/StardewModdingAPI/Framework/SGame.cs index 3d421a37..602a522b 100644 --- a/src/StardewModdingAPI/Framework/SGame.cs +++ b/src/StardewModdingAPI/Framework/SGame.cs @@ -55,37 +55,16 @@ namespace StardewModdingAPI.Framework ** Game state ****/ /// <summary>Arrays of pressed controller buttons indexed by <see cref="PlayerIndex"/>.</summary> - private Buttons[] PreviouslyPressedButtons = new Buttons[0]; - - /// <summary>A record of the keyboard state (i.e. the up/down state for each button) as of the latest tick.</summary> - private KeyboardState KStateNow; + private Buttons[] PreviousPressedButtons = new Buttons[0]; /// <summary>A record of the keyboard state (i.e. the up/down state for each button) as of the previous tick.</summary> - private KeyboardState KStatePrior; - - /// <summary>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.</summary> - private MouseState MStateNow; + private KeyboardState PreviousKeyState; /// <summary>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.</summary> - private MouseState MStatePrior; - - /// <summary>The current mouse position on the screen adjusted for the zoom level.</summary> - private Point MPositionNow; + private MouseState PreviousMouseState; /// <summary>The previous mouse position on the screen adjusted for the zoom level.</summary> - private Point MPositionPrior; - - /// <summary>The keys that were pressed as of the latest tick.</summary> - private Keys[] CurrentlyPressedKeys => this.KStateNow.GetPressedKeys(); - - /// <summary>The keys that were pressed as of the previous tick.</summary> - private Keys[] PreviouslyPressedKeys => this.KStatePrior.GetPressedKeys(); - - /// <summary>The keys that just entered the down state.</summary> - private Keys[] FramePressedKeys => this.CurrentlyPressedKeys.Except(this.PreviouslyPressedKeys).ToArray(); - - /// <summary>The keys that just entered the up state.</summary> - private Keys[] FrameReleasedKeys => this.PreviouslyPressedKeys.Except(this.CurrentlyPressedKeys).ToArray(); + private Point PreviousMousePosition; /// <summary>The previous save ID at last check.</summary> private ulong PreviousSaveID; @@ -350,20 +329,27 @@ namespace StardewModdingAPI.Framework } /********* - ** Input events + ** Input events (if window has focus) *********/ + 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 @@ -391,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(); } /********* @@ -561,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) @@ -602,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 { @@ -1384,7 +1359,7 @@ namespace StardewModdingAPI.Framework /// <param name="buttonState">The last known state.</param> private bool WasButtonJustPressed(Buttons button, ButtonState buttonState) { - return buttonState == ButtonState.Pressed && !this.PreviouslyPressedButtons.Contains(button); + return buttonState == ButtonState.Pressed && !this.PreviousPressedButtons.Contains(button); } /// <summary>Get whether a controller button was released since the last check.</summary> @@ -1392,7 +1367,7 @@ namespace StardewModdingAPI.Framework /// <param name="buttonState">The last known state.</param> private bool WasButtonJustReleased(Buttons button, ButtonState buttonState) { - return buttonState == ButtonState.Released && this.PreviouslyPressedButtons.Contains(button); + return buttonState == ButtonState.Released && this.PreviousPressedButtons.Contains(button); } /// <summary>Get whether an analogue controller button was pressed since the last check.</summary> diff --git a/src/StardewModdingAPI/Framework/TranslationHelper.cs b/src/StardewModdingAPI/Framework/TranslationHelper.cs new file mode 100644 index 00000000..1e73c425 --- /dev/null +++ b/src/StardewModdingAPI/Framework/TranslationHelper.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using StardewValley; + +namespace StardewModdingAPI.Framework +{ + /// <summary>Provides translations stored in the mod's <c>i18n</c> folder, with one file per locale (like <c>en.json</c>) containing a flat key => value structure. Translations are fetched with locale fallback, so missing translations are filled in from broader locales (like <c>pt-BR.json</c> < <c>pt.json</c> < <c>default.json</c>).</summary> + internal class TranslationHelper : ITranslationHelper + { + /********* + ** Properties + *********/ + /// <summary>The name of the relevant mod for error messages.</summary> + private readonly string ModName; + + /// <summary>The translations for each locale.</summary> + private readonly IDictionary<string, IDictionary<string, string>> All = new Dictionary<string, IDictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase); + + /// <summary>The translations for the current locale, with locale fallback taken into account.</summary> + private IDictionary<string, string> ForLocale; + + + /********* + ** Accessors + *********/ + /// <summary>The current locale.</summary> + public string Locale { get; private set; } + + /// <summary>The game's current language code.</summary> + public LocalizedContentManager.LanguageCode LocaleEnum { get; private set; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="modName">The name of the relevant mod for error messages.</param> + /// <param name="locale">The initial locale.</param> + /// <param name="languageCode">The game's current language code.</param> + public TranslationHelper(string modName, string locale, LocalizedContentManager.LanguageCode languageCode) + { + // save data + this.ModName = modName; + + // set locale + this.SetLocale(locale, languageCode); + } + + /// <summary>Get all translations for the current locale.</summary> + public IDictionary<string, string> GetTranslations() + { + return new Dictionary<string, string>(this.ForLocale, StringComparer.InvariantCultureIgnoreCase); + } + + /// <summary>Get a translation for the current locale.</summary> + /// <param name="key">The translation key.</param> + public Translation Get(string key) + { + this.ForLocale.TryGetValue(key, out string text); + return new Translation(this.ModName, this.Locale, key, text); + } + + /// <summary>Get a translation for the current locale.</summary> + /// <param name="key">The translation key.</param> + /// <param name="tokens">An object containing token key/value pairs. This can be an anonymous object (like <c>new { value = 42, name = "Cranberries" }</c>), a dictionary, or a class instance.</param> + public Translation Get(string key, object tokens) + { + return this.Get(key).Tokens(tokens); + } + + /// <summary>Set the translations to use.</summary> + /// <param name="translations">The translations to use.</param> + internal TranslationHelper SetTranslations(IDictionary<string, IDictionary<string, string>> translations) + { + // reset translations + this.All.Clear(); + foreach (var pair in translations) + this.All[pair.Key] = new Dictionary<string, string>(pair.Value, StringComparer.InvariantCultureIgnoreCase); + + // rebuild cache + this.SetLocale(this.Locale, this.LocaleEnum); + + return this; + } + + /// <summary>Set the current locale and precache translations.</summary> + /// <param name="locale">The current locale.</param> + /// <param name="localeEnum">The game's current language code.</param> + internal void SetLocale(string locale, LocalizedContentManager.LanguageCode localeEnum) + { + this.Locale = locale.ToLower().Trim(); + this.LocaleEnum = localeEnum; + + this.ForLocale = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase); + foreach (string next in this.GetRelevantLocales(this.Locale)) + { + // skip if locale not defined + if (!this.All.TryGetValue(next, out IDictionary<string, string> translations)) + continue; + + // add missing translations + foreach (var pair in translations) + { + if (!this.ForLocale.ContainsKey(pair.Key)) + this.ForLocale.Add(pair); + } + } + } + + + /********* + ** Private methods + *********/ + /// <summary>Get the locales which can provide translations for the given locale, in precedence order.</summary> + /// <param name="locale">The locale for which to find valid locales.</param> + private IEnumerable<string> 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"; + } + } +} |