From cecd17ec3c5a46ac37caf926a74eff03d49740a1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 27 Mar 2020 21:03:47 -0400 Subject: [PATCH 01/21] update schema for Content Patcher 1.13 --- docs/release-notes.md | 3 + .../wwwroot/schemas/content-patcher.json | 72 +++++++++++++++++-- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 185ddc695..b404f18d3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,9 @@ Released 24 March 2020 for Stardew Valley 1.4.1 or later. * Asset changes now propagate to NPCs in an event (e.g. wedding sprites). * Fixed mouse input suppression not working in SMAPI 3.4. +* For the web UI: + * Updated the JSON validator and Content Patcher schema for Content Patcher 1.13. + ## 3.4 Released 22 March 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index e6cd4e65b..f627ab957 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -11,9 +11,9 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.", "type": "string", - "const": "1.11.0", + "const": "1.13.0", "@errorMessages": { - "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.11.0'." + "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.13.0'." } }, "ConfigSchema": { @@ -268,6 +268,48 @@ "type": "string" } }, + "MapTiles": { + "title": "Map tiles", + "description": "The individual map tiles to add, edit, or remove.", + "type": "array", + "items": { + "type": "object", + "properties": { + "Layer": { + "description": "The map layer name to change.", + "type": "string" + }, + "Position": { + "description": "The tile coordinates to change. You can use the Debug Mode mod to see tile coordinates in-game.", + "$ref": "#/definitions/Position" + }, + "SetTilesheet": { + "title": "Set tilesheet", + "description": "Sets the tilesheet ID for the tile index.", + "type": "string" + }, + "SetIndex": { + "title": "Set tile index", + "description": "Sets the tile index in the tilesheet.", + "type": [ "string", "number" ] + }, + "SetProperties": { + "title": "Set tile properties", + "description": "The properties to set or remove. This is merged into the existing tile properties, if any. To remove a property, set its value to `null` (not \"null\" in quotes).", + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "Remove": { + "description": "Whether to remove the current tile and all its properties on that layer. If combined with the other fields, a new tile is created from the other fields as if the tile didn't previously exist.", + "type": "boolean" + } + }, + + "required": [ "Layer", "Position" ] + } + }, "When": { "title": "When", "description": "Only apply the patch if the given conditions match.", @@ -335,7 +377,7 @@ } }, "propertyNames": { - "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "MapProperties" ] + "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "MapProperties", "MapTiles" ] } } } @@ -361,17 +403,37 @@ "type": [ "boolean", "string" ] } }, + "Position": { + "type": "object", + "properties": { + "X": { + "title": "X position", + "description": "The X position, measured in pixels for a texture or tiles for a map. This can contain tokens.", + "type": [ "integer", "string" ], + "minimum:": 0 + }, + "Y": { + "title": "Y position", + "description": "The Y position, measured in pixels for a texture or tiles for a map. This can contain tokens.", + "type": [ "integer", "string" ], + "minimum:": 0 + } + }, + + "required": [ "X", "Y" ], + "additionalProperties": false + }, "Rectangle": { "type": "object", "properties": { "X": { - "title": "X-Coordinate", + "title": "X position", "description": "The X position of the area's top-left corner, measured in pixels for a texture or tiles for a map. This can contain tokens.", "type": [ "integer", "string" ], "minimum:": 0 }, "Y": { - "title": "Y-Coordinate", + "title": "Y position", "description": "The Y position of the area's top-left corner, measured in pixels for a texture or tiles for a map. This can contain tokens.", "type": [ "integer", "string" ], "minimum:": 0 From 3e54ac88579ba202d9bac34ca88c1dcb25f90a64 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Mar 2020 12:08:56 -0400 Subject: [PATCH 02/21] fix path segmenting on Linux/Mac in asset propagation --- docs/release-notes.md | 10 +++++++--- src/SMAPI/Metadata/CoreAssetPropagator.cs | 7 ++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index b404f18d3..78d9d8bdd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,13 @@ ← [README](README.md) # Release notes +## Upcoming release +* For the web UI: + * Updated the JSON validator and Content Patcher schema for Content Patcher 1.13. + +* For modders: + * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. + ## 3.4.1 Released 24 March 2020 for Stardew Valley 1.4.1 or later. @@ -8,9 +15,6 @@ Released 24 March 2020 for Stardew Valley 1.4.1 or later. * Asset changes now propagate to NPCs in an event (e.g. wedding sprites). * Fixed mouse input suppression not working in SMAPI 3.4. -* For the web UI: - * Updated the JSON validator and Content Patcher schema for Content Patcher 1.13. - ## 3.4 Released 22 March 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 83e553ffe..30b96c1df 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -5,6 +5,7 @@ using Microsoft.Xna.Framework.Graphics; using Netcode; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using StardewValley.BellsAndWhistles; using StardewValley.Buildings; @@ -1037,9 +1038,9 @@ private bool IsInFolder(string key, string folder, bool allowSubfolders = false) /// The path to check. private string[] GetSegments(string path) { - if (path == null) - return new string[0]; - return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + return path != null + ? PathUtilities.GetSegments(path) + : new string[0]; } /// Count the number of segments in a path (e.g. 'a/b' is 2). From 6f8fb2a68b3e45763c1b71e7f420fd7f174ffc60 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Mar 2020 14:40:17 -0400 Subject: [PATCH 03/21] fix AutoQualityPatch version in compatibility list --- docs/release-notes.md | 3 +++ src/SMAPI.Web/wwwroot/SMAPI.metadata.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 78d9d8bdd..0faf49139 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming release +* For players: + * Updated compatibility list. + * For the web UI: * Updated the JSON validator and Content Patcher schema for Content Patcher 1.13. diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json index 3101fdf13..179ef42aa 100644 --- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json +++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json @@ -155,7 +155,7 @@ *********/ "Auto Quality Patch": { "ID": "SilentOak.AutoQualityPatch", - "~2.1.3-unofficial.7 | Status": "AssumeBroken" // runtime errors + "~2.1.3-unofficial.7-mizzion | Status": "AssumeBroken" // runtime errors }, "Fix Dice": { From 96ec4de7275ae4e0ffc92ca8058c5e04b8ddd20d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Apr 2020 20:09:44 -0400 Subject: [PATCH 04/21] fix marriage dialogue left in invalid state after dialogue propagation --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0faf49139..c72e44544 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * For modders: * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. + * Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses. ## 3.4.1 Released 24 March 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 30b96c1df..0a14086bf 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -892,11 +892,13 @@ private bool ReloadNpcDialogue(string key) // doesn't store the text itself. foreach (NPC villager in villagers) { + bool shouldSayMarriageDialogue = villager.shouldSayMarriageDialogue.Value; MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray(); villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue villager.resetCurrentDialogue(); + villager.shouldSayMarriageDialogue.Set(shouldSayMarriageDialogue); villager.currentMarriageDialogue.Set(marriageDialogue); } From c9b6b04a7502215b94a00560fad905786b144bb2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 5 Apr 2020 13:38:59 -0400 Subject: [PATCH 05/21] fix rare intermittent "CGI application encountered an error" errors --- docs/release-notes.md | 1 + src/SMAPI.Web/Program.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index c72e44544..b7a135b98 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * For the web UI: * Updated the JSON validator and Content Patcher schema for Content Patcher 1.13. + * Fixed rare intermittent "CGI application encountered an error" errors. * For modders: * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs index 5d13cdf3a..700821603 100644 --- a/src/SMAPI.Web/Program.cs +++ b/src/SMAPI.Web/Program.cs @@ -18,6 +18,7 @@ public static void Main(string[] args) .CreateDefaultBuilder(args) .CaptureStartupErrors(true) .UseSetting("detailedErrors", "true") + .UseKestrel().UseIISIntegration() // must be used together; fixes intermittent errors on Azure: https://stackoverflow.com/a/38312175/262123 .UseStartup() .Build() .Run(); From b363e0b6b745b869a684c72b69c713d4817257cf Mon Sep 17 00:00:00 2001 From: Kevin Daughtridge Date: Fri, 10 Apr 2020 12:41:35 -0700 Subject: [PATCH 06/21] set daysPlayed in world_set{day,season,year} commands --- .../Framework/Commands/World/SetDayCommand.cs | 1 + .../Framework/Commands/World/SetSeasonCommand.cs | 1 + .../Framework/Commands/World/SetYearCommand.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs index 8d6bd7594..5fd1b8d4d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs @@ -32,6 +32,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // handle Game1.dayOfMonth = day; + Game1.stats.DaysPlayed = (uint)(Game1.dayOfMonth + 28 * (Utility.getSeasonNumber(Game1.currentSeason) + 4 * (Game1.year - 1))); monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs index 0615afe7e..a28fa2c34 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -40,6 +40,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // handle Game1.currentSeason = season.ToLower(); Game1.setGraphicsForSeason(); + Game1.stats.DaysPlayed = (uint)(Game1.dayOfMonth + 28 * (Utility.getSeasonNumber(Game1.currentSeason) + 4 * (Game1.year - 1))); monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs index 66abd6dce..dceb95afa 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs @@ -32,6 +32,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // handle Game1.year = year; + Game1.stats.DaysPlayed = (uint)(Game1.dayOfMonth + 28 * (Utility.getSeasonNumber(Game1.currentSeason) + 4 * (Game1.year - 1))); monitor.Log($"OK, the year is now {Game1.year}.", LogLevel.Info); } } From e370b084834246b2a0a7e6443f9b8d6e11e94912 Mon Sep 17 00:00:00 2001 From: Kevin Daughtridge Date: Sat, 11 Apr 2020 13:25:12 -0700 Subject: [PATCH 07/21] use SDate to set DaysPlayed --- .../Framework/Commands/World/SetDayCommand.cs | 3 ++- .../Framework/Commands/World/SetSeasonCommand.cs | 3 ++- .../Framework/Commands/World/SetYearCommand.cs | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs index 5fd1b8d4d..23c266ea5 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs @@ -1,4 +1,5 @@ using System.Linq; +using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -32,7 +33,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // handle Game1.dayOfMonth = day; - Game1.stats.DaysPlayed = (uint)(Game1.dayOfMonth + 28 * (Utility.getSeasonNumber(Game1.currentSeason) + 4 * (Game1.year - 1))); + Game1.stats.DaysPlayed = (uint)SDate.Now().DaysSinceStart; monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs index a28fa2c34..676369fe0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -1,4 +1,5 @@ using System.Linq; +using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -40,7 +41,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // handle Game1.currentSeason = season.ToLower(); Game1.setGraphicsForSeason(); - Game1.stats.DaysPlayed = (uint)(Game1.dayOfMonth + 28 * (Utility.getSeasonNumber(Game1.currentSeason) + 4 * (Game1.year - 1))); + Game1.stats.DaysPlayed = (uint)SDate.Now().DaysSinceStart; monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs index dceb95afa..648830c1f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs @@ -1,4 +1,5 @@ using System.Linq; +using StardewModdingAPI.Utilities; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -32,7 +33,7 @@ public override void Handle(IMonitor monitor, string command, ArgumentParser arg // handle Game1.year = year; - Game1.stats.DaysPlayed = (uint)(Game1.dayOfMonth + 28 * (Utility.getSeasonNumber(Game1.currentSeason) + 4 * (Game1.year - 1))); + Game1.stats.DaysPlayed = (uint)SDate.Now().DaysSinceStart; monitor.Log($"OK, the year is now {Game1.year}.", LogLevel.Info); } } From f635c090959935cfc98df913197022fa16ca21fe Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Apr 2020 22:57:52 -0400 Subject: [PATCH 08/21] update release notes --- docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index b7a135b98..66ab97906 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,9 @@ * For players: * Updated compatibility list. +* For the Console Commands mod: + * The date commands like `world_setday` now also set the `daysPlayed` stat, so in-game events/randomization match what you'd get if you played to that date normally (thanks to kdau!). + * For the web UI: * Updated the JSON validator and Content Patcher schema for Content Patcher 1.13. * Fixed rare intermittent "CGI application encountered an error" errors. From 5f73d47fb9dfe7ac2733a0a5fe57cf96639594f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Apr 2020 12:35:34 -0400 Subject: [PATCH 09/21] add config option to disable console colors (#707) --- docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 4 ++-- .../ConsoleWriting/ColorfulConsoleWriter.cs | 16 ++++++++++++---- .../ConsoleWriting/IConsoleWriter.cs | 11 +++++++++++ .../ConsoleWriting/MonitorColorScheme.cs | 5 ++++- src/SMAPI.Internal/SMAPI.Internal.projitems | 1 + src/SMAPI.sln | 6 ++++++ src/SMAPI/Framework/Monitor.cs | 4 ++-- src/SMAPI/SMAPI.config.json | 1 + 9 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 66ab97906..d064f17fd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming release * For players: + * Added config option to disable console colors. * Updated compatibility list. * For the Console Commands mod: diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 2d58baf07..5b0c6e1f4 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -88,8 +88,8 @@ private IEnumerable GetUninstallPaths(DirectoryInfo installDir, Director yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files } - /// Handles writing color-coded text to the console. - private ColorfulConsoleWriter ConsoleWriter; + /// Handles writing text to the console. + private IConsoleWriter ConsoleWriter; /********* diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs index aefda9b63..b5bd46001 100644 --- a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs +++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs @@ -4,8 +4,8 @@ namespace StardewModdingAPI.Internal.ConsoleWriting { - /// Provides a wrapper for writing color-coded text to the console. - internal class ColorfulConsoleWriter + /// Writes color-coded text to the console. + internal class ColorfulConsoleWriter : IConsoleWriter { /********* ** Fields @@ -30,8 +30,16 @@ public ColorfulConsoleWriter(Platform platform) /// The colors to use for text written to the SMAPI console. public ColorfulConsoleWriter(Platform platform, ColorSchemeConfig colorConfig) { - this.SupportsColor = this.TestColorSupport(); - this.Colors = this.GetConsoleColorScheme(platform, colorConfig); + if (colorConfig.UseScheme == MonitorColorScheme.None) + { + this.SupportsColor = false; + this.Colors = null; + } + else + { + this.SupportsColor = this.TestColorSupport(); + this.Colors = this.GetConsoleColorScheme(platform, colorConfig); + } } /// Write a message line to the log. diff --git a/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs new file mode 100644 index 000000000..fbcf161cc --- /dev/null +++ b/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs @@ -0,0 +1,11 @@ +namespace StardewModdingAPI.Internal.ConsoleWriting +{ + /// Writes text to the console. + internal interface IConsoleWriter + { + /// Write a message line to the log. + /// The message to log. + /// The log level. + void WriteLine(string message, ConsoleLogLevel level); + } +} diff --git a/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs index bccb56d73..994ea6a5d 100644 --- a/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs +++ b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs @@ -10,6 +10,9 @@ internal enum MonitorColorScheme DarkBackground, /// Use darker text colors that look better on a white or light background. - LightBackground + LightBackground, + + /// Disable console color. + None } } diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems index 7fcebc94d..0d583a6d8 100644 --- a/src/SMAPI.Internal/SMAPI.Internal.projitems +++ b/src/SMAPI.Internal/SMAPI.Internal.projitems @@ -12,6 +12,7 @@ + \ No newline at end of file diff --git a/src/SMAPI.sln b/src/SMAPI.sln index 62eaa7770..f9c537c44 100644 --- a/src/SMAPI.sln +++ b/src/SMAPI.sln @@ -81,7 +81,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web.LegacyRedirects", EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution + SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5 + SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5 + SMAPI.Internal\SMAPI.Internal.projitems*{1b3821e6-d030-402c-b3a1-7ca45c2800ea}*SharedItemsImports = 5 + SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5 SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 + SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5 + SMAPI.Internal\SMAPI.Internal.projitems*{e6da2198-7686-4f1d-b312-4a4dc70884c0}*SharedItemsImports = 5 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index f630c7fef..44eeabe65 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -15,8 +15,8 @@ internal class Monitor : IMonitor /// The name of the module which logs messages using this instance. private readonly string Source; - /// Handles writing color-coded text to the console. - private readonly ColorfulConsoleWriter ConsoleWriter; + /// Handles writing text to the console. + private readonly IConsoleWriter ConsoleWriter; /// Manages access to the console output. private readonly ConsoleInterceptionManager ConsoleInterceptor; diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 57b4f8851..a426b0ef7 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -73,6 +73,7 @@ copy all the settings, or you may cause bugs due to overridden changes in future * automatically on Linux or Windows. * - LightBackground: use darker text colors that look better on a white or light background. * - DarkBackground: use lighter text colors that look better on a black or dark background. + * - None: disables all colors, so everything is written in the default terminal color. * * For available color codes, see https://docs.microsoft.com/en-us/dotnet/api/system.consolecolor. * From 49c2ee517d3276b4547bb25ceda3ebf5e9707887 Mon Sep 17 00:00:00 2001 From: Kevin Daughtridge Date: Mon, 13 Apr 2020 15:42:00 -0700 Subject: [PATCH 10/21] SDate: Add WorldDate conversions and features - SeasonIndex - FromWorldDate() - FromDaysSinceStart() - ToLocaleString() - ToWorldDate() --- src/SMAPI/Utilities/SDate.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 0ab37aa01..301ea9d2c 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -29,6 +29,9 @@ public class SDate : IEquatable /// The season name. public string Season { get; } + /// The season index. + public int SeasonIndex { get; } + /// The year. public int Year { get; } @@ -63,6 +66,20 @@ public static SDate Now() return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } + /// Get the date equivalent to the given WorldDate. + /// A date returned from a core game property or method. + public static SDate FromWorldDate(WorldDate worldDate) + { + return new SDate(worldDate.DayOfMonth, worldDate.Season, worldDate.Year, allowDayZero: true); + } + + /// Get the date falling the given number of days after 0 spring Y1. + /// The number of days since 0 spring Y1. + public static SDate FromDaysSinceStart(int daysSinceStart) + { + return new SDate(0, "spring", 1, allowDayZero: true).AddDays(daysSinceStart); + } + /// Get a new date with the given number of days added. /// The number of days to add. /// Returns the resulting date. @@ -98,6 +115,18 @@ public override string ToString() return $"{this.Day:00} {this.Season} Y{this.Year}"; } + /// Get a string representation of the date in the current game locale. + public string ToLocaleString() + { + return this.ToWorldDate().Localize(); + } + + /// Get the date as an instance of the game's WorldDate class. This is intended for passing to core game methods. + public WorldDate ToWorldDate() + { + return new WorldDate(this.Year, this.Season, this.Day); + } + /**** ** IEquatable ****/ @@ -200,6 +229,7 @@ private SDate(int day, string season, int year, bool allowDayZero) // initialize this.Day = day; this.Season = season; + this.SeasonIndex = this.GetSeasonIndex(season); this.Year = year; this.DayOfWeek = this.GetDayOfWeek(day); this.DaysSinceStart = this.GetDaysSinceStart(day, season, year); From 7a60dc4ee9d32831f55fe066c20729ca8e9dc8a1 Mon Sep 17 00:00:00 2001 From: Kevin Daughtridge Date: Mon, 13 Apr 2020 23:29:56 -0700 Subject: [PATCH 11/21] SDate: fixes to new methods - FromWorldDate: replace with explicit operator SDate - ToWorldDate: replace with explicit operator WorldDate - ToLocaleString: use Utility.getDateStringFor directly - FromDaysSinceStart: reinterpret exception to an appropriate one --- src/SMAPI/Utilities/SDate.cs | 37 +++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 301ea9d2c..8cb558916 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -66,18 +66,18 @@ public static SDate Now() return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } - /// Get the date equivalent to the given WorldDate. - /// A date returned from a core game property or method. - public static SDate FromWorldDate(WorldDate worldDate) - { - return new SDate(worldDate.DayOfMonth, worldDate.Season, worldDate.Year, allowDayZero: true); - } - /// Get the date falling the given number of days after 0 spring Y1. /// The number of days since 0 spring Y1. public static SDate FromDaysSinceStart(int daysSinceStart) { - return new SDate(0, "spring", 1, allowDayZero: true).AddDays(daysSinceStart); + try + { + return new SDate(0, "spring", 1, allowDayZero: true).AddDays(daysSinceStart); + } + catch (ArithmeticException) + { + throw new ArgumentException($"Invalid daysSinceStart '{daysSinceStart}', must be at least 1."); + } } /// Get a new date with the given number of days added. @@ -118,13 +118,7 @@ public override string ToString() /// Get a string representation of the date in the current game locale. public string ToLocaleString() { - return this.ToWorldDate().Localize(); - } - - /// Get the date as an instance of the game's WorldDate class. This is intended for passing to core game methods. - public WorldDate ToWorldDate() - { - return new WorldDate(this.Year, this.Season, this.Day); + return Utility.getDateStringFor(this.Day, this.SeasonIndex, this.Year); } /**** @@ -153,6 +147,19 @@ public override int GetHashCode() /**** ** Operators ****/ + /// Get the SDate equivalent to the given WorldDate. + /// A date returned from a core game property or method. + public static explicit operator SDate(WorldDate worldDate) + { + return new SDate(worldDate.DayOfMonth, worldDate.Season, worldDate.Year, allowDayZero: true); + } + + /// Get the SDate as an instance of the game's WorldDate class. This is intended for passing to core game methods. + public static explicit operator WorldDate(SDate date) + { + return new WorldDate(date.Year, date.Season, date.Day); + } + /// Get whether one date is equal to another. /// The base date to compare. /// The other date to compare. From 421bcfcd3e1dcd79c4ab9f9c44c300126ce01fe9 Mon Sep 17 00:00:00 2001 From: Kevin Daughtridge Date: Tue, 14 Apr 2020 00:31:47 -0700 Subject: [PATCH 12/21] SDateTests: cover new field and methods --- src/SMAPI.Tests/Utilities/SDateTests.cs | 102 ++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs index d25a101ad..3fca9b294 100644 --- a/src/SMAPI.Tests/Utilities/SDateTests.cs +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -5,6 +5,8 @@ using System.Text.RegularExpressions; using NUnit.Framework; using StardewModdingAPI.Utilities; +using StardewValley; +using LC = StardewValley.LocalizedContentManager.LanguageCode; namespace SMAPI.Tests.Utilities { @@ -81,6 +83,21 @@ public void Constructor_RejectsInvalidValues(int day, string season, int year) Assert.Throws(() => _ = new SDate(day, season, year), "Constructing the invalid date didn't throw the expected exception."); } + /**** + ** SeasonIndex + ****/ + [Test(Description = "Assert the numeric index of the season.")] + [TestCase("01 spring Y1", ExpectedResult = 0)] + [TestCase("02 summer Y1", ExpectedResult = 1)] + [TestCase("28 fall Y1", ExpectedResult = 2)] + [TestCase("01 winter Y1", ExpectedResult = 3)] + [TestCase("01 winter Y2", ExpectedResult = 3)] + public int SeasonIndex(string dateStr) + { + // act + return this.GetDate(dateStr).SeasonIndex; + } + /**** ** DayOfWeek ****/ @@ -147,6 +164,58 @@ public string ToString(string dateStr) return this.GetDate(dateStr).ToString(); } + /**** + ** ToLocaleString + ****/ + // TODO: Provide an appropriate XNA/MonoGame context to run this in, or else remove the test. + // [Test(Description = "Assert that ToLocaleString returns the expected string in various locales.")] + // [TestCase("14 spring Y1", LC.en, ExpectedResult = "Day 14 of Spring, Year 1")] + // [TestCase("01 summer Y16", LC.en, ExpectedResult = "Day 1 of Summer, Year 16")] + // [TestCase("28 fall Y10", LC.en, ExpectedResult = "Day 28 of Fall, Year 10")] + // [TestCase("01 winter Y1", LC.en, ExpectedResult = "Day 1 of Winter, Year 1")] + // [TestCase("14 spring Y1", LC.es, ExpectedResult = "Día 14 de primavera, año 1")] + // [TestCase("01 summer Y16", LC.es, ExpectedResult = "Día 1 de verano, año 16")] + // [TestCase("28 fall Y10", LC.es, ExpectedResult = "Día 28 de otoño, año 10")] + // [TestCase("01 winter Y1", LC.es, ExpectedResult = "Día 1 de invierno, año 1")] + // public string ToLocaleString(string dateStr, LC langCode) + // { + // LC oldCode = LocalizedContentManager.CurrentLanguageCode; + // try + // { + // LocalizedContentManager.CurrentLanguageCode = langCode; + // return this.GetDate(dateStr).ToLocaleString(); + // } + // finally + // { + // LocalizedContentManager.CurrentLanguageCode = oldCode; + // } + // } + + /**** + ** FromDaysSinceStart + ****/ + [Test(Description = "Assert that FromDaysSinceStart returns the expected date.")] + [TestCase(1, ExpectedResult = "01 spring Y1")] + [TestCase(2, ExpectedResult = "02 spring Y1")] + [TestCase(28, ExpectedResult = "28 spring Y1")] + [TestCase(29, ExpectedResult = "01 summer Y1")] + [TestCase(141, ExpectedResult = "01 summer Y2")] + public string FromDaysSinceStart(int daysSinceStart) + { + // act + return SDate.FromDaysSinceStart(daysSinceStart).ToString(); + } + + [Test(Description = "Assert that FromDaysSinceStart throws an exception if the number of days is invalid.")] + [TestCase(-1)] // day < 0 + [TestCase(0)] // day == 0 + [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")] + public void FromDaysSinceStart_RejectsInvalidValues(int daysSinceStart) + { + // act & assert + Assert.Throws(() => _ = SDate.FromDaysSinceStart(daysSinceStart), "Passing the invalid number of days didn't throw the expected exception."); + } + /**** ** AddDays ****/ @@ -166,6 +235,17 @@ public string AddDays(string dateStr, int addDays) return this.GetDate(dateStr).AddDays(addDays).ToString(); } + [Test(Description = "Assert that AddDays throws an exception if the number of days is invalid.")] + [TestCase("01 spring Y1", -1)] + [TestCase("01 summer Y1", -29)] + [TestCase("01 spring Y2", -113)] + [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")] + public void AddDays_RejectsInvalidValues(string dateStr, int addDays) + { + // act & assert + Assert.Throws(() => _ = this.GetDate(dateStr).AddDays(addDays), "Passing the invalid number of days didn't throw the expected exception."); + } + /**** ** GetHashCode ****/ @@ -194,6 +274,28 @@ public void GetHashCode_ReturnsUniqueOrderedValue() } } + [Test(Description = "Assert that the SDate operator for WorldDates returns the corresponding SDate.")] + [TestCase(0, ExpectedResult = "01 spring Y1")] + [TestCase(1, ExpectedResult = "02 spring Y1")] + [TestCase(27, ExpectedResult = "28 spring Y1")] + [TestCase(28, ExpectedResult = "01 summer Y1")] + [TestCase(140, ExpectedResult = "01 summer Y2")] + public string Operators_SDate_WorldDate(int totalDays) + { + return ((SDate)new WorldDate { TotalDays = totalDays }).ToString(); + } + + [Test(Description = "Assert that the WorldDate operator returns the corresponding WorldDate.")] + [TestCase("01 spring Y1", ExpectedResult = 0)] + [TestCase("02 spring Y1", ExpectedResult = 1)] + [TestCase("28 spring Y1", ExpectedResult = 27)] + [TestCase("01 summer Y1", ExpectedResult = 28)] + [TestCase("01 summer Y2", ExpectedResult = 140)] + public int Operators_WorldDate(string dateStr) + { + return ((WorldDate)this.GetDate(dateStr)).TotalDays; + } + [Test(Description = "Assert that the == operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] [TestCase(Dates.Now, null, ExpectedResult = false)] [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] From 97821362daeaa3dd34e3728680760d44043825be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 15 Apr 2020 18:06:37 -0400 Subject: [PATCH 13/21] prevent object.loadDisplayName errors due to invalid/missing item data --- docs/release-notes.md | 1 + src/SMAPI/Framework/Patching/PatchHelper.cs | 34 ++++++++++++++++++ src/SMAPI/Patches/DialogueErrorPatch.cs | 9 ++--- src/SMAPI/Patches/EventErrorPatch.cs | 9 ++--- src/SMAPI/Patches/ObjectErrorPatch.cs | 39 +++++++++++++++++++++ src/SMAPI/Patches/ScheduleErrorPatch.cs | 9 ++--- 6 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 src/SMAPI/Framework/Patching/PatchHelper.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index d064f17fd..d08e74765 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming release * For players: * Added config option to disable console colors. + * SMAPI now prevents more errors/crashes due to invalid item data. * Updated compatibility list. * For the Console Commands mod: diff --git a/src/SMAPI/Framework/Patching/PatchHelper.cs b/src/SMAPI/Framework/Patching/PatchHelper.cs new file mode 100644 index 000000000..4cb436f0a --- /dev/null +++ b/src/SMAPI/Framework/Patching/PatchHelper.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Patching +{ + /// Provides generic methods for implementing Harmony patches. + internal class PatchHelper + { + /********* + ** Fields + *********/ + /// The interception keys currently being intercepted. + private static readonly HashSet InterceptingKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + + + /********* + ** Public methods + *********/ + /// Track a method that will be intercepted. + /// The intercept key. + /// Returns false if the method was already marked for interception, else true. + public static bool StartIntercept(string key) + { + return PatchHelper.InterceptingKeys.Add(key); + } + + /// Track a method as no longer being intercepted. + /// The intercept key. + public static void StopIntercept(string key) + { + PatchHelper.InterceptingKeys.Remove(key); + } + } +} diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs index 24f97259d..1e49826d7 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -24,9 +24,6 @@ internal class DialogueErrorPatch : IHarmonyPatch /// Simplifies access to private code. private static Reflector Reflection; - /// Whether the getter is currently being intercepted. - private static bool IsInterceptingCurrentDialogue; - /********* ** Accessors @@ -112,12 +109,12 @@ private static bool Before_Dialogue_Constructor(Dialogue __instance, string mast /// Returns whether to execute the original method. private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack __result, MethodInfo __originalMethod) { - if (DialogueErrorPatch.IsInterceptingCurrentDialogue) + const string key = nameof(Before_NPC_CurrentDialogue); + if (!PatchHelper.StartIntercept(key)) return true; try { - DialogueErrorPatch.IsInterceptingCurrentDialogue = true; __result = (Stack)__originalMethod.Invoke(__instance, new object[0]); return false; } @@ -129,7 +126,7 @@ private static bool Before_NPC_CurrentDialogue(NPC __instance, ref StackWrites messages to the console and log file on behalf of the game. private static IMonitor MonitorForGame; - /// Whether the method is currently being intercepted. - private static bool IsIntercepted; - /********* ** Accessors @@ -61,12 +58,12 @@ public void Apply(HarmonyInstance harmony) /// Returns whether to execute the original method. private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod) { - if (EventErrorPatch.IsIntercepted) + const string key = nameof(Before_GameLocation_CheckEventPrecondition); + if (!PatchHelper.StartIntercept(key)) return true; try { - EventErrorPatch.IsIntercepted = true; __result = (int)__originalMethod.Invoke(__instance, new object[] { precondition }); return false; } @@ -78,7 +75,7 @@ private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __in } finally { - EventErrorPatch.IsIntercepted = false; + PatchHelper.StopIntercept(key); } } } diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index d716b29bb..d3b8800a9 100644 --- a/src/SMAPI/Patches/ObjectErrorPatch.cs +++ b/src/SMAPI/Patches/ObjectErrorPatch.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using Harmony; using StardewModdingAPI.Framework.Patching; using StardewValley; @@ -33,6 +35,12 @@ public void Apply(HarmonyInstance harmony) prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_GetDescription)) ); + // object.getDisplayName + harmony.Patch( + original: AccessTools.Method(typeof(SObject), "loadDisplayName"), + prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_loadDisplayName)) + ); + // IClickableMenu.drawToolTip harmony.Patch( original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)), @@ -60,6 +68,37 @@ private static bool Before_Object_GetDescription(SObject __instance, ref string return true; } + /// The method to call instead of . + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod) + { + const string key = nameof(Before_Object_loadDisplayName); + if (!PatchHelper.StartIntercept(key)) + return true; + + try + { + __result = (string)__originalMethod.Invoke(__instance, new object[0]); + return false; + } + catch (TargetInvocationException ex) when (ex.InnerException is KeyNotFoundException) + { + __result = "???"; + return false; + } + catch + { + return true; + } + finally + { + PatchHelper.StopIntercept(key); + } + } + /// The method to call instead of . /// The instance being patched. /// The item for which to draw a tooltip. diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs index a23aa6452..799fcb400 100644 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -19,9 +19,6 @@ internal class ScheduleErrorPatch : IHarmonyPatch /// Writes messages to the console and log file on behalf of the game. private static IMonitor MonitorForGame; - /// Whether the target is currently being intercepted. - private static bool IsIntercepting; - /********* ** Accessors @@ -62,12 +59,12 @@ public void Apply(HarmonyInstance harmony) /// Returns whether to execute the original method. private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, MethodInfo __originalMethod) { - if (ScheduleErrorPatch.IsIntercepting) + const string key = nameof(Before_NPC_parseMasterSchedule); + if (!PatchHelper.StartIntercept(key)) return true; try { - ScheduleErrorPatch.IsIntercepting = true; __result = (Dictionary)__originalMethod.Invoke(__instance, new object[] { rawData }); return false; } @@ -79,7 +76,7 @@ private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instanc } finally { - ScheduleErrorPatch.IsIntercepting = false; + PatchHelper.StopIntercept(key); } } } From 3a247fa75c56f315d82ea55143e89d28d61c064c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 15 Apr 2020 19:20:53 -0400 Subject: [PATCH 14/21] tweak new code, update release notes --- docs/release-notes.md | 1 + src/SMAPI.Tests/Utilities/SDateTests.cs | 117 +++++++++++------------- src/SMAPI/Utilities/SDate.cs | 36 ++++---- 3 files changed, 74 insertions(+), 80 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index d08e74765..5bf8a803c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ * Fixed rare intermittent "CGI application encountered an error" errors. * For modders: + * Extended `SDate` with `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` fields/methods (thanks to kdau!). * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. * Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses. diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs index 3fca9b294..0461952e4 100644 --- a/src/SMAPI.Tests/Utilities/SDateTests.cs +++ b/src/SMAPI.Tests/Utilities/SDateTests.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using StardewModdingAPI.Utilities; using StardewValley; -using LC = StardewValley.LocalizedContentManager.LanguageCode; namespace SMAPI.Tests.Utilities { @@ -83,6 +82,46 @@ public void Constructor_RejectsInvalidValues(int day, string season, int year) Assert.Throws(() => _ = new SDate(day, season, year), "Constructing the invalid date didn't throw the expected exception."); } + /**** + ** FromDaysSinceStart + ****/ + [Test(Description = "Assert that FromDaysSinceStart returns the expected date.")] + [TestCase(1, ExpectedResult = "01 spring Y1")] + [TestCase(2, ExpectedResult = "02 spring Y1")] + [TestCase(28, ExpectedResult = "28 spring Y1")] + [TestCase(29, ExpectedResult = "01 summer Y1")] + [TestCase(141, ExpectedResult = "01 summer Y2")] + public string FromDaysSinceStart(int daysSinceStart) + { + // act + return SDate.FromDaysSinceStart(daysSinceStart).ToString(); + } + + [Test(Description = "Assert that FromDaysSinceStart throws an exception if the number of days is invalid.")] + [TestCase(-1)] // day < 0 + [TestCase(0)] // day == 0 + [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")] + public void FromDaysSinceStart_RejectsInvalidValues(int daysSinceStart) + { + // act & assert + Assert.Throws(() => _ = SDate.FromDaysSinceStart(daysSinceStart), "Passing the invalid number of days didn't throw the expected exception."); + } + + /**** + ** From + ****/ + [Test(Description = "Assert that SDate.From constructs the correct instance for a given date.")] + [TestCase(0, ExpectedResult = "01 spring Y1")] + [TestCase(1, ExpectedResult = "02 spring Y1")] + [TestCase(27, ExpectedResult = "28 spring Y1")] + [TestCase(28, ExpectedResult = "01 summer Y1")] + [TestCase(140, ExpectedResult = "01 summer Y2")] + public string From_WorldDate(int totalDays) + { + return SDate.From(new WorldDate { TotalDays = totalDays }).ToString(); + } + + /**** ** SeasonIndex ****/ @@ -98,6 +137,7 @@ public int SeasonIndex(string dateStr) return this.GetDate(dateStr).SeasonIndex; } + /**** ** DayOfWeek ****/ @@ -136,6 +176,7 @@ public DayOfWeek DayOfWeek(string dateStr) return this.GetDate(dateStr).DayOfWeek; } + /**** ** DaysSinceStart ****/ @@ -151,6 +192,7 @@ public int DaysSinceStart(string dateStr) return this.GetDate(dateStr).DaysSinceStart; } + /**** ** ToString ****/ @@ -164,57 +206,6 @@ public string ToString(string dateStr) return this.GetDate(dateStr).ToString(); } - /**** - ** ToLocaleString - ****/ - // TODO: Provide an appropriate XNA/MonoGame context to run this in, or else remove the test. - // [Test(Description = "Assert that ToLocaleString returns the expected string in various locales.")] - // [TestCase("14 spring Y1", LC.en, ExpectedResult = "Day 14 of Spring, Year 1")] - // [TestCase("01 summer Y16", LC.en, ExpectedResult = "Day 1 of Summer, Year 16")] - // [TestCase("28 fall Y10", LC.en, ExpectedResult = "Day 28 of Fall, Year 10")] - // [TestCase("01 winter Y1", LC.en, ExpectedResult = "Day 1 of Winter, Year 1")] - // [TestCase("14 spring Y1", LC.es, ExpectedResult = "Día 14 de primavera, año 1")] - // [TestCase("01 summer Y16", LC.es, ExpectedResult = "Día 1 de verano, año 16")] - // [TestCase("28 fall Y10", LC.es, ExpectedResult = "Día 28 de otoño, año 10")] - // [TestCase("01 winter Y1", LC.es, ExpectedResult = "Día 1 de invierno, año 1")] - // public string ToLocaleString(string dateStr, LC langCode) - // { - // LC oldCode = LocalizedContentManager.CurrentLanguageCode; - // try - // { - // LocalizedContentManager.CurrentLanguageCode = langCode; - // return this.GetDate(dateStr).ToLocaleString(); - // } - // finally - // { - // LocalizedContentManager.CurrentLanguageCode = oldCode; - // } - // } - - /**** - ** FromDaysSinceStart - ****/ - [Test(Description = "Assert that FromDaysSinceStart returns the expected date.")] - [TestCase(1, ExpectedResult = "01 spring Y1")] - [TestCase(2, ExpectedResult = "02 spring Y1")] - [TestCase(28, ExpectedResult = "28 spring Y1")] - [TestCase(29, ExpectedResult = "01 summer Y1")] - [TestCase(141, ExpectedResult = "01 summer Y2")] - public string FromDaysSinceStart(int daysSinceStart) - { - // act - return SDate.FromDaysSinceStart(daysSinceStart).ToString(); - } - - [Test(Description = "Assert that FromDaysSinceStart throws an exception if the number of days is invalid.")] - [TestCase(-1)] // day < 0 - [TestCase(0)] // day == 0 - [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")] - public void FromDaysSinceStart_RejectsInvalidValues(int daysSinceStart) - { - // act & assert - Assert.Throws(() => _ = SDate.FromDaysSinceStart(daysSinceStart), "Passing the invalid number of days didn't throw the expected exception."); - } /**** ** AddDays @@ -246,6 +237,7 @@ public void AddDays_RejectsInvalidValues(string dateStr, int addDays) Assert.Throws(() => _ = this.GetDate(dateStr).AddDays(addDays), "Passing the invalid number of days didn't throw the expected exception."); } + /**** ** GetHashCode ****/ @@ -274,28 +266,25 @@ public void GetHashCode_ReturnsUniqueOrderedValue() } } - [Test(Description = "Assert that the SDate operator for WorldDates returns the corresponding SDate.")] - [TestCase(0, ExpectedResult = "01 spring Y1")] - [TestCase(1, ExpectedResult = "02 spring Y1")] - [TestCase(27, ExpectedResult = "28 spring Y1")] - [TestCase(28, ExpectedResult = "01 summer Y1")] - [TestCase(140, ExpectedResult = "01 summer Y2")] - public string Operators_SDate_WorldDate(int totalDays) - { - return ((SDate)new WorldDate { TotalDays = totalDays }).ToString(); - } + /**** + ** ToWorldDate + ****/ [Test(Description = "Assert that the WorldDate operator returns the corresponding WorldDate.")] [TestCase("01 spring Y1", ExpectedResult = 0)] [TestCase("02 spring Y1", ExpectedResult = 1)] [TestCase("28 spring Y1", ExpectedResult = 27)] [TestCase("01 summer Y1", ExpectedResult = 28)] [TestCase("01 summer Y2", ExpectedResult = 140)] - public int Operators_WorldDate(string dateStr) + public int ToWorldDate(string dateStr) { - return ((WorldDate)this.GetDate(dateStr)).TotalDays; + return this.GetDate(dateStr).ToWorldDate().TotalDays; } + + /**** + ** Operators + ****/ [Test(Description = "Assert that the == operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")] [TestCase(Dates.Now, null, ExpectedResult = false)] [TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)] diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 8cb558916..369077145 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -29,7 +29,8 @@ public class SDate : IEquatable /// The season name. public string Season { get; } - /// The season index. + /// The index of the season (where 0 is spring, 1 is summer, 2 is fall, and 3 is winter). + /// This is used in some game calculations (e.g. seasonal game sprites) and methods (e.g. ). public int SeasonIndex { get; } /// The year. @@ -66,7 +67,7 @@ public static SDate Now() return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true); } - /// Get the date falling the given number of days after 0 spring Y1. + /// Get a date from the number of days after 0 spring Y1. /// The number of days since 0 spring Y1. public static SDate FromDaysSinceStart(int daysSinceStart) { @@ -80,6 +81,16 @@ public static SDate FromDaysSinceStart(int daysSinceStart) } } + /// Get a date from a game date instance. + /// The world date. + public static SDate From(WorldDate date) + { + if (date == null) + return null; + + return new SDate(date.DayOfMonth, date.Season, date.Year, allowDayZero: true); + } + /// Get a new date with the given number of days added. /// The number of days to add. /// Returns the resulting date. @@ -109,13 +120,19 @@ public SDate AddDays(int offset) return new SDate(day, this.Seasons[seasonIndex], year); } + /// Get a game date representation of the date. + public WorldDate ToWorldDate() + { + return new WorldDate(this.Year, this.Season, this.Day); + } + /// Get a string representation of the date. This is mainly intended for debugging or console messages. public override string ToString() { return $"{this.Day:00} {this.Season} Y{this.Year}"; } - /// Get a string representation of the date in the current game locale. + /// Get a translated string representation of the date in the current game locale. public string ToLocaleString() { return Utility.getDateStringFor(this.Day, this.SeasonIndex, this.Year); @@ -147,19 +164,6 @@ public override int GetHashCode() /**** ** Operators ****/ - /// Get the SDate equivalent to the given WorldDate. - /// A date returned from a core game property or method. - public static explicit operator SDate(WorldDate worldDate) - { - return new SDate(worldDate.DayOfMonth, worldDate.Season, worldDate.Year, allowDayZero: true); - } - - /// Get the SDate as an instance of the game's WorldDate class. This is intended for passing to core game methods. - public static explicit operator WorldDate(SDate date) - { - return new WorldDate(date.Year, date.Season, date.Day); - } - /// Get whether one date is equal to another. /// The base date to compare. /// The other date to compare. From de4d4e0bcb2d69622ef9f2e616648f4310047f5f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 15 Apr 2020 19:21:30 -0400 Subject: [PATCH 15/21] update unit test --- src/SMAPI.Tests/Core/ModResolverTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs index a9c88c601..45b3673bf 100644 --- a/src/SMAPI.Tests/Core/ModResolverTests.cs +++ b/src/SMAPI.Tests/Core/ModResolverTests.cs @@ -73,7 +73,7 @@ public void ReadBasicManifest_CanReadFile() [nameof(IManifest.Description)] = Sample.String(), [nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}", [nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll", - [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}-{Sample.String()}", + [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}.{Sample.Int()}-{Sample.String()}", [nameof(IManifest.Dependencies)] = new[] { originalDependency }, ["ExtraString"] = Sample.String(), ["ExtraInt"] = Sample.Int() From 841f85a74331a02bd45f3d40ea1b50e4bc9dd3eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 17 Apr 2020 17:21:34 -0400 Subject: [PATCH 16/21] use better short date translations --- docs/release-notes.md | 6 +++++- src/SMAPI/Framework/SCore.cs | 3 +++ src/SMAPI/Program.cs | 4 ++-- src/SMAPI/Utilities/SDate.cs | 25 ++++++++++++++++++++++--- src/SMAPI/i18n/de.json | 8 +++++++- src/SMAPI/i18n/default.json | 7 ++++++- src/SMAPI/i18n/es.json | 7 ++++++- src/SMAPI/i18n/fr.json | 7 ++++++- src/SMAPI/i18n/hu.json | 7 ++++++- src/SMAPI/i18n/it.json | 7 ++++++- src/SMAPI/i18n/ja.json | 7 ++++++- src/SMAPI/i18n/ko.json | 8 ++++++++ src/SMAPI/i18n/pt.json | 7 ++++++- src/SMAPI/i18n/ru.json | 7 ++++++- src/SMAPI/i18n/tr.json | 7 ++++++- src/SMAPI/i18n/zh.json | 7 ++++++- 16 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 src/SMAPI/i18n/ko.json diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bf8a803c..f9e8932de 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * Added config option to disable console colors. * SMAPI now prevents more errors/crashes due to invalid item data. * Updated compatibility list. + * Improved translations.¹ * For the Console Commands mod: * The date commands like `world_setday` now also set the `daysPlayed` stat, so in-game events/randomization match what you'd get if you played to that date normally (thanks to kdau!). @@ -15,10 +16,13 @@ * Fixed rare intermittent "CGI application encountered an error" errors. * For modders: - * Extended `SDate` with `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` fields/methods (thanks to kdau!). + * Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!). + * Added `SDate` translations taken from the Lookup Anything mod.¹ * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. * Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses. +¹ Date translations were taken from the Lookup Anything mod; thanks to FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Translations for Korean (partial), Hungarian, and Turkish were auto-generated based on the game translations. + ## 3.4.1 Released 24 March 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 50e6ea1ca..de9c955d9 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -32,6 +32,7 @@ using StardewModdingAPI.Toolkit.Framework.ModData; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; +using StardewModdingAPI.Utilities; using StardewValley; using Object = StardewValley.Object; using ThreadState = System.Threading.ThreadState; @@ -176,6 +177,8 @@ public SCore(string modsPath, bool writeToConsole) SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); + SDate.Translations = this.Translator; + // redirect direct console output if (this.MonitorForGame.WriteToConsole) this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message); diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index c26ae29a6..715c85530 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -143,8 +143,8 @@ private static void Start(string[] args) } // load SMAPI - using (SCore core = new SCore(modsPath, writeToConsole)) - core.RunInteractively(); + using SCore core = new SCore(modsPath, writeToConsole); + core.RunInteractively(); } /// Write an error directly to the console and exit. diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 369077145..4d4920abe 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using StardewModdingAPI.Framework; using StardewValley; namespace StardewModdingAPI.Utilities @@ -19,6 +20,9 @@ public class SDate : IEquatable /// The number of days in a season. private readonly int DaysInSeason = 28; + /// The core SMAPI translations. + internal static Translator Translations; + /********* ** Accessors @@ -126,16 +130,31 @@ public WorldDate ToWorldDate() return new WorldDate(this.Year, this.Season, this.Day); } - /// Get a string representation of the date. This is mainly intended for debugging or console messages. + /// Get an untranslated string representation of the date. This is mainly intended for debugging or console messages. public override string ToString() { return $"{this.Day:00} {this.Season} Y{this.Year}"; } /// Get a translated string representation of the date in the current game locale. - public string ToLocaleString() + /// Whether to get a string which includes the year number. + public string ToLocaleString(bool withYear = true) { - return Utility.getDateStringFor(this.Day, this.SeasonIndex, this.Year); + // get fallback translation from game + string fallback = Utility.getDateStringFor(this.Day, this.SeasonIndex, this.Year); + if (SDate.Translations == null) + return fallback; + + // get short format + string seasonName = Utility.getSeasonNameFromNumber(this.SeasonIndex); + return SDate.Translations + .Get(withYear ? "generic.date-with-year" : "generic.date", new + { + day = this.Day, + year = this.Year, + season = Utility.getSeasonNameFromNumber(this.SeasonIndex) + }) + .Default(fallback); } /**** diff --git a/src/SMAPI/i18n/de.json b/src/SMAPI/i18n/de.json index a8b3086fc..47655a637 100644 --- a/src/SMAPI/i18n/de.json +++ b/src/SMAPI/i18n/de.json @@ -1,3 +1,9 @@ { - "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen)." + // error messages + "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen).", + + // short date format for SDate + "generic.date": "{{season}} {{day}}", + "generic.date-with-year": "{{season}} {{day}} im Jahr {{year}}" + } diff --git a/src/SMAPI/i18n/default.json b/src/SMAPI/i18n/default.json index 5a3e4a6e8..ba46241b4 100644 --- a/src/SMAPI/i18n/default.json +++ b/src/SMAPI/i18n/default.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info)." + // error messages + "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info).", + + // short date format for SDate + "generic.date": "{{season}} {{day}}", + "generic.date-with-year": "{{season}} {{day}} in year {{year}}" } diff --git a/src/SMAPI/i18n/es.json b/src/SMAPI/i18n/es.json index f5a74dfeb..dc77c4b72 100644 --- a/src/SMAPI/i18n/es.json +++ b/src/SMAPI/i18n/es.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información)." + // error messages + "warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información).", + + // short date format for SDate + "generic.date": "{{season}} {{day}}", + "generic.date-with-year": "{{season}} {{day}} del año {{year}}" } diff --git a/src/SMAPI/i18n/fr.json b/src/SMAPI/i18n/fr.json index 6d0510259..3b3596ce1 100644 --- a/src/SMAPI/i18n/fr.json +++ b/src/SMAPI/i18n/fr.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations)." + // error messages + "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations).", + + // short date format for SDate + "generic.date": "{{day}} {{season}}", + "generic.date-with-year": "{{day}} {{season}} de l'année {{year}}" } diff --git a/src/SMAPI/i18n/hu.json b/src/SMAPI/i18n/hu.json index aa0c7546c..d89d446f3 100644 --- a/src/SMAPI/i18n/hu.json +++ b/src/SMAPI/i18n/hu.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon)." + // error messages + "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon).", + + // short date format for SDate + "generic.date": "{{season}} {{day}}", + "generic.date-with-year": "{{year}}. év {{season}} {{day}}" } diff --git a/src/SMAPI/i18n/it.json b/src/SMAPI/i18n/it.json index 43493018f..20c91b4fb 100644 --- a/src/SMAPI/i18n/it.json +++ b/src/SMAPI/i18n/it.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)." + // error messages + "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni).", + + // short date format for SDate + "generic.date": "{{day}} {{season}}", + "generic.date-with-year": "{{day}} {{season}} dell'anno {{year}}" } diff --git a/src/SMAPI/i18n/ja.json b/src/SMAPI/i18n/ja.json index 9bbc285ef..1c8d9f0e2 100644 --- a/src/SMAPI/i18n/ja.json +++ b/src/SMAPI/i18n/ja.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)" + // error messages + "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)", + + // short date format for SDate + "generic.date": "{{season}} {{day}}日", + "generic.date-with-year": "{{year}}年目 {{season}} {{day}}日" } diff --git a/src/SMAPI/i18n/ko.json b/src/SMAPI/i18n/ko.json new file mode 100644 index 000000000..6f60ad098 --- /dev/null +++ b/src/SMAPI/i18n/ko.json @@ -0,0 +1,8 @@ +{ + // error messages + "warn.invalid-content-removed": "충돌을 방지하기 위해 잘못된 컨텐츠가 제거되었습니다 (자세한 내용은 SMAPI 콘솔 참조).", + + // short date format for SDate + "generic.date": "{{season}} {{day}}", + "generic.date-with-year": "{{year}} 학년 {{season}} {{day}}" +} diff --git a/src/SMAPI/i18n/pt.json b/src/SMAPI/i18n/pt.json index 592736802..8678a4f78 100644 --- a/src/SMAPI/i18n/pt.json +++ b/src/SMAPI/i18n/pt.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações)." + // error messages + "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações).", + + // short date format for SDate + "generic.date": "{{season}} {{day}}", + "generic.date-with-year": "{{season}} {{day}} no ano {{year}}" } diff --git a/src/SMAPI/i18n/ru.json b/src/SMAPI/i18n/ru.json index a6a242fa7..d773485a0 100644 --- a/src/SMAPI/i18n/ru.json +++ b/src/SMAPI/i18n/ru.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)" + // error messages + "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)", + + // short date format for SDate + "generic.date": "{{season}}, {{day}}-е число", + "generic.date-with-year": "{{season}}, {{day}}-е число, {{year}}-й год" } diff --git a/src/SMAPI/i18n/tr.json b/src/SMAPI/i18n/tr.json index 34229f2b9..654e1acc6 100644 --- a/src/SMAPI/i18n/tr.json +++ b/src/SMAPI/i18n/tr.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut)." + // error messages + "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut).", + + // short date format for SDate + "generic.date": "{{day}} {{season}}", + "generic.date-with-year": "{{day}} {{season}} года {{year}}" } diff --git a/src/SMAPI/i18n/zh.json b/src/SMAPI/i18n/zh.json index 9c0e0c21c..be485a79c 100644 --- a/src/SMAPI/i18n/zh.json +++ b/src/SMAPI/i18n/zh.json @@ -1,3 +1,8 @@ { - "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)" + // error messages + "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)", + + // short date format for SDate + "generic.date": "{{season}}{{day}}日", + "generic.date-with-year": "第{{year}}年{{season}}{{day}}日" } From 06e2cb2e5936a0f33c24c14f118c54b25f0156b6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 17 Apr 2020 17:23:24 -0400 Subject: [PATCH 17/21] support lowercase season names in date translations --- src/SMAPI/Utilities/SDate.cs | 3 ++- src/SMAPI/i18n/de.json | 1 + src/SMAPI/i18n/default.json | 1 + src/SMAPI/i18n/es.json | 5 +++-- src/SMAPI/i18n/fr.json | 5 +++-- src/SMAPI/i18n/hu.json | 1 + src/SMAPI/i18n/it.json | 1 + src/SMAPI/i18n/ja.json | 1 + src/SMAPI/i18n/ko.json | 1 + src/SMAPI/i18n/pt.json | 1 + src/SMAPI/i18n/ru.json | 1 + src/SMAPI/i18n/tr.json | 1 + src/SMAPI/i18n/zh.json | 1 + 13 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 4d4920abe..03230334a 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -152,7 +152,8 @@ public string ToLocaleString(bool withYear = true) { day = this.Day, year = this.Year, - season = Utility.getSeasonNameFromNumber(this.SeasonIndex) + season = seasonName, + seasonLowercase = seasonName?.ToLower() }) .Default(fallback); } diff --git a/src/SMAPI/i18n/de.json b/src/SMAPI/i18n/de.json index 47655a637..a8cbd83b2 100644 --- a/src/SMAPI/i18n/de.json +++ b/src/SMAPI/i18n/de.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen).", // 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}} im Jahr {{year}}" diff --git a/src/SMAPI/i18n/default.json b/src/SMAPI/i18n/default.json index ba46241b4..7a3d3ed55 100644 --- a/src/SMAPI/i18n/default.json +++ b/src/SMAPI/i18n/default.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info).", // 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}} in year {{year}}" } diff --git a/src/SMAPI/i18n/es.json b/src/SMAPI/i18n/es.json index dc77c4b72..c98439911 100644 --- a/src/SMAPI/i18n/es.json +++ b/src/SMAPI/i18n/es.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información).", // short date format for SDate - "generic.date": "{{season}} {{day}}", - "generic.date-with-year": "{{season}} {{day}} del año {{year}}" + // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) + "generic.date": "{{seasonLowercase}} {{day}}", + "generic.date-with-year": "{{seasonLowercase}} {{day}} del año {{year}}" } diff --git a/src/SMAPI/i18n/fr.json b/src/SMAPI/i18n/fr.json index 3b3596ce1..5969aa207 100644 --- a/src/SMAPI/i18n/fr.json +++ b/src/SMAPI/i18n/fr.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations).", // short date format for SDate - "generic.date": "{{day}} {{season}}", - "generic.date-with-year": "{{day}} {{season}} de l'année {{year}}" + // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) + "generic.date": "{{day}} {{seasonLowercase}}", + "generic.date-with-year": "{{day}} {{seasonLowercase}} de l'année {{year}}" } diff --git a/src/SMAPI/i18n/hu.json b/src/SMAPI/i18n/hu.json index d89d446f3..785012f4d 100644 --- a/src/SMAPI/i18n/hu.json +++ b/src/SMAPI/i18n/hu.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon).", // 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": "{{year}}. év {{season}} {{day}}" } diff --git a/src/SMAPI/i18n/it.json b/src/SMAPI/i18n/it.json index 20c91b4fb..3b3351c34 100644 --- a/src/SMAPI/i18n/it.json +++ b/src/SMAPI/i18n/it.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni).", // short date format for SDate + // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{day}} {{season}}", "generic.date-with-year": "{{day}} {{season}} dell'anno {{year}}" } diff --git a/src/SMAPI/i18n/ja.json b/src/SMAPI/i18n/ja.json index 1c8d9f0e2..1f814bfa6 100644 --- a/src/SMAPI/i18n/ja.json +++ b/src/SMAPI/i18n/ja.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)", // 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": "{{year}}年目 {{season}} {{day}}日" } diff --git a/src/SMAPI/i18n/ko.json b/src/SMAPI/i18n/ko.json index 6f60ad098..d5bbffa46 100644 --- a/src/SMAPI/i18n/ko.json +++ b/src/SMAPI/i18n/ko.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "충돌을 방지하기 위해 잘못된 컨텐츠가 제거되었습니다 (자세한 내용은 SMAPI 콘솔 참조).", // 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": "{{year}} 학년 {{season}} {{day}}" } diff --git a/src/SMAPI/i18n/pt.json b/src/SMAPI/i18n/pt.json index 8678a4f78..e84609229 100644 --- a/src/SMAPI/i18n/pt.json +++ b/src/SMAPI/i18n/pt.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações).", // 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}} no ano {{year}}" } diff --git a/src/SMAPI/i18n/ru.json b/src/SMAPI/i18n/ru.json index d773485a0..002fdbf87 100644 --- a/src/SMAPI/i18n/ru.json +++ b/src/SMAPI/i18n/ru.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)", // 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}}-й год" } diff --git a/src/SMAPI/i18n/tr.json b/src/SMAPI/i18n/tr.json index 654e1acc6..2a6e83a13 100644 --- a/src/SMAPI/i18n/tr.json +++ b/src/SMAPI/i18n/tr.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut).", // short date format for SDate + // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2) "generic.date": "{{day}} {{season}}", "generic.date-with-year": "{{day}} {{season}} года {{year}}" } diff --git a/src/SMAPI/i18n/zh.json b/src/SMAPI/i18n/zh.json index be485a79c..cdbe3b747 100644 --- a/src/SMAPI/i18n/zh.json +++ b/src/SMAPI/i18n/zh.json @@ -3,6 +3,7 @@ "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)", // 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": "第{{year}}年{{season}}{{day}}日" } From 4fae0158edd2f809b145ccacf20f082c06cd4a3e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Apr 2020 17:49:25 -0400 Subject: [PATCH 18/21] add map patching API Migrated from the Content Patcher code. I'm the main author, with tile property merging based on contributions by hatrat. --- docs/release-notes.md | 3 +- .../Framework/Content/AssetDataForMap.cs | 186 ++++++++++++++++++ .../Framework/Content/AssetDataForObject.cs | 8 + src/SMAPI/IAssetData.cs | 6 +- src/SMAPI/IAssetDataForMap.cs | 18 ++ 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI/Framework/Content/AssetDataForMap.cs create mode 100644 src/SMAPI/IAssetDataForMap.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index f9e8932de..cde3a50d6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -16,12 +16,13 @@ * Fixed rare intermittent "CGI application encountered an error" errors. * For modders: + * Added map patching to the content API (via `asset.AsMap()`). * Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!). * Added `SDate` translations taken from the Lookup Anything mod.¹ * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. * Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses. -¹ Date translations were taken from the Lookup Anything mod; thanks to FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Translations for Korean (partial), Hungarian, and Turkish were auto-generated based on the game translations. +¹ Date format translations were taken from the Lookup Anything mod; thanks to FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Translations for Korean (partial), Hungarian, and Turkish were auto-generated based on the game translations. ## 3.4.1 Released 24 March 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs new file mode 100644 index 000000000..f66013baf --- /dev/null +++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework; +using StardewModdingAPI.Toolkit.Utilities; +using xTile; +using xTile.Layers; +using xTile.Tiles; + +namespace StardewModdingAPI.Framework.Content +{ + /// Encapsulates access and changes to image content being read from a data file. + internal class AssetDataForMap : AssetData, IAssetDataForMap + { + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content's locale code, if the content is localized. + /// The normalized asset name being read. + /// The content data being read. + /// Normalizes an asset key to match the cache key. + /// A callback to invoke when the data is replaced (if any). + public AssetDataForMap(string locale, string assetName, Map data, Func getNormalizedPath, Action onDataReplaced) + : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { } + + /// Copy layers, tiles, and tilesheets from another map onto the asset. + /// The map from which to copy. + /// The tile area within the source map to copy, or null for the entire source map size. This must be within the bounds of the map. + /// The tile area within the target map to overwrite, or null to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map. + /// Derived from with a few changes: + /// - can be applied directly to the maps when loading, before the location is created; + /// - added support for source/target areas; + /// - added disambiguation if source has a modified version of the same tilesheet, instead of copying tiles into the target tilesheet; + /// - changed to always overwrite tiles within the target area (to avoid edge cases where some tiles are only partly applied); + /// - fixed copying tilesheets (avoid "The specified TileSheet was not created for use with this map" error); + /// - fixed tilesheets not added at the end (via z_ prefix), which can cause crashes in game code which depends on hardcoded tilesheet indexes; + /// - fixed issue where different tilesheets are linked by ID. + /// + public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null) + { + var target = this.Data; + + // get areas + { + Rectangle sourceBounds = this.GetMapArea(source); + Rectangle targetBounds = this.GetMapArea(target); + sourceArea ??= new Rectangle(0, 0, sourceBounds.Width, sourceBounds.Height); + targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, targetBounds.Width), Math.Min(sourceArea.Value.Height, targetBounds.Height)); + + // validate + if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > sourceBounds.Width || sourceArea.Value.Bottom > sourceBounds.Height) + throw new ArgumentOutOfRangeException(nameof(sourceArea), $"The source area ({sourceArea}) is outside the bounds of the source map ({sourceBounds})."); + if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > targetBounds.Width || targetArea.Value.Bottom > targetBounds.Height) + throw new ArgumentOutOfRangeException(nameof(targetArea), $"The target area ({targetArea}) is outside the bounds of the target map ({targetBounds})."); + if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) + throw new InvalidOperationException($"The source area ({sourceArea}) and target area ({targetArea}) must be the same size."); + } + + // apply tilesheets + IDictionary tilesheetMap = new Dictionary(); + foreach (TileSheet sourceSheet in source.TileSheets) + { + // copy tilesheets + TileSheet targetSheet = target.GetTileSheet(sourceSheet.Id); + if (targetSheet == null || this.NormalizeTilesheetPathForComparison(targetSheet.ImageSource) != this.NormalizeTilesheetPathForComparison(sourceSheet.ImageSource)) + { + // change ID if needed so new tilesheets are added after vanilla ones (to avoid errors in hardcoded game logic) + string id = sourceSheet.Id; + if (!id.StartsWith("z_", StringComparison.InvariantCultureIgnoreCase)) + id = $"z_{id}"; + + // change ID if it conflicts with an existing tilesheet + if (target.GetTileSheet(id) != null) + { + int disambiguator = Enumerable.Range(2, int.MaxValue - 1).First(p => target.GetTileSheet($"{id}_{p}") == null); + id = $"{id}_{disambiguator}"; + } + + // add tilesheet + targetSheet = new TileSheet(id, target, sourceSheet.ImageSource, sourceSheet.SheetSize, sourceSheet.TileSize); + for (int i = 0, tileCount = sourceSheet.TileCount; i < tileCount; ++i) + targetSheet.TileIndexProperties[i].CopyFrom(sourceSheet.TileIndexProperties[i]); + target.AddTileSheet(targetSheet); + } + + tilesheetMap[sourceSheet] = targetSheet; + } + + // get layer map + IDictionary layerMap = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id)); + + // apply tiles + for (int x = 0; x < sourceArea.Value.Width; x++) + { + for (int y = 0; y < sourceArea.Value.Height; y++) + { + // calculate tile positions + Point sourcePos = new Point(sourceArea.Value.X + x, sourceArea.Value.Y + y); + Point targetPos = new Point(targetArea.Value.X + x, targetArea.Value.Y + y); + + // merge layers + foreach (Layer sourceLayer in source.Layers) + { + // get layer + Layer targetLayer = layerMap[sourceLayer]; + if (targetLayer == null) + { + target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize)); + layerMap[sourceLayer] = target.GetLayer(sourceLayer.Id); + } + + // copy layer properties + targetLayer.Properties.CopyFrom(sourceLayer.Properties); + + // copy tiles + Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y]; + Tile targetTile; + switch (sourceTile) + { + case StaticTile _: + targetTile = new StaticTile(targetLayer, tilesheetMap[sourceTile.TileSheet], sourceTile.BlendMode, sourceTile.TileIndex); + break; + + case AnimatedTile animatedTile: + { + StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length]; + for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame) + { + StaticTile frameTile = animatedTile.TileFrames[frame]; + tileFrames[frame] = new StaticTile(targetLayer, tilesheetMap[frameTile.TileSheet], frameTile.BlendMode, frameTile.TileIndex); + } + targetTile = new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval); + } + break; + + default: // null or unhandled type + targetTile = null; + break; + } + targetTile?.Properties.CopyFrom(sourceTile.Properties); + targetLayer.Tiles[targetPos.X, targetPos.Y] = targetTile; + } + } + } + } + + + /********* + ** Private methods + *********/ + /// Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path. + /// The path to normalize. + private string NormalizeTilesheetPathForComparison(string path) + { + if (string.IsNullOrWhiteSpace(path)) + return string.Empty; + + path = PathUtilities.NormalizePathSeparators(path.Trim()); + if (path.StartsWith($"Maps{PathUtilities.PreferredPathSeparator}", StringComparison.OrdinalIgnoreCase)) + path = path.Substring($"Maps{PathUtilities.PreferredPathSeparator}".Length); + if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + path = path.Substring(0, path.Length - 4); + + return path; + } + + /// Get a rectangle which encompasses all layers for a map. + /// The map to check. + private Rectangle GetMapArea(Map map) + { + // get max map size + int maxWidth = 0; + int maxHeight = 0; + foreach (Layer layer in map.Layers) + { + if (layer.LayerWidth > maxWidth) + maxWidth = layer.LayerWidth; + if (layer.LayerHeight > maxHeight) + maxHeight = layer.LayerHeight; + } + + return new Rectangle(0, 0, maxWidth, maxHeight); + } + } +} diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs index 4dbc988c2..f00ba1248 100644 --- a/src/SMAPI/Framework/Content/AssetDataForObject.cs +++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using Microsoft.Xna.Framework.Graphics; +using xTile; namespace StardewModdingAPI.Framework.Content { @@ -41,6 +42,13 @@ public IAssetDataForImage AsImage() return new AssetDataForImage(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); } + /// Get a helper to manipulate the data as a map. + /// The content being read isn't a map. + public IAssetDataForMap AsMap() + { + return new AssetDataForMap(this.Locale, this.AssetName, this.GetData(), this.GetNormalizedPath, this.ReplaceWith); + } + /// Get the data as a given type. /// The expected data type. /// The data can't be converted to . diff --git a/src/SMAPI/IAssetData.cs b/src/SMAPI/IAssetData.cs index c30211441..8df59e53d 100644 --- a/src/SMAPI/IAssetData.cs +++ b/src/SMAPI/IAssetData.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace StardewModdingAPI { @@ -39,6 +39,10 @@ public interface IAssetData : IAssetData /// The content being read isn't an image. IAssetDataForImage AsImage(); + /// Get a helper to manipulate the data as a map. + /// The content being read isn't a map. + IAssetDataForMap AsMap(); + /// Get the data as a given type. /// The expected data type. /// The data can't be converted to . diff --git a/src/SMAPI/IAssetDataForMap.cs b/src/SMAPI/IAssetDataForMap.cs new file mode 100644 index 000000000..769ca07cb --- /dev/null +++ b/src/SMAPI/IAssetDataForMap.cs @@ -0,0 +1,18 @@ +using Microsoft.Xna.Framework; +using xTile; + +namespace StardewModdingAPI +{ + /// Encapsulates access and changes to map content being read from a data file. + public interface IAssetDataForMap : IAssetData + { + /********* + ** Public methods + *********/ + /// Copy layers, tiles, and tilesheets from another map onto the asset. + /// The map from which to copy. + /// The tile area within the source map to copy, or null for the entire source map size. This must be within the bounds of the map. + /// The tile area within the target map to overwrite, or null to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map. + public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null); + } +} From beccea7efdd61d6417217eb3f40ca452373ac3d6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Apr 2020 17:53:58 -0400 Subject: [PATCH 19/21] add support for getting a patch helper for arbitrary data --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 14 ++++++++++++++ src/SMAPI/IContentHelper.cs | 6 ++++++ 3 files changed, 21 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index cde3a50d6..1eac1d625 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ * For modders: * Added map patching to the content API (via `asset.AsMap()`). + * Added support for using patch helpers (e.g. for image/map patching) with arbitrary data (via `helper.Content.GetPatchHelper`). * Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!). * Added `SDate` translations taken from the Lookup Anything mod.¹ * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index e9b708459..23e45fd18 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; +using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; using StardewModdingAPI.Framework.Exceptions; using StardewValley; @@ -164,6 +165,19 @@ public bool InvalidateCache(Func predicate) return this.ContentCore.InvalidateCache(predicate).Any(); } + /// Get a patch helper for arbitrary data. + /// The data type. + /// The asset data. + /// The asset name. This is only used for tracking purposes and has no effect on the patch helper. + public IAssetData GetPatchHelper(T data, string assetName = null) + { + if (data == null) + throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value."); + + assetName ??= $"temp/{Guid.NewGuid():N}"; + return new AssetDataForObject(this.CurrentLocale, assetName, data, this.NormalizeAssetName); + } + /********* ** Private methods diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs index dd7eb758a..2936ecfb0 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -64,5 +64,11 @@ public interface IContentHelper : IModLinked /// A predicate matching the assets to invalidate. /// Returns whether any cache entries were invalidated. bool InvalidateCache(Func predicate); + + /// Get a patch helper for arbitrary data. + /// The data type. + /// The asset data. + /// The asset name. This is only used for tracking purposes and has no effect on the patch helper. + IAssetData GetPatchHelper(T data, string assetName = null); } } From cf7bba5453f87e666759c70a892f76f7dae44dc2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Apr 2020 20:05:15 -0400 Subject: [PATCH 20/21] fix asset propagation for maps loaded through a temporary content manager --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 27 ++++++++++++++++++----- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1eac1d625..c708133ac 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -20,6 +20,7 @@ * Added support for using patch helpers (e.g. for image/map patching) with arbitrary data (via `helper.Content.GetPatchHelper`). * Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!). * Added `SDate` translations taken from the Lookup Anything mod.¹ + * Fixed asset propagation for certain maps loaded through temporarily content managers (notably the farmhouse and town). * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. * Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 0b1ccc3c3..47ef30d4f 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -14,6 +14,7 @@ using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; +using xTile; namespace StardewModdingAPI.Framework { @@ -228,16 +229,32 @@ public IEnumerable InvalidateCache(Func predicate, boo public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { // invalidate cache & track removed assets - IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + IDictionary removedAssets = new Dictionary(StringComparer.InvariantCultureIgnoreCase); this.ContentManagerLock.InReadLock(() => { + // cached assets foreach (IContentManager contentManager in this.ContentManagers) { foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) { - if (!removedAssets.TryGetValue(entry.Key, out ISet assets)) - removedAssets[entry.Key] = assets = new HashSet(new ObjectReferenceComparer()); - assets.Add(entry.Value); + if (!removedAssets.TryGetValue(entry.Key, out Type type)) + removedAssets[entry.Key] = entry.Value.GetType(); + } + } + + // special case: maps may be loaded through a temporary content manager that's removed while the map is still in use. + // This notably affects the town and farmhouse maps. + if (Game1.locations != null) + { + foreach (GameLocation location in Game1.locations) + { + if (location.map == null || string.IsNullOrWhiteSpace(location.mapPath.Value)) + continue; + + // get map path + string mapPath = this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value); + if (!removedAssets.ContainsKey(mapPath) && predicate(mapPath, typeof(Map))) + removedAssets[mapPath] = typeof(Map); } } }); @@ -245,7 +262,7 @@ public IEnumerable InvalidateCache(Func predicate, b // reload core game assets if (removedAssets.Any()) { - IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager + IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); } else From f44151dbb47b82250955be7c25145d1774bec705 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 27 Apr 2020 16:30:20 -0400 Subject: [PATCH 21/21] prepare for release, tweak readme --- build/common.targets | 2 +- docs/README.md | 16 ++++++---------- docs/release-notes.md | 18 ++++++++++-------- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 22 insertions(+), 24 deletions(-) diff --git a/build/common.targets b/build/common.targets index 1e14a86c8..41bea8af4 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ - 3.4.1 + 3.5.0 SMAPI $(AssemblySearchPaths);{GAC} diff --git a/docs/README.md b/docs/README.md index 546ee6b3a..4726c1908 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ **SMAPI** is an open-source modding framework and API for [Stardew Valley](https://stardewvalley.net/) that lets you play the game with mods. It's safely installed alongside the game's executable, and -doesn't change any of your game files. It serves eight main purposes: +doesn't change any of your game files. It serves seven main purposes: 1. **Load mods into the game.** _SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't @@ -10,14 +10,10 @@ doesn't change any of your game files. It serves eight main purposes: _SMAPI provides APIs and events which let mods interact with the game in ways they otherwise couldn't._ -3. **Rewrite mods for crossplatform compatibility.** +3. **Rewrite mods for compatibility.** _SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows without the mods needing to handle differences between the Linux/Mac and Windows versions of the - game._ - -4. **Rewrite mods to update them.** - _SMAPI detects when a mod accesses part of the game that changed in a game update which affects - many mods, and rewrites the mod so it's compatible._ + game. In some cases it also rewrites code broken by a game update so the mod doesn't break._ 5. **Intercept errors and automatically fix saves.** _SMAPI intercepts errors, shows the error info in the SMAPI console, and in most cases @@ -37,8 +33,8 @@ doesn't change any of your game files. It serves eight main purposes: they cause problems._ 8. **Back up your save files.** - _SMAPI automatically creates a daily backup of your saves and keeps ten backups, in case - something goes wrong. (Via the bundled SaveBackup mod.)_ + _SMAPI automatically creates a daily backup of your saves and keeps ten backups (via the bundled + Save Backup mod), in case something goes wrong._ ## Documentation Have questions? Come [ask the community](https://smapi.io/community) to get help from SMAPI @@ -69,7 +65,7 @@ German | ✓ [fully translated](../src/SMAPI/i18n/de.json) Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json) Italian | ✓ [fully translated](../src/SMAPI/i18n/it.json) Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json) -Korean | ❑ not translated +Korean | ✓ [fully translated](../src/SMAPI/i18n/ko.json) Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json) Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json) Spanish | ✓ [fully translated](../src/SMAPI/i18n/es.json) diff --git a/docs/release-notes.md b/docs/release-notes.md index c708133ac..97aabd379 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,30 +1,32 @@ ← [README](README.md) # Release notes -## Upcoming release +## 3.5 +Released 27 April 2020 for Stardew Valley 1.4.1 or later. + * For players: - * Added config option to disable console colors. - * SMAPI now prevents more errors/crashes due to invalid item data. + * SMAPI now prevents more game errors due to broken items, so you no longer need save editing to remove them. + * Added option to disable console colors. * Updated compatibility list. * Improved translations.¹ * For the Console Commands mod: - * The date commands like `world_setday` now also set the `daysPlayed` stat, so in-game events/randomization match what you'd get if you played to that date normally (thanks to kdau!). + * Commands like `world_setday` now also affect the 'days played' stat, so in-game events/randomization match what you'd get if you played to that date normally (thanks to kdau!). * For the web UI: - * Updated the JSON validator and Content Patcher schema for Content Patcher 1.13. + * Updated the JSON validator/schema for Content Patcher 1.13. * Fixed rare intermittent "CGI application encountered an error" errors. * For modders: * Added map patching to the content API (via `asset.AsMap()`). - * Added support for using patch helpers (e.g. for image/map patching) with arbitrary data (via `helper.Content.GetPatchHelper`). + * Added support for using patch helpers with arbitrary data (via `helper.Content.GetPatchHelper`). * Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!). * Added `SDate` translations taken from the Lookup Anything mod.¹ - * Fixed asset propagation for certain maps loaded through temporarily content managers (notably the farmhouse and town). + * Fixed asset propagation for certain maps loaded through temporary content managers. This notably fixes unreliable patches to the farmhouse and town maps. * Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules. * Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses. -¹ Date format translations were taken from the Lookup Anything mod; thanks to FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Translations for Korean (partial), Hungarian, and Turkish were auto-generated based on the game translations. +¹ Date format translations were taken from the Lookup Anything mod; thanks to translators FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Some translations for Korean, Hungarian, and Turkish were derived from the game translations. ## 3.4.1 Released 24 March 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index a55d168f1..908d4f658 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.4.1", + "Version": "3.5.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.4.1" + "MinimumApiVersion": "3.5.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 5bf35b5c5..cd42459e7 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.4.1", + "Version": "3.5.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.4.1" + "MinimumApiVersion": "3.5.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 128e23bd2..a898fccd3 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -20,7 +20,7 @@ public static class Constants ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.4.1"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.5.0"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1");