diff options
Diffstat (limited to 'src/SMAPI')
-rw-r--r-- | src/SMAPI/Constants.cs | 15 | ||||
-rw-r--r-- | src/SMAPI/Framework/Content/TilesheetReference.cs | 15 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentCoordinator.cs | 4 | ||||
-rw-r--r-- | src/SMAPI/Framework/ContentManagers/GameContentManager.cs | 39 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/InterceptingTextWriter.cs | 40 | ||||
-rw-r--r-- | src/SMAPI/Framework/Logging/LogManager.cs | 37 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Framework/RewriteHelper.cs | 24 | ||||
-rw-r--r-- | src/SMAPI/Framework/ModLoading/Rewriters/FieldReplaceRewriter.cs | 54 | ||||
-rw-r--r-- | src/SMAPI/Framework/Monitor.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Metadata/InstructionMetadata.cs | 9 | ||||
-rw-r--r-- | src/SMAPI/i18n/uk.json | 6 |
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}}" +} |