From 7b041c2b444636b07a3316ded449ecca2d94d6cf Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 16 Jul 2025 23:10:53 -0400 Subject: [PATCH 01/43] fix 'AddNpcWarps' field in Content Patcher schema --- docs/release-notes.md | 4 ++++ src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 9d33cc97..21df9f52 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,10 @@ ← [README](README.md) # Release notes +## Upcoming release +* For the web UI: + * Fixed Content Patcher schema requiring `AddNPCWarps` instead of `AddNpcWarps`. + ## 4.3.2 Released 14 July 2025 for Stardew Valley 1.6.14 or later. See [4.3 release highlights](https://www.patreon.com/posts/133992196). diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index d0f6dafe..5993ebac 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -572,7 +572,7 @@ "When", "AddWarps", - "AddNPCWarps", + "AddNpcWarps", "FromArea", "MapProperties", "MapTiles", From 75d927a922c440a81d8bad70142c69e5a372580b Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 20 Jul 2025 22:57:56 -0400 Subject: [PATCH 02/43] fix item console commands not including some newer juice items --- docs/release-notes.md | 3 +++ .../Framework/ItemRepository.cs | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 21df9f52..8505791f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming release +* For players: + * Fixed `player_add` and `list_items` console commands not including some newer juice items. + * For the web UI: * Fixed Content Patcher schema requiring `AddNPCWarps` instead of `AddNpcWarps`. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 64df125b..1a288ec8 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -171,6 +171,8 @@ internal class ItemRepository string id = item.ItemId; // by category + bool createdJuice = false; + bool createdPickle = false; switch (item.Category) { // fish @@ -190,12 +192,23 @@ internal class ItemRepository // greens case SObject.GreensCategory: yield return this.TryCreate(itemType.Identifier, $"342/{id}", _ => objectDataDefinition.CreateFlavoredPickle(item)); + createdPickle = true; + + if (item.Edibility > 0 && !item.HasContextTag("edible_mushroom")) + { + yield return this.TryCreate(itemType.Identifier, $"350/{id}", _ => objectDataDefinition.CreateFlavoredJuice(item)); + createdJuice = true; + } + break; // vegetable products case SObject.VegetableCategory: yield return this.TryCreate(itemType.Identifier, $"350/{id}", _ => objectDataDefinition.CreateFlavoredJuice(item)); + createdJuice = true; + yield return this.TryCreate(itemType.Identifier, $"342/{id}", _ => objectDataDefinition.CreateFlavoredPickle(item)); + createdPickle = true; break; // flower honey @@ -216,8 +229,12 @@ internal class ItemRepository yield return this.TryCreate(itemType.Identifier, $"447/{item.ItemId}", _ => objectDataDefinition.CreateFlavoredAgedRoe(roeObj)); } + // juice + if (!createdJuice && item.HasContextTag("keg_juice")) + yield return this.TryCreate(itemType.Identifier, $"350/{id}", _ => objectDataDefinition.CreateFlavoredJuice(item)); + // pickles - if (item.HasContextTag("preserves_pickle") && item.Category is not (SObject.GreensCategory or SObject.VegetableCategory)) + if (!createdPickle && item.HasContextTag("preserves_pickle") && item.Category is not (SObject.GreensCategory or SObject.VegetableCategory)) yield return this.TryCreate(itemType.Identifier, $"342/{id}", _ => objectDataDefinition.CreateFlavoredPickle(item)); // dried mushrooms From 26a66cc20abf81e57028b7390a20a1471f192fc4 Mon Sep 17 00:00:00 2001 From: irocendar <97824271+irocendar@users.noreply.github.com> Date: Fri, 25 Jul 2025 06:36:50 +0100 Subject: [PATCH 03/43] Add token support to Content Patcher schema for AddWarps and AddNpcWarps fields --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 5993ebac..a145549b 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -353,7 +353,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+ *$", + "pattern": " *((.*\\{\\{.*\\}\\})|-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's Warp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } @@ -365,7 +365,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+ *$", + "pattern": " *((.*\\{\\{.*\\}\\})|(-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+)) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's NPCWarp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } From 02ac41a970d9113bf3d266db1c00678f836d3dbd Mon Sep 17 00:00:00 2001 From: irocendar <97824271+irocendar@users.noreply.github.com> Date: Fri, 25 Jul 2025 06:39:46 +0100 Subject: [PATCH 04/43] Fix regex --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index a145549b..b6aa4b80 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -353,7 +353,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *((.*\\{\\{.*\\}\\})|-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+) *$", + "pattern": " *((.*\\{\\{.*\\}\\}.*)|-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's Warp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } @@ -365,7 +365,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *((.*\\{\\{.*\\}\\})|(-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+)) *$", + "pattern": " *((.*\\{\\{.*\\}\\}.*)|(-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+)) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's NPCWarp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } From 139dd2660060d30386ba75cc5cf258670439fa0e Mon Sep 17 00:00:00 2001 From: irocendar <97824271+irocendar@users.noreply.github.com> Date: Fri, 25 Jul 2025 06:52:02 +0100 Subject: [PATCH 05/43] Add support for multiple spaces in warps fields --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index b6aa4b80..cf9b5549 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -353,7 +353,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *((.*\\{\\{.*\\}\\}.*)|-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+) *$", + "pattern": " *((.*\\{\\{.*\\}\\}.*)|(-?\\d+ +-?\\d+ +[A-Za-z0-9_\\.]+ +-?\\d+ +-?\\d+)) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's Warp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } @@ -365,7 +365,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *((.*\\{\\{.*\\}\\}.*)|(-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+)) *$", + "pattern": " *((.*\\{\\{.*\\}\\}.*)|(-?\\d+ +-?\\d+ +[A-Za-z0-9_\\.]+ +-?\\d+ +-?\\d+)) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's NPCWarp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } From dc912eccb3ea0cd1059f784e57f2beb3059a4b68 Mon Sep 17 00:00:00 2001 From: irocendar <97824271+irocendar@users.noreply.github.com> Date: Mon, 4 Aug 2025 18:47:17 +0100 Subject: [PATCH 06/43] Fix broken FromFile in VSCode and multiple comma-delimited targets --- src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index cf9b5549..0bcc6728 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -114,7 +114,7 @@ "allOf": [ { "not": { - "pattern": "\\b\\.\\.[/\\]" + "pattern": "\\b\\.\\.[/\\\\]" } }, { @@ -195,7 +195,7 @@ "description": "The game asset you want to patch (or multiple comma-delimited assets). This is the file path inside your game's Content folder, without the file extension or language (like Animals/Dinosaur to edit Content/Animals/Dinosaur.xnb). This field supports tokens and capitalization doesn't matter. Your changes are applied in all languages unless you specify a language condition.", "type": "string", "not": { - "pattern": "^ *[cC][oO][nN][tT][eE][nN][tT]/|\\.[xX][nN][bB] *$|\\.[a-zA-Z][a-zA-Z]-[a-zA-Z][a-zA-Z](?:.xnb)? *$" + "pattern": "(?i)(?:\\bcontent/|\\.xnb\\b|\\.[a-zA-Z][a-zA-Z]-[a-zA-Z][a-zA-Z](?:.xnb)\\b)" }, "@errorMessages": { "not": "Invalid target; it shouldn't include the 'Content/' folder, '.xnb' extension, or locale code." @@ -236,7 +236,7 @@ "allOf": [ { "not": { - "pattern": "\\b\\.\\.[/\\]" + "pattern": "\\b\\.\\.[/\\\\]" } }, { @@ -353,7 +353,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *((.*\\{\\{.*\\}\\}.*)|(-?\\d+ +-?\\d+ +[A-Za-z0-9_\\.]+ +-?\\d+ +-?\\d+)) *$", + "pattern": " *((.*\\{\\{.*\\}\\})|-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's Warp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } @@ -365,7 +365,7 @@ "type": "array", "items": { "type": "string", - "pattern": " *((.*\\{\\{.*\\}\\}.*)|(-?\\d+ +-?\\d+ +[A-Za-z0-9_\\.]+ +-?\\d+ +-?\\d+)) *$", + "pattern": " *((.*\\{\\{.*\\}\\})|(-?\\d+ -?\\d+ [A-Za-z0-9_\\.]+ -?\\d+ -?\\d+)) *$", "@errorMessages": { "pattern": "Each warp must match the exact format recognized by the game's NPCWarp map property (i.e. 'fromX fromY targetMap targetX targetY', like '10 10 Town 0 30')." } From 4250853b82af17e12f4c78c9d1c01842961f6aff Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 4 Aug 2025 21:40:24 -0400 Subject: [PATCH 07/43] add new 'reason abandoned' field to compatibility data model --- .../CompatibilityRepoClient.cs | 4 +++- .../CompatibilityRepo/ModCompatibilityInfo.cs | 7 ++++++- .../ModCompatibilityReasonAbandoned.cs | 17 +++++++++++++++++ .../RawDataModels/RawModEntry.cs | 11 +++++++++++ 4 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityReasonAbandoned.cs diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs index 19459c5e..ba61202e 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs @@ -100,6 +100,7 @@ public class CompatibilityRepoClient : IDisposable string[] modNames = this.GetCsv(rawModEntry.Name); string[] authorNames = this.GetCsv(rawModEntry.Author); ModCompatibilityStatus status = rawModEntry.GetStatus(); + ModCompatibilityReasonAbandoned reasonAbandoned = rawModEntry.GetReasonAbandoned(); rawModEntry.GetCompatibilitySummary(out string summary, out bool hasMarkdown); // get HTML summary @@ -130,7 +131,8 @@ public class CompatibilityRepoClient : IDisposable htmlSummary: htmlSummary, brokeIn: rawModEntry.BrokeIn, unofficialVersion: this.GetSemanticVersion(rawModEntry.UnofficialUpdate?.Version), - unofficialUrl: rawModEntry.UnofficialUpdate?.Url + unofficialUrl: rawModEntry.UnofficialUpdate?.Url, + abandonedReason: reasonAbandoned ), warnings: rawModEntry.Warnings ?? [], devNote: rawModEntry.DeveloperNotes, diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs index 9d33bd54..d8966cb4 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs @@ -24,6 +24,9 @@ public class ModCompatibilityInfo /// The URL to the latest unofficial update, if applicable. public string? UnofficialUrl { get; } + /// If the compatibility status is , the reason it was abandoned. + public ModCompatibilityReasonAbandoned AbandonedReason { get; } + /********* ** Accessors @@ -35,7 +38,8 @@ public class ModCompatibilityInfo /// The game or SMAPI version which broke this mod, if applicable. /// The version of the latest unofficial update, if applicable. /// The URL to the latest unofficial update, if applicable. - public ModCompatibilityInfo(ModCompatibilityStatus status, string? summary, string? htmlSummary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl) + /// If the compatibility status is , the reason it was abandoned. + public ModCompatibilityInfo(ModCompatibilityStatus status, string? summary, string? htmlSummary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl, ModCompatibilityReasonAbandoned abandonedReason) { this.Status = status; this.Summary = summary; @@ -43,6 +47,7 @@ public class ModCompatibilityInfo this.BrokeIn = brokeIn; this.UnofficialVersion = unofficialVersion; this.UnofficialUrl = unofficialUrl; + this.AbandonedReason = abandonedReason; } /// Get whether this mod is compatible with no notes. diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityReasonAbandoned.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityReasonAbandoned.cs new file mode 100644 index 00000000..f10d8d8c --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityReasonAbandoned.cs @@ -0,0 +1,17 @@ +namespace StardewModdingAPI.Toolkit.Framework.Clients.CompatibilityRepo; + +/// The reason a mod is marked abandoned. +public enum ModCompatibilityReasonAbandoned +{ + /// Not applicable. + None, + + /// The mod has been explicitly and definitively abandoned by the author. + Author, + + /// The mod pages or files have been deleted, so the mod is no longer available. + Deleted, + + /// The mod pages have been hidden, so the mod is no longer available. + Hidden +} diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs index ec8d19ab..be0859b2 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs @@ -65,6 +65,9 @@ public class RawModEntry /// The unofficial update which fixes compatibility with the latest Stardew Valley and SMAPI versions. public RawModUnofficialUpdate? UnofficialUpdate { get; set; } + /// If the compatibility status is , the reason it was abandoned. + public string? AbandonedReason { get; set; } + /**** ** Content packs only ****/ @@ -105,6 +108,14 @@ public class RawModEntry return status; } + /// Get the reason the mod was abandoned, if applicable. + public ModCompatibilityReasonAbandoned GetReasonAbandoned() + { + return Enum.TryParse(this.AbandonedReason, true, out ModCompatibilityReasonAbandoned reason) + ? reason + : ModCompatibilityReasonAbandoned.None; + } + /// Get the compatibility summary as shown on the compatibility list. /// The compatibility summary. /// Whether the summary may contain Markdown formatting. From 697efb908e2807297998993e536de22d37c26ab1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 3 Sep 2025 21:45:47 -0400 Subject: [PATCH 08/43] update release notes --- docs/release-notes.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8505791f..711f0dc9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,7 +6,11 @@ * Fixed `player_add` and `list_items` console commands not including some newer juice items. * For the web UI: - * Fixed Content Patcher schema requiring `AddNPCWarps` instead of `AddNpcWarps`. + * Improved Content Patcher [JSON schemas](technical/web.md#using-a-schema-file-directly): + * Fixed schema requiring `AddNPCWarps` instead of `AddNpcWarps`. + * Fixed validation error if a warp field contains tokens or consecutive spaces (thanks to irocendar!). + * Fixed validation error if a `Target` contains multiple targets (thanks to irocendar!). + * Fixed `FromFile` errors like "_matches a schema that is not allowed_" (thanks to irocendar!). ## 4.3.2 Released 14 July 2025 for Stardew Valley 1.6.14 or later. See [4.3 release highlights](https://www.patreon.com/posts/133992196). From e2e800660ee811141d9c7e67351aa3dd1223ea6c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 3 Sep 2025 21:46:43 -0400 Subject: [PATCH 09/43] update schema for Content Patcher 2.8.0 --- docs/release-notes.md | 1 + src/SMAPI.Web/wwwroot/schemas/content-patcher.json | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 711f0dc9..0b721c97 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * For the web UI: * Improved Content Patcher [JSON schemas](technical/web.md#using-a-schema-file-directly): + * Updated for Content Patcher 2.8.0. * Fixed schema requiring `AddNPCWarps` instead of `AddNpcWarps`. * Fixed validation error if a warp field contains tokens or consecutive spaces (thanks to irocendar!). * Fixed validation error if a `Target` contains multiple targets (thanks to irocendar!). diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json index 0bcc6728..bafcb5bd 100644 --- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json +++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json @@ -14,9 +14,9 @@ "title": "Format version", "description": "The format version. You should always use the latest version to enable the latest features, avoid obsolete behavior, and reduce load times.", "type": "string", - "pattern": "^2\\.7\\.[0-9]+$", + "pattern": "^2\\.8\\.[0-9]+$", "@errorMessages": { - "pattern": "Incorrect value '@value'. You should always use the latest format version (currently 2.7.0) to enable the latest features, avoid obsolete behavior, and reduce load times." + "pattern": "Incorrect value '@value'. You should always use the latest format version (currently 2.8.0) to enable the latest features, avoid obsolete behavior, and reduce load times." } }, "ConfigSchema": { From 1cf2de01600d4a6e5fd98b9f2f2f87224246bb4a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 6 Sep 2025 14:10:43 -0400 Subject: [PATCH 10/43] add modCompatibilityEntry.HasSiteId method for conbenience --- .../CompatibilityRepo/ModCompatibilityEntry.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs index a53acaa8..a2800b93 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs @@ -136,4 +136,19 @@ public class ModCompatibilityEntry if (!anyFound && !string.IsNullOrWhiteSpace(this.GitHubRepo)) yield return new KeyValuePair(ModSiteKey.GitHub, $"https://github.com/{this.GitHubRepo}/releases"); } + + /// Get whether this compatibility entry refers to the given mod site page ID. + /// The mod site. + /// The mod page ID. + public bool HasSiteId(ModSiteKey site, int id) + { + return site switch + { + ModSiteKey.Chucklefish => this.ChucklefishID == id, + ModSiteKey.CurseForge => this.CurseForgeID == id, + ModSiteKey.ModDrop => this.ModDropID == id, + ModSiteKey.Nexus => this.NexusID == id, + _ => false + }; + } } From fcfd9061e8ac51457bd8b322855206bd7b67cbf0 Mon Sep 17 00:00:00 2001 From: SinZ Date: Wed, 17 Sep 2025 00:10:29 +1000 Subject: [PATCH 11/43] Make farmhouse reset spouseroom --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b4136032..3ad65b47 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -701,6 +701,10 @@ internal class CoreAssetPropagator // reapply map changes // This must happen after updating warps (since some map modifications like the community shortcuts add warps) // and after resetting interior doors' state (so they apply their modifications too). + if (location is FarmHouse) + { + this.Reflection.GetField(location, "displayingSpouseRoom", true).SetValue(false); + } location.MakeMapModifications(force: true); // reset player position From 2ee9a7ba4e7445a4833a257e2c34c46a3471f043 Mon Sep 17 00:00:00 2001 From: Martiandweller Date: Fri, 31 Oct 2025 11:58:30 +0300 Subject: [PATCH 12/43] Expose OverrideButton. --- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 6 ++++++ src/SMAPI/IInputHelper.cs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index 8713cc48..c9cac9c1 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -85,4 +85,10 @@ internal class InputHelper : BaseHelper, IInputHelper { return this.CurrentInputState().GetState(button); } + + /// + public void OverrideButton(SButton button, bool setDown) + { + this.CurrentInputState().OverrideButton(button, setDown); + } } diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs index 053c371e..dbcf1350 100644 --- a/src/SMAPI/IInputHelper.cs +++ b/src/SMAPI/IInputHelper.cs @@ -30,4 +30,9 @@ public interface IInputHelper : IModLinked /// Get the state of a button. /// The button to check. SButtonState GetState(SButton button); + + /// Override the state of a button. + /// The button to override. + /// A new button state. + void OverrideButton(SButton button, bool setDown); } From 604b2acf3ad2121d4c16fbb895883c5e5538db64 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Nov 2025 19:53:47 -0500 Subject: [PATCH 13/43] use raw strings for console command descriptions --- .../Commands/Other/ApplySaveFixCommand.cs | 15 +++++- .../Commands/Other/RegenerateBundles.cs | 14 +++++- .../Framework/Commands/Player/AddCommand.cs | 46 +++++++++---------- .../Commands/Player/ListItemsCommand.cs | 12 ++++- .../Commands/Player/SetColorCommand.cs | 13 +++++- .../Commands/Player/SetFarmTypeCommand.cs | 12 ++++- .../Commands/Player/SetHealthCommand.cs | 12 ++++- .../Commands/Player/SetMaxHealthCommand.cs | 12 ++++- .../Commands/Player/SetMaxStaminaCommand.cs | 12 ++++- .../Commands/Player/SetMoneyCommand.cs | 12 ++++- .../Commands/Player/SetNameCommand.cs | 13 +++++- .../Commands/Player/SetStaminaCommand.cs | 12 ++++- .../Commands/Player/SetStyleCommand.cs | 13 +++++- .../Commands/World/FreezeTimeCommand.cs | 13 +++++- .../Framework/Commands/World/SetDayCommand.cs | 12 ++++- .../Commands/World/SetMineLevelCommand.cs | 12 ++++- .../Commands/World/SetSeasonCommand.cs | 12 ++++- .../Commands/World/SetTimeCommand.cs | 12 ++++- .../Commands/World/SetYearCommand.cs | 12 ++++- .../Commands/HarmonySummaryCommand.cs | 11 ++++- src/SMAPI/Framework/Commands/HelpCommand.cs | 11 ++++- .../Framework/Commands/ReloadI18nCommand.cs | 7 ++- 22 files changed, 255 insertions(+), 45 deletions(-) diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs index 771079fe..c5ec5bc6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/ApplySaveFixCommand.cs @@ -15,7 +15,20 @@ internal class ApplySaveFixCommand : ConsoleCommand *********/ /// Construct an instance. public ApplySaveFixCommand() - : base("apply_save_fix", "Apply one of the game's save migrations to the currently loaded save. WARNING: This may corrupt or make permanent changes to your save. DO NOT USE THIS unless you're absolutely sure.\n\nUsage: apply_save_fix list\nList all valid save IDs.\n\nUsage: apply_save_fix \nApply the named save fix.") { } + : base( + name: "apply_save_fix", + description: + """ + Apply one of the game's save migrations to the currently loaded save. WARNING: This may corrupt or make permanent changes to your save. DO NOT USE THIS unless you're absolutely sure. + + Usage: apply_save_fix list + List all valid save IDs. + + Usage: apply_save_fix + Apply the named save fix. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs index 4b4256b2..1a995c3c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/RegenerateBundles.cs @@ -18,7 +18,19 @@ internal class RegenerateBundlesCommand : ConsoleCommand *********/ /// Construct an instance. public RegenerateBundlesCommand() - : base("regenerate_bundles", $"Regenerate the game's community center bundle data. WARNING: this will reset all bundle progress, and may have unintended effects if you've already completed bundles. DO NOT USE THIS unless you're absolutely sure.\n\nUsage: regenerate_bundles confirm [] [ignore_seed]\nRegenerate all bundles for this save. If the is set to '{string.Join("' or '", Enum.GetNames(typeof(Game1.BundleType)))}', change the bundle type for the save. If an 'ignore_seed' option is included, remixed bundles are re-randomized without using the predetermined save seed.\n\nExample: regenerate_bundles remixed confirm") { } + : base( + name: "regenerate_bundles", + description: + $""" + Regenerate the game's community center bundle data. WARNING: this will reset all bundle progress, and may have unintended effects if you've already completed bundles. DO NOT USE THIS unless you're absolutely sure. + + Usage: regenerate_bundles confirm [] [ignore_seed] + Regenerate all bundles for this save. If the is set to '{string.Join("' or '", Enum.GetNames(typeof(Game1.BundleType)))}', change the bundle type for the save. If an 'ignore_seed' option is included, remixed bundles are re-randomized without using the predetermined save seed. + + Example: regenerate_bundles remixed confirm + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs index ae2c1dfe..f46b3e0e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/AddCommand.cs @@ -20,7 +20,28 @@ internal class AddCommand : ConsoleCommand *********/ /// Construct an instance. public AddCommand() - : base("player_add", AddCommand.GetDescription()) { } + : base( + name: "player_add", + description: + $""" + Gives the player an item. + + Usage: player_add [count] [quality] + - item id: the item ID (use the 'list_items' command to see a list). + - count (optional): how many of the item to give. + - quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium). + + Usage: player_add name "" [count] [quality] + - item name: the item name to search (use the 'list_items' command to see a list). This will add the item immediately if it's an exact match, else show a table of matching items. + - count (optional): how many of the item to give. + - quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium). + + These examples both add the galaxy sword to your inventory: + player_add weapon 4 + player_add name "Galaxy Sword" + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. @@ -162,27 +183,4 @@ internal class AddCommand : ConsoleCommand monitor.Log($"Multiple items have a name containing '{name}'. Do you mean one of these?\n\n{options}", LogLevel.Info); return null; } - - /// Get the command description. - private static string GetDescription() - { - return - $""" - Gives the player an item. - - Usage: player_add [count] [quality] - - item id: the item ID (use the 'list_items' command to see a list). - - count (optional): how many of the item to give. - - quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium). - - Usage: player_add name "" [count] [quality] - - item name: the item name to search (use the 'list_items' command to see a list). This will add the item immediately if it's an exact match, else show a table of matching items. - - count (optional): how many of the item to give. - - quality (optional): one of {Object.lowQuality} (normal), {Object.medQuality} (silver), {Object.highQuality} (gold), or {Object.bestQuality} (iridium). - - These examples both add the galaxy sword to your inventory: - player_add weapon 4 - player_add name "Galaxy Sword" - """; - } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs index f664c586..db11f16b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/ListItemsCommand.cs @@ -21,7 +21,17 @@ internal class ListItemsCommand : ConsoleCommand *********/ /// Construct an instance. public ListItemsCommand() - : base("list_items", "Lists and searches items in the game data.\n\nUsage: list_items [search]\n- search (optional): an arbitrary search string to filter by.") { } + : base( + name: "list_items", + description: + """ + Lists and searches items in the game data. + + Usage: list_items [search] + - search (optional): an arbitrary search string to filter by. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs index 26cde8fa..b928507e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetColorCommand.cs @@ -13,7 +13,18 @@ internal class SetColorCommand : ConsoleCommand *********/ /// Construct an instance. public SetColorCommand() - : base("player_changecolor", "Sets the color of a player feature.\n\nUsage: player_changecolor \n- target: what to change (one of 'hair', 'eyes', or 'pants').\n- color: a color value in RGB format, like (255,255,255).") { } + : base( + name: "player_changecolor", + description: + """ + Sets the color of a player feature. + + Usage: player_changecolor + - target: what to change (one of 'hair', 'eyes', or 'pants'). + - color: a color value in RGB format, like (255,255,255). + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs index b40aeff2..b4497ec7 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetFarmTypeCommand.cs @@ -19,7 +19,17 @@ internal class SetFarmTypeCommand : ConsoleCommand *********/ /// Construct an instance. public SetFarmTypeCommand() - : base("set_farm_type", "Sets the current player's farm type.\n\nUsage: set_farm_type \n- farm type: the farm type to set. Enter `set_farm_type list` for a list of available farm types.") { } + : base( + name: "set_farm_type", + description: + """ + Sets the current player's farm type. + + Usage: set_farm_type + - farm type: the farm type to set. Enter `set_farm_type list` for a list of available farm types. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index 9d951229..65b8ff55 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -13,7 +13,17 @@ internal class SetHealthCommand : ConsoleCommand *********/ /// Construct an instance. public SetHealthCommand() - : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount.") { } + : base( + name: "player_sethealth", + description: + """ + Sets the player's health. + + Usage: player_sethealth [value] + - value: an integer amount. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs index c35255ae..0fcfaaf2 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxHealthCommand.cs @@ -13,7 +13,17 @@ internal class SetMaxHealthCommand : ConsoleCommand *********/ /// Construct an instance. public SetMaxHealthCommand() - : base("player_setmaxhealth", "Sets the player's max health.\n\nUsage: player_setmaxhealth [value]\n- value: an integer amount.") { } + : base( + name: "player_setmaxhealth", + description: + """ + Sets the player's max health. + + Usage: player_setmaxhealth [value] + - value: an integer amount. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs index 9fb73283..2bb76dce 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMaxStaminaCommand.cs @@ -13,7 +13,17 @@ internal class SetMaxStaminaCommand : ConsoleCommand *********/ /// Construct an instance. public SetMaxStaminaCommand() - : base("player_setmaxstamina", "Sets the player's max stamina.\n\nUsage: player_setmaxstamina [value]\n- value: an integer amount.") { } + : base( + name: "player_setmaxstamina", + description: + """ + Sets the player's max stamina. + + Usage: player_setmaxstamina [value] + - value: an integer amount. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index fae4edbf..2ee28eb3 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -13,7 +13,17 @@ internal class SetMoneyCommand : ConsoleCommand *********/ /// Construct an instance. public SetMoneyCommand() - : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount.") { } + : base( + name: "player_setmoney", + description: + """ + Sets the player's money. + + Usage: player_setmoney + - value: an integer amount. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs index 6be5aa9b..49ddb3ed 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetNameCommand.cs @@ -12,7 +12,18 @@ internal class SetNameCommand : ConsoleCommand *********/ /// Construct an instance. public SetNameCommand() - : base("player_setname", "Sets the player's name.\n\nUsage: player_setname \n- target: what to rename (one of 'player' or 'farm').\n- name: the new name to set.") { } + : base( + name: "player_setname", + description: + """ + Sets the player's name. + + Usage: player_setname + - target: what to rename (one of 'player' or 'farm'). + - name: the new name to set. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index 84d95097..2edd683d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -13,7 +13,17 @@ internal class SetStaminaCommand : ConsoleCommand *********/ /// Construct an instance. public SetStaminaCommand() - : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount.") { } + : base( + name: "player_setstamina", + description: + """ + Sets the player's stamina. + + Usage: player_setstamina [value] + - value: an integer amount. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs index 9e20ec2d..27c43610 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStyleCommand.cs @@ -12,7 +12,18 @@ internal class SetStyleCommand : ConsoleCommand *********/ /// Construct an instance. public SetStyleCommand() - : base("player_changestyle", "Sets the style of a player feature.\n\nUsage: player_changestyle .\n- target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender').\n- value: the style ID.") { } + : base( + name: "player_changestyle", + description: + """ + Sets the style of a player feature. + + Usage: player_changestyle + - target: what to change (one of 'hair', 'shirt', 'skin', 'acc', 'shoe', 'swim', or 'gender'). + - value: the style ID. + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs index 4fe2e383..25d7828b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -21,7 +21,18 @@ internal class FreezeTimeCommand : ConsoleCommand *********/ /// Construct an instance. public FreezeTimeCommand() - : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).", mayNeedUpdate: true) { } + : base( + name: "world_freezetime", + description: + """ + Freezes or resumes time. + + Usage: world_freezetime [value] + - value: one of 0 (resume), 1 (freeze), or blank (toggle). + """, + mayNeedUpdate: true + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs index af0dffca..000e28c0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs @@ -14,7 +14,17 @@ internal class SetDayCommand : ConsoleCommand *********/ /// Construct an instance. public SetDayCommand() - : base("world_setday", "Sets the day to the specified value.\n\nUsage: world_setday .\n- value: the target day (a number from 1 to 28).") { } + : base( + name: "world_setday", + description: + """ + Sets the day to the specified value. + + Usage: world_setday . + - value: the target day (a number from 1 to 28). + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs index 1d996673..ada5c155 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetMineLevelCommand.cs @@ -13,7 +13,17 @@ internal class SetMineLevelCommand : ConsoleCommand *********/ /// Construct an instance. public SetMineLevelCommand() - : base("world_setminelevel", "Sets the mine level?\n\nUsage: world_setminelevel \n- value: The target level (a number starting at 1).") { } + : base( + name: "world_setminelevel", + description: + """ + Sets the mine level? + + Usage: world_setminelevel + - value: The target level (a number starting at 1). + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs index 9f87270d..6f09bf05 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs @@ -21,7 +21,17 @@ internal class SetSeasonCommand : ConsoleCommand *********/ /// Construct an instance. public SetSeasonCommand() - : base("world_setseason", "Sets the season to the specified value.\n\nUsage: world_setseason \n- season: the target season (one of 'spring', 'summer', 'fall', 'winter').") { } + : base( + name: "world_setseason", + description: + """ + Sets the season to the specified value. + + Usage: world_setseason + - season: the target season (one of 'spring', 'summer', 'fall', 'winter'). + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs index 9d636620..f53fa8b4 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetTimeCommand.cs @@ -14,7 +14,17 @@ internal class SetTimeCommand : ConsoleCommand *********/ /// Construct an instance. public SetTimeCommand() - : base("world_settime", "Sets the time to the specified value.\n\nUsage: world_settime \n- value: the target time in military time (like 0600 for 6am and 1800 for 6pm).") { } + : base( + name: "world_settime", + description: + """ + Sets the time to the specified value. + + Usage: world_settime + - value: the target time in military time (like 0600 for 6am and 1800 for 6pm). + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs index 544e39de..f65c1ec7 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs @@ -14,7 +14,17 @@ internal class SetYearCommand : ConsoleCommand *********/ /// Construct an instance. public SetYearCommand() - : base("world_setyear", "Sets the year to the specified value.\n\nUsage: world_setyear \n- year: the target year (a number starting from 1).") { } + : base( + name: "world_setyear", + description: + """ + Sets the year to the specified value. + + Usage: world_setyear + - year: the target year (a number starting from 1). + """ + ) + { } /// Handle the command. /// Writes messages to the console and log file. diff --git a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs index 3fcec7f4..f318f335 100644 --- a/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs +++ b/src/SMAPI/Framework/Commands/HarmonySummaryCommand.cs @@ -17,7 +17,16 @@ internal class HarmonySummaryCommand : IInternalCommand public string Name { get; } = "harmony_summary"; /// The human-readable documentation shown when the player runs the built-in 'help' command. - public string Description { get; } = "Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches.\n\nUsage: harmony_summary\nList all Harmony patches.\n\nUsage: harmony_summary \n- search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method."; + public string Description { get; } = + """ + Harmony is a library which rewrites game code, used by SMAPI and some mods. This command lists current Harmony patches. + + Usage: harmony_summary + List all Harmony patches. + + Usage: harmony_summary + - search: one more more words to search. If any word matches a method name, the method and all its patchers will be listed; otherwise only matching patchers will be listed for the method. + """; /********* diff --git a/src/SMAPI/Framework/Commands/HelpCommand.cs b/src/SMAPI/Framework/Commands/HelpCommand.cs index e7d1f4af..59bd34b4 100644 --- a/src/SMAPI/Framework/Commands/HelpCommand.cs +++ b/src/SMAPI/Framework/Commands/HelpCommand.cs @@ -19,7 +19,16 @@ internal class HelpCommand : IInternalCommand public string Name { get; } = "help"; /// The human-readable documentation shown when the player runs the built-in 'help' command. - public string Description { get; } = "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display."; + public string Description { get; } = + """ + Lists command documentation. + + Usage: help + Lists all available commands. + + Usage: help + - cmd: The name of a command whose documentation to display. + """; /********* diff --git a/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs index cb6824fd..5a8b2866 100644 --- a/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs +++ b/src/SMAPI/Framework/Commands/ReloadI18nCommand.cs @@ -21,7 +21,12 @@ internal class ReloadI18nCommand : IInternalCommand public string Name { get; } = "reload_i18n"; /// The human-readable documentation shown when the player runs the built-in 'help' command. - public string Description { get; } = "Reloads translation files for all mods.\n\nUsage: reload_i18n"; + public string Description { get; } = + """ + Reloads translation files for all mods. + + Usage: reload_i18n + """; /********* From 01521661c261ce9b8fa9b54e6a5742be9bd0fbb1 Mon Sep 17 00:00:00 2001 From: Pink Serenity Date: Sat, 22 Nov 2025 12:35:57 +0100 Subject: [PATCH 14/43] Add PatchMode.Mask --- .../Framework/Content/AssetDataForImage.cs | 51 ++++++++++++++----- src/SMAPI/PatchMode.cs | 5 +- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index f59e73be..f6c1e46a 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -213,22 +213,47 @@ internal class AssetDataForImage : AssetData, IAssetDataForImage // shortcut transparency if (above.A < AssetDataForImage.MinOpacity) continue; - if (below.A < AssetDataForImage.MinOpacity || above.A == byte.MaxValue) - mergedData[targetIndex] = above; - // merge pixels + if (patchMode == PatchMode.Overlay) + { + if (below.A < AssetDataForImage.MinOpacity || above.A == byte.MaxValue) + mergedData[targetIndex] = above; + + // merge pixels + else + { + // This performs a conventional alpha blend for the pixels, which are already + // premultiplied by the content pipeline. The formula is derived from + // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. + float alphaBelow = 1 - (above.A / 255f); + mergedData[targetIndex] = new Color( + r: (int)(above.R + (below.R * alphaBelow)), + g: (int)(above.G + (below.G * alphaBelow)), + b: (int)(above.B + (below.B * alphaBelow)), + alpha: Math.Max(above.A, below.A) + ); + } + } else { - // This performs a conventional alpha blend for the pixels, which are already - // premultiplied by the content pipeline. The formula is derived from - // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. - float alphaBelow = 1 - (above.A / 255f); - mergedData[targetIndex] = new Color( - r: (int)(above.R + (below.R * alphaBelow)), - g: (int)(above.G + (below.G * alphaBelow)), - b: (int)(above.B + (below.B * alphaBelow)), - alpha: Math.Max(above.A, below.A) - ); + // compute new alpha by subtracting mask alpha + int newAlphaInt = below.A - above.A; + + // fully masked out + if (newAlphaInt <= 0) + mergedData[targetIndex] = new Color((byte)0, (byte)0, (byte)0, (byte)0); + + else + { + // This blends the pixels by removing the alpha defined in the mask. + float scale = (float)newAlphaInt / below.A; + mergedData[targetIndex] = new Color( + r: (int)Math.Clamp(Math.Round(below.R * scale), 0, 255), + g: (int)Math.Clamp(Math.Round(below.G * scale), 0, 255), + b: (int)Math.Clamp(Math.Round(below.B * scale), 0, 255), + alpha: newAlphaInt + ); + } } } diff --git a/src/SMAPI/PatchMode.cs b/src/SMAPI/PatchMode.cs index 8575e5d0..669a0f03 100644 --- a/src/SMAPI/PatchMode.cs +++ b/src/SMAPI/PatchMode.cs @@ -7,5 +7,8 @@ public enum PatchMode Replace, /// Draw the new content over the original content, so the original content shows through any transparent or semi-transparent pixels. - Overlay + Overlay, + + /// Masks the original content so that the alpha value of every pixel in the new content gets subtracted from the corresponding pixel in the original content. + Mask } From 0316a84b03151be971162fa50596b7abfcc73a9c Mon Sep 17 00:00:00 2001 From: Spiderbuttons Date: Sun, 22 Jun 2025 22:06:52 -0400 Subject: [PATCH 15/43] Enable single directory climbing to root content folder for tilesheet image sources in mod-loaded maps. --- .../ContentManagers/ModContentManager.cs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index f11ce964..adb09556 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -413,8 +413,19 @@ internal sealed class ModContentManager : BaseContentManager // validate tilesheet path string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'."; - if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) - throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../)."); + if (Path.IsPathRooted(imageSource)) + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must not be an absolute path ({Path.GetPathRoot(imageSource)})."); + + string[] imageSourcePathSegments = PathUtilities.GetSegments(imageSource); + if (imageSourcePathSegments.Contains("..")) + { + int climbingCount = imageSourcePathSegments.Count(segment => segment.Equals("..", StringComparison.OrdinalIgnoreCase)); + if (climbingCount > 1) + { + string[] offendingSegments = imageSourcePathSegments.TakeWhile(seg => seg.Equals("..")).ToArray(); + throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must not climb more than one directory ({string.Join('/', offendingSegments)}/)."); + } + } // load best match try @@ -467,8 +478,8 @@ internal sealed class ModContentManager : BaseContentManager relativePath = Path.Combine(Path.GetDirectoryName(relativePath) ?? "", filename.TrimStart('.')); } - // get relative to map file - { + // get relative to map file unless path has directory climbing + if (!PathUtilities.GetSegments(relativePath).Contains("..")) { string localKey = Path.Combine(modRelativeMapFolder, relativePath); if (this.GetModFile(localKey).Exists) { @@ -524,8 +535,12 @@ internal sealed class ModContentManager : BaseContentManager string key = relativePath; string topFolder = PathUtilities.GetSegments(key, limit: 2)[0]; - // convert image source relative to map file into asset key - if (!topFolder.Equals("Maps", StringComparison.OrdinalIgnoreCase)) + // remove previously validated directory climbing if it exists + if (topFolder.Equals("..", StringComparison.OrdinalIgnoreCase)) + key = key[2..]; + + // else convert image source relative to map file into asset key + else if (!topFolder.Equals("Maps", StringComparison.OrdinalIgnoreCase)) key = Path.Combine("Maps", key); // remove file extension from unpacked file From cb53d1861b452212cbe1d0be2eddd29c92962607 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 Dec 2025 13:03:25 -0500 Subject: [PATCH 16/43] avoid unneeded allocations --- .../ContentManagers/ModContentManager.cs | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index adb09556..2ee2d616 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -412,9 +412,8 @@ internal sealed class ModContentManager : BaseContentManager imageSource = imageSource[(relativeMapFolder.Length + 1)..]; // validate tilesheet path - string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'."; if (Path.IsPathRooted(imageSource)) - throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must not be an absolute path ({Path.GetPathRoot(imageSource)})."); + throw this.GetPathError(relativeMapFolder, imageSource, $"Tilesheet paths must not be an absolute path ({Path.GetPathRoot(imageSource)})."); string[] imageSourcePathSegments = PathUtilities.GetSegments(imageSource); if (imageSourcePathSegments.Contains("..")) @@ -422,8 +421,8 @@ internal sealed class ModContentManager : BaseContentManager int climbingCount = imageSourcePathSegments.Count(segment => segment.Equals("..", StringComparison.OrdinalIgnoreCase)); if (climbingCount > 1) { - string[] offendingSegments = imageSourcePathSegments.TakeWhile(seg => seg.Equals("..")).ToArray(); - throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} Tilesheet paths must not climb more than one directory ({string.Join('/', offendingSegments)}/)."); + var offendingSegments = imageSourcePathSegments.TakeWhile(seg => seg.Equals("..")); + throw this.GetPathError(relativeMapFolder, imageSource, $"Tilesheet paths must not climb more than one directory ({string.Join('/', offendingSegments)}/)."); } } @@ -431,7 +430,7 @@ internal sealed class ModContentManager : BaseContentManager try { if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, out IAssetName? assetName, out string? error)) - throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} {error}"); + throw this.GetPathError(relativeMapFolder, imageSource, error); if (assetName is not null) { @@ -446,7 +445,7 @@ internal sealed class ModContentManager : BaseContentManager if (ex is SContentLoadException) throw; - throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{errorPrefix} The tilesheet couldn't be loaded.", ex); + throw this.GetPathError(relativeMapFolder, imageSource, "The tilesheet couldn't be loaded.", ex); } } } @@ -455,10 +454,10 @@ internal sealed class ModContentManager : BaseContentManager /// The folder path containing the map, relative to the mod folder. /// The tilesheet path to load. /// The found asset name. - /// A message indicating why the file couldn't be loaded. + /// A message indicating why the file couldn't be loaded, if applicable. /// Returns whether the asset name was found. /// See remarks on . - private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName? assetName, out string? error) + private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string relativePath, out IAssetName? assetName, [NotNullWhen(false)] out string? error) { error = null; @@ -479,7 +478,8 @@ internal sealed class ModContentManager : BaseContentManager } // get relative to map file unless path has directory climbing - if (!PathUtilities.GetSegments(relativePath).Contains("..")) { + if (!PathUtilities.GetSegments(relativePath).Contains("..")) + { string localKey = Path.Combine(modRelativeMapFolder, relativePath); if (this.GetModFile(localKey).Exists) { @@ -549,4 +549,14 @@ internal sealed class ModContentManager : BaseContentManager return key; } + + /// Get an exception indicating that a map asset can't be loaded because one of its tilesheets has an invalid path. + /// The relative path to the folder which contains the map asset, relative to the mod folder. + /// The tilesheet's path to the image file. + /// The error message indicating why loading it failed. (Basic metadata like the image source is prepended automatically.) + /// The inner exception which caused the error, if applicable. + private SContentLoadException GetPathError(string relativeMapPath, string imageSource, string error, Exception? ex = null) + { + return new SContentLoadException(ContentLoadErrorType.InvalidData, $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'. {error}", ex); + } } From 28bc30953d3b1faeda5febdb1c0228c6fa4768b2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 Dec 2025 13:03:25 -0500 Subject: [PATCH 17/43] avoid unneeded case-insensitive equals for ".." --- .../Framework/ContentManagers/ModContentManager.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 2ee2d616..66672f23 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -415,14 +415,16 @@ internal sealed class ModContentManager : BaseContentManager if (Path.IsPathRooted(imageSource)) throw this.GetPathError(relativeMapFolder, imageSource, $"Tilesheet paths must not be an absolute path ({Path.GetPathRoot(imageSource)})."); - string[] imageSourcePathSegments = PathUtilities.GetSegments(imageSource); - if (imageSourcePathSegments.Contains("..")) { - int climbingCount = imageSourcePathSegments.Count(segment => segment.Equals("..", StringComparison.OrdinalIgnoreCase)); - if (climbingCount > 1) + string[] imageSourcePathSegments = PathUtilities.GetSegments(imageSource); + if (imageSourcePathSegments.Contains("..")) { - var offendingSegments = imageSourcePathSegments.TakeWhile(seg => seg.Equals("..")); - throw this.GetPathError(relativeMapFolder, imageSource, $"Tilesheet paths must not climb more than one directory ({string.Join('/', offendingSegments)}/)."); + int climbingCount = imageSourcePathSegments.Count(segment => segment == ".."); + if (climbingCount > 1) + { + var offendingSegments = imageSourcePathSegments.TakeWhile(segment => segment == ".."); + throw this.GetPathError(relativeMapFolder, imageSource, $"Tilesheet paths must not climb more than one directory ({string.Join('/', offendingSegments)}/)."); + } } } From d9ccf08b71b9c223bbf042d5b4e30e71cdd86298 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 Dec 2025 13:03:25 -0500 Subject: [PATCH 18/43] adjust validation to account for climbing after first path segment --- .../ContentManagers/ModContentManager.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 66672f23..87e2809c 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -399,32 +399,33 @@ internal sealed class ModContentManager : BaseContentManager this.Monitor.VerboseLog($"Fixing tilesheet paths for map '{relativeMapPath}' from mod '{this.ModName}'..."); foreach (TileSheet tilesheet in map.TileSheets) { - // get image source + // get image path tilesheet.ImageSource = this.NormalizePathSeparators(tilesheet.ImageSource); string imageSource = tilesheet.ImageSource; + if (fixEagerPathPrefixes && relativeMapFolder.Length > 0 && imageSource?.StartsWith(relativeMapFolder) is true) + imageSource = imageSource[(relativeMapFolder.Length + 1)..]; - // validate image source + // ensure path isn't empty if (string.IsNullOrWhiteSpace(imageSource)) throw new SContentLoadException(ContentLoadErrorType.InvalidData, $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet '{tilesheet.Id}'. This tilesheet has no image source."); - // reverse incorrect eager tilesheet path prefixing - if (fixEagerPathPrefixes && relativeMapFolder.Length > 0 && imageSource.StartsWith(relativeMapFolder)) - imageSource = imageSource[(relativeMapFolder.Length + 1)..]; - - // validate tilesheet path + // ensure path is relative if (Path.IsPathRooted(imageSource)) throw this.GetPathError(relativeMapFolder, imageSource, $"Tilesheet paths must not be an absolute path ({Path.GetPathRoot(imageSource)})."); + // ensure any directory climbing is valid + // Tilesheet paths are relative to either the `Content/Maps` folder or the map file. Directory climbing is + // restricted for safety and simplicity: + // 1. Can only climb at the start of the path (e.g. `../LooseSprites/Cursors` but not `Mines/../Barn`). + // 2. Can only climb once (to avoid escaping the `Content` folder). + // 3. Always relative to the `Content/Maps` folder (not the map file). { - string[] imageSourcePathSegments = PathUtilities.GetSegments(imageSource); - if (imageSourcePathSegments.Contains("..")) + const string climbSegment = ".."; + string[] pathSegments = PathUtilities.GetSegments(imageSource); + if (pathSegments.Contains(climbSegment)) { - int climbingCount = imageSourcePathSegments.Count(segment => segment == ".."); - if (climbingCount > 1) - { - var offendingSegments = imageSourcePathSegments.TakeWhile(segment => segment == ".."); - throw this.GetPathError(relativeMapFolder, imageSource, $"Tilesheet paths must not climb more than one directory ({string.Join('/', offendingSegments)}/)."); - } + if (pathSegments[0] != climbSegment || pathSegments.Count(segment => segment == climbSegment) > 1) + throw this.GetPathError(relativeMapFolder, imageSource, $"Directory climbing ({climbSegment}/) is only permitted once at the start of the path."); } } @@ -480,7 +481,7 @@ internal sealed class ModContentManager : BaseContentManager } // get relative to map file unless path has directory climbing - if (!PathUtilities.GetSegments(relativePath).Contains("..")) + if (!relativePath.StartsWith("..") && PathUtilities.GetSegments(relativePath, 2)[0] != "..") // directory climbing can only be at the start of the path { string localKey = Path.Combine(modRelativeMapFolder, relativePath); if (this.GetModFile(localKey).Exists) From 29deb0a019efd211cf5c3328fc7f83ef9f6c6ce2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 Dec 2025 13:25:18 -0500 Subject: [PATCH 19/43] add constant for climb segment to simplify tracking related logic --- .../ContentManagers/ModContentManager.cs | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 87e2809c..52287445 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -30,6 +30,9 @@ internal sealed class ModContentManager : BaseContentManager /********* ** Fields *********/ + /// A path segment which navigates to the parent directory. + private const string DirectoryClimbingPathSegment = ".."; + /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; @@ -420,7 +423,7 @@ internal sealed class ModContentManager : BaseContentManager // 2. Can only climb once (to avoid escaping the `Content` folder). // 3. Always relative to the `Content/Maps` folder (not the map file). { - const string climbSegment = ".."; + const string climbSegment = ModContentManager.DirectoryClimbingPathSegment; string[] pathSegments = PathUtilities.GetSegments(imageSource); if (pathSegments.Contains(climbSegment)) { @@ -481,7 +484,8 @@ internal sealed class ModContentManager : BaseContentManager } // get relative to map file unless path has directory climbing - if (!relativePath.StartsWith("..") && PathUtilities.GetSegments(relativePath, 2)[0] != "..") // directory climbing can only be at the start of the path + const string climbSegment = ModContentManager.DirectoryClimbingPathSegment; + if (!relativePath.StartsWith(climbSegment) && PathUtilities.GetSegments(relativePath, 2)[0] != climbSegment) // directory climbing can only be at the start of the path { string localKey = Path.Combine(modRelativeMapFolder, relativePath); if (this.GetModFile(localKey).Exists) @@ -536,15 +540,17 @@ internal sealed class ModContentManager : BaseContentManager private string GetContentKeyForTilesheetImageSource(string relativePath) { string key = relativePath; - string topFolder = PathUtilities.GetSegments(key, limit: 2)[0]; - // remove previously validated directory climbing if it exists - if (topFolder.Equals("..", StringComparison.OrdinalIgnoreCase)) - key = key[2..]; + // make path relative to Content folder + { + string topFolder = PathUtilities.GetSegments(key, limit: 2)[0]; + const string climbSegment = ModContentManager.DirectoryClimbingPathSegment; - // else convert image source relative to map file into asset key - else if (!topFolder.Equals("Maps", StringComparison.OrdinalIgnoreCase)) - key = Path.Combine("Maps", key); + if (topFolder == climbSegment) + key = key[(climbSegment.Length + 1)..]; + else if (!topFolder.Equals("Maps", StringComparison.OrdinalIgnoreCase)) + key = Path.Combine("Maps", key); + } // remove file extension from unpacked file if (key.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) From 3a5dc95e97b8892c952ed3c4a30ba88fe313ce8f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 Dec 2025 13:35:53 -0500 Subject: [PATCH 20/43] update release notes --- docs/release-notes.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 0b721c97..f938aae9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,9 @@ * For players: * Fixed `player_add` and `list_items` console commands not including some newer juice items. +* For mod authors: + * Map tilesheet paths can now start with `../` to reference an asset under the `Content` folder that's not in `Content/Maps`. + * For the web UI: * Improved Content Patcher [JSON schemas](technical/web.md#using-a-schema-file-directly): * Updated for Content Patcher 2.8.0. From 58e59103de23ebd9744ddd9f328c16c6bf168cf2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 Dec 2025 13:35:53 -0500 Subject: [PATCH 21/43] remove unused code --- .../ContentManagers/ModContentManager.cs | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 52287445..503fd984 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -1,5 +1,4 @@ using System; -using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -355,38 +354,6 @@ internal sealed class ModContentManager : BaseContentManager return file; } - /// Premultiply a texture's alpha values to avoid transparency issues in the game. - /// The texture to premultiply. - /// Returns a premultiplied texture. - /// Based on code by David Gouveia. - private void PremultiplyTransparency(Texture2D texture) - { - int count = texture.Width * texture.Height; - Color[] data = ArrayPool.Shared.Rent(count); - try - { - texture.GetData(data, 0, count); - - bool changed = false; - for (int i = 0; i < count; i++) - { - ref Color pixel = ref data[i]; - if (pixel.A is (byte.MinValue or byte.MaxValue)) - continue; // no need to change fully transparent/opaque pixels - - data[i] = new Color(pixel.R * pixel.A / byte.MaxValue, pixel.G * pixel.A / byte.MaxValue, pixel.B * pixel.A / byte.MaxValue, pixel.A); // slower version: Color.FromNonPremultiplied(data[i].ToVector4()) - changed = true; - } - - if (changed) - texture.SetData(data, 0, count); - } - finally - { - ArrayPool.Shared.Return(data); - } - } - /// Fix custom map tilesheet paths so they can be found by the content manager. /// The map whose tilesheets to fix. /// The relative map path within the mod folder. From 90f3af14234eb8ec8cecb55a3623e00947fcad08 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 26 Dec 2025 13:35:53 -0500 Subject: [PATCH 22/43] migrate to new `field` keyword --- src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs | 11 ++--------- src/SMAPI/Framework/Content/AssetInfo.cs | 5 +---- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs index f6f9b0e5..451b52f5 100644 --- a/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs +++ b/src/SMAPI.Toolkit/Framework/ModScanning/ModFolder.cs @@ -10,13 +10,6 @@ namespace StardewModdingAPI.Toolkit.Framework.ModScanning; /// The info about a mod read from its folder. public class ModFolder { - /********* - ** Fields - *********/ - /// The backing field for . - private DirectoryInfo? DirectoryImpl; - - /********* ** Accessors *********/ @@ -28,7 +21,7 @@ public class ModFolder /// The folder containing the mod's manifest.json. [JsonIgnore] - public DirectoryInfo Directory => this.DirectoryImpl ??= new DirectoryInfo(this.DirectoryPath); + public DirectoryInfo Directory => field ??= new DirectoryInfo(this.DirectoryPath); /// The mod type. public ModType Type { get; } @@ -64,7 +57,7 @@ public class ModFolder public ModFolder(DirectoryInfo root, DirectoryInfo directory, ModType type, Manifest? manifest, ModParseError manifestParseError, string? manifestParseErrorText) { // save info - this.DirectoryImpl = directory; + this.Directory = directory; this.DirectoryPath = directory.FullName; this.Type = type; this.Manifest = manifest; diff --git a/src/SMAPI/Framework/Content/AssetInfo.cs b/src/SMAPI/Framework/Content/AssetInfo.cs index ec067b8f..324e7afd 100644 --- a/src/SMAPI/Framework/Content/AssetInfo.cs +++ b/src/SMAPI/Framework/Content/AssetInfo.cs @@ -12,9 +12,6 @@ internal class AssetInfo : IAssetInfo /// Normalizes an asset key to match the cache key. protected readonly Func GetNormalizedPath; - /// The backing field for . - private IAssetName? NameWithoutLocaleImpl; - /********* ** Accessors @@ -26,7 +23,7 @@ internal class AssetInfo : IAssetInfo public IAssetName Name { get; } /// - public IAssetName NameWithoutLocale => this.NameWithoutLocaleImpl ??= this.Name.GetBaseAssetName(); + public IAssetName NameWithoutLocale => field ??= this.Name.GetBaseAssetName(); /// public Type DataType { get; } From 5ff2d93559990fde3b889b904ac2394930615197 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 Dec 2025 02:50:57 -0500 Subject: [PATCH 23/43] add support for links in compatibility list warnings --- docs/release-notes.md | 2 + .../CompatibilityRepoClient.cs | 35 +++++++++++++--- .../ModCompatibilityEntry.cs | 41 +++++++++++-------- .../CompatibilityRepo/ModCompatibilityInfo.cs | 14 +++---- .../RawDataModels/RawModEntry.cs | 2 +- src/SMAPI.Web/ViewModels/ModModel.cs | 29 +++++++------ src/SMAPI.Web/Views/Mods/Index.cshtml | 2 +- 7 files changed, 79 insertions(+), 46 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index f938aae9..1e66c94d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,8 @@ * Map tilesheet paths can now start with `../` to reference an asset under the `Content` folder that's not in `Content/Maps`. * For the web UI: + * Improved mod compatibility list: + * Added support for mod links in warnings. * Improved Content Patcher [JSON schemas](technical/web.md#using-a-schema-file-directly): * Updated for Content Patcher 2.8.0. * Fixed schema requiring `AddNPCWarps` instead of `AddNpcWarps`. diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs index ba61202e..4883a0ea 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/CompatibilityRepoClient.cs @@ -57,7 +57,7 @@ public class CompatibilityRepoClient : IDisposable /// Fetch mods from the compatibility list by reading a local copy of the compatibility list repo. /// The full path to the compatibility list repo folder. - public async Task FetchModsFromLocalGitFolderAsync(string gitRepoPath) + public Task FetchModsFromLocalGitFolderAsync(string gitRepoPath) { string modsJsonPath = Path.Combine(gitRepoPath, "data", "mods.jsonc"); string contentPacksJsonPath = Path.Combine(gitRepoPath, "data", "broken-content-packs.jsonc"); @@ -67,11 +67,12 @@ public class CompatibilityRepoClient : IDisposable RawCompatibilityList? mods = JsonConvert.DeserializeObject(File.ReadAllText(modsJsonPath)); RawCompatibilityList? brokenContentPacks = JsonConvert.DeserializeObject(File.ReadAllText(contentPacksJsonPath)); - return + return Task.FromResult( (mods?.Mods ?? []) - .Concat(brokenContentPacks?.BrokenContentPacks ?? []) - .Select(this.ParseRawModEntry) - .ToArray(); + .Concat(brokenContentPacks?.BrokenContentPacks ?? []) + .Select(this.ParseRawModEntry) + .ToArray() + ); } /// Get the inline HTML produced by a Markdown string in a compatibility repo field. @@ -99,6 +100,7 @@ public class CompatibilityRepoClient : IDisposable string[] modIds = this.GetCsv(rawModEntry.Id); string[] modNames = this.GetCsv(rawModEntry.Name); string[] authorNames = this.GetCsv(rawModEntry.Author); + string[] warnings = (rawModEntry.Warnings?.Where(warning => !string.IsNullOrWhiteSpace(warning)).ToArray() ?? [])!; ModCompatibilityStatus status = rawModEntry.GetStatus(); ModCompatibilityReasonAbandoned reasonAbandoned = rawModEntry.GetReasonAbandoned(); rawModEntry.GetCompatibilitySummary(out string summary, out bool hasMarkdown); @@ -112,6 +114,26 @@ public class CompatibilityRepoClient : IDisposable htmlSummary = null; } + // get HTML warnings + string[]? htmlWarnings = null; + if (warnings.Length > 0) + { + bool anyDifferent = false; + + htmlWarnings = warnings + .Select(warning => + { + string html = this.ParseMarkdownToInlineHtml(warning); + if (html != warning) + anyDifferent = true; + return html; + }) + .ToArray(); + + if (!anyDifferent) + htmlWarnings = null; + } + // build model return new ModCompatibilityEntry( id: modIds, @@ -134,7 +156,8 @@ public class CompatibilityRepoClient : IDisposable unofficialUrl: rawModEntry.UnofficialUpdate?.Url, abandonedReason: reasonAbandoned ), - warnings: rawModEntry.Warnings ?? [], + warnings: warnings, + htmlWarnings: htmlWarnings, devNote: rawModEntry.DeveloperNotes, overrides: this.ParseOverrideEntries(modIds, rawModEntry.OverrideModData), anchor: PathUtilities.CreateSlug(modNames.FirstOrDefault())?.ToLower() diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs index a2800b93..848d9202 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityEntry.cs @@ -45,9 +45,12 @@ public class ModCompatibilityEntry /// The mod's compatibility with the latest stable version of the game. public ModCompatibilityInfo Compatibility { get; } - /// The human-readable warnings for players about this mod. + /// The human-readable warnings for players about this mod, in Markdown format. public string[] Warnings { get; } + /// The HTML version of , if different. + public string[]? HtmlWarnings { get; } + /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string? DevNote { get; } @@ -62,23 +65,24 @@ public class ModCompatibilityEntry ** Public methods *********/ /// Construct an instance. - /// The mod's unique ID. If the mod has alternate/old IDs, they're listed in latest to oldest order. - /// The mod's display name. If the mod has multiple names, the first one is the most canonical name. - /// The mod's author name. If the author has multiple names, the first one is the most canonical name. - /// The mod ID on Nexus. - /// The mod ID in the Chucklefish mod repo. - /// The mod ID in the CurseForge mod repo. - /// The mod ID in the ModDrop mod repo. - /// The GitHub repository in the form 'owner/repo'. - /// The URL to a non-GitHub source repo. - /// The custom mod page URL (if applicable). - /// The name of the mod which loads this content pack, if applicable. - /// The mod's compatibility with the latest stable version of the game. - /// The human-readable warnings for players about this mod. - /// Special notes intended for developers who maintain unofficial updates or submit pull requests. - /// The data overrides to apply to the mod's manifest or remote mod page data, if any. - /// The link anchor for the mod entry in the compatibility list. - public ModCompatibilityEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, ModCompatibilityInfo compatibility, string[] warnings, string? devNote, ModDataOverrideEntry? overrides, string? anchor) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public ModCompatibilityEntry(string[] id, string[] name, string[] author, int? nexusId, int? chucklefishId, int? curseForgeId, int? modDropId, string? githubRepo, string? customSourceUrl, string? customUrl, string? contentPackFor, ModCompatibilityInfo compatibility, string[] warnings, string[]? htmlWarnings, string? devNote, ModDataOverrideEntry? overrides, string? anchor) { this.ID = id; this.Name = name; @@ -93,6 +97,7 @@ public class ModCompatibilityEntry this.ContentPackFor = contentPackFor; this.Compatibility = compatibility; this.Warnings = warnings; + this.HtmlWarnings = htmlWarnings; this.DevNote = devNote; this.Overrides = overrides; this.Anchor = anchor; diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs index d8966cb4..446b6833 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/ModCompatibilityInfo.cs @@ -32,13 +32,13 @@ public class ModCompatibilityInfo ** Accessors *********/ /// Construct an instance. - /// The compatibility status. - /// The human-readable summary of the compatibility status or workaround, in Markdown format. - /// An HTML version of , if different. - /// The game or SMAPI version which broke this mod, if applicable. - /// The version of the latest unofficial update, if applicable. - /// The URL to the latest unofficial update, if applicable. - /// If the compatibility status is , the reason it was abandoned. + /// + /// + /// + /// + /// + /// + /// public ModCompatibilityInfo(ModCompatibilityStatus status, string? summary, string? htmlSummary, string? brokeIn, ISemanticVersion? unofficialVersion, string? unofficialUrl, ModCompatibilityReasonAbandoned abandonedReason) { this.Status = status; diff --git a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs index be0859b2..98a32e17 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/CompatibilityRepo/RawDataModels/RawModEntry.cs @@ -45,7 +45,7 @@ public class RawModEntry public string? Source { get; set; } /// Custom text indicating compatibility issues with the mod (e.g. not compatible with Linux/Mac). - public string[]? Warnings { get; set; } + public string?[]? Warnings { get; set; } /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string? DeveloperNotes { get; set; } diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index ebcfc107..6d8c108c 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -53,18 +53,18 @@ public class ModModel ** Public methods *********/ /// Construct an instance. - /// The mod IDs. - /// The mod name. - /// The mod's alternative names, if any. - /// The mod author's name. - /// The mod author's alternative names, if any. - /// The GitHub repo, if any. - /// The URL to the mod's source code, if any. - /// The compatibility status for the stable version of the game. - /// Links to the available mod pages. - /// The human-readable warnings for players about this mod. - /// Special notes intended for developers who maintain unofficial updates or submit pull requests. - /// A unique identifier for the mod that can be used in an anchor URL. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// [JsonConstructor] public ModModel(string[] id, string? name, string? alternateNames, string? author, string? alternateAuthors, string? gitHubRepo, string? sourceUrl, ModCompatibilityModel? compatibility, ModLinkModel[] modPages, string[]? warnings, string? devNote, string? slug) { @@ -96,9 +96,12 @@ public class ModModel this.SourceUrl = this.GetSourceUrl(entry); this.Compatibility = !entry.Compatibility.IsDefault() ? new ModCompatibilityModel(entry.Compatibility) : null; this.ModPages = this.GetModPageUrls(entry).ToArray(); - this.Warnings = entry.Warnings.Length > 1 ? entry.Warnings : null; + this.Warnings = entry.HtmlWarnings ?? entry.Warnings; this.DevNote = entry.DevNote; this.Slug = entry.Anchor; + + if (this.Warnings.Length == 0) + this.Warnings = null; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index dd9d6a8c..4577e33e 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -109,7 +109,7 @@ else
-
⚠ {{warning}}
+
⚠
From d1099ac8e947975b65d0ffc49693387c58b62dcb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 Dec 2025 12:10:04 -0500 Subject: [PATCH 24/43] simplify localized/base asset handling during texture propagation This reduces abstraction a bit so it's easier to follow, and avoids repeating common logic like the DoesAssetExist check. --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 73 +++++++++++------------ 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b4136032..43d39666 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -148,54 +148,51 @@ internal class CoreAssetPropagator { bool changed = false; - // get asset names to replace - // We propagate non-textures by comparing base asset names, to update any localized version like - // `asset.fr-FR` too. We need to check every content manager for in-place texture edits though, so we - // should avoid iterating their assets if possible. So here we just check for the current localized name - // and base name, which should cover normal cases. - IAssetName[] assetNames = assetName.LocaleCode != null - ? [assetName, assetName.GetBaseAssetName()] - : [assetName]; - - // update textures in-place + // update textures in-place (0 = localized asset name, 1 = base asset name) + for (int i = 0; i < 2; i++) { - // get new textures to copy - Lazy[] newTextures = new Lazy[assetNames.Length]; - newTextures[0] = new Lazy(() => this.DisposableContentManager.LoadLocalized(assetName, language, useCache: false)); - if (assetNames.Length > 1) - newTextures[1] = new Lazy(() => this.DisposableContentManager.LoadLocalized(assetNames[1], language, useCache: false)); + bool forLocalizedAsset = i == 0; + + // if the asset name is non-localized, only propagate it once + if (forLocalizedAsset && assetName.LocaleCode is null) + continue; + + // get asset name to replace + // We propagate non-textures by comparing base asset names, to update any localized version like + // `asset.fr-FR` too. We need to check every content manager for in-place texture edits though, so we + // should avoid iterating their assets if possible. So here we just check for the current localized name + // and base name, which should cover normal cases. + IAssetName name = forLocalizedAsset + ? assetName + : assetName.GetBaseAssetName(); + + // get new texture to copy + Lazy newTexture = new(() => + { + if (this.DisposableContentManager.DoesAssetExist(name)) + return this.DisposableContentManager.LoadLocalized(name, language, useCache: false); + + this.Monitor.Log($"Skipped reload for '{name.Name}' because the underlying asset no longer exists.", LogLevel.Warn); + return null; + }); // apply to content managers foreach (IContentManager contentManager in contentManagers) { - for (int i = 0; i < assetNames.Length; i++) + if (contentManager.IsLoaded(name)) { - IAssetName name = assetNames[i]; + if (newTexture.Value is null) + break; - if (contentManager.IsLoaded(name)) - { - if (this.DisposableContentManager.DoesAssetExist(name)) - { - changed = true; - - Texture2D texture = contentManager.LoadLocalized(name, language, useCache: true); - texture.CopyFromTexture(newTextures[i].Value); - } - else - { - this.Monitor.Log($"Skipped reload for '{name.Name}' because the underlying asset no longer exists.", LogLevel.Warn); - break; - } - } + Texture2D texture = contentManager.LoadLocalized(name, language, useCache: true); + texture.CopyFromTexture(newTexture.Value); + changed = true; } } - // drop temporary textures - foreach (Lazy newTexture in newTextures) - { - if (newTexture.IsValueCreated) - newTexture.Value.Dispose(); - } + // drop temporary texture + if (newTexture.IsValueCreated) + newTexture.Value?.Dispose(); } // update game state if needed From 7383dff3248dc4b66fafc5eb6967c43aded3f77d Mon Sep 17 00:00:00 2001 From: SinZ Date: Mon, 15 Sep 2025 20:41:57 +1000 Subject: [PATCH 25/43] Use correct language when propagating textures --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 43d39666..9f110721 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -184,7 +184,7 @@ internal class CoreAssetPropagator if (newTexture.Value is null) break; - Texture2D texture = contentManager.LoadLocalized(name, language, useCache: true); + Texture2D texture = contentManager.LoadLocalized(name, name.LanguageCode ?? contentManager.Language, useCache: true); texture.CopyFromTexture(newTexture.Value); changed = true; } From 58228984c00a62860cb6504814b323a08658148d Mon Sep 17 00:00:00 2001 From: SinZ Date: Mon, 15 Sep 2025 23:26:07 +1000 Subject: [PATCH 26/43] Be consistent with cache reads vs fresh load --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 9f110721..1e5a9ba3 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -170,7 +170,11 @@ internal class CoreAssetPropagator Lazy newTexture = new(() => { if (this.DisposableContentManager.DoesAssetExist(name)) - return this.DisposableContentManager.LoadLocalized(name, language, useCache: false); + { + return forLocalizedAsset + ? this.DisposableContentManager.LoadLocalized(name, language, useCache: false) + : this.DisposableContentManager.LoadLocalized(name, name.LanguageCode ?? this.DisposableContentManager.Language, useCache: false); + } this.Monitor.Log($"Skipped reload for '{name.Name}' because the underlying asset no longer exists.", LogLevel.Warn); return null; From 5119d85e4079021a2fb71ad4ca0d127f29cc447f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 27 Dec 2025 12:43:29 -0500 Subject: [PATCH 27/43] remove custom language handling entirely during asset propagation This is meant to change the texture that would be retrieved by a fresh `Load`, and `contentManager.Load(assetName)` uses the current language as the default instead of the asset locale. --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 25 +++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 1e5a9ba3..358440a6 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -91,15 +91,9 @@ internal class CoreAssetPropagator if (textureAssets.Any()) { - var defaultLanguage = this.MainContentManager.GetCurrentLanguage(); - foreach (IAssetName assetName in textureAssets) { - var language = assetName.LanguageCode ?? defaultLanguage; - if (language == LocalizedContentManager.LanguageCode.mod && LocalizedContentManager.CurrentModLanguage is null) - language = defaultLanguage; - - bool changed = this.PropagateTexture(assetName, language, contentManagers, ignoreWorld); + bool changed = this.PropagateTexture(assetName, contentManagers, ignoreWorld); if (changed) propagatedAssets[assetName] = true; } @@ -139,15 +133,20 @@ internal class CoreAssetPropagator *********/ /// Propagate changes to a cached texture asset. /// The asset name to reload. - /// The language for which to get assets. /// The content managers whose assets to update. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. /// Returns whether an asset was loaded. [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateTexture(IAssetName assetName, LocalizedContentManager.LanguageCode language, IList contentManagers, bool ignoreWorld) + private bool PropagateTexture(IAssetName assetName, IList contentManagers, bool ignoreWorld) { bool changed = false; + // get default language + // This method replaces the textures that would be loaded if you called `contentManager.Load(assetName)`, + // which internally maps to `contentManager.LoadLocalized(assetName, currentLanguage)` regardless of + // the asset name's language. If the asset name includes a locale, `LoadLocalized` handles that internally. + LocalizedContentManager.LanguageCode currentLanguage = LocalizedContentManager.CurrentLanguageCode; + // update textures in-place (0 = localized asset name, 1 = base asset name) for (int i = 0; i < 2; i++) { @@ -170,11 +169,7 @@ internal class CoreAssetPropagator Lazy newTexture = new(() => { if (this.DisposableContentManager.DoesAssetExist(name)) - { - return forLocalizedAsset - ? this.DisposableContentManager.LoadLocalized(name, language, useCache: false) - : this.DisposableContentManager.LoadLocalized(name, name.LanguageCode ?? this.DisposableContentManager.Language, useCache: false); - } + return this.DisposableContentManager.LoadLocalized(name, currentLanguage, useCache: false); this.Monitor.Log($"Skipped reload for '{name.Name}' because the underlying asset no longer exists.", LogLevel.Warn); return null; @@ -188,7 +183,7 @@ internal class CoreAssetPropagator if (newTexture.Value is null) break; - Texture2D texture = contentManager.LoadLocalized(name, name.LanguageCode ?? contentManager.Language, useCache: true); + Texture2D texture = contentManager.LoadLocalized(name, currentLanguage, useCache: true); texture.CopyFromTexture(newTexture.Value); changed = true; } From 5acfa914388a63e1a36dd993ec57869013e64012 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Dec 2025 12:20:58 -0500 Subject: [PATCH 28/43] replace `OverrideButton` with `Press` to match existing API, expand docs --- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 12 ++++++------ src/SMAPI/IInputHelper.cs | 19 ++++++++++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index c9cac9c1..9f634f45 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -46,6 +46,12 @@ internal class InputHelper : BaseHelper, IInputHelper return this.CurrentInputState().IsSuppressed(button); } + /// + public void Press(SButton button) + { + this.CurrentInputState().OverrideButton(button, setDown: true); + } + /// public void Suppress(SButton button) { @@ -85,10 +91,4 @@ internal class InputHelper : BaseHelper, IInputHelper { return this.CurrentInputState().GetState(button); } - - /// - public void OverrideButton(SButton button, bool setDown) - { - this.CurrentInputState().OverrideButton(button, setDown); - } } diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs index dbcf1350..fdcd383f 100644 --- a/src/SMAPI/IInputHelper.cs +++ b/src/SMAPI/IInputHelper.cs @@ -16,8 +16,22 @@ public interface IInputHelper : IModLinked /// The button. bool IsSuppressed(SButton button); + /// Mark a button as pressed. + /// The button to press. + /// + /// For both mods and the base game, this is equivalent to the button being physically pressed by the player. It will be released on the next input tick by default; it can be pressed again each tick to hold it down. + /// + /// See for the inverse. + /// + void Press(SButton button); + /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. /// The button to suppress. + /// + /// If the button is being held, it'll remain suppressed until the player releases it or until is called for the same button. + /// + /// See for the inverse. + /// void Suppress(SButton button); /// Suppress the immediate change to a scroll wheel value. @@ -30,9 +44,4 @@ public interface IInputHelper : IModLinked /// Get the state of a button. /// The button to check. SButtonState GetState(SButton button); - - /// Override the state of a button. - /// The button to override. - /// A new button state. - void OverrideButton(SButton button, bool setDown); } From 0d403fcebce07d65b2228b628db8a3610b8e8ab9 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Dec 2025 18:26:39 -0500 Subject: [PATCH 29/43] tweak image patching code This optimizes slightly and expands docs. --- .../Framework/Content/AssetDataForImage.cs | 32 +++++++++---------- src/SMAPI/PatchMode.cs | 3 +- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs index f6c1e46a..e27dc1ca 100644 --- a/src/SMAPI/Framework/Content/AssetDataForImage.cs +++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs @@ -205,21 +205,21 @@ internal class AssetDataForImage : AssetData, IAssetDataForImage for (int i = startIndex; i <= endIndex; i++) { - int targetIndex = i - sourceOffset; - + // get source pixel Color above = sourceData[i]; - Color below = mergedData[targetIndex]; - - // shortcut transparency if (above.A < AssetDataForImage.MinOpacity) continue; + // get target pixel + int targetIndex = i - sourceOffset; + Color below = mergedData[targetIndex]; + + // apply if (patchMode == PatchMode.Overlay) { + // merge pixels if (below.A < AssetDataForImage.MinOpacity || above.A == byte.MaxValue) mergedData[targetIndex] = above; - - // merge pixels else { // This performs a conventional alpha blend for the pixels, which are already @@ -236,22 +236,20 @@ internal class AssetDataForImage : AssetData, IAssetDataForImage } else { - // compute new alpha by subtracting mask alpha - int newAlphaInt = below.A - above.A; - - // fully masked out - if (newAlphaInt <= 0) - mergedData[targetIndex] = new Color((byte)0, (byte)0, (byte)0, (byte)0); - + // subtract mask alpha + int newAlpha = below.A - above.A; + if (newAlpha <= 0) + mergedData[targetIndex] = Color.Transparent; else { - // This blends the pixels by removing the alpha defined in the mask. - float scale = (float)newAlphaInt / below.A; + // Since the pixels are already premultiplied by the pipeline based on the + // alpha, rescale the RGB channels too to match the new alpha. + float scale = (float)newAlpha / below.A; mergedData[targetIndex] = new Color( r: (int)Math.Clamp(Math.Round(below.R * scale), 0, 255), g: (int)Math.Clamp(Math.Round(below.G * scale), 0, 255), b: (int)Math.Clamp(Math.Round(below.B * scale), 0, 255), - alpha: newAlphaInt + alpha: newAlpha ); } } diff --git a/src/SMAPI/PatchMode.cs b/src/SMAPI/PatchMode.cs index 669a0f03..352df15c 100644 --- a/src/SMAPI/PatchMode.cs +++ b/src/SMAPI/PatchMode.cs @@ -9,6 +9,7 @@ public enum PatchMode /// Draw the new content over the original content, so the original content shows through any transparent or semi-transparent pixels. Overlay, - /// Masks the original content so that the alpha value of every pixel in the new content gets subtracted from the corresponding pixel in the original content. + /// Apply the new content over the original content as a transparency mask. + /// This subtracts the alpha value of each pixel in the new content from the corresponding pixel in the original content. Colors in the new content are ignored. For example, a fully opaque pixel in the new content will result in a fully transparent pixel in the final image. Mask } From a482d77521f4ecaf3482cef548b0af53293c8e34 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Dec 2025 19:05:18 -0500 Subject: [PATCH 30/43] update release notes --- docs/release-notes.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 1e66c94d..8c00b0de 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,7 +6,11 @@ * Fixed `player_add` and `list_items` console commands not including some newer juice items. * For mod authors: - * Map tilesheet paths can now start with `../` to reference an asset under the `Content` folder that's not in `Content/Maps`. + * Added [input API to send button presses to the game](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#Send_input) (thanks to martiandweller!). + * Added `PatchMode.Mask` option when editing textures, which lets you apply a transparency mask to the target texture (thanks to PinkSerenity!). + * Added support for map tilesheet paths starting with `../` to reference an asset under the `Content` folder that's not in `Content/Maps` (thanks to Spiderbuttons!). + * Improved performance when propagating localized assets in some cases (thanks to SinZ!). + * Fixed farmhouse map edits sometimes removing the spouse room (thanks to SinZ!). * For the web UI: * Improved mod compatibility list: From 50e364d297d72b597d32732f2403acaa1ceee62c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Dec 2025 22:39:49 -0500 Subject: [PATCH 31/43] improve error-handling during asset propagation --- docs/release-notes.md | 3 +- src/SMAPI/Framework/ContentCoordinator.cs | 2 +- src/SMAPI/Metadata/CoreAssetPropagator.cs | 61 +++++++++-------------- 3 files changed, 27 insertions(+), 39 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 8c00b0de..b229aa5d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,13 +4,14 @@ ## Upcoming release * For players: * Fixed `player_add` and `list_items` console commands not including some newer juice items. + * Fixed farmhouse map edits sometimes removing the spouse room (thanks to SinZ!). * For mod authors: * Added [input API to send button presses to the game](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#Send_input) (thanks to martiandweller!). * Added `PatchMode.Mask` option when editing textures, which lets you apply a transparency mask to the target texture (thanks to PinkSerenity!). * Added support for map tilesheet paths starting with `../` to reference an asset under the `Content` folder that's not in `Content/Maps` (thanks to Spiderbuttons!). * Improved performance when propagating localized assets in some cases (thanks to SinZ!). - * Fixed farmhouse map edits sometimes removing the spouse room (thanks to SinZ!). + * Improved error-handling during asset propagation. * For the web UI: * Improved mod compatibility list: diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 0780a431..15ac93f2 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -452,7 +452,7 @@ internal class ContentCoordinator : IDisposable contentManagers: this.ContentManagers, assets: invalidatedAssets.ToDictionary(p => p.Key, p => p.Value), ignoreWorld: Context.IsWorldFullyUnloaded, - out IDictionary propagated, + out Dictionary propagated, out bool updatedWarpRoutes ); diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 250f8cec..3d89cbc8 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -74,54 +74,41 @@ internal class CoreAssetPropagator /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. /// A lookup of asset names to whether they've been propagated. /// Whether the NPC pathfinding warp route cache was reloaded. - public void Propagate(IList contentManagers, IDictionary assets, bool ignoreWorld, out IDictionary propagatedAssets, out bool changedWarpRoutes) + public void Propagate(IList contentManagers, IDictionary assets, bool ignoreWorld, out Dictionary propagatedAssets, out bool changedWarpRoutes) { - // get base name lookup - propagatedAssets = assets - .Select(asset => asset.Key.GetBaseAssetName()) - .Distinct() - .ToDictionary(name => name, _ => false); + propagatedAssets = new Dictionary(assets.Count); - // edit textures in-place + // propagate each asset + changedWarpRoutes = false; { - IAssetName[] textureAssets = assets - .Where(p => typeof(Texture2D).IsAssignableFrom(p.Value)) - .Select(p => p.Key) - .ToArray(); + Type imageType = typeof(Texture2D); - if (textureAssets.Any()) + foreach ((IAssetName assetName, Type assetType) in assets) { - foreach (IAssetName assetName in textureAssets) + bool changed = false; + + try { - bool changed = this.PropagateTexture(assetName, contentManagers, ignoreWorld); - if (changed) - propagatedAssets[assetName] = true; + // image + if (imageType.IsAssignableFrom(assetType)) + changed = this.PropagateTexture(assetName, contentManagers, ignoreWorld); + + // any other type + else + { + changed = this.PropagateOther(assetName, assetType, ignoreWorld, out bool curChangedMapRoutes); + changedWarpRoutes |= curChangedMapRoutes; + } + } + catch (Exception ex) + { + this.Monitor.Log($"An error occurred while propagating changes to asset '{assetName.Name}'. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); } - foreach (IAssetName assetName in textureAssets) - assets.Remove(assetName); + propagatedAssets[assetName] = changed; } } - // reload other assets - changedWarpRoutes = false; - foreach (var entry in assets) - { - bool changed = false; - bool curChangedMapRoutes = false; - try - { - changed = this.PropagateOther(entry.Key, entry.Value, ignoreWorld, out curChangedMapRoutes); - } - catch (Exception ex) - { - this.Monitor.Log($"An error occurred while propagating asset changes. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); - } - - propagatedAssets[entry.Key] = changed; - changedWarpRoutes = changedWarpRoutes || curChangedMapRoutes; - } - // reload NPC pathfinding cache if any map routes changed if (changedWarpRoutes) WarpPathfindingCache.PopulateCache(); From 9bf37463e052215f3ee8761448b1d4ee72625a4e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Dec 2025 22:39:49 -0500 Subject: [PATCH 32/43] move map asset propagation into its own method (#896) --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 144 +++++++++++----------- 1 file changed, 73 insertions(+), 71 deletions(-) diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 3d89cbc8..5e2d41f2 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -82,6 +82,7 @@ internal class CoreAssetPropagator changedWarpRoutes = false; { Type imageType = typeof(Texture2D); + Type mapType = typeof(Map); foreach ((IAssetName assetName, Type assetType) in assets) { @@ -93,12 +94,16 @@ internal class CoreAssetPropagator if (imageType.IsAssignableFrom(assetType)) changed = this.PropagateTexture(assetName, contentManagers, ignoreWorld); - // any other type - else + // map + else if (assetType == mapType) { - changed = this.PropagateOther(assetName, assetType, ignoreWorld, out bool curChangedMapRoutes); + changed = this.PropagateMap(assetName, ignoreWorld, out bool curChangedMapRoutes); changedWarpRoutes |= curChangedMapRoutes; } + + // any other type + else + changed = this.PropagateOther(assetName, ignoreWorld); } catch (Exception ex) { @@ -118,12 +123,58 @@ internal class CoreAssetPropagator /********* ** Private methods *********/ + /// Propagate changes to a map asset. + /// The asset name that changed. + /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. + /// Whether the locations reachable by warps from this location changed as part of this propagation. + /// Returns whether any assets were updated. + private bool PropagateMap(IAssetName assetName, bool ignoreWorld, out bool changedWarpRoutes) + { + bool changed = false; + changedWarpRoutes = false; + + if (!ignoreWorld) + { + foreach (LocationInfo info in this.GetLocationsWithInfo()) + { + GameLocation location = info.Location; + + if (this.IsSameBaseName(assetName, location.mapPath.Value)) + { + static ISet GetWarpSet(GameLocation location) + { + HashSet targetNames = []; + + foreach (Warp warp in location.warps) + targetNames.Add(warp.TargetName); + + if (location.doors?.Count() > 0) + { + foreach (string targetName in location.doors.Values) + targetNames.Add(targetName); + } + + return targetNames; + } + + var oldWarps = GetWarpSet(location); + this.UpdateMap(info); + var newWarps = GetWarpSet(location); + + changedWarpRoutes = changedWarpRoutes || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p)); + changed = true; + } + } + } + + return changed; + } + /// Propagate changes to a cached texture asset. - /// The asset name to reload. + /// The asset name that changed. /// The content managers whose assets to update. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. - /// Returns whether an asset was loaded. - [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] + /// Returns whether any assets were updated. private bool PropagateTexture(IAssetName assetName, IList contentManagers, bool ignoreWorld) { bool changed = false; @@ -218,69 +269,20 @@ internal class CoreAssetPropagator return changed; } - /// Reload one of the game's core assets (if applicable). - /// The asset name to reload. - /// The asset type to reload. + /// Propagate changes to an asset which isn't a map (handled by ) or texture (handled by ). + /// The asset name that changed. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. - /// Whether the locations reachable by warps from this location changed as part of this propagation. - /// Returns whether an asset was loaded. + /// Returns whether any assets were updated. [SuppressMessage("ReSharper", "StringLiteralTypo", Justification = "These deliberately match the asset names.")] - private bool PropagateOther(IAssetName assetName, Type type, bool ignoreWorld, out bool changedWarpRoutes) + private bool PropagateOther(IAssetName assetName, bool ignoreWorld) { - bool changed = false; var content = this.MainContentManager; - string key = assetName.BaseName; - changedWarpRoutes = false; + string baseName = assetName.BaseName; - /**** - ** Propagate map changes - ****/ - if (type == typeof(Map)) - { - if (!ignoreWorld) - { - foreach (LocationInfo info in this.GetLocationsWithInfo()) - { - GameLocation location = info.Location; - - if (this.IsSameBaseName(assetName, location.mapPath.Value)) - { - static ISet GetWarpSet(GameLocation location) - { - HashSet targetNames = []; - - foreach (Warp warp in location.warps) - targetNames.Add(warp.TargetName); - - if (location.doors?.Count() > 0) - { - foreach (string targetName in location.doors.Values) - targetNames.Add(targetName); - } - - return targetNames; - } - - var oldWarps = GetWarpSet(location); - this.UpdateMap(info); - var newWarps = GetWarpSet(location); - - changedWarpRoutes = changedWarpRoutes || oldWarps.Count != newWarps.Count || oldWarps.Any(p => !newWarps.Contains(p)); - changed = true; - } - } - } - - return changed; - } - - /**** - ** Propagate by key - ****/ - switch (assetName.BaseName.ToLower().Replace("\\", "/")) // normalized key so we can compare statically + switch (baseName.ToLower().Replace("\\", "/")) // normalized key so we can compare statically { /**** - ** Content\Data + ** Content/Data ****/ case "data/achievements": // Game1.LoadContent Game1.achievements = DataLoader.Achievements(content); @@ -369,7 +371,7 @@ internal class CoreAssetPropagator return true; case "data/hairdata": // Farmer.GetHairStyleMetadataFile - return changed | this.UpdateHairData(); + return this.UpdateHairData(); case "data/hats": // HatDataDefinition ItemRegistry.ResetCache(); @@ -439,25 +441,25 @@ internal class CoreAssetPropagator return true; /**** - ** Content\Fonts + ** Content/Fonts ****/ case "fonts/spritefont1": // Game1.LoadContent - Game1.dialogueFont = content.Load(key); + Game1.dialogueFont = content.Load(baseName); return true; case "fonts/smallfont": // Game1.LoadContent - Game1.smallFont = content.Load(key); + Game1.smallFont = content.Load(baseName); return true; case "fonts/tinyfont": // Game1.LoadContent - Game1.tinyFont = content.Load(key); + Game1.tinyFont = content.Load(baseName); return true; /**** - ** Content\Strings + ** Content/Strings ****/ case "strings/stringsfromcsfiles": - return changed | this.UpdateStringsFromCsFiles(content); + return this.UpdateStringsFromCsFiles(content); /**** ** Dynamic keys @@ -466,10 +468,10 @@ internal class CoreAssetPropagator if (!ignoreWorld) { if (assetName.IsDirectlyUnderPath("Characters/Dialogue")) - return changed | this.UpdateNpcDialogue(assetName); + return this.UpdateNpcDialogue(assetName); if (assetName.IsDirectlyUnderPath("Characters/schedules")) - return changed | this.UpdateNpcSchedules(assetName); + return this.UpdateNpcSchedules(assetName); } return false; From 2cb7e688512a3c7cabc38aa085c51c6e6979c2dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 28 Dec 2025 22:39:49 -0500 Subject: [PATCH 33/43] add asset propagation for spouse room map edits (#896) --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 53 ++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index b229aa5d..13017cfc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * Added [input API to send button presses to the game](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#Send_input) (thanks to martiandweller!). * Added `PatchMode.Mask` option when editing textures, which lets you apply a transparency mask to the target texture (thanks to PinkSerenity!). * Added support for map tilesheet paths starting with `../` to reference an asset under the `Content` folder that's not in `Content/Maps` (thanks to Spiderbuttons!). + * Added asset propagation for spouse room map edits. * Improved performance when propagating localized assets in some cases (thanks to SinZ!). * Improved error-handling during asset propagation. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 5e2d41f2..a8cb5fdc 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -11,6 +11,7 @@ using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Internal; using StardewValley; using StardewValley.Buildings; +using StardewValley.GameData.Characters; using StardewValley.Locations; using StardewValley.Pathfinding; using StardewValley.TerrainFeatures; @@ -83,6 +84,7 @@ internal class CoreAssetPropagator { Type imageType = typeof(Texture2D); Type mapType = typeof(Map); + Dictionary? spouseRoomMapPathCache = null; // constructed later if needed foreach ((IAssetName assetName, Type assetType) in assets) { @@ -97,7 +99,7 @@ internal class CoreAssetPropagator // map else if (assetType == mapType) { - changed = this.PropagateMap(assetName, ignoreWorld, out bool curChangedMapRoutes); + changed = this.PropagateMap(assetName, ref spouseRoomMapPathCache, ignoreWorld, out bool curChangedMapRoutes); changedWarpRoutes |= curChangedMapRoutes; } @@ -125,10 +127,11 @@ internal class CoreAssetPropagator *********/ /// Propagate changes to a map asset. /// The asset name that changed. + /// A cache of spouse room map path lookups by farmhouse or cabin instance. This will be created the first time it's needed. /// Whether the in-game world is fully unloaded (e.g. on the title screen), so there's no need to propagate changes into the world. /// Whether the locations reachable by warps from this location changed as part of this propagation. /// Returns whether any assets were updated. - private bool PropagateMap(IAssetName assetName, bool ignoreWorld, out bool changedWarpRoutes) + private bool PropagateMap(IAssetName assetName, ref Dictionary? spouseRoomMapPathCache, bool ignoreWorld, out bool changedWarpRoutes) { bool changed = false; changedWarpRoutes = false; @@ -139,7 +142,20 @@ internal class CoreAssetPropagator { GameLocation location = info.Location; - if (this.IsSameBaseName(assetName, location.mapPath.Value)) + bool shouldUpdateMap = + // edited this map + this.IsSameBaseName(assetName, location.mapPath.Value) + + // edited spouse room for this farmhouse + || ( + location is FarmHouse farmhouse + && this.IsSameBaseName( + assetName, + this.GetDisplayedSpouseRoomPath(farmhouse, ref spouseRoomMapPathCache) + ) + ); + + if (shouldUpdateMap) { static ISet GetWarpSet(GameLocation location) { @@ -687,9 +703,7 @@ internal class CoreAssetPropagator // This must happen after updating warps (since some map modifications like the community shortcuts add warps) // and after resetting interior doors' state (so they apply their modifications too). if (location is FarmHouse) - { - this.Reflection.GetField(location, "displayingSpouseRoom", true).SetValue(false); - } + this.Reflection.GetField(location, "displayingSpouseRoom").SetValue(false); location.MakeMapModifications(force: true); // reset player position @@ -797,6 +811,33 @@ internal class CoreAssetPropagator }); } + /// Get the asset name for a farmhouse's spouse room, if it's currently displaying one. + /// The farmhouse whose spouse room to get. + /// A cache of spouse room map path lookups by farmhouse or cabin instance. This is created if it's null. + private string? GetDisplayedSpouseRoomPath(FarmHouse farmhouse, ref Dictionary? cache) + { + // from cache + if (cache is null) + cache = []; + else if (cache.TryGetValue(farmhouse, out string? spouseRoomMapPath)) + return spouseRoomMapPath; + + // no spouse room shown + Farmer? owner = farmhouse.owner; + if (owner?.spouse is null || !this.Reflection.GetField(farmhouse, "displayingSpouseRoom").GetValue()) + { + cache[farmhouse] = null; + return null; + } + + // else get map path + string mapPath = NPC.TryGetData(owner.spouse, out CharacterData? spouseData) && spouseData?.SpouseRoom?.MapAsset is { } mapName + ? $"Maps/{mapName}" + : "Maps/spouseRooms"; + cache[farmhouse] = mapPath; + return mapPath; + } + /// Get whether two asset names are equivalent if you ignore the locale code. /// The first value to compare. /// The second value to compare. From 8157b845c97109edae584af9e9c1a784a8b1ab52 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 29 Dec 2025 01:40:29 -0500 Subject: [PATCH 34/43] update web & test projects to .NET 10 --- src/Directory.Packages.props | 2 +- .../SMAPI.Tests.ModApiConsumer.csproj | 2 +- .../SMAPI.Tests.ModApiProvider.csproj | 2 +- src/SMAPI.Tests/SMAPI.Tests.csproj | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 943d16eb..e89b6a0a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -34,7 +34,7 @@ - + diff --git a/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj b/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj index b4b1a14e..722a0203 100644 --- a/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj +++ b/src/SMAPI.Tests.ModApiConsumer/SMAPI.Tests.ModApiConsumer.csproj @@ -1,6 +1,6 @@  - net6.0 + net10.0 diff --git a/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj b/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj index b4b1a14e..722a0203 100644 --- a/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj +++ b/src/SMAPI.Tests.ModApiProvider/SMAPI.Tests.ModApiProvider.csproj @@ -1,6 +1,6 @@  - net6.0 + net10.0 diff --git a/src/SMAPI.Tests/SMAPI.Tests.csproj b/src/SMAPI.Tests/SMAPI.Tests.csproj index 9ca9f41b..c3dc17de 100644 --- a/src/SMAPI.Tests/SMAPI.Tests.csproj +++ b/src/SMAPI.Tests/SMAPI.Tests.csproj @@ -1,6 +1,6 @@  - net6.0 + net10.0 diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 85c9594d..1f8421fb 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -2,7 +2,7 @@ SMAPI.Web StardewModdingAPI.Web - net8.0 + net10.0 latest From 45381add1bf2d65d0a7bd92ce85ca8db60844874 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 29 Dec 2025 01:40:29 -0500 Subject: [PATCH 35/43] update NuGet dependencies --- docs/release-notes.md | 3 +++ src/Directory.Packages.props | 26 +++++++++++++------------- src/SMAPI.Web/BackgroundService.cs | 1 - src/SMAPI.Web/Views/Mods/Index.cshtml | 1 - 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 13017cfc..79d1ec63 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,9 @@ * Added asset propagation for spouse room map edits. * Improved performance when propagating localized assets in some cases (thanks to SinZ!). * Improved error-handling during asset propagation. + * Updated dependencies, including... + * [Newtonsoft.Json](https://www.newtonsoft.com/json) 13.0.3 → 13.0.4 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.4)); + * [Pintail](https://github.com/Nanoray-pl/Pintail) 2.8.1 → 2.9.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#291)). * For the web UI: * Improved mod compatibility list: diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index e89b6a0a..6cf3719c 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -5,35 +5,35 @@ - + - + - + - + - + - - + + - - + + - - - - + + + + diff --git a/src/SMAPI.Web/BackgroundService.cs b/src/SMAPI.Web/BackgroundService.cs index 4849bbb2..23838254 100644 --- a/src/SMAPI.Web/BackgroundService.cs +++ b/src/SMAPI.Web/BackgroundService.cs @@ -6,7 +6,6 @@ using Hangfire; using Hangfire.Console; using Hangfire.Server; using Humanizer; -using Humanizer.Localisation; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using StardewModdingAPI.Toolkit; diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 4577e33e..f3e0e500 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -1,5 +1,4 @@ @using Humanizer -@using Humanizer.Localisation @using StardewModdingAPI.Web.Framework @model StardewModdingAPI.Web.ViewModels.ModListModel @{ From e229801d95e4b2b6e52b88baa14e2de22a66a77e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Jan 2026 20:41:07 -0500 Subject: [PATCH 36/43] fix installer error if Steam game path in registry is blank --- docs/release-notes.md | 1 + .../Framework/GameScanning/GameScanner.cs | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 79d1ec63..c960d401 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * Fixed `player_add` and `list_items` console commands not including some newer juice items. * Fixed farmhouse map edits sometimes removing the spouse room (thanks to SinZ!). + * Fixed installer error if Steam has an empty game path saved to the registry. * For mod authors: * Added [input API to send button presses to the game](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#Send_input) (thanks to martiandweller!). diff --git a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs index ae033333..ff28a3dc 100644 --- a/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs +++ b/src/SMAPI.Toolkit/Framework/GameScanning/GameScanner.cs @@ -62,7 +62,17 @@ public class GameScanner // yield valid folders foreach (string path in paths) { - DirectoryInfo folder = new(path); + DirectoryInfo folder; + + try + { + folder = new DirectoryInfo(path); + } + catch (ArgumentException) + { + continue; // invalid path format + } + if (folder.Exists) yield return (folder, this.GetGameFolderType(folder)); } @@ -159,7 +169,7 @@ public class GameScanner // via Steam library path string? steamPath = this.GetCurrentUserRegistryValue(@"Software\Valve\Steam", "SteamPath"); - if (steamPath != null) + if (!string.IsNullOrWhiteSpace(steamPath)) { // conventional path yield return Path.Combine(steamPath.Replace('/', '\\'), @"steamapps\common\Stardew Valley"); From ea139a32aae0e0c89ca111ff16f5f22637e1f39f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Jan 2026 20:41:07 -0500 Subject: [PATCH 37/43] drop Win32_OperatingSystem query for OS name In newer .NET versions, this fails with a PlatformNotSupportedException ("System.Management currently is only supported for Windows desktop applications"). --- build/deploy-local-smapi.targets | 3 --- build/unix/prepare-install-package.sh | 5 ----- build/windows/prepare-install-package.ps1 | 5 ----- docs/release-notes.md | 1 + src/Directory.Packages.props | 1 - .../Framework/LowLevelEnvironmentUtility.cs | 21 ++----------------- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 1 - 7 files changed, 3 insertions(+), 34 deletions(-) diff --git a/build/deploy-local-smapi.targets b/build/deploy-local-smapi.targets index 7829a6fb..14c4d816 100644 --- a/build/deploy-local-smapi.targets +++ b/build/deploy-local-smapi.targets @@ -39,9 +39,6 @@ This assumes `find-game-folder.targets` has already been imported and validated. - - -
diff --git a/build/unix/prepare-install-package.sh b/build/unix/prepare-install-package.sh index 44987b66..837f8884 100755 --- a/build/unix/prepare-install-package.sh +++ b/build/unix/prepare-install-package.sh @@ -172,11 +172,6 @@ for folder in ${folders[@]}; do cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config" fi - # copy .NET dependencies - if [ $folder == "windows" ]; then - cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal" - fi - # copy bundled mods for modName in ${bundleModNames[@]}; do fromPath="src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish" diff --git a/build/windows/prepare-install-package.ps1 b/build/windows/prepare-install-package.ps1 index c79ac5f0..a5a6b043 100644 --- a/build/windows/prepare-install-package.ps1 +++ b/build/windows/prepare-install-package.ps1 @@ -197,11 +197,6 @@ foreach ($folder in $folders) { cp "$installAssets/windows-exe-config.xml" "$bundlePath/StardewModdingAPI.exe.config" } - # copy .NET dependencies - if ($folder -eq "windows") { - cp "$smapiBin/System.Management.dll" "$bundlePath/smapi-internal" - } - # copy bundled mods foreach ($modName in $bundleModNames) { $fromPath = "src/SMAPI.Mods.$modName/bin/$buildConfig/$runtime/publish" diff --git a/docs/release-notes.md b/docs/release-notes.md index c960d401..74cd4bdb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,7 @@ * Updated dependencies, including... * [Newtonsoft.Json](https://www.newtonsoft.com/json) 13.0.3 → 13.0.4 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.4)); * [Pintail](https://github.com/Nanoray-pl/Pintail) 2.8.1 → 2.9.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#291)). + * Removed `System.Management.dll`, which SMAPI no longer uses. * For the web UI: * Improved mod compatibility list: diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 6cf3719c..28b4f77e 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -15,7 +15,6 @@ - diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index 9afbf6f6..d3b37a1e 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -1,10 +1,8 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; #if SMAPI_FOR_WINDOWS -using System.Management; #endif using System.Runtime.InteropServices; using StardewModdingAPI.Toolkit.Utilities; @@ -54,24 +52,8 @@ internal static class LowLevelEnvironmentUtility /// The current platform. public static string GetFriendlyPlatformName(string platform) { -#if SMAPI_FOR_WINDOWS - try - { - string? result = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") - .Get() - .Cast() - .Select(entry => entry.GetPropertyValue("Caption").ToString()) - .FirstOrDefault(); - - return result ?? "Windows"; - } - catch - { - // fallback to default behavior - } -#endif - string name = Environment.OSVersion.ToString(); + switch (platform) { case nameof(Platform.Android): @@ -82,6 +64,7 @@ internal static class LowLevelEnvironmentUtility name = $"macOS {name}"; break; } + return name; } diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 34dd3aaf..6766cb25 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -15,7 +15,6 @@ - From e0220478ed3656366a86c3f6bc4a9e46228e0ef3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Jan 2026 20:41:07 -0500 Subject: [PATCH 38/43] add friendly OS name for Windows 10/11 in log --- docs/release-notes.md | 1 + .../Framework/LowLevelEnvironmentUtility.cs | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docs/release-notes.md b/docs/release-notes.md index 74cd4bdb..f7d705bb 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming release * For players: + * The SMAPI console/log now shows "Windows 10" or "Windows 11" in the log instead of its internal OS identifier. * Fixed `player_add` and `list_items` console commands not including some newer juice items. * Fixed farmhouse map edits sometimes removing the spouse room (thanks to SinZ!). * Fixed installer error if Steam has an empty game path saved to the registry. diff --git a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs index d3b37a1e..dc920550 100644 --- a/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs +++ b/src/SMAPI.Toolkit/Framework/LowLevelEnvironmentUtility.cs @@ -63,6 +63,15 @@ internal static class LowLevelEnvironmentUtility case nameof(Platform.Mac): name = $"macOS {name}"; break; + + case nameof(Platform.Windows): + // version 10 + 11 + if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version is { Major: 10, Minor: 0 } version) + { + int mainVersion = version.Build switch { < 22000 => 10, _ => 11 }; + name = $"Windows {mainVersion} ({name.Replace("Microsoft Windows NT ", "")})"; + } + break; } return name; From 278d14f444c8d1b47c2a8953c5027a25956eea76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Jan 2026 23:02:41 -0500 Subject: [PATCH 39/43] rename solution file to *.slnx --- src/{SMAPI.sln => SMAPI.slnx} | 0 src/{SMAPI.sln.DotSettings => SMAPI.slnx.DotSettings} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{SMAPI.sln => SMAPI.slnx} (100%) rename src/{SMAPI.sln.DotSettings => SMAPI.slnx.DotSettings} (100%) diff --git a/src/SMAPI.sln b/src/SMAPI.slnx similarity index 100% rename from src/SMAPI.sln rename to src/SMAPI.slnx diff --git a/src/SMAPI.sln.DotSettings b/src/SMAPI.slnx.DotSettings similarity index 100% rename from src/SMAPI.sln.DotSettings rename to src/SMAPI.slnx.DotSettings From 11c69bf30e0d48a2278a4505fa35f4cb0fc84949 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 4 Jan 2026 23:02:41 -0500 Subject: [PATCH 40/43] migrate solution to *.slnx format --- src/SMAPI.slnx | 262 +++++++++++++------------------------------------ 1 file changed, 68 insertions(+), 194 deletions(-) diff --git a/src/SMAPI.slnx b/src/SMAPI.slnx index a20c18e2..2e93538e 100644 --- a/src/SMAPI.slnx +++ b/src/SMAPI.slnx @@ -1,194 +1,68 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31912.275 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".root", ".root", "{86C452BE-D2D8-45B4-B63F-E329EB06CEDA}" - ProjectSection(SolutionItems) = preProject - ..\.editorconfig = ..\.editorconfig - ..\.gitattributes = ..\.gitattributes - ..\.gitignore = ..\.gitignore - Directory.Packages.props = Directory.Packages.props - ..\LICENSE.txt = ..\LICENSE.txt - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{4B1CEB70-F756-4A57-AAE8-8CD78C475F25}" - ProjectSection(SolutionItems) = preProject - ..\.github\CONTRIBUTING.md = ..\.github\CONTRIBUTING.md - ..\.github\SUPPORT.md = ..\.github\SUPPORT.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ISSUE_TEMPLATE", "ISSUE_TEMPLATE", "{F4453AB6-D7D6-447F-A973-956CC777968F}" - ProjectSection(SolutionItems) = preProject - ..\.github\ISSUE_TEMPLATE\config.yml = ..\.github\ISSUE_TEMPLATE\config.yml - ..\.github\ISSUE_TEMPLATE\custom.md = ..\.github\ISSUE_TEMPLATE\custom.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{09CF91E5-5BAB-4650-A200-E5EA9A633046}" - ProjectSection(SolutionItems) = preProject - ..\build\common.targets = ..\build\common.targets - ..\build\deploy-local-smapi.targets = ..\build\deploy-local-smapi.targets - ..\build\find-game-folder.targets = ..\build\find-game-folder.targets - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{EB35A917-67B9-4EFA-8DFC-4FB49B3949BB}" - ProjectSection(SolutionItems) = preProject - ..\docs\mod-build-config.md = ..\docs\mod-build-config.md - ..\docs\README.md = ..\docs\README.md - ..\docs\release-notes-archived.md = ..\docs\release-notes-archived.md - ..\docs\release-notes.md = ..\docs\release-notes.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "technical", "technical", "{5947303D-3512-413A-9009-7AC43F5D3513}" - ProjectSection(SolutionItems) = preProject - ..\docs\technical\mod-package-release-notes.md = ..\docs\technical\mod-package-release-notes.md - ..\docs\technical\mod-package.md = ..\docs\technical\mod-package.md - ..\docs\technical\smapi.md = ..\docs\technical\smapi.md - ..\docs\technical\web.md = ..\docs\technical\web.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Internal", "Internal", "{82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mods", "Mods", "{AE9A4D46-E910-4293-8BC4-673F85FFF6A5}" -EndProject -Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SMAPI.Internal", "SMAPI.Internal\SMAPI.Internal.shproj", "{85208F8D-6FD1-4531-BE05-7142490F59FE}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer.Tests", "SMAPI.ModBuildConfig.Analyzer.Tests\SMAPI.ModBuildConfig.Analyzer.Tests.csproj", "{680B2641-81EA-467C-86A5-0E81CDC57ED0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests", "SMAPI.Tests\SMAPI.Tests.csproj", "{AA95884B-7097-476E-92C8-D0500DE9D6D1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI", "SMAPI\SMAPI.csproj", "{E6DA2198-7686-4F1D-B312-4A4DC70884C0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Installer", "SMAPI.Installer\SMAPI.Installer.csproj", "{0A9BB24F-15FF-4C26-B1A2-81F7AE316518}" - ProjectSection(ProjectDependencies) = postProject - {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} - {CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {CD53AD6F-97F4-4872-A212-50C2A0FD3601} - {E6DA2198-7686-4F1D-B312-4A4DC70884C0} = {E6DA2198-7686-4F1D-B312-4A4DC70884C0} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig", "SMAPI.ModBuildConfig\SMAPI.ModBuildConfig.csproj", "{1B3821E6-D030-402C-B3A1-7CA45C2800EA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.ModBuildConfig.Analyzer", "SMAPI.ModBuildConfig.Analyzer\SMAPI.ModBuildConfig.Analyzer.csproj", "{517677D7-7299-426F-B1A3-47BDCC2F1214}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.ConsoleCommands", "SMAPI.Mods.ConsoleCommands\SMAPI.Mods.ConsoleCommands.csproj", "{0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Mods.SaveBackup", "SMAPI.Mods.SaveBackup\SMAPI.Mods.SaveBackup.csproj", "{CD53AD6F-97F4-4872-A212-50C2A0FD3601}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit", "SMAPI.Toolkit\SMAPI.Toolkit.csproj", "{08184F74-60AD-4EEE-A78C-F4A35ADE6246}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Toolkit.CoreInterfaces", "SMAPI.Toolkit.CoreInterfaces\SMAPI.Toolkit.CoreInterfaces.csproj", "{ED8E41FA-DDFA-4A77-932E-9853D279A129}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web", "SMAPI.Web\SMAPI.Web.csproj", "{80EFD92F-728F-41E0-8A5B-9F6F49A91899}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "windows", "windows", "{4D661178-38FB-43E4-AA5F-9B0406919344}" - ProjectSection(SolutionItems) = preProject - ..\build\windows\finalize-install-package.sh = ..\build\windows\finalize-install-package.sh - ..\build\windows\prepare-install-package.ps1 = ..\build\windows\prepare-install-package.ps1 - ..\build\windows\set-smapi-version.ps1 = ..\build\windows\set-smapi-version.ps1 - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "unix", "unix", "{CAA1488E-842B-433D-994D-1D3D0B5DD125}" - ProjectSection(SolutionItems) = preProject - ..\build\unix\prepare-install-package.sh = ..\build\unix\prepare-install-package.sh - ..\build\unix\set-smapi-version.sh = ..\build\unix\set-smapi-version.sh - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{3B5BF14D-F612-4C83-9EF6-E3EBFCD08766}" - ProjectSection(SolutionItems) = preProject - ..\build\windows\lib\in-place-regex.ps1 = ..\build\windows\lib\in-place-regex.ps1 - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests.ModApiProvider", "SMAPI.Tests.ModApiProvider\SMAPI.Tests.ModApiProvider.csproj", "{239AEEAC-07D1-4A3F-AA99-8C74F5038F50}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Tests.ModApiConsumer", "SMAPI.Tests.ModApiConsumer\SMAPI.Tests.ModApiConsumer.csproj", "{2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}" -EndProject -Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5 - SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5 - SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13 - SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5 - SMAPI.Internal\SMAPI.Internal.projitems*{e6da2198-7686-4f1d-b312-4a4dc70884c0}*SharedItemsImports = 5 - EndGlobalSection - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {680B2641-81EA-467C-86A5-0E81CDC57ED0}.Release|Any CPU.Build.0 = Release|Any CPU - {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA95884B-7097-476E-92C8-D0500DE9D6D1}.Release|Any CPU.Build.0 = Release|Any CPU - {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E6DA2198-7686-4F1D-B312-4A4DC70884C0}.Release|Any CPU.Build.0 = Release|Any CPU - {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0A9BB24F-15FF-4C26-B1A2-81F7AE316518}.Release|Any CPU.Build.0 = Release|Any CPU - {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1B3821E6-D030-402C-B3A1-7CA45C2800EA}.Release|Any CPU.Build.0 = Release|Any CPU - {517677D7-7299-426F-B1A3-47BDCC2F1214}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {517677D7-7299-426F-B1A3-47BDCC2F1214}.Debug|Any CPU.Build.0 = Debug|Any CPU - {517677D7-7299-426F-B1A3-47BDCC2F1214}.Release|Any CPU.ActiveCfg = Release|Any CPU - {517677D7-7299-426F-B1A3-47BDCC2F1214}.Release|Any CPU.Build.0 = Release|Any CPU - {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F}.Release|Any CPU.Build.0 = Release|Any CPU - {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CD53AD6F-97F4-4872-A212-50C2A0FD3601}.Release|Any CPU.Build.0 = Release|Any CPU - {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08184F74-60AD-4EEE-A78C-F4A35ADE6246}.Release|Any CPU.Build.0 = Release|Any CPU - {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Debug|Any CPU.Build.0 = Debug|Any CPU - {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Release|Any CPU.ActiveCfg = Release|Any CPU - {ED8E41FA-DDFA-4A77-932E-9853D279A129}.Release|Any CPU.Build.0 = Release|Any CPU - {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Debug|Any CPU.Build.0 = Debug|Any CPU - {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.ActiveCfg = Release|Any CPU - {80EFD92F-728F-41E0-8A5B-9F6F49A91899}.Release|Any CPU.Build.0 = Release|Any CPU - {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Debug|Any CPU.Build.0 = Debug|Any CPU - {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Release|Any CPU.ActiveCfg = Release|Any CPU - {239AEEAC-07D1-4A3F-AA99-8C74F5038F50}.Release|Any CPU.Build.0 = Release|Any CPU - {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {4B1CEB70-F756-4A57-AAE8-8CD78C475F25} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} - {F4453AB6-D7D6-447F-A973-956CC777968F} = {4B1CEB70-F756-4A57-AAE8-8CD78C475F25} - {09CF91E5-5BAB-4650-A200-E5EA9A633046} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} - {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} = {86C452BE-D2D8-45B4-B63F-E329EB06CEDA} - {5947303D-3512-413A-9009-7AC43F5D3513} = {EB35A917-67B9-4EFA-8DFC-4FB49B3949BB} - {85208F8D-6FD1-4531-BE05-7142490F59FE} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {680B2641-81EA-467C-86A5-0E81CDC57ED0} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {AA95884B-7097-476E-92C8-D0500DE9D6D1} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {0634EA4C-3B8F-42DB-AEA6-CA9E4EF6E92F} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} - {CD53AD6F-97F4-4872-A212-50C2A0FD3601} = {AE9A4D46-E910-4293-8BC4-673F85FFF6A5} - {4D661178-38FB-43E4-AA5F-9B0406919344} = {09CF91E5-5BAB-4650-A200-E5EA9A633046} - {CAA1488E-842B-433D-994D-1D3D0B5DD125} = {09CF91E5-5BAB-4650-A200-E5EA9A633046} - {3B5BF14D-F612-4C83-9EF6-E3EBFCD08766} = {4D661178-38FB-43E4-AA5F-9B0406919344} - {239AEEAC-07D1-4A3F-AA99-8C74F5038F50} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - {2A4DF030-E8B1-4BBD-AA93-D4DE68CB9D85} = {82D22ED7-A0A7-4D64-8E92-4B6A5E74ED11} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {70143042-A862-47A8-A677-7C819DDC90DC} - EndGlobalSection -EndGlobal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 597438ff5f6ffc5faa3d4cf3568c2064fb6c0fd4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 5 Jan 2026 20:42:38 -0500 Subject: [PATCH 41/43] add set_verbose console command --- docs/release-notes.md | 1 + .../Commands/Other/SetVerboseCommand.cs | 123 ++++++++++++++++++ src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 16 ++- src/SMAPI/Framework/Logging/LogManager.cs | 2 +- src/SMAPI/Framework/Monitor.cs | 17 ++- 5 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/SetVerboseCommand.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index f7d705bb..2c3ff715 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming release * For players: * The SMAPI console/log now shows "Windows 10" or "Windows 11" in the log instead of its internal OS identifier. + * Added [`set_verbose` console command](https://stardewvalleywiki.com/Modding:Console_commands#set_verbose). * Fixed `player_add` and `list_items` console commands not including some newer juice items. * Fixed farmhouse map edits sometimes removing the spouse room (thanks to SinZ!). * Fixed installer error if Steam has an empty game path saved to the registry. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/SetVerboseCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/SetVerboseCommand.cs new file mode 100644 index 00000000..91b8b4cf --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/SetVerboseCommand.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using StardewModdingAPI.Framework; +using StardewValley.Extensions; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other; + +/// A command which toggles verbose mode in the SMAPI log. +internal class SetVerboseCommand : ConsoleCommand +{ + /********* + ** Fields + *********/ + /// The mod register with which to validate mod IDs. + private readonly IModRegistry ModRegistry; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod register with which to validate mod IDs. + public SetVerboseCommand(IModRegistry modRegistry) + : base( + name: "set_verbose", + description: + """ + Toggles whether more detailed information is written to the SMAPI log file (and console in developer mode). This may impact performance. This doesn't affect mods manually set to verbose in the config file. + + Usage: set_verbose + Toggles verbose logging for SMAPI and all mods. + + Usage: set_verbose [true|false] + Sets whether verbose logging is enabled (true) or disabled (false) for SMAPI and all mods. + + Usage: set_verbose [true|false] [modId]+ + Sets whether verbose logging is enabled (true) or disabled (false) for the specified mod IDs. You can specify 'SMAPI' to set it for SMAPI itself. + """, + mayNeedUpdate: true + ) + { + this.ModRegistry = modRegistry; + } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + // parse mode + bool setTo = !(Monitor.ForceVerboseLoggingForAll || Monitor.ForceVerboseLogging.Count > 0); + if (args.Count > 0) + { + if (int.TryParse(args[0], out int numeric) && numeric is 0 or 1) + setTo = numeric is 1; + else if (!bool.TryParse(args[0], out setTo)) + { + monitor.Log("Invalid argument: if specified, the first argument should be 'true' or 'false' to indicate whether to enable or disable verbose logging."); + return; + } + } + + // apply + if (args.Count is 0 or 1) + { + if (setTo) + { + Monitor.ForceVerboseLoggingForAll = true; + Monitor.ForceVerboseLogging.Clear(); + monitor.Log("Enabled verbose logs for SMAPI and all mods.", LogLevel.Info); + } + else + { + Monitor.ForceVerboseLoggingForAll = false; + Monitor.ForceVerboseLogging.Clear(); + monitor.Log("Reset to normal.", LogLevel.Info); + } + } + else + { + List toggled = []; + List unknown = []; + + for (int i = 1; i < args.Count; i++) + { + // get mod + string modId = args[i]; + IModInfo? mod; + if (modId.EqualsIgnoreCase("SMAPI")) + { + modId = "SMAPI"; + mod = null; + } + else + { + mod = this.ModRegistry.Get(modId); + if (mod is null) + { + unknown.Add(modId); + continue; + } + + modId = mod.Manifest.UniqueID; + } + + // toggle + Monitor.ForceVerboseLogging.Toggle(modId, setTo); + toggled.Add(mod is null ? "SMAPI" : mod.Manifest.Name); + } + + if (toggled.Count > 0) + monitor.Log($"{(setTo ? "Enabled" : "Disabled")} verbose logging for mod{(toggled.Count > 1 ? "s" : "")} '{string.Join("', '", toggled.OrderBy(p => p, StringComparer.OrdinalIgnoreCase))}'.", LogLevel.Info); + + if (unknown.Count > 0) + { + bool plural = unknown.Count > 1; + monitor.Log($"No mod{(plural ? "s" : "")} found with ID{(plural ? "s" : "")} '{string.Join("', '", unknown)}'.", LogLevel.Warn); + } + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs index 82f83273..29e0bb3a 100644 --- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; +using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other; namespace StardewModdingAPI.Mods.ConsoleCommands; @@ -63,10 +64,15 @@ public class ModEntry : Mod /// Find all commands in the assembly. private IEnumerable ScanForCommands() { - return ( - from type in this.GetType().Assembly.GetTypes() - where !type.IsAbstract && typeof(IConsoleCommand).IsAssignableFrom(type) - select (IConsoleCommand)Activator.CreateInstance(type)! - ); + foreach (Type type in this.GetType().Assembly.GetTypes()) + { + if (type.IsAbstract || !typeof(IConsoleCommand).IsAssignableFrom(type)) + continue; + + if (type == typeof(SetVerboseCommand)) + yield return new SetVerboseCommand(this.Helper.ModRegistry); + else + yield return (IConsoleCommand)Activator.CreateInstance(type)!; + } } } diff --git a/src/SMAPI/Framework/Logging/LogManager.cs b/src/SMAPI/Framework/Logging/LogManager.cs index 66b93f52..5d93fc10 100644 --- a/src/SMAPI/Framework/Logging/LogManager.cs +++ b/src/SMAPI/Framework/Logging/LogManager.cs @@ -58,7 +58,7 @@ internal class LogManager : IDisposable this.LogFile = new LogFileManager(logPath); // init monitor - this.GetMonitorImpl = (id, name) => new Monitor(name, this.LogFile, colorConfig, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog) + this.GetMonitorImpl = (id, name) => new Monitor(id, name, this.LogFile, colorConfig, verboseLogging.Contains("*") || verboseLogging.Contains(id), getScreenIdForLog) { WriteToConsole = writeToConsole, ShowTraceInConsole = isDeveloperMode, diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 2c0f94f0..608d87f6 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -13,6 +13,9 @@ internal class Monitor : IMonitor /********* ** Fields *********/ + /// The mod ID, if applicable. + private readonly string ModId; + /// The name of the module which logs messages using this instance. private readonly string Source; @@ -41,11 +44,17 @@ internal class Monitor : IMonitor /// Whether to log basic contextual info (like buttons pressed and menus opened) even if is disabled. public static bool ForceLogContext { get; set; } + /// The log contexts for which to enable verbose logging regardless of the configured settings. + public static HashSet ForceVerboseLogging { get; } = []; + + /// Whether to force verbose logging for SMAPI and all mods. + public static bool ForceVerboseLoggingForAll { get; set; } + /// The current log level for contextual info that's relevant to the flag. public static LogLevel ContextLogLevel => Monitor.ForceLogContext ? LogLevel.Info : LogLevel.Trace; /// - public bool IsVerbose { get; } + public bool IsVerbose => field || Monitor.ForceVerboseLoggingForAll || Monitor.ForceVerboseLogging.Contains(this.ModId); /// Whether to show the full log stamps (with time/level/logger) in the console. If false, shows a simplified stamp with only the logger. internal bool ShowFullStampInConsole { get; set; } @@ -61,18 +70,20 @@ internal class Monitor : IMonitor ** Public methods *********/ /// Construct an instance. + /// The mod ID, if applicable. /// The name of the module which logs messages using this instance. /// The log file to which to write messages. /// The colors to use for text written to the SMAPI console. /// Whether verbose logging is enabled. This enables more detailed diagnostic messages than are normally needed. /// Get the screen ID that should be logged to distinguish between players in split-screen mode, if any. - public Monitor(string source, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose, Func getScreenIdForLog) + public Monitor(string modId, string source, LogFileManager logFile, ColorSchemeConfig colorConfig, bool isVerbose, Func getScreenIdForLog) { // validate if (string.IsNullOrWhiteSpace(source)) throw new ArgumentException("The log source cannot be empty."); // initialize + this.ModId = modId; this.Source = source; this.LogFile = logFile ?? throw new ArgumentNullException(nameof(logFile), "The log file manager cannot be null."); this.ConsoleWriter = new ColorfulConsoleWriter(Constants.Platform, colorConfig); @@ -147,7 +158,7 @@ internal class Monitor : IMonitor string consoleMessage = this.ShowFullStampInConsole ? fullMessage : $"[{source}] {message}"; // write to console - if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace)) + if (this.WriteToConsole && (this.ShowTraceInConsole || level != ConsoleLogLevel.Trace || Monitor.ForceVerboseLoggingForAll || Monitor.ForceVerboseLogging.Contains(this.ModId))) this.ConsoleWriter.WriteLine(consoleMessage, level); // write to log file From 460e24464fd10ae62baecc24f5a48a06fa70f442 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 10 Jan 2026 00:33:21 -0500 Subject: [PATCH 42/43] fix asset propagation for farmer sprites before save is loaded --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 29 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 2c3ff715..3261b0aa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * Updated dependencies, including... * [Newtonsoft.Json](https://www.newtonsoft.com/json) 13.0.3 → 13.0.4 (see [changes](https://github.com/JamesNK/Newtonsoft.Json/releases/tag/13.0.4)); * [Pintail](https://github.com/Nanoray-pl/Pintail) 2.8.1 → 2.9.1 (see [changes](https://github.com/Nanoray-pl/Pintail/blob/master/docs/release-notes.md#291)). + * Fixed asset propagation for farmer sprites before a save is loaded. * Removed `System.Management.dll`, which SMAPI no longer uses. * For the web UI: diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index a8cb5fdc..ef8abc73 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -260,8 +260,7 @@ internal class CoreAssetPropagator case "characters/farmer/farmer_base_bald": case "characters/farmer/farmer_girl_base": case "characters/farmer/farmer_girl_base_bald": - if (!ignoreWorld) - this.UpdatePlayerSprites(assetName); + this.UpdatePlayerSprites(assetName); break; /**** @@ -530,19 +529,19 @@ internal class CoreAssetPropagator /// The asset name to update. private void UpdatePlayerSprites(IAssetName assetName) { - Farmer[] players = - ( - from player in Game1.getOnlineFarmers() - where this.IsSameBaseName(assetName, player.getTexture()) - select player - ) - .ToArray(); + // reset recolors + FarmerRenderer.recolorOffsets?.Clear(); - foreach (Farmer player in players) + // reset local player + // This is handled separately since Game1.getOnlineFarmers() doesn't include the local player before the save is loaded + if (this.IsSameBaseName(assetName, Game1.player?.getTexture())) + Game1.player.FarmerRenderer.MarkSpriteDirty(); + + // reset other player + foreach (Farmer player in Game1.getOnlineFarmers()) { - FarmerRenderer.recolorOffsets?.Clear(); - - player.FarmerRenderer.MarkSpriteDirty(); + if (!object.ReferenceEquals(player, Game1.player) && this.IsSameBaseName(assetName, player.getTexture())) + player.FarmerRenderer.MarkSpriteDirty(); } } @@ -841,7 +840,7 @@ internal class CoreAssetPropagator /// Get whether two asset names are equivalent if you ignore the locale code. /// The first value to compare. /// The second value to compare. - private bool IsSameBaseName(IAssetName? left, string? right) + private bool IsSameBaseName([NotNullWhen(true)] IAssetName? left, [NotNullWhen(true)] string? right) { if (left is null || right is null) return false; @@ -853,7 +852,7 @@ internal class CoreAssetPropagator /// Get whether two asset names are equivalent if you ignore the locale code. /// The first value to compare. /// The second value to compare. - private bool IsSameBaseName(IAssetName? left, IAssetName? right) + private bool IsSameBaseName([NotNullWhen(true)] IAssetName? left, [NotNullWhen(true)] IAssetName? right) { if (left is null || right is null) return false; From 6bf7a3e6eee1a6335b08e87260cef674b1c64c03 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 10 Jan 2026 20:22:38 -0500 Subject: [PATCH 43/43] prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 10 ++++++---- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/build/common.targets b/build/common.targets index 4a078f80..af24c8d7 100644 --- a/build/common.targets +++ b/build/common.targets @@ -7,7 +7,7 @@ repo. It imports the other MSBuild files as needed. - 4.3.2 + 4.4.0 SMAPI latest $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index 3261b0aa..563d5f85 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,18 +1,20 @@ ← [README](README.md) # Release notes -## Upcoming release +## 4.4.0 +Released 10 January 2026 for Stardew Valley 1.6.14 or later. See [release highlights](https://www.patreon.com/posts/147916705). + * For players: - * The SMAPI console/log now shows "Windows 10" or "Windows 11" in the log instead of its internal OS identifier. * Added [`set_verbose` console command](https://stardewvalleywiki.com/Modding:Console_commands#set_verbose). + * The SMAPI log now shows a friendly Windows name (like "Windows 11") instead of its internal identifier. * Fixed `player_add` and `list_items` console commands not including some newer juice items. * Fixed farmhouse map edits sometimes removing the spouse room (thanks to SinZ!). * Fixed installer error if Steam has an empty game path saved to the registry. * For mod authors: * Added [input API to send button presses to the game](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Input#Send_input) (thanks to martiandweller!). - * Added `PatchMode.Mask` option when editing textures, which lets you apply a transparency mask to the target texture (thanks to PinkSerenity!). - * Added support for map tilesheet paths starting with `../` to reference an asset under the `Content` folder that's not in `Content/Maps` (thanks to Spiderbuttons!). + * Added transparency masks via `PatchMode.Mask` when editing images (thanks to PinkSerenity!). + * Added support for map tilesheets referencing an asset outside `Content/Maps` using a relative `../` path (thanks to Spiderbuttons!). * Added asset propagation for spouse room map edits. * Improved performance when propagating localized assets in some cases (thanks to SinZ!). * Improved error-handling during asset propagation. diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 295c75eb..3f44f80c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "4.3.2", + "Version": "4.4.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueId": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "4.3.2" + "MinimumApiVersion": "4.4.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index b915aa08..d3d054e6 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "4.3.2", + "Version": "4.4.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueId": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "4.3.2" + "MinimumApiVersion": "4.4.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 15977de8..06e066eb 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -49,7 +49,7 @@ internal static class EarlyConstants internal static int? LogScreenId { get; set; } /// SMAPI's current raw semantic version. - internal static string RawApiVersion = "4.3.2"; + internal static string RawApiVersion = "4.4.0"; } /// Contains SMAPI's constants and assumptions.