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 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') 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; -- 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 --- 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 (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index d064f17f..d08e7476 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 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 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 --- 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 (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5bf8a803..f9e8932d 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 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 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. --- docs/release-notes.md | 3 +- src/SMAPI/Framework/Content/AssetDataForMap.cs | 186 ++++++++++++++++++++++ src/SMAPI/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 (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index f9e8932d..cde3a50d 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 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 --- docs/release-notes.md | 1 + src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 14 ++++++++++++++ src/SMAPI/IContentHelper.cs | 6 ++++++ 3 files changed, 21 insertions(+) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index cde3a50d..1eac1d62 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 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 -0400 Subject: 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(-) (limited to 'src/SMAPI/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index 1eac1d62..c708133a 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 0b1ccc3c..47ef30d4 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -14,6 +14,7 @@ using StardewModdingAPI.Metadata; using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; +using xTile; namespace StardewModdingAPI.Framework { @@ -228,16 +229,32 @@ namespace StardewModdingAPI.Framework 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 @@ namespace StardewModdingAPI.Framework // 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 -- cgit