summaryrefslogtreecommitdiff
path: root/src/StardewModdingAPI/Framework
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <github@jplamondonw.com>2017-06-02 18:25:34 -0400
committerJesse Plamondon-Willard <github@jplamondonw.com>2017-06-02 18:25:34 -0400
commit559203922bcad4071f8be53b1a61b0026da14396 (patch)
tree2ed664798183d77779beb68b61bd7622140413df /src/StardewModdingAPI/Framework
parent933e889c24e565d9028d3719ba2d65d512890564 (diff)
parent3a8e77a3098572fa413a27f41f832563daec3453 (diff)
downloadSMAPI-559203922bcad4071f8be53b1a61b0026da14396.tar.gz
SMAPI-559203922bcad4071f8be53b1a61b0026da14396.tar.bz2
SMAPI-559203922bcad4071f8be53b1a61b0026da14396.zip
Merge branch 'develop' into stable
Diffstat (limited to 'src/StardewModdingAPI/Framework')
-rw-r--r--src/StardewModdingAPI/Framework/CommandHelper.cs2
-rw-r--r--src/StardewModdingAPI/Framework/ContentHelper.cs113
-rw-r--r--src/StardewModdingAPI/Framework/Exceptions/SContentLoadException.cs18
-rw-r--r--src/StardewModdingAPI/Framework/InternalExtensions.cs38
-rw-r--r--src/StardewModdingAPI/Framework/Logging/LogFileManager.cs11
-rw-r--r--src/StardewModdingAPI/Framework/ModHelper.cs8
-rw-r--r--src/StardewModdingAPI/Framework/ModLoading/ModResolver.cs10
-rw-r--r--src/StardewModdingAPI/Framework/ModRegistry.cs10
-rw-r--r--src/StardewModdingAPI/Framework/Models/Manifest.cs2
-rw-r--r--src/StardewModdingAPI/Framework/SContentManager.cs6
-rw-r--r--src/StardewModdingAPI/Framework/SGame.cs83
-rw-r--r--src/StardewModdingAPI/Framework/TranslationHelper.cs137
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> &lt; <c>pt.json</c> &lt; <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> &lt; <c>pt.json</c> &lt; <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";
+ }
+ }
+}