From 3e54ac88579ba202d9bac34ca88c1dcb25f90a64 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 29 Mar 2020 12:08:56 -0400 Subject: fix path segmenting on Linux/Mac in asset propagation --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 83e553ff..30b96c1d 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -5,6 +5,7 @@ using System.Linq; 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 @@ namespace StardewModdingAPI.Metadata /// 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). -- cgit From 96ec4de7275ae4e0ffc92ca8058c5e04b8ddd20d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 1 Apr 2020 20:09:44 -0400 Subject: fix marriage dialogue left in invalid state after dialogue propagation --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 30b96c1d..0a14086b 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -892,11 +892,13 @@ namespace StardewModdingAPI.Metadata // 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); } -- cgit From 5f73d47fb9dfe7ac2733a0a5fe57cf96639594f9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Apr 2020 12:35:34 -0400 Subject: add config option to disable console colors (#707) --- src/SMAPI/Framework/Monitor.cs | 4 ++-- src/SMAPI/SMAPI.config.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index f630c7fe..44eeabe6 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -15,8 +15,8 @@ namespace StardewModdingAPI.Framework /// 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 57b4f885..a426b0ef 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. * -- cgit From 49c2ee517d3276b4547bb25ceda3ebf5e9707887 Mon Sep 17 00:00:00 2001 From: Kevin Daughtridge Date: Mon, 13 Apr 2020 15:42:00 -0700 Subject: SDate: Add WorldDate conversions and features - SeasonIndex - FromWorldDate() - FromDaysSinceStart() - ToLocaleString() - ToWorldDate() --- src/SMAPI/Utilities/SDate.cs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 0ab37aa0..301ea9d2 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -29,6 +29,9 @@ namespace StardewModdingAPI.Utilities /// The season name. public string Season { get; } + /// The season index. + public int SeasonIndex { get; } + /// The year. public int Year { get; } @@ -63,6 +66,20 @@ namespace StardewModdingAPI.Utilities 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 @@ namespace StardewModdingAPI.Utilities 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 @@ namespace StardewModdingAPI.Utilities // 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); -- cgit From 7a60dc4ee9d32831f55fe066c20729ca8e9dc8a1 Mon Sep 17 00:00:00 2001 From: Kevin Daughtridge Date: Mon, 13 Apr 2020 23:29:56 -0700 Subject: 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(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 301ea9d2..8cb55891 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -66,18 +66,18 @@ namespace StardewModdingAPI.Utilities 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 @@ namespace StardewModdingAPI.Utilities /// 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 @@ namespace StardewModdingAPI.Utilities /**** ** 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. -- cgit From 97821362daeaa3dd34e3728680760d44043825be Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 15 Apr 2020 18:06:37 -0400 Subject: prevent object.loadDisplayName errors due to invalid/missing item data --- 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 +++---- 5 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 src/SMAPI/Framework/Patching/PatchHelper.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Patching/PatchHelper.cs b/src/SMAPI/Framework/Patching/PatchHelper.cs new file mode 100644 index 00000000..4cb436f0 --- /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 24f97259..1e49826d 100644 --- a/src/SMAPI/Patches/DialogueErrorPatch.cs +++ b/src/SMAPI/Patches/DialogueErrorPatch.cs @@ -24,9 +24,6 @@ namespace StardewModdingAPI.Patches /// 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 @@ namespace StardewModdingAPI.Patches /// 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 @@ namespace StardewModdingAPI.Patches } finally { - DialogueErrorPatch.IsInterceptingCurrentDialogue = false; + PatchHelper.StopIntercept(key); } } } diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs index 1dc7e8c3..504d1d2e 100644 --- a/src/SMAPI/Patches/EventErrorPatch.cs +++ b/src/SMAPI/Patches/EventErrorPatch.cs @@ -18,9 +18,6 @@ namespace StardewModdingAPI.Patches /// Writes 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 @@ namespace StardewModdingAPI.Patches /// 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 @@ namespace StardewModdingAPI.Patches } finally { - EventErrorPatch.IsIntercepted = false; + PatchHelper.StopIntercept(key); } } } diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs index d716b29b..d3b8800a 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 @@ namespace StardewModdingAPI.Patches 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 @@ namespace StardewModdingAPI.Patches 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 a23aa645..799fcb40 100644 --- a/src/SMAPI/Patches/ScheduleErrorPatch.cs +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -19,9 +19,6 @@ namespace StardewModdingAPI.Patches /// 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 @@ namespace StardewModdingAPI.Patches /// 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 @@ namespace StardewModdingAPI.Patches } finally { - ScheduleErrorPatch.IsIntercepting = false; + PatchHelper.StopIntercept(key); } } } -- cgit From 3a247fa75c56f315d82ea55143e89d28d61c064c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 15 Apr 2020 19:20:53 -0400 Subject: tweak new code, update release notes --- src/SMAPI/Utilities/SDate.cs | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 8cb55891..36907714 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -29,7 +29,8 @@ namespace StardewModdingAPI.Utilities /// 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 @@ namespace StardewModdingAPI.Utilities 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 @@ namespace StardewModdingAPI.Utilities } } + /// 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 @@ namespace StardewModdingAPI.Utilities 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 @@ namespace StardewModdingAPI.Utilities /**** ** 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. -- cgit From 841f85a74331a02bd45f3d40ea1b50e4bc9dd3eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 17 Apr 2020 17:21:34 -0400 Subject: use better short date translations --- 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 ++++++- 15 files changed, 102 insertions(+), 16 deletions(-) create mode 100644 src/SMAPI/i18n/ko.json (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 50e6ea1c..de9c955d 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -32,6 +32,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; 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 @@ namespace StardewModdingAPI.Framework 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 c26ae29a..715c8553 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -143,8 +143,8 @@ namespace StardewModdingAPI } // 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 36907714..4d4920ab 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 @@ namespace StardewModdingAPI.Utilities /// 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 @@ namespace StardewModdingAPI.Utilities 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 a8b3086f..47655a63 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 5a3e4a6e..ba46241b 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 f5a74dfe..dc77c4b7 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 6d051025..3b3596ce 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 aa0c7546..d89d446f 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 43493018..20c91b4f 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 9bbc285e..1c8d9f0e 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 00000000..6f60ad09 --- /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 59273680..8678a4f7 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 a6a242fa..d773485a 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 34229f2b..654e1acc 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 9c0e0c21..be485a79 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}}日" } -- cgit From 06e2cb2e5936a0f33c24c14f118c54b25f0156b6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 17 Apr 2020 17:23:24 -0400 Subject: 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(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs index 4d4920ab..03230334 100644 --- a/src/SMAPI/Utilities/SDate.cs +++ b/src/SMAPI/Utilities/SDate.cs @@ -152,7 +152,8 @@ namespace StardewModdingAPI.Utilities { 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 47655a63..a8cbd83b 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 ba46241b..7a3d3ed5 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 dc77c4b7..c9843991 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 3b3596ce..5969aa20 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 d89d446f..785012f4 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 20c91b4f..3b3351c3 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 1c8d9f0e..1f814bfa 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 6f60ad09..d5bbffa4 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 8678a4f7..e8460922 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 d773485a..002fdbf8 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 654e1acc..2a6e83a1 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 be485a79..cdbe3b74 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}}日" } -- cgit From 4fae0158edd2f809b145ccacf20f082c06cd4a3e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Apr 2020 17:49:25 -0400 Subject: add map patching API Migrated from the Content Patcher code. I'm the main author, with tile property merging based on contributions by hatrat. --- src/SMAPI/Framework/Content/AssetDataForMap.cs | 186 ++++++++++++++++++++++ src/SMAPI/Framework/Content/AssetDataForObject.cs | 8 + src/SMAPI/IAssetData.cs | 6 +- src/SMAPI/IAssetDataForMap.cs | 18 +++ 4 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/Content/AssetDataForMap.cs create mode 100644 src/SMAPI/IAssetDataForMap.cs (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs new file mode 100644 index 00000000..f66013ba --- /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 4dbc988c..f00ba124 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 @@ namespace StardewModdingAPI.Framework.Content 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 c3021144..8df59e53 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 @@ namespace StardewModdingAPI /// 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 00000000..769ca07c --- /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); + } +} -- cgit From beccea7efdd61d6417217eb3f40ca452373ac3d6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 24 Apr 2020 17:53:58 -0400 Subject: add support for getting a patch helper for arbitrary data --- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 14 ++++++++++++++ src/SMAPI/IContentHelper.cs | 6 ++++++ 2 files changed, 20 insertions(+) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs index e9b70845..23e45fd1 100644 --- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs @@ -7,6 +7,7 @@ using System.IO; 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 @@ namespace StardewModdingAPI.Framework.ModHelpers 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 dd7eb758..2936ecfb 100644 --- a/src/SMAPI/IContentHelper.cs +++ b/src/SMAPI/IContentHelper.cs @@ -64,5 +64,11 @@ namespace StardewModdingAPI /// 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); } } -- cgit From cf7bba5453f87e666759c70a892f76f7dae44dc2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Apr 2020 20:05:15 -04