Merge branch 'stable' of https://github.com/Pathoschild/SMAPI into android

# Conflicts:
#	src/SMAPI.sln
#	src/SMAPI/Constants.cs
#	src/SMAPI/i18n/default.json
#	src/SMAPI/i18n/zh.json
This commit is contained in:
ZaneYork 2020-04-28 09:52:21 +08:00
commit 6276eff2ca
50 changed files with 742 additions and 71 deletions

View File

@ -4,7 +4,7 @@
<!--set properties -->
<PropertyGroup>
<Version>3.4.1</Version>
<Version>3.5.0</Version>
<Product>SMAPI</Product>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>

View File

@ -1,6 +1,6 @@
**SMAPI** is an open-source modding framework and API for [Stardew Valley](https://stardewvalley.net/)
that lets you play the game with mods. It's safely installed alongside the game's executable, and
doesn't change any of your game files. It serves eight main purposes:
doesn't change any of your game files. It serves seven main purposes:
1. **Load mods into the game.**
_SMAPI loads mods when the game is starting up so they can interact with it. (Code mods aren't
@ -10,14 +10,10 @@ doesn't change any of your game files. It serves eight main purposes:
_SMAPI provides APIs and events which let mods interact with the game in ways they otherwise
couldn't._
3. **Rewrite mods for crossplatform compatibility.**
3. **Rewrite mods for compatibility.**
_SMAPI rewrites mods' compiled code before loading them so they work on Linux/Mac/Windows
without the mods needing to handle differences between the Linux/Mac and Windows versions of the
game._
4. **Rewrite mods to update them.**
_SMAPI detects when a mod accesses part of the game that changed in a game update which affects
many mods, and rewrites the mod so it's compatible._
game. In some cases it also rewrites code broken by a game update so the mod doesn't break._
5. **Intercept errors and automatically fix saves.**
_SMAPI intercepts errors, shows the error info in the SMAPI console, and in most cases
@ -37,8 +33,8 @@ doesn't change any of your game files. It serves eight main purposes:
they cause problems._
8. **Back up your save files.**
_SMAPI automatically creates a daily backup of your saves and keeps ten backups, in case
something goes wrong. (Via the bundled SaveBackup mod.)_
_SMAPI automatically creates a daily backup of your saves and keeps ten backups (via the bundled
Save Backup mod), in case something goes wrong._
## Documentation
Have questions? Come [ask the community](https://smapi.io/community) to get help from SMAPI
@ -69,7 +65,7 @@ German | ✓ [fully translated](../src/SMAPI/i18n/de.json)
Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json)
Italian | ✓ [fully translated](../src/SMAPI/i18n/it.json)
Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json)
Korean | ❑ not translated
Korean | ✓ [fully translated](../src/SMAPI/i18n/ko.json)
Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json)
Russian | ✓ [fully translated](../src/SMAPI/i18n/ru.json)
Spanish | ✓ [fully translated](../src/SMAPI/i18n/es.json)

View File

@ -1,6 +1,33 @@
&larr; [README](README.md)
# Release notes
## 3.5
Released 27 April 2020 for Stardew Valley 1.4.1 or later.
* For players:
* SMAPI now prevents more game errors due to broken items, so you no longer need save editing to remove them.
* Added option to disable console colors.
* Updated compatibility list.
* Improved translations.¹
* For the Console Commands mod:
* Commands like `world_setday` now also affect the 'days played' stat, so in-game events/randomization match what you'd get if you played to that date normally (thanks to kdau!).
* For the web UI:
* Updated the JSON validator/schema for Content Patcher 1.13.
* Fixed rare intermittent "CGI application encountered an error" errors.
* For modders:
* Added map patching to the content API (via `asset.AsMap()`).
* Added support for using patch helpers with arbitrary data (via `helper.Content.GetPatchHelper`).
* Added `SDate` fields/methods: `SeasonIndex`, `FromDaysSinceStart`, `FromWorldDate`, `ToWorldDate`, and `ToLocaleString` (thanks to kdau!).
* Added `SDate` translations taken from the Lookup Anything mod.¹
* Fixed asset propagation for certain maps loaded through temporary content managers. This notably fixes unreliable patches to the farmhouse and town maps.
* Fixed asset propagation on Linux/Mac for monster sprites, NPC dialogue, and NPC schedules.
* Fixed asset propagation for NPC dialogue sometimes causing a spouse to skip marriage dialogue or not allow kisses.
¹ Date format translations were taken from the Lookup Anything mod; thanks to translators FixThisPlz (improved Russian), LeecanIt (added Italian), pomepome (added Japanese), S2SKY (added Korean), Sasara (added German), SteaNN (added Russian), ThomasGabrielDelavault (added Spanish), VincentRoth (added French), Yllelder (improved Spanish), and yuwenlan (added Chinese). Some translations for Korean, Hungarian, and Turkish were derived from the game translations.
## 3.4.1
Released 24 March 2020 for Stardew Valley 1.4.1 or later.

View File

@ -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;
/*********

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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()

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using NUnit.Framework;
using StardewModdingAPI.Utilities;
using StardewValley;
namespace SMAPI.Tests.Utilities
{
@ -81,6 +82,62 @@ namespace SMAPI.Tests.Utilities
Assert.Throws<ArgumentException>(() => _ = new SDate(day, season, year), "Constructing the invalid date didn't throw the expected exception.");
}
/****
** FromDaysSinceStart
****/
[Test(Description = "Assert that FromDaysSinceStart returns the expected date.")]
[TestCase(1, ExpectedResult = "01 spring Y1")]
[TestCase(2, ExpectedResult = "02 spring Y1")]
[TestCase(28, ExpectedResult = "28 spring Y1")]
[TestCase(29, ExpectedResult = "01 summer Y1")]
[TestCase(141, ExpectedResult = "01 summer Y2")]
public string FromDaysSinceStart(int daysSinceStart)
{
// act
return SDate.FromDaysSinceStart(daysSinceStart).ToString();
}
[Test(Description = "Assert that FromDaysSinceStart throws an exception if the number of days is invalid.")]
[TestCase(-1)] // day < 0
[TestCase(0)] // day == 0
[SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Deliberate for unit test.")]
public void FromDaysSinceStart_RejectsInvalidValues(int daysSinceStart)
{
// act & assert
Assert.Throws<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
****/
@ -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)]

View File

@ -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();

View File

@ -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": {

View File

@ -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

View File

@ -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.7", allowNonStandard: true);
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.5");

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -34,6 +34,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;
@ -178,6 +179,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);

View File

@ -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>

View File

@ -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>
void PatchMap(Map source, Rectangle? sourceArea = null, Rectangle? targetArea = null);
}
}

View File

@ -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);
}
}

View File

@ -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;
@ -892,11 +893,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);
}
@ -1039,9 +1042,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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -154,8 +154,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>

View File

@ -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.
*

View File

@ -191,6 +191,7 @@
<Compile Include="Framework\Content\AssetData.cs" />
<Compile Include="Framework\Content\AssetDataForDictionary.cs" />
<Compile Include="Framework\Content\AssetDataForImage.cs" />
<Compile Include="Framework\Content\AssetDataForMap.cs" />
<Compile Include="Framework\Content\AssetDataForObject.cs" />
<Compile Include="Framework\Content\AssetInfo.cs" />
<Compile Include="Framework\Content\AssetInterceptorChange.cs" />
@ -279,6 +280,7 @@
<Compile Include="Framework\Networking\RemoteContextModModel.cs" />
<Compile Include="Framework\Patching\GamePatcher.cs" />
<Compile Include="Framework\Patching\IHarmonyPatch.cs" />
<Compile Include="Framework\Patching\PatchHelper.cs" />
<Compile Include="Framework\PerformanceMonitoring\AlertContext.cs" />
<Compile Include="Framework\PerformanceMonitoring\AlertEntry.cs" />
<Compile Include="Framework\PerformanceMonitoring\PeakEntry.cs" />
@ -368,6 +370,7 @@
<Compile Include="IAssetData.cs" />
<Compile Include="IAssetDataForDictionary.cs" />
<Compile Include="IAssetDataForImage.cs" />
<Compile Include="IAssetDataForMap.cs" />
<Compile Include="IAssetEditor.cs" />
<Compile Include="IAssetInfo.cs" />
<Compile Include="IAssetLoader.cs" />
@ -444,6 +447,7 @@
<Folder Include="Resources\drawable\" />
</ItemGroup>
<ItemGroup>
<Content Include="i18n\ko.json" />
<Content Include="icon.ico" />
<Content Include="steam_appid.txt" />
</ItemGroup>

View File

@ -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);

View File

@ -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}}"
}

View File

@ -1,5 +1,11 @@
{
// 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}}",
"warn.save-broken": "Load save failed, your save file may be broken.You can swap to previous day's save, or restore save files manually with save backups.Backup provided by SMAPI was located at StardewValley/smapi-internal/save-backups",
"btn.swap": "Swap",
"btn.back": "Back"

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}日"
}

9
src/SMAPI/i18n/ko.json Normal file
View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}-й год"
}

View File

@ -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}}"
}

View File

@ -1,5 +1,11 @@
{
// 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}}日"
"warn.save-broken": "无法加载该存档,存档可能已损坏.你可以切换到前一日的存档,或者手动恢复到备份的存档.SMAPI提供的备份文件位于: StardewValley/smapi-internal/save-backups",
"btn.swap": "切换",
"btn.back": "返回"