summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/SMAPI.Installer/InteractiveInstaller.cs4
-rw-r--r--src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs16
-rw-r--r--src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs11
-rw-r--r--src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs5
-rw-r--r--src/SMAPI.Internal/SMAPI.Internal.projitems1
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Tests/Core/ModResolverTests.cs2
-rw-r--r--src/SMAPI.Tests/Utilities/SDateTests.cs91
-rw-r--r--src/SMAPI.Web/Program.cs1
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json2
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/content-patcher.json72
-rw-r--r--src/SMAPI.sln6
-rw-r--r--src/SMAPI/Constants.cs2
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForMap.cs186
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForObject.cs8
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs27
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs14
-rw-r--r--src/SMAPI/Framework/Monitor.cs4
-rw-r--r--src/SMAPI/Framework/Patching/PatchHelper.cs34
-rw-r--r--src/SMAPI/Framework/SCore.cs3
-rw-r--r--src/SMAPI/IAssetData.cs6
-rw-r--r--src/SMAPI/IAssetDataForMap.cs18
-rw-r--r--src/SMAPI/IContentHelper.cs6
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs9
-rw-r--r--src/SMAPI/Patches/DialogueErrorPatch.cs9
-rw-r--r--src/SMAPI/Patches/EventErrorPatch.cs9
-rw-r--r--src/SMAPI/Patches/ObjectErrorPatch.cs39
-rw-r--r--src/SMAPI/Patches/ScheduleErrorPatch.cs9
-rw-r--r--src/SMAPI/Program.cs4
-rw-r--r--src/SMAPI/SMAPI.config.json1
-rw-r--r--src/SMAPI/Utilities/SDate.cs63
-rw-r--r--src/SMAPI/i18n/de.json9
-rw-r--r--src/SMAPI/i18n/default.json8
-rw-r--r--src/SMAPI/i18n/es.json8
-rw-r--r--src/SMAPI/i18n/fr.json8
-rw-r--r--src/SMAPI/i18n/hu.json8
-rw-r--r--src/SMAPI/i18n/it.json8
-rw-r--r--src/SMAPI/i18n/ja.json8
-rw-r--r--src/SMAPI/i18n/ko.json9
-rw-r--r--src/SMAPI/i18n/pt.json8
-rw-r--r--src/SMAPI/i18n/ru.json8
-rw-r--r--src/SMAPI/i18n/tr.json8
-rw-r--r--src/SMAPI/i18n/zh.json8
47 files changed, 712 insertions, 62 deletions
diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs
index 2d58baf0..5b0c6e1f 100644
--- a/src/SMAPI.Installer/InteractiveInstaller.cs
+++ b/src/SMAPI.Installer/InteractiveInstaller.cs
@@ -88,8 +88,8 @@ namespace StardewModdingApi.Installer
yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley", "ErrorLogs"); // remove old log files
}
- /// <summary>Handles writing color-coded text to the console.</summary>
- private ColorfulConsoleWriter ConsoleWriter;
+ /// <summary>Handles writing text to the console.</summary>
+ private IConsoleWriter ConsoleWriter;
/*********
diff --git a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs
index aefda9b6..b5bd4600 100644
--- a/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs
+++ b/src/SMAPI.Internal/ConsoleWriting/ColorfulConsoleWriter.cs
@@ -4,8 +4,8 @@ using StardewModdingAPI.Toolkit.Utilities;
namespace StardewModdingAPI.Internal.ConsoleWriting
{
- /// <summary>Provides a wrapper for writing color-coded text to the console.</summary>
- internal class ColorfulConsoleWriter
+ /// <summary>Writes color-coded text to the console.</summary>
+ internal class ColorfulConsoleWriter : IConsoleWriter
{
/*********
** Fields
@@ -30,8 +30,16 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
/// <param name="colorConfig">The colors to use for text written to the SMAPI console.</param>
public ColorfulConsoleWriter(Platform platform, ColorSchemeConfig colorConfig)
{
- this.SupportsColor = this.TestColorSupport();
- this.Colors = this.GetConsoleColorScheme(platform, colorConfig);
+ if (colorConfig.UseScheme == MonitorColorScheme.None)
+ {
+ this.SupportsColor = false;
+ this.Colors = null;
+ }
+ else
+ {
+ this.SupportsColor = this.TestColorSupport();
+ this.Colors = this.GetConsoleColorScheme(platform, colorConfig);
+ }
}
/// <summary>Write a message line to the log.</summary>
diff --git a/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs b/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs
new file mode 100644
index 00000000..fbcf161c
--- /dev/null
+++ b/src/SMAPI.Internal/ConsoleWriting/IConsoleWriter.cs
@@ -0,0 +1,11 @@
+namespace StardewModdingAPI.Internal.ConsoleWriting
+{
+ /// <summary>Writes text to the console.</summary>
+ internal interface IConsoleWriter
+ {
+ /// <summary>Write a message line to the log.</summary>
+ /// <param name="message">The message to log.</param>
+ /// <param name="level">The log level.</param>
+ void WriteLine(string message, ConsoleLogLevel level);
+ }
+}
diff --git a/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs
index bccb56d7..994ea6a5 100644
--- a/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs
+++ b/src/SMAPI.Internal/ConsoleWriting/MonitorColorScheme.cs
@@ -10,6 +10,9 @@ namespace StardewModdingAPI.Internal.ConsoleWriting
DarkBackground,
/// <summary>Use darker text colors that look better on a white or light background.</summary>
- LightBackground
+ LightBackground,
+
+ /// <summary>Disable console color.</summary>
+ None
}
}
diff --git a/src/SMAPI.Internal/SMAPI.Internal.projitems b/src/SMAPI.Internal/SMAPI.Internal.projitems
index 7fcebc94..0d583a6d 100644
--- a/src/SMAPI.Internal/SMAPI.Internal.projitems
+++ b/src/SMAPI.Internal/SMAPI.Internal.projitems
@@ -12,6 +12,7 @@
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ColorfulConsoleWriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ColorSchemeConfig.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\ConsoleLogLevel.cs" />
+ <Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\IConsoleWriter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleWriting\MonitorColorScheme.cs" />
</ItemGroup>
</Project> \ No newline at end of file
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs
index 8d6bd759..23c266ea 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetDayCommand.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using StardewModdingAPI.Utilities;
using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
@@ -32,6 +33,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
// handle
Game1.dayOfMonth = day;
+ Game1.stats.DaysPlayed = (uint)SDate.Now().DaysSinceStart;
monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info);
}
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs
index 0615afe7..676369fe 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetSeasonCommand.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using StardewModdingAPI.Utilities;
using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
@@ -40,6 +41,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
// handle
Game1.currentSeason = season.ToLower();
Game1.setGraphicsForSeason();
+ Game1.stats.DaysPlayed = (uint)SDate.Now().DaysSinceStart;
monitor.Log($"OK, the date is now {Game1.currentSeason} {Game1.dayOfMonth}.", LogLevel.Info);
}
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs
index 66abd6dc..648830c1 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/SetYearCommand.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using StardewModdingAPI.Utilities;
using StardewValley;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
@@ -32,6 +33,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World
// handle
Game1.year = year;
+ Game1.stats.DaysPlayed = (uint)SDate.Now().DaysSinceStart;
monitor.Log($"OK, the year is now {Game1.year}.", LogLevel.Info);
}
}
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index a55d168f..908d4f65 100644
--- a/src/SMAPI.Mods.ConsoleCommands/manifest.json
+++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Console Commands",
"Author": "SMAPI",
- "Version": "3.4.1",
+ "Version": "3.5.0",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.4.1"
+ "MinimumApiVersion": "3.5.0"
}
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 5bf35b5c..cd42459e 100644
--- a/src/SMAPI.Mods.SaveBackup/manifest.json
+++ b/src/SMAPI.Mods.SaveBackup/manifest.json
@@ -1,9 +1,9 @@
{
"Name": "Save Backup",
"Author": "SMAPI",
- "Version": "3.4.1",
+ "Version": "3.5.0",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.4.1"
+ "MinimumApiVersion": "3.5.0"
}
diff --git a/src/SMAPI.Tests/Core/ModResolverTests.cs b/src/SMAPI.Tests/Core/ModResolverTests.cs
index a9c88c60..45b3673b 100644
--- a/src/SMAPI.Tests/Core/ModResolverTests.cs
+++ b/src/SMAPI.Tests/Core/ModResolverTests.cs
@@ -73,7 +73,7 @@ namespace SMAPI.Tests.Core
[nameof(IManifest.Description)] = Sample.String(),
[nameof(IManifest.UniqueID)] = $"{Sample.String()}.{Sample.String()}",
[nameof(IManifest.EntryDll)] = $"{Sample.String()}.dll",
- [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}-{Sample.String()}",
+ [nameof(IManifest.MinimumApiVersion)] = $"{Sample.Int()}.{Sample.Int()}.{Sample.Int()}-{Sample.String()}",
[nameof(IManifest.Dependencies)] = new[] { originalDependency },
["ExtraString"] = Sample.String(),
["ExtraInt"] = Sample.Int()
diff --git a/src/SMAPI.Tests/Utilities/SDateTests.cs b/src/SMAPI.Tests/Utilities/SDateTests.cs
index d25a101a..0461952e 100644
--- a/src/SMAPI.Tests/Utilities/SDateTests.cs
+++ b/src/SMAPI.Tests/Utilities/SDateTests.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using NUnit.Framework;
using StardewModdingAPI.Utilities;
+using StardewValley;
namespace SMAPI.Tests.Utilities
{
@@ -82,6 +83,62 @@ namespace SMAPI.Tests.Utilities
}
/****
+ ** FromDaysSinceStart
+ ****/
+ [Test(Description = "Assert that FromDaysSinceStart returns the expected date.")]
+ [TestCase(1, ExpectedResult = "01 spring Y1")]
+ [TestCase(2, ExpectedResult = "02 spring Y1")]
+ [TestCase(28, ExpectedResult = "28 spring Y1")]
+ [TestCase(29, ExpectedResult = "01 summer Y1")]
+ [TestCase(141, ExpectedResult = "01 summer Y2")]
+ public string FromDaysSinceStart(int daysSinceStart)
+ {
+ // act
+ return SDate.FromDaysSinceStart(daysSinceStart).ToString();
+ }
+
+ [Test(Description = "Assert that FromDaysSinceStart throws an exception if the number of days is invalid.")]
+ [TestCase(-1)] // day < 0
+ [TestCase(0)] // day == 0
+ [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")]
+ public void FromDaysSinceStart_RejectsInvalidValues(int daysSinceStart)
+ {
+ // act & assert
+ Assert.Throws<ArgumentException>(() => _ = SDate.FromDaysSinceStart(daysSinceStart), "Passing the invalid number of days didn't throw the expected exception.");
+ }
+
+ /****
+ ** From
+ ****/
+ [Test(Description = "Assert that SDate.From constructs the correct instance for a given date.")]
+ [TestCase(0, ExpectedResult = "01 spring Y1")]
+ [TestCase(1, ExpectedResult = "02 spring Y1")]
+ [TestCase(27, ExpectedResult = "28 spring Y1")]
+ [TestCase(28, ExpectedResult = "01 summer Y1")]
+ [TestCase(140, ExpectedResult = "01 summer Y2")]
+ public string From_WorldDate(int totalDays)
+ {
+ return SDate.From(new WorldDate { TotalDays = totalDays }).ToString();
+ }
+
+
+ /****
+ ** SeasonIndex
+ ****/
+ [Test(Description = "Assert the numeric index of the season.")]
+ [TestCase("01 spring Y1", ExpectedResult = 0)]
+ [TestCase("02 summer Y1", ExpectedResult = 1)]
+ [TestCase("28 fall Y1", ExpectedResult = 2)]
+ [TestCase("01 winter Y1", ExpectedResult = 3)]
+ [TestCase("01 winter Y2", ExpectedResult = 3)]
+ public int SeasonIndex(string dateStr)
+ {
+ // act
+ return this.GetDate(dateStr).SeasonIndex;
+ }
+
+
+ /****
** DayOfWeek
****/
[Test(Description = "Assert the day of week.")]
@@ -119,6 +176,7 @@ namespace SMAPI.Tests.Utilities
return this.GetDate(dateStr).DayOfWeek;
}
+
/****
** DaysSinceStart
****/
@@ -134,6 +192,7 @@ namespace SMAPI.Tests.Utilities
return this.GetDate(dateStr).DaysSinceStart;
}
+
/****
** ToString
****/
@@ -147,6 +206,7 @@ namespace SMAPI.Tests.Utilities
return this.GetDate(dateStr).ToString();
}
+
/****
** AddDays
****/
@@ -166,6 +226,18 @@ namespace SMAPI.Tests.Utilities
return this.GetDate(dateStr).AddDays(addDays).ToString();
}
+ [Test(Description = "Assert that AddDays throws an exception if the number of days is invalid.")]
+ [TestCase("01 spring Y1", -1)]
+ [TestCase("01 summer Y1", -29)]
+ [TestCase("01 spring Y2", -113)]
+ [SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")]
+ public void AddDays_RejectsInvalidValues(string dateStr, int addDays)
+ {
+ // act & assert
+ Assert.Throws<ArithmeticException>(() => _ = this.GetDate(dateStr).AddDays(addDays), "Passing the invalid number of days didn't throw the expected exception.");
+ }
+
+
/****
** GetHashCode
****/
@@ -194,6 +266,25 @@ namespace SMAPI.Tests.Utilities
}
}
+
+ /****
+ ** ToWorldDate
+ ****/
+ [Test(Description = "Assert that the WorldDate operator returns the corresponding WorldDate.")]
+ [TestCase("01 spring Y1", ExpectedResult = 0)]
+ [TestCase("02 spring Y1", ExpectedResult = 1)]
+ [TestCase("28 spring Y1", ExpectedResult = 27)]
+ [TestCase("01 summer Y1", ExpectedResult = 28)]
+ [TestCase("01 summer Y2", ExpectedResult = 140)]
+ public int ToWorldDate(string dateStr)
+ {
+ return this.GetDate(dateStr).ToWorldDate().TotalDays;
+ }
+
+
+ /****
+ ** Operators
+ ****/
[Test(Description = "Assert that the == operator returns the expected values. We only need a few test cases, since it's based on GetHashCode which is tested more thoroughly.")]
[TestCase(Dates.Now, null, ExpectedResult = false)]
[TestCase(Dates.Now, Dates.PrevDay, ExpectedResult = false)]
diff --git a/src/SMAPI.Web/Program.cs b/src/SMAPI.Web/Program.cs
index 5d13cdf3..70082160 100644
--- a/src/SMAPI.Web/Program.cs
+++ b/src/SMAPI.Web/Program.cs
@@ -18,6 +18,7 @@ namespace StardewModdingAPI.Web
.CreateDefaultBuilder(args)
.CaptureStartupErrors(true)
.UseSetting("detailedErrors", "true")
+ .UseKestrel().UseIISIntegration() // must be used together; fixes intermittent errors on Azure: https://stackoverflow.com/a/38312175/262123
.UseStartup<Startup>()
.Build()
.Run();
diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
index 3101fdf1..179ef42a 100644
--- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
@@ -155,7 +155,7 @@
*********/
"Auto Quality Patch": {
"ID": "SilentOak.AutoQualityPatch",
- "~2.1.3-unofficial.7 | Status": "AssumeBroken" // runtime errors
+ "~2.1.3-unofficial.7-mizzion | Status": "AssumeBroken" // runtime errors
},
"Fix Dice": {
diff --git a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
index e6cd4e65..f627ab95 100644
--- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
+++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
@@ -11,9 +11,9 @@
"title": "Format version",
"description": "The format version. You should always use the latest version to enable the latest features and avoid obsolete behavior.",
"type": "string",
- "const": "1.11.0",
+ "const": "1.13.0",
"@errorMessages": {
- "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.11.0'."
+ "const": "Incorrect value '@value'. This should be set to the latest format version, currently '1.13.0'."
}
},
"ConfigSchema": {
@@ -268,6 +268,48 @@
"type": "string"
}
},
+ "MapTiles": {
+ "title": "Map tiles",
+ "description": "The individual map tiles to add, edit, or remove.",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "Layer": {
+ "description": "The map layer name to change.",
+ "type": "string"
+ },
+ "Position": {
+ "description": "The tile coordinates to change. You can use the Debug Mode mod to see tile coordinates in-game.",
+ "$ref": "#/definitions/Position"
+ },
+ "SetTilesheet": {
+ "title": "Set tilesheet",
+ "description": "Sets the tilesheet ID for the tile index.",
+ "type": "string"
+ },
+ "SetIndex": {
+ "title": "Set tile index",
+ "description": "Sets the tile index in the tilesheet.",
+ "type": [ "string", "number" ]
+ },
+ "SetProperties": {
+ "title": "Set tile properties",
+ "description": "The properties to set or remove. This is merged into the existing tile properties, if any. To remove a property, set its value to `null` (not \"null\" in quotes).",
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "Remove": {
+ "description": "Whether to remove the current tile and all its properties on that layer. If combined with the other fields, a new tile is created from the other fields as if the tile didn't previously exist.",
+ "type": "boolean"
+ }
+ },
+
+ "required": [ "Layer", "Position" ]
+ }
+ },
"When": {
"title": "When",
"description": "Only apply the patch if the given conditions match.",
@@ -335,7 +377,7 @@
}
},
"propertyNames": {
- "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "MapProperties" ]
+ "enum": [ "Action", "Target", "LogName", "Enabled", "When", "FromFile", "FromArea", "ToArea", "MapProperties", "MapTiles" ]
}
}
}
@@ -361,17 +403,37 @@
"type": [ "boolean", "string" ]
}
},
+ "Position": {
+ "type": "object",
+ "properties": {
+ "X": {
+ "title": "X position",
+ "description": "The X position, measured in pixels for a texture or tiles for a map. This can contain tokens.",
+ "type": [ "integer", "string" ],
+ "minimum:": 0
+ },
+ "Y": {
+ "title": "Y position",
+ "description": "The Y position, measured in pixels for a texture or tiles for a map. This can contain tokens.",
+ "type": [ "integer", "string" ],
+ "minimum:": 0
+ }
+ },
+
+ "required": [ "X", "Y" ],
+ "additionalProperties": false
+ },
"Rectangle": {
"type": "object",
"properties": {
"X": {
- "title": "X-Coordinate",
+ "title": "X position",
"description": "The X position of the area's top-left corner, measured in pixels for a texture or tiles for a map. This can contain tokens.",
"type": [ "integer", "string" ],
"minimum:": 0
},
"Y": {
- "title": "Y-Coordinate",
+ "title": "Y position",
"description": "The Y position of the area's top-left corner, measured in pixels for a texture or tiles for a map. This can contain tokens.",
"type": [ "integer", "string" ],
"minimum:": 0
diff --git a/src/SMAPI.sln b/src/SMAPI.sln
index 62eaa777..f9c537c4 100644
--- a/src/SMAPI.sln
+++ b/src/SMAPI.sln
@@ -81,7 +81,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMAPI.Web.LegacyRedirects",
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
+ SMAPI.Internal\SMAPI.Internal.projitems*{0634ea4c-3b8f-42db-aea6-ca9e4ef6e92f}*SharedItemsImports = 5
+ SMAPI.Internal\SMAPI.Internal.projitems*{0a9bb24f-15ff-4c26-b1a2-81f7ae316518}*SharedItemsImports = 5
+ SMAPI.Internal\SMAPI.Internal.projitems*{1b3821e6-d030-402c-b3a1-7ca45c2800ea}*SharedItemsImports = 5
+ SMAPI.Internal\SMAPI.Internal.projitems*{80efd92f-728f-41e0-8a5b-9f6f49a91899}*SharedItemsImports = 5
SMAPI.Internal\SMAPI.Internal.projitems*{85208f8d-6fd1-4531-be05-7142490f59fe}*SharedItemsImports = 13
+ SMAPI.Internal\SMAPI.Internal.projitems*{cd53ad6f-97f4-4872-a212-50c2a0fd3601}*SharedItemsImports = 5
+ SMAPI.Internal\SMAPI.Internal.projitems*{e6da2198-7686-4f1d-b312-4a4dc70884c0}*SharedItemsImports = 5
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 128e23bd..a898fccd 100644
--- a/src/SMAPI/Constants.cs
+++ b/src/SMAPI/Constants.cs
@@ -20,7 +20,7 @@ namespace StardewModdingAPI
** Public
****/
/// <summary>SMAPI's current semantic version.</summary>
- public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.4.1");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.5.0");
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1");
diff --git a/src/SMAPI/Framework/Content/AssetDataForMap.cs b/src/SMAPI/Framework/Content/AssetDataForMap.cs
new file mode 100644
index 00000000..f66013ba
--- /dev/null
+++ b/src/SMAPI/Framework/Content/AssetDataForMap.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using StardewModdingAPI.Toolkit.Utilities;
+using xTile;
+using xTile.Layers;
+using xTile.Tiles;
+
+namespace StardewModdingAPI.Framework.Content
+{
+ /// <summary>Encapsulates access and changes to image content being read from a data file.</summary>
+ internal class AssetDataForMap : AssetData<Map>, IAssetDataForMap
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="locale">The content's locale code, if the content is localized.</param>
+ /// <param name="assetName">The normalized asset name being read.</param>
+ /// <param name="data">The content data being read.</param>
+ /// <param name="getNormalizedPath">Normalizes an asset key to match the cache key.</param>
+ /// <param name="onDataReplaced">A callback to invoke when the data is replaced (if any).</param>
+ public AssetDataForMap(string locale, string assetName, Map data, Func<string, string> getNormalizedPath, Action<Map> onDataReplaced)
+ : base(locale, assetName, data, getNormalizedPath, onDataReplaced) { }
+
+ /// <summary>Copy layers, tiles, and tilesheets from another map onto the asset.</summary>
+ /// <param name="source">The map from which to copy.</param>
+ /// <param name="sourceArea">The tile area within the source map to copy, or <c>null</c> for the entire source map size. This must be within the bounds of the <paramref name="source"/> map.</param>
+ /// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param>
+ /// <remarks>Derived from <see cref="StardewValley.GameLocation.ApplyMapOverride"/> with a few changes:
+ /// - can be applied directly to the maps when loading, before the location is created;
+ /// - added support for source/target areas;
+ /// - added disambiguation if source has a modified version of the same tilesheet, instead of copying tiles into the target tilesheet;
+ /// - changed to always overwrite tiles within the target area (to avoid edge cases where some tiles are only partly applied);
+ /// - fixed copying tilesheets (avoid "The specified TileSheet was not created for use with this map" error);
+ /// - fixed tilesheets not added at the end (via z_ prefix), which can cause crashes in game code which depends on hardcoded tilesheet indexes;
+ /// - fixed issue where different tilesheets are linked by ID.
+ /// </remarks>
+ public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null)
+ {
+ var target = this.Data;
+
+ // get areas
+ {
+ Rectangle sourceBounds = this.GetMapArea(source);
+ Rectangle targetBounds = this.GetMapArea(target);
+ sourceArea ??= new Rectangle(0, 0, sourceBounds.Width, sourceBounds.Height);
+ targetArea ??= new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, targetBounds.Width), Math.Min(sourceArea.Value.Height, targetBounds.Height));
+
+ // validate
+ if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > sourceBounds.Width || sourceArea.Value.Bottom > sourceBounds.Height)
+ throw new ArgumentOutOfRangeException(nameof(sourceArea), $"The source area ({sourceArea}) is outside the bounds of the source map ({sourceBounds}).");
+ if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > targetBounds.Width || targetArea.Value.Bottom > targetBounds.Height)
+ throw new ArgumentOutOfRangeException(nameof(targetArea), $"The target area ({targetArea}) is outside the bounds of the target map ({targetBounds}).");
+ if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height)
+ throw new InvalidOperationException($"The source area ({sourceArea}) and target area ({targetArea}) must be the same size.");
+ }
+
+ // apply tilesheets
+ IDictionary<TileSheet, TileSheet> tilesheetMap = new Dictionary<TileSheet, TileSheet>();
+ foreach (TileSheet sourceSheet in source.TileSheets)
+ {
+ // copy tilesheets
+ TileSheet targetSheet = target.GetTileSheet(sourceSheet.Id);
+ if (targetSheet == null || this.NormalizeTilesheetPathForComparison(targetSheet.ImageSource) != this.NormalizeTilesheetPathForComparison(sourceSheet.ImageSource))
+ {
+ // change ID if needed so new tilesheets are added after vanilla ones (to avoid errors in hardcoded game logic)
+ string id = sourceSheet.Id;
+ if (!id.StartsWith("z_", StringComparison.InvariantCultureIgnoreCase))
+ id = $"z_{id}";
+
+ // change ID if it conflicts with an existing tilesheet
+ if (target.GetTileSheet(id) != null)
+ {
+ int disambiguator = Enumerable.Range(2, int.MaxValue - 1).First(p => target.GetTileSheet($"{id}_{p}") == null);
+ id = $"{id}_{disambiguator}";
+ }
+
+ // add tilesheet
+ targetSheet = new TileSheet(id, target, sourceSheet.ImageSource, sourceSheet.SheetSize, sourceSheet.TileSize);
+ for (int i = 0, tileCount = sourceSheet.TileCount; i < tileCount; ++i)
+ targetSheet.TileIndexProperties[i].CopyFrom(sourceSheet.TileIndexProperties[i]);
+ target.AddTileSheet(targetSheet);
+ }
+
+ tilesheetMap[sourceSheet] = targetSheet;
+ }
+
+ // get layer map
+ IDictionary<Layer, Layer> layerMap = source.Layers.ToDictionary(p => p, p => target.GetLayer(p.Id));
+
+ // apply tiles
+ for (int x = 0; x < sourceArea.Value.Width; x++)
+ {
+ for (int y = 0; y < sourceArea.Value.Height; y++)
+ {
+ // calculate tile positions
+ Point sourcePos = new Point(sourceArea.Value.X + x, sourceArea.Value.Y + y);
+ Point targetPos = new Point(targetArea.Value.X + x, targetArea.Value.Y + y);
+
+ // merge layers
+ foreach (Layer sourceLayer in source.Layers)
+ {
+ // get layer
+ Layer targetLayer = layerMap[sourceLayer];
+ if (targetLayer == null)
+ {
+ target.AddLayer(targetLayer = new Layer(sourceLayer.Id, target, target.Layers[0].LayerSize, Layer.m_tileSize));
+ layerMap[sourceLayer] = target.GetLayer(sourceLayer.Id);
+ }
+
+ // copy layer properties
+ targetLayer.Properties.CopyFrom(sourceLayer.Properties);
+
+ // copy tiles
+ Tile sourceTile = sourceLayer.Tiles[sourcePos.X, sourcePos.Y];
+ Tile targetTile;
+ switch (sourceTile)
+ {
+ case StaticTile _:
+ targetTile = new StaticTile(targetLayer, tilesheetMap[sourceTile.TileSheet], sourceTile.BlendMode, sourceTile.TileIndex);
+ break;
+
+ case AnimatedTile animatedTile:
+ {
+ StaticTile[] tileFrames = new StaticTile[animatedTile.TileFrames.Length];
+ for (int frame = 0; frame < animatedTile.TileFrames.Length; ++frame)
+ {
+ StaticTile frameTile = animatedTile.TileFrames[frame];
+ tileFrames[frame] = new StaticTile(targetLayer, tilesheetMap[frameTile.TileSheet], frameTile.BlendMode, frameTile.TileIndex);
+ }
+ targetTile = new AnimatedTile(targetLayer, tileFrames, animatedTile.FrameInterval);
+ }
+ break;
+
+ default: // null or unhandled type
+ targetTile = null;
+ break;
+ }
+ targetTile?.Properties.CopyFrom(sourceTile.Properties);
+ targetLayer.Tiles[targetPos.X, targetPos.Y] = targetTile;
+ }
+ }
+ }
+ }
+
+
+ /*********
+ ** Private methods
+ *********/
+ /// <summary>Normalize a map tilesheet path for comparison. This value should *not* be used as the actual tilesheet path.</summary>
+ /// <param name="path">The path to normalize.</param>
+ private string NormalizeTilesheetPathForComparison(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ return string.Empty;
+
+ path = PathUtilities.NormalizePathSeparators(path.Trim());
+ if (path.StartsWith($"Maps{PathUtilities.PreferredPathSeparator}", StringComparison.OrdinalIgnoreCase))
+ path = path.Substring($"Maps{PathUtilities.PreferredPathSeparator}".Length);
+ if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase))
+ path = path.Substring(0, path.Length - 4);
+
+ return path;
+ }
+
+ /// <summary>Get a rectangle which encompasses all layers for a map.</summary>
+ /// <param name="map">The map to check.</param>
+ private Rectangle GetMapArea(Map map)
+ {
+ // get max map size
+ int maxWidth = 0;
+ int maxHeight = 0;
+ foreach (Layer layer in map.Layers)
+ {
+ if (layer.LayerWidth > maxWidth)
+ maxWidth = layer.LayerWidth;
+ if (layer.LayerHeight > maxHeight)
+ maxHeight = layer.LayerHeight;
+ }
+
+ return new Rectangle(0, 0, maxWidth, maxHeight);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/Content/AssetDataForObject.cs b/src/SMAPI/Framework/Content/AssetDataForObject.cs
index 4dbc988c..f00ba124 100644
--- a/src/SMAPI/Framework/Content/AssetDataForObject.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForObject.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
+using xTile;
namespace StardewModdingAPI.Framework.Content
{
@@ -41,6 +42,13 @@ namespace StardewModdingAPI.Framework.Content
return new AssetDataForImage(this.Locale, this.AssetName, this.GetData<Texture2D>(), this.GetNormalizedPath, this.ReplaceWith);
}
+ /// <summary>Get a helper to manipulate the data as a map.</summary>
+ /// <exception cref="InvalidOperationException">The content being read isn't a map.</exception>
+ public IAssetDataForMap AsMap()
+ {
+ return new AssetDataForMap(this.Locale, this.AssetName, this.GetData<Map>(), this.GetNormalizedPath, this.ReplaceWith);
+ }
+
/// <summary>Get the data as a given type.</summary>
/// <typeparam name="TData">The expected data type.</typeparam>
/// <exception cref="InvalidCastException">The data can't be converted to <typeparamref name="TData"/>.</exception>
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 0b1ccc3c..47ef30d4 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -14,6 +14,7 @@ using StardewModdingAPI.Metadata;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
+using xTile;
namespace StardewModdingAPI.Framework
{
@@ -228,16 +229,32 @@ namespace StardewModdingAPI.Framework
public IEnumerable<string> InvalidateCache(Func<string, Type, bool> predicate, bool dispose = false)
{
// invalidate cache & track removed assets
- IDictionary<string, ISet<object>> removedAssets = new Dictionary<string, ISet<object>>(StringComparer.InvariantCultureIgnoreCase);
+ IDictionary<string, Type> removedAssets = new Dictionary<string, Type>(StringComparer.InvariantCultureIgnoreCase);
this.ContentManagerLock.InReadLock(() =>
{
+ // cached assets
foreach (IContentManager contentManager in this.ContentManagers)
{
foreach (var entry in contentManager.InvalidateCache(predicate, dispose))
{
- if (!removedAssets.TryGetValue(entry.Key, out ISet<object> assets))
- removedAssets[entry.Key] = assets = new HashSet<object>(new ObjectReferenceComparer<object>());
- assets.Add(entry.Value);
+ if (!removedAssets.TryGetValue(entry.Key, out Type type))
+ removedAssets[entry.Key] = entry.Value.GetType();
+ }
+ }
+
+ // special case: maps may be loaded through a temporary content manager that's removed while the map is still in use.
+ // This notably affects the town and farmhouse maps.
+ if (Game1.locations != null)
+ {
+ foreach (GameLocation location in Game1.locations)
+ {
+ if (location.map == null || string.IsNullOrWhiteSpace(location.mapPath.Value))
+ continue;
+
+ // get map path
+ string mapPath = this.MainContentManager.AssertAndNormalizeAssetName(location.mapPath.Value);
+ if (!removedAssets.ContainsKey(mapPath) && predicate(mapPath, typeof(Map)))
+ removedAssets[mapPath] = typeof(Map);
}
}
});
@@ -245,7 +262,7 @@ namespace StardewModdingAPI.Framework
// reload core game assets
if (removedAssets.Any())
{
- IDictionary<string, bool> propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager
+ IDictionary<string, bool> propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value)); // use an intercepted content manager
this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace);
}
else
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index e9b70845..23e45fd1 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
+using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.ContentManagers;
using StardewModdingAPI.Framework.Exceptions;
using StardewValley;
@@ -164,6 +165,19 @@ namespace StardewModdingAPI.Framework.ModHelpers
return this.ContentCore.InvalidateCache(predicate).Any();
}
+ /// <summary>Get a patch helper for arbitrary data.</summary>
+ /// <typeparam name="T">The data type.</typeparam>
+ /// <param name="data">The asset data.</param>
+ /// <param name="assetName">The asset name. This is only used for tracking purposes and has no effect on the patch helper.</param>
+ public IAssetData GetPatchHelper<T>(T data, string assetName = null)
+ {
+ if (data == null)
+ throw new ArgumentNullException(nameof(data), "Can't get a patch helper for a null value.");
+
+ assetName ??= $"temp/{Guid.NewGuid():N}";
+ return new AssetDataForObject(this.CurrentLocale, assetName, data, this.NormalizeAssetName);
+ }
+
/*********
** Private methods
diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs
index f630c7fe..44eeabe6 100644
--- a/src/SMAPI/Framework/Monitor.cs
+++ b/src/SMAPI/Framework/Monitor.cs
@@ -15,8 +15,8 @@ namespace StardewModdingAPI.Framework
/// <summary>The name of the module which logs messages using this instance.</summary>
private readonly string Source;
- /// <summary>Handles writing color-coded text to the console.</summary>
- private readonly ColorfulConsoleWriter ConsoleWriter;
+ /// <summary>Handles writing text to the console.</summary>
+ private readonly IConsoleWriter ConsoleWriter;
/// <summary>Manages access to the console output.</summary>
private readonly ConsoleInterceptionManager ConsoleInterceptor;
diff --git a/src/SMAPI/Framework/Patching/PatchHelper.cs b/src/SMAPI/Framework/Patching/PatchHelper.cs
new file mode 100644
index 00000000..4cb436f0
--- /dev/null
+++ b/src/SMAPI/Framework/Patching/PatchHelper.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+
+namespace StardewModdingAPI.Framework.Patching
+{
+ /// <summary>Provides generic methods for implementing Harmony patches.</summary>
+ internal class PatchHelper
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The interception keys currently being intercepted.</summary>
+ private static readonly HashSet<string> InterceptingKeys = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Track a method that will be intercepted.</summary>
+ /// <param name="key">The intercept key.</param>
+ /// <returns>Returns false if the method was already marked for interception, else true.</returns>
+ public static bool StartIntercept(string key)
+ {
+ return PatchHelper.InterceptingKeys.Add(key);
+ }
+
+ /// <summary>Track a method as no longer being intercepted.</summary>
+ /// <param name="key">The intercept key.</param>
+ public static void StopIntercept(string key)
+ {
+ PatchHelper.InterceptingKeys.Remove(key);
+ }
+ }
+}
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs
index 50e6ea1c..de9c955d 100644
--- a/src/SMAPI/Framework/SCore.cs
+++ b/src/SMAPI/Framework/SCore.cs
@@ -32,6 +32,7 @@ using StardewModdingAPI.Toolkit.Framework.Clients.WebApi;
using StardewModdingAPI.Toolkit.Framework.ModData;
using StardewModdingAPI.Toolkit.Serialization;
using StardewModdingAPI.Toolkit.Utilities;
+using StardewModdingAPI.Utilities;
using StardewValley;
using Object = StardewValley.Object;
using ThreadState = System.Threading.ThreadState;
@@ -176,6 +177,8 @@ namespace StardewModdingAPI.Framework
SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry);
+ SDate.Translations = this.Translator;
+
// redirect direct console output
if (this.MonitorForGame.WriteToConsole)
this.ConsoleManager.OnMessageIntercepted += message => this.HandleConsoleMessage(this.MonitorForGame, message);
diff --git a/src/SMAPI/IAssetData.cs b/src/SMAPI/IAssetData.cs
index c3021144..8df59e53 100644
--- a/src/SMAPI/IAssetData.cs
+++ b/src/SMAPI/IAssetData.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
namespace StardewModdingAPI
{
@@ -39,6 +39,10 @@ namespace StardewModdingAPI
/// <exception cref="InvalidOperationException">The content being read isn't an image.</exception>
IAssetDataForImage AsImage();
+ /// <summary>Get a helper to manipulate the data as a map.</summary>
+ /// <exception cref="InvalidOperationException">The content being read isn't a map.</exception>
+ IAssetDataForMap AsMap();
+
/// <summary>Get the data as a given type.</summary>
/// <typeparam name="TData">The expected data type.</typeparam>
/// <exception cref="InvalidCastException">The data can't be converted to <typeparamref name="TData"/>.</exception>
diff --git a/src/SMAPI/IAssetDataForMap.cs b/src/SMAPI/IAssetDataForMap.cs
new file mode 100644
index 00000000..769ca07c
--- /dev/null
+++ b/src/SMAPI/IAssetDataForMap.cs
@@ -0,0 +1,18 @@
+using Microsoft.Xna.Framework;
+using xTile;
+
+namespace StardewModdingAPI
+{
+ /// <summary>Encapsulates access and changes to map content being read from a data file.</summary>
+ public interface IAssetDataForMap : IAssetData<Map>
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Copy layers, tiles, and tilesheets from another map onto the asset.</summary>
+ /// <param name="source">The map from which to copy.</param>
+ /// <param name="sourceArea">The tile area within the source map to copy, or <c>null</c> for the entire source map size. This must be within the bounds of the <paramref name="source"/> map.</param>
+ /// <param name="targetArea">The tile area within the target map to overwrite, or <c>null</c> to patch the whole map. The original content within this area will be erased. This must be within the bounds of the existing map.</param>
+ public void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null);
+ }
+}
diff --git a/src/SMAPI/IContentHelper.cs b/src/SMAPI/IContentHelper.cs
index dd7eb758..2936ecfb 100644
--- a/src/SMAPI/IContentHelper.cs
+++ b/src/SMAPI/IContentHelper.cs
@@ -64,5 +64,11 @@ namespace StardewModdingAPI
/// <param name="predicate">A predicate matching the assets to invalidate.</param>
/// <returns>Returns whether any cache entries were invalidated.</returns>
bool InvalidateCache(Func<IAssetInfo, bool> predicate);
+
+ /// <summary>Get a patch helper for arbitrary data.</summary>
+ /// <typeparam name="T">The data type.</typeparam>
+ /// <param name="data">The asset data.</param>
+ /// <param name="assetName">The asset name. This is only used for tracking purposes and has no effect on the patch helper.</param>
+ IAssetData GetPatchHelper<T>(T data, string assetName = null);
}
}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 83e553ff..0a14086b 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Microsoft.Xna.Framework.Graphics;
using Netcode;
using StardewModdingAPI.Framework.Reflection;
+using StardewModdingAPI.Toolkit.Utilities;
using StardewValley;
using StardewValley.BellsAndWhistles;
using StardewValley.Buildings;
@@ -891,11 +892,13 @@ namespace StardewModdingAPI.Metadata
// doesn't store the text itself.
foreach (NPC villager in villagers)
{
+ bool shouldSayMarriageDialogue = villager.shouldSayMarriageDialogue.Value;
MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray();
villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue
villager.resetCurrentDialogue();
+ villager.shouldSayMarriageDialogue.Set(shouldSayMarriageDialogue);
villager.currentMarriageDialogue.Set(marriageDialogue);
}
@@ -1037,9 +1040,9 @@ namespace StardewModdingAPI.Metadata
/// <param name="path">The path to check.</param>
private string[] GetSegments(string path)
{
- if (path == null)
- return new string[0];
- return path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ return path != null
+ ? PathUtilities.GetSegments(path)
+ : new string[0];
}
/// <summary>Count the number of segments in a path (e.g. 'a/b' is 2).</summary>
diff --git a/src/SMAPI/Patches/DialogueErrorPatch.cs b/src/SMAPI/Patches/DialogueErrorPatch.cs
index 24f97259..1e49826d 100644
--- a/src/SMAPI/Patches/DialogueErrorPatch.cs
+++ b/src/SMAPI/Patches/DialogueErrorPatch.cs
@@ -24,9 +24,6 @@ namespace StardewModdingAPI.Patches
/// <summary>Simplifies access to private code.</summary>
private static Reflector Reflection;
- /// <summary>Whether the <see cref="NPC.CurrentDialogue"/> getter is currently being intercepted.</summary>
- private static bool IsInterceptingCurrentDialogue;
-
/*********
** Accessors
@@ -112,12 +109,12 @@ namespace StardewModdingAPI.Patches
/// <returns>Returns whether to execute the original method.</returns>
private static bool Before_NPC_CurrentDialogue(NPC __instance, ref Stack<Dialogue> __result, MethodInfo __originalMethod)
{
- if (DialogueErrorPatch.IsInterceptingCurrentDialogue)
+ const string key = nameof(Before_NPC_CurrentDialogue);
+ if (!PatchHelper.StartIntercept(key))
return true;
try
{
- DialogueErrorPatch.IsInterceptingCurrentDialogue = true;
__result = (Stack<Dialogue>)__originalMethod.Invoke(__instance, new object[0]);
return false;
}
@@ -129,7 +126,7 @@ namespace StardewModdingAPI.Patches
}
finally
{
- DialogueErrorPatch.IsInterceptingCurrentDialogue = false;
+ PatchHelper.StopIntercept(key);
}
}
}
diff --git a/src/SMAPI/Patches/EventErrorPatch.cs b/src/SMAPI/Patches/EventErrorPatch.cs
index 1dc7e8c3..504d1d2e 100644
--- a/src/SMAPI/Patches/EventErrorPatch.cs
+++ b/src/SMAPI/Patches/EventErrorPatch.cs
@@ -18,9 +18,6 @@ namespace StardewModdingAPI.Patches
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
private static IMonitor MonitorForGame;
- /// <summary>Whether the method is currently being intercepted.</summary>
- private static bool IsIntercepted;
-
/*********
** Accessors
@@ -61,12 +58,12 @@ namespace StardewModdingAPI.Patches
/// <returns>Returns whether to execute the original method.</returns>
private static bool Before_GameLocation_CheckEventPrecondition(GameLocation __instance, ref int __result, string precondition, MethodInfo __originalMethod)
{
- if (EventErrorPatch.IsIntercepted)
+ const string key = nameof(Before_GameLocation_CheckEventPrecondition);
+ if (!PatchHelper.StartIntercept(key))
return true;
try
{
- EventErrorPatch.IsIntercepted = true;
__result = (int)__originalMethod.Invoke(__instance, new object[] { precondition });
return false;
}
@@ -78,7 +75,7 @@ namespace StardewModdingAPI.Patches
}
finally
{
- EventErrorPatch.IsIntercepted = false;
+ PatchHelper.StopIntercept(key);
}
}
}
diff --git a/src/SMAPI/Patches/ObjectErrorPatch.cs b/src/SMAPI/Patches/ObjectErrorPatch.cs
index d716b29b..d3b8800a 100644
--- a/src/SMAPI/Patches/ObjectErrorPatch.cs
+++ b/src/SMAPI/Patches/ObjectErrorPatch.cs
@@ -1,4 +1,6 @@
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
using Harmony;
using StardewModdingAPI.Framework.Patching;
using StardewValley;
@@ -33,6 +35,12 @@ namespace StardewModdingAPI.Patches
prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_GetDescription))
);
+ // object.getDisplayName
+ harmony.Patch(
+ original: AccessTools.Method(typeof(SObject), "loadDisplayName"),
+ prefix: new HarmonyMethod(this.GetType(), nameof(ObjectErrorPatch.Before_Object_loadDisplayName))
+ );
+
// IClickableMenu.drawToolTip
harmony.Patch(
original: AccessTools.Method(typeof(IClickableMenu), nameof(IClickableMenu.drawToolTip)),
@@ -60,6 +68,37 @@ namespace StardewModdingAPI.Patches
return true;
}
+ /// <summary>The method to call instead of <see cref="StardewValley.Object.loadDisplayName"/>.</summary>
+ /// <param name="__instance">The instance being patched.</param>
+ /// <param name="__result">The patched method's return value.</param>
+ /// <param name="__originalMethod">The method being wrapped.</param>
+ /// <returns>Returns whether to execute the original method.</returns>
+ private static bool Before_Object_loadDisplayName(SObject __instance, ref string __result, MethodInfo __originalMethod)
+ {
+ const string key = nameof(Before_Object_loadDisplayName);
+ if (!PatchHelper.StartIntercept(key))
+ return true;
+
+ try
+ {
+ __result = (string)__originalMethod.Invoke(__instance, new object[0]);
+ return false;
+ }
+ catch (TargetInvocationException ex) when (ex.InnerException is KeyNotFoundException)
+ {
+ __result = "???";
+ return false;
+ }
+ catch
+ {
+ return true;
+ }
+ finally
+ {
+ PatchHelper.StopIntercept(key);
+ }
+ }
+
/// <summary>The method to call instead of <see cref="IClickableMenu.drawToolTip"/>.</summary>
/// <param name="__instance">The instance being patched.</param>
/// <param name="hoveredItem">The item for which to draw a tooltip.</param>
diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs
index a23aa645..799fcb40 100644
--- a/src/SMAPI/Patches/ScheduleErrorPatch.cs
+++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs
@@ -19,9 +19,6 @@ namespace StardewModdingAPI.Patches
/// <summary>Writes messages to the console and log file on behalf of the game.</summary>
private static IMonitor MonitorForGame;
- /// <summary>Whether the target is currently being intercepted.</summary>
- private static bool IsIntercepting;
-
/*********
** Accessors
@@ -62,12 +59,12 @@ namespace StardewModdingAPI.Patches
/// <returns>Returns whether to execute the original method.</returns>
private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary<int, SchedulePathDescription> __result, MethodInfo __originalMethod)
{
- if (ScheduleErrorPatch.IsIntercepting)
+ const string key = nameof(Before_NPC_parseMasterSchedule);
+ if (!PatchHelper.StartIntercept(key))
return true;
try
{
- ScheduleErrorPatch.IsIntercepting = true;
__result = (Dictionary<int, SchedulePathDescription>)__originalMethod.Invoke(__instance, new object[] { rawData });
return false;
}
@@ -79,7 +76,7 @@ namespace StardewModdingAPI.Patches
}
finally
{
- ScheduleErrorPatch.IsIntercepting = false;
+ PatchHelper.StopIntercept(key);
}
}
}
diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs
index c26ae29a..715c8553 100644
--- a/src/SMAPI/Program.cs
+++ b/src/SMAPI/Program.cs
@@ -143,8 +143,8 @@ namespace StardewModdingAPI
}
// load SMAPI
- using (SCore core = new SCore(modsPath, writeToConsole))
- core.RunInteractively();
+ using SCore core = new SCore(modsPath, writeToConsole);
+ core.RunInteractively();
}
/// <summary>Write an error directly to the console and exit.</summary>
diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json
index 57b4f885..a426b0ef 100644
--- a/src/SMAPI/SMAPI.config.json
+++ b/src/SMAPI/SMAPI.config.json
@@ -73,6 +73,7 @@ copy all the settings, or you may cause bugs due to overridden changes in future
* automatically on Linux or Windows.
* - LightBackground: use darker text colors that look better on a white or light background.
* - DarkBackground: use lighter text colors that look better on a black or dark background.
+ * - None: disables all colors, so everything is written in the default terminal color.
*
* For available color codes, see https://docs.microsoft.com/en-us/dotnet/api/system.consolecolor.
*
diff --git a/src/SMAPI/Utilities/SDate.cs b/src/SMAPI/Utilities/SDate.cs
index 0ab37aa0..03230334 100644
--- a/src/SMAPI/Utilities/SDate.cs
+++ b/src/SMAPI/Utilities/SDate.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using StardewModdingAPI.Framework;
using StardewValley;
namespace StardewModdingAPI.Utilities
@@ -19,6 +20,9 @@ namespace StardewModdingAPI.Utilities
/// <summary>The number of days in a season.</summary>
private readonly int DaysInSeason = 28;
+ /// <summary>The core SMAPI translations.</summary>
+ internal static Translator Translations;
+
/*********
** Accessors
@@ -29,6 +33,10 @@ namespace StardewModdingAPI.Utilities
/// <summary>The season name.</summary>
public string Season { get; }
+ /// <summary>The index of the season (where 0 is spring, 1 is summer, 2 is fall, and 3 is winter).</summary>
+ /// <remarks>This is used in some game calculations (e.g. seasonal game sprites) and methods (e.g. <see cref="Utility.getSeasonNameFromNumber"/>).</remarks>
+ public int SeasonIndex { get; }
+
/// <summary>The year.</summary>
public int Year { get; }
@@ -63,6 +71,30 @@ namespace StardewModdingAPI.Utilities
return new SDate(Game1.dayOfMonth, Game1.currentSeason, Game1.year, allowDayZero: true);
}
+ /// <summary>Get a date from the number of days after 0 spring Y1.</summary>
+ /// <param name="daysSinceStart">The number of days since 0 spring Y1.</param>
+ public static SDate FromDaysSinceStart(int daysSinceStart)
+ {
+ try
+ {
+ return new SDate(0, "spring", 1, allowDayZero: true).AddDays(daysSinceStart);
+ }
+ catch (ArithmeticException)
+ {
+ throw new ArgumentException($"Invalid daysSinceStart '{daysSinceStart}', must be at least 1.");
+ }
+ }
+
+ /// <summary>Get a date from a game date instance.</summary>
+ /// <param name="date">The world date.</param>
+ public static SDate From(WorldDate date)
+ {
+ if (date == null)
+ return null;
+
+ return new SDate(date.DayOfMonth, date.Season, date.Year, allowDayZero: true);
+ }
+
/// <summary>Get a new date with the given number of days added.</summary>
/// <param name="offset">The number of days to add.</param>
/// <returns>Returns the resulting date.</returns>
@@ -92,12 +124,40 @@ namespace StardewModdingAPI.Utilities
return new SDate(day, this.Seasons[seasonIndex], year);
}
- /// <summary>Get a string representation of the date. This is mainly intended for debugging or console messages.</summary>
+ /// <summary>Get a game date representation of the date.</summary>
+ public WorldDate ToWorldDate()
+ {
+ return new WorldDate(this.Year, this.Season, this.Day);
+ }
+
+ /// <summary>Get an untranslated string representation of the date. This is mainly intended for debugging or console messages.</summary>
public override string ToString()
{
return $"{this.Day:00} {this.Season} Y{this.Year}";
}
+ /// <summary>Get a translated string representation of the date in the current game locale.</summary>
+ /// <param name="withYear">Whether to get a string which includes the year number.</param>
+ public string ToLocaleString(bool withYear = true)
+ {
+ // get fallback translation from game
+ string fallback = Utility.getDateStringFor(this.Day, this.SeasonIndex, this.Year);
+ if (SDate.Translations == null)
+ return fallback;
+
+ // get short format
+ string seasonName = Utility.getSeasonNameFromNumber(this.SeasonIndex);
+ return SDate.Translations
+ .Get(withYear ? "generic.date-with-year" : "generic.date", new
+ {
+ day = this.Day,
+ year = this.Year,
+ season = seasonName,
+ seasonLowercase = seasonName?.ToLower()
+ })
+ .Default(fallback);
+ }
+
/****
** IEquatable
****/
@@ -200,6 +260,7 @@ namespace StardewModdingAPI.Utilities
// initialize
this.Day = day;
this.Season = season;
+ this.SeasonIndex = this.GetSeasonIndex(season);
this.Year = year;
this.DayOfWeek = this.GetDayOfWeek(day);
this.DaysSinceStart = this.GetDaysSinceStart(day, season, year);
diff --git a/src/SMAPI/i18n/de.json b/src/SMAPI/i18n/de.json
index a8b3086f..a8cbd83b 100644
--- a/src/SMAPI/i18n/de.json
+++ b/src/SMAPI/i18n/de.json
@@ -1,3 +1,10 @@
{
- "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen)."
+ // error messages
+ "warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}} {{day}}",
+ "generic.date-with-year": "{{season}} {{day}} im Jahr {{year}}"
+
}
diff --git a/src/SMAPI/i18n/default.json b/src/SMAPI/i18n/default.json
index 5a3e4a6e..7a3d3ed5 100644
--- a/src/SMAPI/i18n/default.json
+++ b/src/SMAPI/i18n/default.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info)."
+ // error messages
+ "warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}} {{day}}",
+ "generic.date-with-year": "{{season}} {{day}} in year {{year}}"
}
diff --git a/src/SMAPI/i18n/es.json b/src/SMAPI/i18n/es.json
index f5a74dfe..c9843991 100644
--- a/src/SMAPI/i18n/es.json
+++ b/src/SMAPI/i18n/es.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información)."
+ // error messages
+ "warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{seasonLowercase}} {{day}}",
+ "generic.date-with-year": "{{seasonLowercase}} {{day}} del año {{year}}"
}
diff --git a/src/SMAPI/i18n/fr.json b/src/SMAPI/i18n/fr.json
index 6d051025..5969aa20 100644
--- a/src/SMAPI/i18n/fr.json
+++ b/src/SMAPI/i18n/fr.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations)."
+ // error messages
+ "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{day}} {{seasonLowercase}}",
+ "generic.date-with-year": "{{day}} {{seasonLowercase}} de l'année {{year}}"
}
diff --git a/src/SMAPI/i18n/hu.json b/src/SMAPI/i18n/hu.json
index aa0c7546..785012f4 100644
--- a/src/SMAPI/i18n/hu.json
+++ b/src/SMAPI/i18n/hu.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon)."
+ // error messages
+ "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}} {{day}}",
+ "generic.date-with-year": "{{year}}. év {{season}} {{day}}"
}
diff --git a/src/SMAPI/i18n/it.json b/src/SMAPI/i18n/it.json
index 43493018..3b3351c3 100644
--- a/src/SMAPI/i18n/it.json
+++ b/src/SMAPI/i18n/it.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)."
+ // error messages
+ "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{day}} {{season}}",
+ "generic.date-with-year": "{{day}} {{season}} dell'anno {{year}}"
}
diff --git a/src/SMAPI/i18n/ja.json b/src/SMAPI/i18n/ja.json
index 9bbc285e..1f814bfa 100644
--- a/src/SMAPI/i18n/ja.json
+++ b/src/SMAPI/i18n/ja.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)"
+ // error messages
+ "warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}} {{day}}日",
+ "generic.date-with-year": "{{year}}年目 {{season}} {{day}}日"
}
diff --git a/src/SMAPI/i18n/ko.json b/src/SMAPI/i18n/ko.json
new file mode 100644
index 00000000..d5bbffa4
--- /dev/null
+++ b/src/SMAPI/i18n/ko.json
@@ -0,0 +1,9 @@
+{
+ // error messages
+ "warn.invalid-content-removed": "충돌을 방지하기 위해 잘못된 컨텐츠가 제거되었습니다 (자세한 내용은 SMAPI 콘솔 참조).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}} {{day}}",
+ "generic.date-with-year": "{{year}} 학년 {{season}} {{day}}"
+}
diff --git a/src/SMAPI/i18n/pt.json b/src/SMAPI/i18n/pt.json
index 59273680..e8460922 100644
--- a/src/SMAPI/i18n/pt.json
+++ b/src/SMAPI/i18n/pt.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações)."
+ // error messages
+ "warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}} {{day}}",
+ "generic.date-with-year": "{{season}} {{day}} no ano {{year}}"
}
diff --git a/src/SMAPI/i18n/ru.json b/src/SMAPI/i18n/ru.json
index a6a242fa..002fdbf8 100644
--- a/src/SMAPI/i18n/ru.json
+++ b/src/SMAPI/i18n/ru.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)"
+ // error messages
+ "warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}}, {{day}}-е число",
+ "generic.date-with-year": "{{season}}, {{day}}-е число, {{year}}-й год"
}
diff --git a/src/SMAPI/i18n/tr.json b/src/SMAPI/i18n/tr.json
index 34229f2b..2a6e83a1 100644
--- a/src/SMAPI/i18n/tr.json
+++ b/src/SMAPI/i18n/tr.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut)."
+ // error messages
+ "warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut).",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{day}} {{season}}",
+ "generic.date-with-year": "{{day}} {{season}} года {{year}}"
}
diff --git a/src/SMAPI/i18n/zh.json b/src/SMAPI/i18n/zh.json
index 9c0e0c21..cdbe3b74 100644
--- a/src/SMAPI/i18n/zh.json
+++ b/src/SMAPI/i18n/zh.json
@@ -1,3 +1,9 @@
{
- "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)"
+ // error messages
+ "warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)",
+
+ // short date format for SDate
+ // tokens: {{day}} (like 15), {{season}} (like Spring), {{seasonLowercase}} (like spring), {{year}} (like 2)
+ "generic.date": "{{season}}{{day}}日",
+ "generic.date-with-year": "第{{year}}年{{season}}{{day}}日"
}