summaryrefslogtreecommitdiff
path: root/src/SMAPI
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI')
-rw-r--r--src/SMAPI/Constants.cs15
-rw-r--r--src/SMAPI/Framework/Content/TilesheetReference.cs15
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs39
-rw-r--r--src/SMAPI/Framework/Logging/InterceptingTextWriter.cs40
-rw-r--r--src/SMAPI/Framework/Logging/LogManager.cs37
-rw-r--r--src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs24
-rw-r--r--src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs54
-rw-r--r--src/SMAPI/Framework/Monitor.cs2
-rw-r--r--src/SMAPI/Metadata/InstructionMetadata.cs9
-rw-r--r--src/SMAPI/i18n/uk.json6
11 files changed, 134 insertions, 111 deletions
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 5de28f84..455cfd7e 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -49,7 +49,7 @@ namespace StardewModdingAPI
internal static int? LogScreenId { get; set; }
/// <summary>SMAPI's current raw semantic version.</summary>
- internal static string RawApiVersion = "3.13.2";
+ internal static string RawApiVersion = "3.13.3";
}
/// <summary>Contains SMAPI's constants and assumptions.</summary>
@@ -65,7 +65,7 @@ namespace StardewModdingAPI
public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion(EarlyConstants.RawApiVersion);
/// <summary>The minimum supported version of Stardew Valley.</summary>
- public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.5");
+ public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.5.6");
/// <summary>The maximum supported version of Stardew Valley.</summary>
public static ISemanticVersion MaximumGameVersion { get; } = null;
@@ -340,5 +340,16 @@ namespace StardewModdingAPI
// if save doesn't exist yet, return the default one we expect to be created
return folder;
}
+
+ /// <summary>Get a display label for the game's build number.</summary>
+ internal static string GetBuildVersionLabel()
+ {
+ string version = typeof(Game1).Assembly.GetName().Version?.ToString() ?? "unknown";
+
+ if (version.StartsWith($"{Game1.version}."))
+ version = version.Substring(Game1.version.Length + 1);
+
+ return version;
+ }
}
}
diff --git a/src/SMAPI/Framework/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs
index 2ea38430..0919bb44 100644
--- a/src/SMAPI/Framework/Content/TilesheetReference.cs
+++ b/src/SMAPI/Framework/Content/TilesheetReference.cs
@@ -1,3 +1,6 @@
+using System.Numerics;
+using xTile.Dimensions;
+
namespace StardewModdingAPI.Framework.Content
{
/// <summary>Basic metadata about a vanilla tilesheet.</summary>
@@ -15,6 +18,12 @@ namespace StardewModdingAPI.Framework.Content
/// <summary>The asset path for the tilesheet texture.</summary>
public readonly string ImageSource;
+ /// <summary>The number of tiles in the tilesheet.</summary>
+ public readonly Size SheetSize;
+
+ /// <summary>The size of each tile in pixels.</summary>
+ public readonly Size TileSize;
+
/*********
** Public methods
@@ -23,11 +32,15 @@ namespace StardewModdingAPI.Framework.Content
/// <param name="index">The tilesheet's index in the list.</param>
/// <param name="id">The tilesheet's unique ID in the map.</param>
/// <param name="imageSource">The asset path for the tilesheet texture.</param>
- public TilesheetReference(int index, string id, string imageSource)
+ /// <param name="sheetSize">The number of tiles in the tilesheet.</param>
+ /// <param name="tileSize">The size of each tile in pixels.</param>
+ public TilesheetReference(int index, string id, string imageSource, Size sheetSize, Size tileSize)
{
this.Index = index;
this.Id = id;
this.ImageSource = imageSource;
+ this.SheetSize = sheetSize;
+ this.TileSize = tileSize;
}
}
}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index b6f1669a..99091f3e 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -406,14 +406,14 @@ namespace StardewModdingAPI.Framework
if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[] tilesheets))
{
tilesheets = this.TryLoadVanillaAsset(assetName, out Map map)
- ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource)).ToArray()
+ ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource, sheet.SheetSize, sheet.TileSize)).ToArray()
: null;
this.VanillaTilesheets[assetName] = tilesheets;
this.VanillaContentManager.Unload();
}
- return tilesheets ?? new TilesheetReference[0];
+ return tilesheets ?? Array.Empty<TilesheetReference>();
}
/// <summary>Get the language enum which corresponds to a locale code (e.g. <see cref="LocalizedContentManager.LanguageCode.fr"/> given <c>fr-FR</c>).</summary>
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index 7a49dd36..ab198076 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -13,6 +13,7 @@ using StardewModdingAPI.Framework.Utilities;
using StardewModdingAPI.Internal;
using StardewValley;
using xTile;
+using xTile.Tiles;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -308,7 +309,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
}
// return matched asset
- return this.TryValidateLoadedAsset(info, data, mod)
+ return this.TryFixAndValidateLoadedAsset(info, data, mod)
? new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName)
: null;
}
@@ -381,12 +382,13 @@ namespace StardewModdingAPI.Framework.ContentManagers
return asset;
}
- /// <summary>Validate that an asset loaded by a mod is valid and won't cause issues.</summary>
+ /// <summary>Validate that an asset loaded by a mod is valid and won't cause issues, and fix issues if possible.</summary>
/// <typeparam name="T">The asset type.</typeparam>
/// <param name="info">The basic asset metadata.</param>
/// <param name="data">The loaded asset data.</param>
/// <param name="mod">The mod which loaded the asset.</param>
- private bool TryValidateLoadedAsset<T>(IAssetInfo info, T data, IModMetadata mod)
+ /// <returns>Returns whether the asset passed validation checks (after any fixes were applied).</returns>
+ private bool TryFixAndValidateLoadedAsset<T>(IAssetInfo info, T data, IModMetadata mod)
{
// can't load a null asset
if (data == null)
@@ -401,20 +403,23 @@ namespace StardewModdingAPI.Framework.ContentManagers
TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName);
foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs)
{
- // skip if match
- if (loadedMap.TileSheets.Count > vanillaSheet.Index && loadedMap.TileSheets[vanillaSheet.Index].Id == vanillaSheet.Id)
- continue;
+ // add missing tilesheet
+ if (loadedMap.GetTileSheet(vanillaSheet.Id) == null)
+ {
+ mod.Monitor.LogOnce("SMAPI fixed maps loaded by this mod to prevent errors. See the log file for details.", LogLevel.Warn);
+ this.Monitor.Log($"Fixed broken map replacement: {mod.DisplayName} loaded '{info.AssetName}' without a required tilesheet (id: {vanillaSheet.Id}, source: {vanillaSheet.ImageSource}).");
+
+ loadedMap.AddTileSheet(new TileSheet(vanillaSheet.Id, loadedMap, vanillaSheet.ImageSource, vanillaSheet.SheetSize, vanillaSheet.TileSize));
+ }
// handle mismatch
+ if (loadedMap.TileSheets.Count <= vanillaSheet.Index || loadedMap.TileSheets[vanillaSheet.Index].Id != vanillaSheet.Id)
{
// only show warning if not farm map
// This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting.
bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining");
- int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id);
- string reason = loadedIndex != -1
- ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help."
- : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes.";
+ string reason = $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\nTechnical details for mod author: Expected order: {string.Join(", ", vanillaTilesheetRefs.Select(p => p.Id))}. See https://stardewvalleywiki.com/Modding:Maps#Tilesheet_order for help.";
SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval);
if (isFarmMap)
@@ -429,19 +434,5 @@ namespace StardewModdingAPI.Framework.ContentManagers
return true;
}
-
- /// <summary>Find a map tilesheet by ID.</summary>
- /// <param name="map">The map whose tilesheets to search.</param>
- /// <param name="id">The tilesheet ID to match.</param>
- private int TryFindTilesheet(Map map, string id)
- {
- for (int i = 0; i < map.TileSheets.Count; i++)
- {
- if (map.TileSheets[i].Id == id)
- return i;
- }
-
- return -1;
- }
}
}
diff --git a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
index d99f1dd2..bad69a2a 100644
--- a/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
+++ b/src/SMAPI/Framework/Logging/InterceptingTextWriter.cs
@@ -8,15 +8,11 @@ namespace StardewModdingAPI.Framework.Logging
internal class InterceptingTextWriter : TextWriter
{
/*********
- ** Fields
+ ** Accessors
*********/
/// <summary>Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
- private readonly char IgnoreChar;
-
+ public const char IgnoreChar = '\u200B';
- /*********
- ** Accessors
- *********/
/// <summary>The underlying console output.</summary>
public TextWriter Out { get; }
@@ -26,30 +22,48 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>The event raised when a message is written to the console directly.</summary>
public event Action<string> OnMessageIntercepted;
+ /// <summary>Whether the text writer should ignore the next input if it's a newline.</summary>
+ /// <remarks>This is used when log output is suppressed from the console, since <c>Console.WriteLine</c> writes the trailing newline as a separate call.</remarks>
+ public bool IgnoreNextIfNewline { get; set; }
+
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="output">The underlying output writer.</param>
- /// <param name="ignoreChar">Prefixing a message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</param>
- public InterceptingTextWriter(TextWriter output, char ignoreChar)
+ public InterceptingTextWriter(TextWriter output)
{
this.Out = output;
- this.IgnoreChar = ignoreChar;
}
/// <inheritdoc />
public override void Write(char[] buffer, int index, int count)
{
- if (buffer.Length == 0)
+ // track newline skip
+ bool ignoreIfNewline = this.IgnoreNextIfNewline;
+ this.IgnoreNextIfNewline = false;
+
+ // get first character if valid
+ if (count == 0 || index < 0 || index >= buffer.Length)
+ {
this.Out.Write(buffer, index, count);
- else if (buffer[0] == this.IgnoreChar)
+ return;
+ }
+ char firstChar = buffer[index];
+
+ // handle output
+ if (firstChar == InterceptingTextWriter.IgnoreChar)
this.Out.Write(buffer, index + 1, count - 1);
- else if (this.IsEmptyOrNewline(buffer))
+ else if (char.IsControl(firstChar) && firstChar is not ('\r' or '\n'))
this.Out.Write(buffer, index, count);
+ else if (this.IsEmptyOrNewline(buffer))
+ {
+ if (!ignoreIfNewline)
+ this.Out.Write(buffer, index, count);
+ }
else
- this.OnMessageIntercepted?.Invoke(new string(buffer, index, count).TrimEnd('\r', '\n'));
+ this.OnMessageIntercepted?.Invoke(new string(buffer, index, count));
}
/// <inheritdoc />
diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs
index 5a291d0a..a8a8b6ee 100644
--- a/src/SMAPI/Framework/Logging/LogManager.cs
+++ b/src/SMAPI/Framework/Logging/LogManager.cs
@@ -25,8 +25,11 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>The log file to which to write messages.</summary>
private readonly LogFileManager LogFile;
+ /// <summary>The text writer which intercepts console output.</summary>
+ private readonly InterceptingTextWriter ConsoleInterceptor;
+
/// <summary>Prefixing a low-level message with this character indicates that the console interceptor should write the string without intercepting it. (The character itself is not written.)</summary>
- private readonly char IgnoreChar = '\u200B';
+ private const char IgnoreChar = InterceptingTextWriter.IgnoreChar;
/// <summary>Get a named monitor instance.</summary>
private readonly Func<string, Monitor> GetMonitorImpl;
@@ -34,22 +37,22 @@ namespace StardewModdingAPI.Framework.Logging
/// <summary>Regex patterns which match console non-error messages to suppress from the console and log.</summary>
private readonly Regex[] SuppressConsolePatterns =
{
- new Regex(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^DebugOutput:\s+(?:added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant),
- new Regex(@"^Ignoring keys: ", RegexOptions.Compiled | RegexOptions.CultureInvariant)
+ new(@"^TextBox\.Selected is now '(?:True|False)'\.$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^loadPreferences\(\); begin", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^savePreferences\(\); async=", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^DebugOutput:\s+(?:added cricket|dismount tile|Ping|playerPos)", RegexOptions.Compiled | RegexOptions.CultureInvariant),
+ new(@"^Ignoring keys: ", RegexOptions.Compiled | RegexOptions.CultureInvariant)
};
/// <summary>Regex patterns which match console messages to show a more friendly error for.</summary>
private readonly ReplaceLogPattern[] ReplaceConsolePatterns =
{
// Steam not loaded
- new ReplaceLogPattern(
+ new(
search: new Regex(@"^System\.InvalidOperationException: Steamworks is not initialized\.[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
replacement:
#if SMAPI_FOR_WINDOWS
- "Oops! Steam achievements won't work because Steam isn't loaded. See 'Launch SMAPI through Steam or GOG Galaxy' in the install guide for more info: https://smapi.io/install.",
+ "Oops! Steam achievements won't work because Steam isn't loaded. See 'Configure your game client' in the install guide for more info: https://smapi.io/install.",
#else
"Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.",
#endif
@@ -57,7 +60,7 @@ namespace StardewModdingAPI.Framework.Logging
),
// save file not found error
- new ReplaceLogPattern(
+ new(
search: new Regex(@"^System\.IO\.FileNotFoundException: [^\n]+\n[^:]+: '[^\n]+[/\\]Saves[/\\]([^'\r\n]+)[/\\]([^'\r\n]+)'[\s\S]+LoadGameMenu\.FindSaveGames[\s\S]+$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
replacement: "The game can't find the '$2' file for your '$1' save. See https://stardewvalleywiki.com/Saves#Troubleshooting for help.",
logLevel: LogLevel.Error
@@ -91,7 +94,7 @@ namespace StardewModdingAPI.Framework.Logging
public LogManager(string logPath, ColorSchemeConfig colorConfig, bool writeToConsole, bool isVerbose, bool isDeveloperMode, Func<int?> getScreenIdForLog)
{
// init construction logic
- this.GetMonitorImpl = name => new Monitor(name, this.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog)
+ this.GetMonitorImpl = name => new Monitor(name, LogManager.IgnoreChar, this.LogFile, colorConfig, isVerbose, getScreenIdForLog)
{
WriteToConsole = writeToConsole,
ShowTraceInConsole = isDeveloperMode,
@@ -104,10 +107,10 @@ namespace StardewModdingAPI.Framework.Logging
this.MonitorForGame = this.GetMonitor("game");
// redirect direct console output
- var output = new InterceptingTextWriter(Console.Out, this.IgnoreChar);
+ this.ConsoleInterceptor = new InterceptingTextWriter(Console.Out);
if (writeToConsole)
- output.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
- Console.SetOut(output);
+ this.ConsoleInterceptor.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
+ Console.SetOut(this.ConsoleInterceptor);
// enable Unicode handling on Windows
// (the terminal defaults to UTF-8 on Linux/macOS)
@@ -146,7 +149,7 @@ namespace StardewModdingAPI.Framework.Logging
.Add(new ReloadI18nCommand(reloadTranslations), this.Monitor);
// start handling command line input
- Thread inputThread = new Thread(() =>
+ Thread inputThread = new(() =>
{
while (true)
{
@@ -262,7 +265,7 @@ namespace StardewModdingAPI.Framework.Logging
public void LogIntro(string modsPath, IDictionary<string, object> customSettings)
{
// log platform
- this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
+ this.Monitor.Log($"SMAPI {Constants.ApiVersion} with Stardew Valley {Constants.GameVersion} (build {Constants.GetBuildVersionLabel()}) on {EnvironmentUtility.GetFriendlyPlatformName(Constants.Platform)}", LogLevel.Info);
// log basic info
this.Monitor.Log($"Mods go here: {modsPath}", LogLevel.Info);
@@ -363,7 +366,10 @@ namespace StardewModdingAPI.Framework.Logging
// ignore suppressed message
if (level != LogLevel.Error && this.SuppressConsolePatterns.Any(p => p.IsMatch(message)))
+ {
+ this.ConsoleInterceptor.IgnoreNextIfNewline = true;
return;
+ }
// show friendly error if applicable
foreach (ReplaceLogPattern entry in this.ReplaceConsolePatterns)
@@ -383,6 +389,7 @@ namespace StardewModdingAPI.Framework.Logging
// forward to monitor
gameMonitor.Log(message, level);
+ this.ConsoleInterceptor.IgnoreNextIfNewline = true;
}
/// <summary>Write a summary of mod warnings to the console and log.</summary>
diff --git a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
index 60bbd2c7..d7cb2471 100644
--- a/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
+++ b/src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs
@@ -13,7 +13,7 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
** Fields
*********/
/// <summary>The comparer which heuristically compares type definitions.</summary>
- private static readonly TypeReferenceComparer TypeDefinitionComparer = new TypeReferenceComparer();
+ private static readonly TypeReferenceComparer TypeDefinitionComparer = new();
/*********
@@ -28,28 +28,6 @@ namespace StardewModdingAPI.Framework.ModLoading.Framework
: null;
}
- /// <summary>Get whether the field is a reference to the expected type and field.</summary>
- /// <param name="instruction">The IL instruction.</param>
- /// <param name="fullTypeName">The full type name containing the expected field.</param>
- /// <param name="fieldName">The name of the expected field.</param>
- public static bool IsFieldReferenceTo(Instruction instruction, string fullTypeName, string fieldName)
- {
- FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
- return RewriteHelper.IsFieldReferenceTo(fieldRef, fullTypeName, fieldName);
- }
-
- /// <summary>Get whether the field is a reference to the expected type and field.</summary>
- /// <param name="fieldRef">The field reference to check.</param>
- /// <param name="fullTypeName">The full type name containing the expected field.</param>
- /// <param name="fieldName">The name of the expected field.</param>
- public static bool IsFieldReferenceTo(FieldReference fieldRef, string fullTypeName, string fieldName)
- {
- return
- fieldRef != null
- && fieldRef.DeclaringType.FullName == fullTypeName
- && fieldRef.Name == fieldName;
- }
-
/// <summary>Get the method reference from an instruction if it matches.</summary>
/// <param name="instruction">The IL instruction.</param>
public static MethodReference AsMethodReference(Instruction instruction)
diff --git a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
index 0b679e9d..857a2230 100644
--- a/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
+++ b/src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;
@@ -12,54 +13,55 @@ namespace StardewModdingAPI.Framework.ModLoading.Rewriters
/*********
** Fields
*********/
- /// <summary>The type containing the field to which references should be rewritten.</summary>
- private readonly Type Type;
-
- /// <summary>The field name to which references should be rewritten.</summary>
- private readonly string FromFieldName;
-
- /// <summary>The new field to reference.</summary>
- private readonly FieldInfo ToField;
+ /// <summary>The new fields to reference indexed by the old field/type names.</summary>
+ private readonly Dictionary<string, Dictionary<string, FieldInfo>> FieldMaps = new();
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
+ public FieldReplaceRewriter()
+ : base(defaultPhrase: "field replacement") { } // will be overridden when a field is replaced
+
+ /// <summary>Add a field to replace.</summary>
/// <param name="fromType">The type whose field to rewrite.</param>
/// <param name="fromFieldName">The field name to rewrite.</param>
/// <param name="toType">The new type which will have the field.</param>
/// <param name="toFieldName">The new field name to reference.</param>
- public FieldReplaceRewriter(Type fromType, string fromFieldName, Type toType, string toFieldName)
- : base(defaultPhrase: $"{fromType.FullName}.{fromFieldName} field")
+ public FieldReplaceRewriter AddField(Type fromType, string fromFieldName, Type toType, string toFieldName)
{
- this.Type = fromType;
- this.FromFieldName = fromFieldName;
- this.ToField = toType.GetField(toFieldName);
- if (this.ToField == null)
+ // get full type name
+ string fromTypeName = fromType?.FullName;
+ if (fromTypeName == null)
+ throw new InvalidOperationException($"Can't replace field for invalid type reference {toType}.");
+
+ // get target field
+ FieldInfo toField = toType.GetField(toFieldName);
+ if (toField == null)
throw new InvalidOperationException($"The {toType.FullName} class doesn't have a {toFieldName} field.");
- }
- /// <summary>Construct an instance.</summary>
- /// <param name="type">The type whose field to rewrite.</param>
- /// <param name="fromFieldName">The field name to rewrite.</param>
- /// <param name="toFieldName">The new field name to reference.</param>
- public FieldReplaceRewriter(Type type, string fromFieldName, string toFieldName)
- : this(type, fromFieldName, type, toFieldName)
- {
+ // add mapping
+ if (!this.FieldMaps.TryGetValue(fromTypeName, out var fieldMap))
+ this.FieldMaps[fromTypeName] = fieldMap = new();
+ fieldMap[fromFieldName] = toField;
+
+ return this;
}
/// <inheritdoc />
public override bool Handle(ModuleDefinition module, ILProcessor cil, Instruction instruction)
{
- // get field reference
FieldReference fieldRef = RewriteHelper.AsFieldReference(instruction);
- if (!RewriteHelper.IsFieldReferenceTo(fieldRef, this.Type.FullName, this.FromFieldName))
+ string declaringType = fieldRef?.DeclaringType?.FullName;
+
+ // get mapped field
+ if (declaringType == null || !this.FieldMaps.TryGetValue(declaringType, out var fieldMap) || !fieldMap.TryGetValue(fieldRef.Name, out FieldInfo toField))
return false;
// replace with new field
- instruction.Operand = module.ImportReference(this.ToField);
-
+ this.Phrases.Add($"{fieldRef.DeclaringType.Name}.{fieldRef.Name} field");
+ instruction.Operand = module.ImportReference(toField);
return this.MarkRewritten();
}
}
diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs
index 04e67d68..ab76e7c0 100644
--- a/src/SMAPI/Framework/Monitor.cs
+++ b/src/SMAPI/Framework/Monitor.cs
@@ -28,7 +28,7 @@ namespace StardewModdingAPI.Framework
private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>() select level.ToString().Length).Max();
/// <summary>A cache of messages that should only be logged once.</summary>
- private readonly HashSet<string> LogOnceCache = new HashSet<string>();
+ private readonly HashSet<string> LogOnceCache = new();
/// <summary>Get the screen ID that should be logged to distinguish between players in split-screen mode, if any.</summary>
private readonly Func<int?> GetScreenIdForLog;
diff --git a/src/SMAPI/Metadata/InstructionMetadata.cs b/src/SMAPI/Metadata/InstructionMetadata.cs
index 232e54ce..367372b2 100644
--- a/src/SMAPI/Metadata/InstructionMetadata.cs
+++ b/src/SMAPI/Metadata/InstructionMetadata.cs
@@ -37,9 +37,10 @@ namespace StardewModdingAPI.Metadata
if (rewriteMods)
{
// rewrite for Stardew Valley 1.5
- yield return new FieldReplaceRewriter(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture));
- yield return new FieldReplaceRewriter(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps));
- yield return new FieldReplaceRewriter(typeof(MineShaft), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps));
+ yield return new FieldReplaceRewriter()
+ .AddField(typeof(DecoratableLocation), "furniture", typeof(GameLocation), nameof(GameLocation.furniture))
+ .AddField(typeof(Farm), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps))
+ .AddField(typeof(MineShaft), "resourceClumps", typeof(GameLocation), nameof(GameLocation.resourceClumps));
// heuristic rewrites
yield return new HeuristicFieldRewriter(this.ValidateReferencesToAssemblies);
@@ -87,7 +88,7 @@ namespace StardewModdingAPI.Metadata
typeof(System.IO.DirectoryInfo).FullName,
typeof(System.IO.DriveInfo).FullName,
typeof(System.IO.FileSystemWatcher).FullName
- },
+ },
InstructionHandleResult.DetectedFilesystemAccess
);
diff --git a/src/SMAPI/i18n/uk.json b/src/SMAPI/i18n/uk.json
new file mode 100644
index 00000000..d84aabcf
--- /dev/null
+++ b/src/SMAPI/i18n/uk.json
@@ -0,0 +1,6 @@
+{
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}} День {{day}}",
+ "generic.date-with-year": "{{season}} День {{day}}, Рік {{year}}"
+}