summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build/common.targets2
-rw-r--r--docs/release-notes.md36
-rw-r--r--src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj2
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/manifest.json4
-rw-r--r--src/SMAPI.Mods.SaveBackup/ModEntry.cs46
-rw-r--r--src/SMAPI.Mods.SaveBackup/manifest.json4
-rw-r--r--src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs4
-rw-r--r--src/SMAPI.Toolkit/SMAPI.Toolkit.csproj2
-rw-r--r--src/SMAPI.Toolkit/SemanticVersion.cs9
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/NonStandardSemanticVersionConverter.cs15
-rw-r--r--src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs9
-rw-r--r--src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs14
-rw-r--r--src/SMAPI.Web/Controllers/ModsApiController.cs54
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs15
-rw-r--r--src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs3
-rw-r--r--src/SMAPI.Web/Framework/VersionConstraint.cs2
-rw-r--r--src/SMAPI.Web/SMAPI.Web.csproj6
-rw-r--r--src/SMAPI.Web/Views/Mods/Index.cshtml4
-rw-r--r--src/SMAPI.Web/appsettings.json13
-rw-r--r--src/SMAPI.Web/wwwroot/Content/css/mods.css5
-rw-r--r--src/SMAPI.Web/wwwroot/Content/js/mods.js24
-rw-r--r--src/SMAPI.Web/wwwroot/SMAPI.metadata.json10
-rw-r--r--src/SMAPI.Web/wwwroot/schemas/content-patcher.json8
-rw-r--r--src/SMAPI/Constants.cs55
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs17
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs17
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs142
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs53
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/InputHelper.cs7
-rw-r--r--src/SMAPI/Framework/Networking/ModMessageModel.cs2
-rw-r--r--src/SMAPI/Framework/SGame.cs166
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs84
-rw-r--r--src/SMAPI/IAssetDataForImage.cs6
-rw-r--r--src/SMAPI/IInputHelper.cs4
-rw-r--r--src/SMAPI/Metadata/CoreAssetPropagator.cs7
-rw-r--r--src/SMAPI/SButtonState.cs (renamed from src/SMAPI/Framework/Input/InputStatus.cs)14
-rw-r--r--src/SMAPI/SMAPI.csproj4
-rw-r--r--src/SMAPI/i18n/it.json3
40 files changed, 599 insertions, 285 deletions
diff --git a/build/common.targets b/build/common.targets
index 8b0d1301..626eeef6 100644
--- a/build/common.targets
+++ b/build/common.targets
@@ -4,7 +4,7 @@
<!--set properties -->
<PropertyGroup>
- <Version>3.2.0</Version>
+ <Version>3.3.0</Version>
<Product>SMAPI</Product>
<AssemblySearchPaths>$(AssemblySearchPaths);{GAC}</AssemblySearchPaths>
diff --git a/docs/release-notes.md b/docs/release-notes.md
index f1981218..26515b61 100644
--- a/docs/release-notes.md
+++ b/docs/release-notes.md
@@ -1,6 +1,40 @@
&larr; [README](README.md)
# Release notes
+## 3.3
+Released 22 February 2020 for Stardew Valley 1.4.1 or later.
+
+* For players:
+ * Improved performance for mods which load many images.
+ * Reduced network traffic for mod broadcasts to players who can't process them.
+ * Fixed update-check errors for recent versions of SMAPI on Android.
+ * Updated draw logic to match recent game updates.
+ * Updated compatibility list.
+ * Updated SMAPI/game version map.
+ * Updated translations. Thanks to xCarloC (added Italian)!
+
+* For the Save Backup mod:
+ * Fixed warning on MacOS when you have no saves yet.
+ * Reduced log messages.
+
+* For modders:
+ * Added support for [message sending](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Message_sending) to mods on the current computer (in addition to remote computers).
+ * Added `ExtendImage` method to content API when editing files to resize textures.
+ * Added `helper.Input.GetState` to get the low-level state of a button.
+ * **[Breaking change]** Map tilesheets are no loaded from `Content` if they can't be found in `Content/Maps`. This reflects an upcoming change in the game to delete duplicate map tilesheets under `Content`. Most mods should be unaffected.
+ * Improved map tilesheet errors so they provide more info.
+ * When mods load an asset using a more general type like `content.Load<object>`, SMAPI now calls `IAssetEditor` instances with the actual asset type instead of the specified one.
+ * Updated dependencies (including Mono.Cecil 0.11.1 → 0.11.2).
+ * Fixed dialogue propagation clearing marriage dialogue.
+
+* For the web UI:
+ * Updated the JSON validator and Content Patcher schema for `.tmx` support.
+ * The mod compatibility page now has a sticky table header.
+
+* For SMAPI/tool developers:
+ * Improved support for four-part versions to support SMAPI on Android.
+ * The SMAPI log now prefixes the OS name with `Android` on Android.
+
## 3.2
Released 01 February 2020 for Stardew Valley 1.4.1 or later.
@@ -23,7 +57,7 @@ Released 01 February 2020 for Stardew Valley 1.4.1 or later.
* Fixed Android issue where game files were backed up.
* For modders:
- * Added support for `.tmx` map files.
+ * Added support for `.tmx` map files. (Thanks to [Platonymous for the underlying library](https://github.com/Platonymous/TMXTile)!)
* Added special handling for `Vector2` values in `.json` files, so they work consistently crossplatform.
* Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on.
* Fixed incorrect warning about mods adding invalid schedules in some cases. The validation was unreliable, and has been removed.
diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
index c1d5626f..e2be66d9 100644
--- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
+++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj
@@ -7,7 +7,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
<PrivateAssets>all</PrivateAssets>
diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json
index 0d0e4901..971c591a 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.2.0",
+ "Version": "3.3.0",
"Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll",
- "MinimumApiVersion": "3.2.0"
+ "MinimumApiVersion": "3.3.0"
}
diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
index 8b139d8f..b8d3be1c 100644
--- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs
+++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs
@@ -66,29 +66,37 @@ namespace StardewModdingAPI.Mods.SaveBackup
FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName));
DirectoryInfo fallbackDir = new DirectoryInfo(Path.Combine(backupFolder.FullName, this.BackupLabel));
if (targetFile.Exists || fallbackDir.Exists)
+ {
+ this.Monitor.Log("Already backed up today.");
return;
+ }
// copy saves to fallback directory (ignore non-save files/folders)
- this.Monitor.Log($"Backing up saves to {fallbackDir.FullName}...", LogLevel.Trace);
DirectoryInfo savesDir = new DirectoryInfo(Constants.SavesPath);
- this.RecursiveCopy(savesDir, fallbackDir, entry => this.MatchSaveFolders(savesDir, entry), copyRoot: false);
+ if (!this.RecursiveCopy(savesDir, fallbackDir, entry => this.MatchSaveFolders(savesDir, entry), copyRoot: false))
+ {
+ this.Monitor.Log("No saves found.");
+ return;
+ }
// compress backup if possible
- this.Monitor.Log("Compressing backup if possible...", LogLevel.Trace);
if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError))
{
- if (Constants.TargetPlatform != GamePlatform.Android) // expected to fail on Android
- this.Monitor.Log($"Couldn't compress backup, leaving it uncompressed.\n{compressError}", LogLevel.Trace);
+ this.Monitor.Log(Constants.TargetPlatform != GamePlatform.Android
+ ? $"Backed up to {fallbackDir.FullName}." // expected to fail on Android
+ : $"Backed up to {fallbackDir.FullName}. Couldn't compress backup:\n{compressError}"
+ );
}
else
+ {
+ this.Monitor.Log($"Backed up to {targetFile.FullName}.");
fallbackDir.Delete(recursive: true);
-
- this.Monitor.Log("Backup done!", LogLevel.Trace);
+ }
}
catch (Exception ex)
{
- this.Monitor.Log("Couldn't back up save files (see log file for details).", LogLevel.Warn);
- this.Monitor.Log(ex.ToString(), LogLevel.Trace);
+ this.Monitor.Log("Couldn't back up saves (see log file for details).", LogLevel.Warn);
+ this.Monitor.Log(ex.ToString());
}
}
@@ -108,7 +116,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
{
try
{
- this.Monitor.Log($"Deleting {entry.Name}...", LogLevel.Trace);
+ this.Monitor.Log($"Deleting {entry.Name}...");
if (entry is DirectoryInfo folder)
folder.Delete(recursive: true);
else
@@ -123,7 +131,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
catch (Exception ex)
{
this.Monitor.Log("Couldn't remove old backups (see log file for details).", LogLevel.Warn);
- this.Monitor.Log(ex.ToString(), LogLevel.Trace);
+ this.Monitor.Log(ex.ToString());
}
}
@@ -199,29 +207,33 @@ namespace StardewModdingAPI.Mods.SaveBackup
/// <param name="copyRoot">Whether to copy the root folder itself, or <c>false</c> to only copy its contents.</param>
/// <param name="filter">A filter which matches the files or directories to copy, or <c>null</c> to copy everything.</param>
/// <remarks>Derived from the SMAPI installer code.</remarks>
- private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter, bool copyRoot = true)
+ /// <returns>Returns whether any files were copied.</returns>
+ private bool RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func<FileSystemInfo, bool> filter, bool copyRoot = true)
{
- if (!targetFolder.Exists)
- targetFolder.Create();
+ if (!source.Exists || filter?.Invoke(source) == false)
+ return false;
- if (filter?.Invoke(source) == false)
- return;
+ bool anyCopied = false;
switch (source)
{
case FileInfo sourceFile:
+ targetFolder.Create();
sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name));
+ anyCopied = true;
break;
case DirectoryInfo sourceDir:
DirectoryInfo targetSubfolder = copyRoot ? new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)) : targetFolder;
foreach (var entry in sourceDir.EnumerateFileSystemInfos())
- this.RecursiveCopy(entry, targetSubfolder, filter);
+ anyCopied = this.RecursiveCopy(entry, targetSubfolder, filter) || anyCopied;
break;
default:
throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'.");
}
+
+ return anyCopied;
}
/// <summary>A copy filter which matches save folders.</summary>
diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json
index 74256013..4559d1b0 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.2.0",
+ "Version": "3.3.0",
"Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll",
- "MinimumApiVersion": "3.2.0"
+ "MinimumApiVersion": "3.3.0"
}
diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
index dadb8c10..188db31d 100644
--- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
+++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryVersionModel.cs
@@ -1,3 +1,6 @@
+using Newtonsoft.Json;
+using StardewModdingAPI.Toolkit.Serialization.Converters;
+
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{
/// <summary>Metadata about a version.</summary>
@@ -7,6 +10,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
** Accessors
*********/
/// <summary>The version number.</summary>
+ [JsonConverter(typeof(NonStandardSemanticVersionConverter))]
public ISemanticVersion Version { get; set; }
/// <summary>The mod page URL.</summary>
diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
index 16a97dbf..a7de7166 100644
--- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
+++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj
@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="HtmlAgilityPack" Version="1.11.18" />
+ <PackageReference Include="HtmlAgilityPack" Version="1.11.20" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
<PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" />
diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs
index 5ead6dc8..86db2820 100644
--- a/src/SMAPI.Toolkit/SemanticVersion.cs
+++ b/src/SMAPI.Toolkit/SemanticVersion.cs
@@ -199,18 +199,19 @@ namespace StardewModdingAPI.Toolkit
/// <returns>Returns whether parsing the version succeeded.</returns>
public static bool TryParse(string version, out ISemanticVersion parsed)
{
- return SemanticVersion.TryParseNonStandard(version, out parsed) && !parsed.IsNonStandard();
+ return SemanticVersion.TryParse(version, allowNonStandard: false, out parsed);
}
- /// <summary>Parse a version string without throwing an exception if it fails, including support for non-standard extensions like <see cref="IPlatformSpecificVersion"/>.</summary>
+ /// <summary>Parse a version string without throwing an exception if it fails.</summary>
/// <param name="version">The version string.</param>
+ /// <param name="allowNonStandard">Whether to allow non-standard extensions to semantic versioning.</param>
/// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns>
- public static bool TryParseNonStandard(string version, out ISemanticVersion parsed)
+ public static bool TryParse(string version, bool allowNonStandard, out ISemanticVersion parsed)
{
try
{
- parsed = new SemanticVersion(version, true);
+ parsed = new SemanticVersion(version, allowNonStandard);
return true;
}
catch
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/NonStandardSemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/NonStandardSemanticVersionConverter.cs
new file mode 100644
index 00000000..6f870bcf
--- /dev/null
+++ b/src/SMAPI.Toolkit/Serialization/Converters/NonStandardSemanticVersionConverter.cs
@@ -0,0 +1,15 @@
+namespace StardewModdingAPI.Toolkit.Serialization.Converters
+{
+ /// <summary>Handles deserialization of <see cref="ISemanticVersion"/>, allowing for non-standard extensions.</summary>
+ internal class NonStandardSemanticVersionConverter : SemanticVersionConverter
+ {
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public NonStandardSemanticVersionConverter()
+ {
+ this.AllowNonStandard = true;
+ }
+ }
+}
diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
index e1b9db1d..3604956b 100644
--- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
+++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs
@@ -8,6 +8,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
internal class SemanticVersionConverter : JsonConverter
{
/*********
+ ** Fields
+ *********/
+ /// <summary>Whether to allow non-standard extensions to semantic versioning.</summary>
+ protected bool AllowNonStandard { get; set; }
+
+
+ /*********
** Accessors
*********/
/// <summary>Get whether this converter can read JSON.</summary>
@@ -78,7 +85,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
{
if (string.IsNullOrWhiteSpace(str))
return null;
- if (!SemanticVersion.TryParse(str, out ISemanticVersion version))
+ if (!SemanticVersion.TryParse(str, allowNonStandard: this.AllowNonStandard, out ISemanticVersion version))
throw new SParseException($"Can't parse semantic version from invalid value '{str}', should be formatted like 1.2, 1.2.30, or 1.2.30-beta (path: {path}).");
return version;
}
diff --git a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
index 2a01fe4b..c45448f3 100644
--- a/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
+++ b/src/SMAPI.Toolkit/Utilities/EnvironmentUtility.cs
@@ -53,7 +53,19 @@ namespace StardewModdingAPI.Toolkit.Utilities
}
catch { }
#endif
- return (platform == Platform.Mac ? "MacOS " : "") + Environment.OSVersion;
+
+ string name = Environment.OSVersion.ToString();
+ switch (platform)
+ {
+ case Platform.Android:
+ name = $"Android {name}";
+ break;
+
+ case Platform.Mac:
+ name = $"MacOS {name}";
+ break;
+ }
+ return name;
}
/// <summary>Get the name of the Stardew Valley executable.</summary>
diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs
index f194b4d0..06768f03 100644
--- a/src/SMAPI.Web/Controllers/ModsApiController.cs
+++ b/src/SMAPI.Web/Controllers/ModsApiController.cs
@@ -41,11 +41,8 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>The cache in which to store mod data.</summary>
private readonly IModCacheRepository ModCache;
- /// <summary>The number of minutes successful update checks should be cached before refetching them.</summary>
- private readonly int SuccessCacheMinutes;
-
- /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
- private readonly int ErrorCacheMinutes;
+ /// <summary>The config settings for mod update checks.</summary>
+ private readonly IOptions<ModUpdateCheckConfig> Config;
/// <summary>The internal mod metadata list.</summary>
private readonly ModDatabase ModDatabase;
@@ -58,21 +55,19 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="environment">The web hosting environment.</param>
/// <param name="wikiCache">The cache in which to store wiki data.</param>
/// <param name="modCache">The cache in which to store mod metadata.</param>
- /// <param name="configProvider">The config settings for mod update checks.</param>
+ /// <param name="config">The config settings for mod update checks.</param>
/// <param name="chucklefish">The Chucklefish API client.</param>
/// <param name="curseForge">The CurseForge API client.</param>
/// <param name="github">The GitHub API client.</param>
/// <param name="modDrop">The ModDrop API client.</param>
/// <param name="nexus">The Nexus API client.</param>
- public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> configProvider, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
+ public ModsApiController(IHostingEnvironment environment, IWikiCacheRepository wikiCache, IModCacheRepository modCache, IOptions<ModUpdateCheckConfig> config, IChucklefishClient chucklefish, ICurseForgeClient curseForge, IGitHubClient github, IModDropClient modDrop, INexusClient nexus)
{
this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
- ModUpdateCheckConfig config = configProvider.Value;
this.WikiCache = wikiCache;
this.ModCache = modCache;
- this.SuccessCacheMinutes = config.SuccessCacheMinutes;
- this.ErrorCacheMinutes = config.ErrorCacheMinutes;
+ this.Config = config;
this.Repositories =
new IModRepository[]
{
@@ -133,6 +128,8 @@ namespace StardewModdingAPI.Web.Controllers
ModDataRecord record = this.ModDatabase.Get(search.ID);
WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray();
+ ModOverrideConfig overrides = this.Config.Value.ModOverrides.FirstOrDefault(p => p.ID.Equals(search.ID?.Trim(), StringComparison.InvariantCultureIgnoreCase));
+ bool allowNonStandardVersions = overrides?.AllowNonStandardVersions ?? false;
// get latest versions
ModEntryModel result = new ModEntryModel { ID = search.ID };
@@ -151,7 +148,7 @@ namespace StardewModdingAPI.Web.Controllers
}
// fetch data
- ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey);
+ ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions);
if (data.Error != null)
{
errors.Add(data.Error);
@@ -161,7 +158,7 @@ namespace StardewModdingAPI.Web.Controllers
// handle main version
if (data.Version != null)
{
- ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions);
+ ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
if (version == null)
{
errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'.");
@@ -175,7 +172,7 @@ namespace StardewModdingAPI.Web.Controllers
// handle optional version
if (data.PreviewVersion != null)
{
- ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions);
+ ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
if (version == null)
{
errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'.");
@@ -215,16 +212,16 @@ namespace StardewModdingAPI.Web.Controllers
}
// special cases
- if (result.ID == "Pathoschild.SMAPI")
+ if (overrides?.SetUrl != null)
{
if (main != null)
- main.Url = "https://smapi.io/";
+ main.Url = overrides.SetUrl;
if (optional != null)
- optional.Url = "https://smapi.io/";
+ optional.Url = overrides.SetUrl;
}
// get recommended update (if any)
- ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions);
+ ISemanticVersion installedVersion = this.GetMappedVersion(search.InstalledVersion?.ToString(), wikiEntry?.MapLocalVersions, allowNonStandard: allowNonStandardVersions);
if (apiVersion != null && installedVersion != null)
{
// get newer versions
@@ -283,10 +280,11 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get the mod info for an update key.</summary>
/// <param name="updateKey">The namespaced update key.</param>
- private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey)
+ /// <param name="allowNonStandardVersions">Whether to allow non-standard versions.</param>
+ private async Task<ModInfoModel> GetInfoForUpdateKeyAsync(UpdateKey updateKey, bool allowNonStandardVersions)
{
// get mod
- if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.ErrorCacheMinutes : this.SuccessCacheMinutes))
+ if (!this.ModCache.TryGetMod(updateKey.Repository, updateKey.ID, out CachedMod mod) || this.ModCache.IsStale(mod.LastUpdated, mod.FetchStatus == RemoteModStatus.TemporaryError ? this.Config.Value.ErrorCacheMinutes : this.Config.Value.SuccessCacheMinutes))
{
// get site
if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
@@ -298,7 +296,7 @@ namespace StardewModdingAPI.Web.Controllers
{
if (result.Version == null)
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number.");
- else if (!SemanticVersion.TryParse(result.Version, out _))
+ else if (!SemanticVersion.TryParse(result.Version, allowNonStandardVersions, out _))
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with invalid semantic version '{result.Version}'.");
}
@@ -357,15 +355,16 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to parse.</param>
/// <param name="map">A map of version replacements.</param>
- private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map)
+ /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
+ private ISemanticVersion GetMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
{
// try mapped version
- string rawNewVersion = this.GetRawMappedVersion(version, map);
- if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew))
+ string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard);
+ if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew))
return parsedNew;
// return original version
- return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld)
+ return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld)
? parsedOld
: null;
}
@@ -373,7 +372,8 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to map.</param>
/// <param name="map">A map of version replacements.</param>
- private string GetRawMappedVersion(string version, IDictionary<string, string> map)
+ /// <param name="allowNonStandard">Whether to allow non-standard versions.</param>
+ private string GetRawMappedVersion(string version, IDictionary<string, string> map, bool allowNonStandard)
{
if (version == null || map == null || !map.Any())
return version;
@@ -383,14 +383,14 @@ namespace StardewModdingAPI.Web.Controllers
return map[version];
// match parsed version
- if (SemanticVersion.TryParse(version, out ISemanticVersion parsed))
+ if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed))
{
if (map.ContainsKey(parsed.ToString()))
return map[parsed.ToString()];
foreach (var pair in map)
{
- if (SemanticVersion.TryParse(pair.Key, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, out ISemanticVersion newVersion))
+ if (SemanticVersion.TryParse(pair.Key, allowNonStandard, out ISemanticVersion target) && parsed.Equals(target) && SemanticVersion.TryParse(pair.Value, allowNonStandard, out ISemanticVersion newVersion))
return newVersion.ToString();
}
}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
new file mode 100644
index 00000000..f382d7b5
--- /dev/null
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModOverrideConfig.cs
@@ -0,0 +1,15 @@
+namespace StardewModdingAPI.Web.Framework.ConfigModels
+{
+ /// <summary>Override update-check metadata for a mod.</summary>
+ internal class ModOverrideConfig
+ {
+ /// <summary>The unique ID from the mod's manifest.</summary>
+ public string ID { get; set; }
+
+ /// <summary>Whether to allow non-standard versions.</summary>
+ public bool AllowNonStandardVersions { get; set; }
+
+ /// <summary>The mod page URL to use regardless of which site has the update, or <c>null</c> to use the site URL.</summary>
+ public string SetUrl { get; set; }
+ }
+}
diff --git a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
index 46073eb8..bd58dba0 100644
--- a/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
+++ b/src/SMAPI.Web/Framework/ConfigModels/ModUpdateCheckConfig.cs
@@ -11,5 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
public int ErrorCacheMinutes { get; set; }
+
+ /// <summary>Update-check metadata to override.</summary>
+ public ModOverrideConfig[] ModOverrides { get; set; }
}
}
diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs
index 72f5ef84..f0c57c41 100644
--- a/src/SMAPI.Web/Framework/VersionConstraint.cs
+++ b/src/SMAPI.Web/Framework/VersionConstraint.cs
@@ -28,7 +28,7 @@ namespace StardewModdingAPI.Web.Framework
return
values.TryGetValue(routeKey, out object routeValue)
&& routeValue is string routeStr
- && SemanticVersion.TryParseNonStandard(routeStr, out _);
+ && SemanticVersion.TryParse(routeStr, allowNonStandard: true, out _);
}
}
}
diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj
index 148631a9..97bea0fb 100644
--- a/src/SMAPI.Web/SMAPI.Web.csproj
+++ b/src/SMAPI.Web/SMAPI.Web.csproj
@@ -12,11 +12,11 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Azure.Storage.Blobs" Version="12.2.0" />
+ <PackageReference Include="Azure.Storage.Blobs" Version="12.3.0" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.7.9" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.6.3" />
<PackageReference Include="Hangfire.Mongo" Version="0.6.6" />
- <PackageReference Include="HtmlAgilityPack" Version="1.11.18" />
+ <PackageReference Include="HtmlAgilityPack" Version="1.11.20" />
<PackageReference Include="Humanizer.Core" Version="2.7.9" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="Markdig" Version="0.18.1" />
@@ -25,7 +25,7 @@
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Mongo2Go" Version="2.2.12" />
- <PackageReference Include="MongoDB.Driver" Version="2.10.1" />
+ <PackageReference Include="MongoDB.Driver" Version="2.10.2" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Pathoschild.FluentNexus" Version="0.8.0" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml
index 5b310d55..b1d9ae2c 100644
--- a/src/SMAPI.Web/Views/Mods/Index.cshtml
+++ b/src/SMAPI.Web/Views/Mods/Index.cshtml
@@ -8,11 +8,11 @@
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
}
@section Head {
- <link rel="stylesheet" href="~/Content/css/mods.css?r=20190302" />
+ <link rel="stylesheet" href="~/Content/css/mods.css?r=20200218" />
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.3.1/dist/jquery.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/tablesorter@2.31.0/dist/js/jquery.tablesorter.combined.min.js" crossorigin="anonymous"></script>
- <script src="~/Content/js/mods.js?r=20190302"></script>
+ <script src="~/Content/js/mods.js?r=20200218"></script>
<script>
$(function() {
var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None });
diff --git a/src/SMAPI.Web/appsettings.json b/src/SMAPI.Web/appsettings.json
index caeb381f..9cd1efc8 100644
--- a/src/SMAPI.Web/appsettings.json
+++ b/src/SMAPI.Web/appsettings.json
@@ -64,6 +64,17 @@
"ModUpdateCheck": {
"SuccessCacheMinutes": 60,
- "ErrorCacheMinutes": 5
+ "ErrorCacheMinutes": 5,
+ "ModOverrides": [
+ {
+ "ID": "Pathoschild.SMAPI",
+ "AllowNonStandardVersions": true,
+ "SetUrl": "https://smapi.io"
+ },
+ {
+ "ID": "MartyrPher.SMAPI-Android-Installer",
+ "AllowNonStandardVersions": true
+ }
+ ]
}
}
diff --git a/src/SMAPI.Web/wwwroot/Content/css/mods.css b/src/SMAPI.Web/wwwroot/Content/css/mods.css
index 1c2b8056..697ba514 100644
--- a/src/SMAPI.Web/wwwroot/Content/css/mods.css
+++ b/src/SMAPI.Web/wwwroot/Content/css/mods.css
@@ -86,6 +86,11 @@ table.wikitable > caption {
font-size: 0.9em;
}
+#mod-list thead tr {
+ position: sticky;
+ top: 0;
+}
+
#mod-list th.header {
background-repeat: no-repeat;
background-position: center right;
diff --git a/src/SMAPI.Web/wwwroot/Content/js/mods.js b/src/SMAPI.Web/wwwroot/Content/js/mods.js
index 0394ac4f..35098b60 100644
--- a/src/SMAPI.Web/wwwroot/Content/js/mods.js
+++ b/src/SMAPI.Web/wwwroot/Content/js/mods.js
@@ -102,7 +102,7 @@ smapi.modList = function (mods, enableBeta) {
app = new Vue({
el: "#app",
data: data,
- mounted: function() {
+ mounted: function () {
// enable table sorting
$("#mod-list").tablesorter({
cssHeader: "header",
@@ -115,11 +115,7 @@ smapi.modList = function (mods, enableBeta) {
$("#search-box").focus();
// jump to anchor (since table is added after page load)
- if (location.hash) {
- var row = $(location.hash).get(0);
- if (row)
- row.scrollIntoView();
- }
+ this.fixHashPosition();
},
methods: {
/**
@@ -144,6 +140,18 @@ smapi.modList = function (mods, enableBeta) {
}
},
+ /**
+ * Fix the window position for the current hash.
+ */
+ fixHashPosition: function () {
+ if (!location.hash)
+ return;
+
+ var row = $(location.hash);
+ var target = row.prev().get(0) || row.get(0);
+ if (target)
+ target.scrollIntoView();
+ },
/**
* Get whether a mod matches the current filters.
@@ -151,7 +159,7 @@ smapi.modList = function (mods, enableBeta) {
* @param {string[]} searchWords The search words to match.
* @returns {bool} Whether the mod matches the filters.
*/
- matchesFilters: function(mod, searchWords) {
+ matchesFilters: function (mod, searchWords) {
var filters = data.filters;
// check hash
@@ -249,7 +257,9 @@ smapi.modList = function (mods, enableBeta) {
}
});
app.applyFilters();
+ app.fixHashPosition();
window.addEventListener("hashchange", function () {
app.applyFilters();
+ app.fixHashPosition();
});
};
diff --git a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
index 78918bac..3101fdf1 100644
--- a/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
+++ b/src/SMAPI.Web/wwwroot/SMAPI.metadata.json
@@ -112,7 +112,7 @@
"Default | UpdateKey": "Nexus:2341"
},
- "TMX Loader": {
+ "TMXL Map Toolkit": {
"ID": "Platonymous.TMXLoader",
"Default | UpdateKey": "Nexus:1820"
},
@@ -129,7 +129,7 @@
"Bee House Flower Range Fix": {
"ID": "kirbylink.beehousefix",
"~ | Status": "Obsolete",
- "~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4."
+ "~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4."
},
"Colored Chests": {
@@ -153,9 +153,9 @@
/*********
** Broke in SDV 1.4
*********/
- "Fix Dice": {
- "ID": "ashley.fixdice",
- "~1.1.2 | Status": "AssumeBroken" // crashes game on startup
+ "Auto Quality Patch": {
+ "ID": "SilentOak.AutoQualityPatch",
+ "~2.1.3-unofficial.7 | 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 7e00c28e..e6cd4e65 100644
--- a/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
+++ b/src/SMAPI.Web/wwwroot/schemas/content-patcher.json
@@ -142,7 +142,7 @@
},
"FromFile": {
"title": "Source file",
- "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin (map), or .xnb file. This field supports tokens and capitalization doesn't matter.",
+ "description": "The relative file path in your content pack folder to load instead (like 'assets/dinosaur.png'). This can be a .json (data), .png (image), .tbin or .tmx (map), or .xnb file. This field supports tokens and capitalization doesn't matter.",
"type": "string",
"allOf": [
{
@@ -151,12 +151,12 @@
}
},
{
- "pattern": "\\.(json|png|tbin|xnb) *$"
+ "pattern": "\\.(json|png|tbin|tmx|xnb) *$"
}
],
"@errorMessages": {
"allOf:indexes: 0": "Invalid value; must not contain directory climbing (like '../').",
- "allOf:indexes: 1": "Invalid value; must be a file path ending with .json, .png, .tbin, or .xnb."
+ "allOf:indexes: 1": "Invalid value; must be a file path ending with .json, .png, .tbin, .tmx, or .xnb."
}
},
"FromArea": {
@@ -325,7 +325,7 @@
"then": {
"properties": {
"FromFile": {
- "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin or .xnb file. This field supports tokens and capitalization doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder."
+ "description": "The relative path to the map in your content pack folder from which to copy (like assets/town.tbin). This can be a .tbin, .tmx, or .xnb file. This field supports tokens and capitalization doesn't matter.\nContent Patcher will handle tilesheets referenced by the FromFile map for you:\n - If a tilesheet isn't referenced by the target map, Content Patcher will add it for you (with a z_ ID prefix to avoid conflicts with hardcoded game logic). If the source map has a custom version of a tilesheet that's already referenced, it'll be added as a separate tilesheet only used by your tiles.\n - If you include the tilesheet file in your mod folder, Content Patcher will use that one automatically; otherwise it will be loaded from the game's Content/Maps folder."
},
"FromArea": {
"description": "The part of the source map to copy. Defaults to the whole source map."
diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs
index 201d3166..670dc494 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.2.0");
+ public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.3.0");
/// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1");
@@ -115,26 +115,59 @@ namespace StardewModdingAPI
/// <returns>Returns the compatible SMAPI version, or <c>null</c> if none was found.</returns>
internal static ISemanticVersion GetCompatibleApiVersion(ISemanticVersion version)
{
+ // This covers all officially supported public game updates. It might seem like version
+ // ranges would be better, but the given SMAPI versions may not be compatible with
+ // intermediate unlisted versions (e.g. private beta updates).
+ //
+ // Nonstandard versions are normalized by GameVersion (e.g. 1.07 => 1.0.7).
switch (version.ToString())
{
+ case "1.4.1":
+ case "1.4.0":
+ return new SemanticVersion("3.0.1");
+
case "1.3.36":
- return new SemanticVersion(2, 11, 2);
+ return new SemanticVersion("2.11.2");
- case "1.3.32":
case "1.3.33":
- return new SemanticVersion(2, 10, 2);
+ case "1.3.32":
+ return new SemanticVersion("2.10.2");
case "1.3.28":
- return new SemanticVersion(2, 7, 0);
+ return new SemanticVersion("2.7.0");
- case "1.2.30":
- case "1.2.31":
- case "1.2.32":
case "1.2.33":
- return new SemanticVersion(2, 5, 5);
- }
+ case "1.2.32":
+ case "1.2.31":
+ case "1.2.30":
+ return new SemanticVersion("2.5.5");
+
+ case "1.2.29":
+ case "1.2.28":
+ case "1.2.27":
+ case "1.2.26":
+ return new SemanticVersion("1.13.1");
+
+ case "1.1.1":
+ case "1.1.0":
+ return new SemanticVersion("1.9.0");
+
+ case "1.0.7.1":
+ case "1.0.7":
+ case "1.0.6":
+ case "1.0.5.2":
+ case "1.0.5.1":
+ case "1.0.5":
+ case "1.0.4":
+ case "1.0.3":
+ case "1.0.2":
+ case "1.0.1":
+ case "1.0.0":
+ return new SemanticVersion("0.40.0");
- return null;
+ default:
+ return null;
+ }
}
/// <summary>Get metadata for mapping assemblies to the current platform.</summary>
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index aa615a0b..44a97136 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -1,6 +1,7 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
+using StardewValley;
namespace StardewModdingAPI.Framework.Content
{
@@ -102,5 +103,21 @@ namespace StardewModdingAPI.Framework.Content
// patch target texture
target.SetData(0, targetArea, sourceData, 0, pixelCount);
}
+
+ /// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary>
+ /// <param name="minWidth">The minimum texture width.</param>
+ /// <param name="minHeight">The minimum texture height.</param>
+ /// <returns>Whether the texture was resized.</returns>
+ public bool ExtendImage(int minWidth, int minHeight)
+ {
+ if (this.Data.Width >= minWidth && this.Data.Height >= minHeight)
+ return false;
+
+ Texture2D original = this.Data;
+ Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight));
+ this.ReplaceWith(texture);
+ this.PatchImage(original);
+ return true;
+ }
}
}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 2fd31263..0b1ccc3c 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -112,9 +112,10 @@ namespace StardewModdingAPI.Framework
/// <summary>Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary>
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
+ /// <param name="modName">The mod display name to show in errors.</param>
/// <param name="rootDirectory">The root directory to search for content (or <c>null</c> for the default).</param>
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
- public ModContentManager CreateModContentManager(string name, string rootDirectory, IContentManager gameContentManager)
+ public ModContentManager CreateModContentManager(string name, string modName, string rootDirectory, IContentManager gameContentManager)
{
return this.ContentManagerLock.InWriteLock(() =>
{
@@ -123,6 +124,7 @@ namespace StardewModdingAPI.Framework
gameContentManager: gameContentManager,
serviceProvider: this.MainContentManager.ServiceProvider,
rootDirectory: rootDirectory,
+ modName: modName,
currentCulture: this.MainContentManager.CurrentCulture,
coordinator: this,
monitor: this.Monitor,
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index eecdda74..eaaf0e6f 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -2,12 +2,15 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Reflection;
using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewValley;
+using xTile;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -337,6 +340,20 @@ namespace StardewModdingAPI.Framework.ContentManagers
{
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
+ // special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead.
+ {
+ Type actualType = asset.Data.GetType();
+ Type actualOpenType = actualType.IsGenericType ? actualType.GetGenericTypeDefinition() : null;
+
+ if (typeof(T) != actualType && (actualOpenType == typeof(Dictionary<,>) || actualOpenType == typeof(List<>) || actualType == typeof(Texture2D) || actualType == typeof(Map)))
+ {
+ return (IAssetData)this.GetType()
+ .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)
+ .MakeGenericMethod(actualType)
+ .Invoke(this, new object[] { info, asset });
+ }
+ }
+
// edit asset
foreach (var entry in this.Editors)
{
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 0a526fc8..7d274eb7 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
+ /// <summary>The mod display name to show in errors.</summary>
+ private readonly string ModName;
+
/// <summary>The game content manager used for map tilesheets not provided by the mod.</summary>
private readonly IContentManager GameContentManager;
@@ -40,6 +43,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
/// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="modName">The mod display name to show in errors.</param>
/// <param name="rootDirectory">The root directory to search for content.</param>
/// <param name="currentCulture">The current culture for which to localize content.</param>
/// <param name="coordinator">The central coordinator which manages content managers.</param>
@@ -47,11 +51,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
- public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
+ public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
{
this.GameContentManager = gameContentManager;
this.JsonHelper = jsonHelper;
+ this.ModName = modName;
}
/// <summary>Load an asset that has been processed by the content pipeline.</summary>
@@ -248,8 +253,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
texture.GetData(data);
for (int i = 0; i < data.Length; i++)
{
- if (data[i].A == 0)
- continue; // no need to change fully transparent pixels
+ if (data[i].A == byte.MinValue || data[i].A == byte.MaxValue)
+ continue; // no need to change fully transparent/opaque pixels
data[i] = Color.FromNonPremultiplied(data[i].ToVector4());
}
@@ -297,98 +302,99 @@ namespace StardewModdingAPI.Framework.ContentManagers
foreach (TileSheet tilesheet in map.TileSheets)
{
string imageSource = tilesheet.ImageSource;
+ string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
// validate tilesheet path
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains(".."))
- throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../).");
-
- // get seasonal name (if applicable)
- string seasonalImageSource = null;
- if (isOutdoors && Context.IsSaveLoaded && Game1.currentSeason != null)
- {
- string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null.");
- bool hasSeasonalPrefix =
- filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase);
- if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_"))
- {
- string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase));
- seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}";
- }
- }
+ throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../).");
// load best match
try
{
- string key =
- this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource)
- ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
- if (key != null)
- {
- tilesheet.ImageSource = key;
- continue;
- }
+ if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, isOutdoors, out string assetName, out string error))
+ throw new SContentLoadException($"{errorPrefix} {error}");
+
+ tilesheet.ImageSource = assetName;
}
- catch (Exception ex)
+ catch (Exception ex) when (!(ex is SContentLoadException))
{
- throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex);
+ throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex);
}
-
- // none found
- throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.");
}
}
/// <summary>Get the actual asset name for a tilesheet.</summary>
/// <param name="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
- /// <param name="imageSource">The tilesheet image source to load.</param>
- /// <returns>Returns the asset name.</returns>
+ /// <param name="originalPath">The tilesheet path to load.</param>
+ /// <param name="willSeasonalize">Whether the game will apply seasonal logic to the tilesheet.</param>
+ /// <param name="assetName">The found asset name.</param>
+ /// <param name="error">A message indicating why the file couldn't be loaded.</param>
+ /// <returns>Returns whether the asset name was found.</returns>
/// <remarks>See remarks on <see cref="FixCustomTilesheetPaths"/>.</remarks>
- private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource)
+ private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string originalPath, bool willSeasonalize, out string assetName, out string error)
{
- if (imageSource == null)
- return null;
+ assetName = null;
+ error = null;
+
+ // nothing to do
+ if (string.IsNullOrWhiteSpace(originalPath))
+ {
+ assetName = originalPath;
+ return true;
+ }
- // check relative to map file
+ // parse path
+ string filename = Path.GetFileName(originalPath);
+ bool isSeasonal = filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase)
+ || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase)
+ || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase)
+ || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase);
+ string relativePath = originalPath;
+ if (willSeasonalize && isSeasonal)
{
- string localKey = Path.Combine(modRelativeMapFolder, imageSource);
- FileInfo localFile = this.GetModFile(localKey);
- if (localFile.Exists)
- return this.GetInternalAssetKey(localKey);
+ string dirPath = Path.GetDirectoryName(originalPath);
+ relativePath = Path.Combine(dirPath, $"{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}");
}
- // check relative to content folder
+ // get relative to map file
{
- foreach (string candidateKey in new[] { imageSource, Path.Combine("Maps", imageSource) })
+ string localKey = Path.Combine(modRelativeMapFolder, relativePath);
+ if (this.GetModFile(localKey).Exists)
+ {
+ assetName = this.GetInternalAssetKey(localKey);
+ return true;
+ }
+ }
+
+ // get from game assets
+ {
+ string contentKey = Path.Combine("Maps", relativePath);
+ if (contentKey.EndsWith(".png"))
+ contentKey = contentKey.Substring(0, contentKey.Length - 4);
+
+ try
+ {
+ this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
+ assetName = contentKey;
+ return true;
+ }
+ catch
{
- string contentKey = candidateKey.EndsWith(".png")
- ? candidateKey.Substring(0, candidateKey.Length - 4)
- : candidateKey;
-
- try
- {
- this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
- return contentKey;
- }
- catch
- {
- // ignore file-not-found errors
- // TODO: while it's useful to suppress an asset-not-found error here to avoid
- // confusion, this is a pretty naive approach. Even if the file doesn't exist,
- // the file may have been loaded through an IAssetLoader which failed. So even
- // if the content file doesn't exist, that doesn't mean the error here is a
- // content-not-found error. Unfortunately XNA doesn't provide a good way to
- // detect the error type.
- if (this.GetContentFolderFileExists(contentKey))
- throw;
- }
+ // ignore file-not-found errors
+ // TODO: while it's useful to suppress an asset-not-found error here to avoid
+ // confusion, this is a pretty naive approach. Even if the file doesn't exist,
+ // the file may have been loaded through an IAssetLoader which failed. So even
+ // if the content file doesn't exist, that doesn't mean the error here is a
+ // content-not-found error. Unfortunately XNA doesn't provide a good way to
+ // detect the error type.
+ if (this.GetContentFolderFileExists(contentKey))
+ throw;
}
}
// not found
- return null;
+ error = "The tilesheet couldn't be found relative to either map file or the game's content folder.";
+ return false;
}
/// <summary>Get whether a file from the game's content folder exists.</summary>
diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs
index 84cea36c..4eaa9ca6 100644
--- a/src/SMAPI/Framework/Input/SInputState.cs
+++ b/src/SMAPI/Framework/Input/SInputState.cs
@@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.Input
public ICursorPosition CursorPosition => this.CursorPositionImpl;
/// <summary>The buttons which were pressed, held, or released.</summary>
- public IDictionary<SButton, InputStatus> ActiveButtons { get; private set; } = new Dictionary<SButton, InputStatus>();
+ public IDictionary<SButton, SButtonState> ActiveButtons { get; private set; } = new Dictionary<SButton, SButtonState>();
/// <summary>The buttons to suppress when the game next handles input. Each button is suppressed until it's released.</summary>
public HashSet<SButton> SuppressButtons { get; } = new HashSet<SButton>();
@@ -75,7 +75,7 @@ namespace StardewModdingAPI.Framework.Input
[Obsolete("This method should only be called by the game itself.")]
public override void Update() { }
- /// <summary>Update the current button statuses for the given tick.</summary>
+ /// <summary>Update the current button states for the given tick.</summary>
public void TrueUpdate()
{
try
@@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Input
GamePadState realController = GamePad.GetState(PlayerIndex.One);
KeyboardState realKeyboard = Keyboard.GetState();
MouseState realMouse = Mouse.GetState();
- var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
+ var activeButtons = this.DeriveStates(this.ActiveButtons, realKeyboard, realMouse, realController);
Vector2 cursorAbsolutePos = new Vector2((realMouse.X * zoomMultiplier) + Game1.viewport.X, (realMouse.Y * zoomMultiplier) + Game1.viewport.Y);
Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
@@ -102,7 +102,7 @@ namespace StardewModdingAPI.Framework.Input
}
// update suppressed states
- this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown());
+ this.SuppressButtons.RemoveWhere(p => !this.GetState(activeButtons, p).IsDown());
this.UpdateSuppression();
}
catch (InvalidOperationException)
@@ -159,7 +159,7 @@ namespace StardewModdingAPI.Framework.Input
/// <param name="button">The button to check.</param>
public bool IsDown(SButton button)
{
- return this.GetStatus(this.ActiveButtons, button).IsDown();
+ return this.GetState(this.ActiveButtons, button).IsDown();
}
/// <summary>Get whether any of the given buttons were pressed or held.</summary>
@@ -169,6 +169,13 @@ namespace StardewModdingAPI.Framework.Input
return buttons.Any(button => this.IsDown(button.ToSButton()));
}
+ /// <summary>Get the state of a button.</summary>
+ /// <param name="button">The button to check.</param>
+ public SButtonState GetState(SButton button)
+ {
+ return this.GetState(this.ActiveButtons, button);
+ }
+
/*********
** Private methods
@@ -198,7 +205,7 @@ namespace StardewModdingAPI.Framework.Input
/// <param name="keyboardState">The game's keyboard state for the current tick.</param>
/// <param name="mouseState">The game's mouse state for the current tick.</param>
/// <param name="gamePadState">The game's controller state for the current tick.</param>
- private void SuppressGivenStates(IDictionary<SButton, InputStatus> activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
+ private void SuppressGivenStates(IDictionary<SButton, SButtonState> activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
{
if (this.SuppressButtons.Count == 0)
return;
@@ -245,48 +252,48 @@ namespace StardewModdingAPI.Framework.Input
}
}
- /// <summary>Get the status of all pressed or released buttons relative to their previous status.</summary>
- /// <param name="previousStatuses">The previous button statuses.</param>
+ /// <summary>Get the state of all pressed or released buttons relative to their previous state.</summary>
+ /// <param name="previousStates">The previous button states.</param>
/// <param name="keyboard">The keyboard state.</param>
/// <param name="mouse">The mouse state.</param>
/// <param name="controller">The controller state.</param>
- private IDictionary<SButton, InputStatus> DeriveStatuses(IDictionary<SButton, InputStatus> previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller)
+ private IDictionary<SButton, SButtonState> DeriveStates(IDictionary<SButton, SButtonState> previousStates, KeyboardState keyboard, MouseState mouse, GamePadState controller)
{
- IDictionary<SButton, InputStatus> activeButtons = new Dictionary<SButton, InputStatus>();
+ IDictionary<SButton, SButtonState> activeButtons = new Dictionary<SButton, SButtonState>();
// handle pressed keys
SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray();
foreach (SButton button in down)
- activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true);
+ activeButtons[button] = this.DeriveState(this.GetState(previousStates, button), isDown: true);
// handle released keys
- foreach (KeyValuePair<SButton, InputStatus> prev in previousStatuses)
+ foreach (KeyValuePair<SButton, SButtonState> prev in previousStates)
{
if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key))
- activeButtons[prev.Key] = InputStatus.Released;
+ activeButtons[prev.Key] = SButtonState.Released;
}
return activeButtons;
}
- /// <summary>Get the status of a button relative to its previous status.</summary>
- /// <param name="oldStatus">The previous button status.</param>
+ /// <summary>Get the state of a button relative to its previous state.</summary>
+ /// <param name="oldState">The previous button state.</param>
/// <param name="isDown">Whether the button is currently down.</param>
- private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown)
+ private SButtonState DeriveState(SButtonState oldState, bool isDown)
{
- if (isDown && oldStatus.IsDown())
- return InputStatus.Held;
+ if (isDown && oldState.IsDown())
+ return SButtonState.Held;
if (isDown)
- return InputStatus.Pressed;
- return InputStatus.Released;
+ return SButtonState.Pressed;
+ return SButtonState.Released;
}
- /// <summary>Get the status of a button.</summary>
+ /// <summary>Get the state of a button.</summary>
/// <param name="activeButtons">The current button states to check.</param>
/// <param name="button">The button to check.</param>
- private InputStatus GetStatus(IDictionary<SButton, InputStatus> activeButtons, SButton button)
+ private SButtonState GetState(IDictionary<SButton, SButtonState> activeButtons, SButton button)
{
- return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None;
+ return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.None;
}
/// <summary>Get the buttons pressed in the given stats.</summary>
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 043ae376..e9b70845 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The friendly mod name for use in errors.</summary>
private readonly string ModName;
- /// <summary>Encapsulates monitoring and logging for a given module.</summary>
+ /// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
@@ -70,9 +70,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor)
: base(modID)
{
+ string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
+
this.ContentCore = contentCore;
- this.GameContentManager = contentCore.CreateGameContentManager(this.ContentCore.GetManagedAssetPrefix(modID) + ".content");
- this.ModContentManager = contentCore.CreateModContentManager(this.ContentCore.GetManagedAssetPrefix(modID), modFolderPath, this.GameContentManager);
+ this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content");
+ this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, this.GameContentManager);
this.ModName = modName;
this.Monitor = monitor;
}
diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs
index f4cd12b6..f8ff0355 100644
--- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs
@@ -50,5 +50,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
this.InputState.SuppressButtons.Add(button);
}
+
+ /// <summary>Get the state of a button.</summary>
+ /// <param name="button">The button to check.</param>
+ public SButtonState GetState(SButton button)
+ {
+ return this.InputState.GetState(button);
+ }
}
}
diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs
index 7ee39863..4f694f9c 100644
--- a/src/SMAPI/Framework/Networking/ModMessageModel.cs
+++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs
@@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Networking
/****
** Destination
****/
- /// <summary>The players who should receive the message, or <c>null</c> for all players.</summary>
+ /// <summary>The players who should receive the message.</summary>
public long[] ToPlayerIDs { get; set; }
/// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary>
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 4b346059..6b9c1365 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -635,16 +635,16 @@ namespace StardewModdingAPI.Framework
foreach (var pair in inputState.ActiveButtons)
{
SButton button = pair.Key;
- InputStatus status = pair.Value;
+ SButtonState status = pair.Value;
- if (status == InputStatus.Pressed)
+ if (status == SButtonState.Pressed)
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace);
events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState));
}
- else if (status == InputStatus.Released)
+ else if (status == SButtonState.Released)
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace);
@@ -893,6 +893,7 @@ namespace StardewModdingAPI.Framework
{
var events = this.Events;
+ Game1.showingHealthBar = false;
if (Game1._newDayTask != null)
{
this.GraphicsDevice.Clear(Game1.bgColor);
@@ -934,7 +935,7 @@ namespace StardewModdingAPI.Framework
else
{
this.GraphicsDevice.Clear(Game1.bgColor);
- if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet())
+ if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && (Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet() && !this.takingMapScreenshot))
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@@ -1081,6 +1082,7 @@ namespace StardewModdingAPI.Framework
{
byte batchOpens = 0; // used for rendering event
+ Microsoft.Xna.Framework.Rectangle rectangle;
Viewport viewport;
if (Game1.gameMode == (byte)0)
{
@@ -1097,21 +1099,20 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
if (++batchOpens == 1)
events.Rendering.RaiseEmpty();
- Microsoft.Xna.Framework.Color color = !Game1.currentLocation.Name.StartsWith("UndergroundMine") || !(Game1.currentLocation is MineShaft) ? (Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight) : (Game1.currentLocation as MineShaft).getLightingColor(gameTime);
+ Microsoft.Xna.Framework.Color color = !Game1.currentLocation.Name.StartsWith("UndergroundMine") || !(Game1.currentLocation is MineShaft) ? (Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || Game1.isRaining && (bool)(NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight) : (Game1.currentLocation as MineShaft).getLightingColor(gameTime);
Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, color);
- for (int index = 0; index < Game1.currentLightSources.Count; ++index)
+ foreach (LightSource currentLightSource in Game1.currentLightSources)
{
- LightSource lightSource = Game1.currentLightSources.ElementAt<LightSource>(index);
- if (!Game1.isRaining && !Game1.isDarkOut() || lightSource.lightContext.Value != LightSource.LightContext.WindowLight)
+ if (!Game1.isRaining && !Game1.isDarkOut() || currentLightSource.lightContext.Value != LightSource.LightContext.WindowLight)
{
- if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID)
+ if (currentLightSource.PlayerID != 0L && currentLightSource.PlayerID != Game1.player.UniqueMultiplayerID)
{
- Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(lightSource.PlayerID);
- if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)((NetFieldBase<bool, NetBool>)farmerMaybeOffline.hidden))
+ Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(currentLightSource.PlayerID);
+ if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)(NetFieldBase<bool, NetBool>)farmerMaybeOffline.hidden)
continue;
}
- if (Utility.isOnScreen((Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position), (int)((double)(float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) * 64.0 * 4.0)))
- Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase<Microsoft.Xna.Framework.Color, NetColor>)Game1.currentLightSources.ElementAt<LightSource>(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f);
+ if (Utility.isOnScreen((Vector2)(NetFieldBase<Vector2, NetVector2>)currentLightSource.position, (int)((double)(float)(NetFieldBase<float, NetFloat>)currentLightSource.radius * 64.0 * 4.0)))
+ Game1.spriteBatch.Draw(currentLightSource.lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)(NetFieldBase<Vector2, NetVector2>)currentLightSource.position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(currentLightSource.lightTexture.Bounds), (Microsoft.Xna.Framework.Color)(NetFieldBase<Microsoft.Xna.Framework.Color, NetColor>)currentLightSource.color, 0.0f, new Vector2((float)currentLightSource.lightTexture.Bounds.Center.X, (float)currentLightSource.lightTexture.Bounds.Center.Y), (float)(NetFieldBase<float, NetFloat>)currentLightSource.radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f);
}
}
Game1.spriteBatch.End();
@@ -1134,7 +1135,7 @@ namespace StardewModdingAPI.Framework
{
foreach (Farmer farmerActor in Game1.currentLocation.currentEvent.farmerActors)
{
- if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase<bool, NetBool>)farmerActor.hidden))
+ if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase<bool, NetBool>)farmerActor.hidden)
this._farmerShadows.Add(farmerActor);
}
}
@@ -1142,7 +1143,7 @@ namespace StardewModdingAPI.Framework
{
foreach (Farmer farmer in Game1.currentLocation.farmers)
{
- if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase<bool, NetBool>)farmer.hidden))
+ if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase<bool, NetBool>)farmer.hidden)
this._farmerShadows.Add(farmer);
}
}
@@ -1152,26 +1153,39 @@ namespace StardewModdingAPI.Framework
{
foreach (NPC character in Game1.currentLocation.characters)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)character.swimming && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
}
}
else
{
foreach (NPC actor in Game1.CurrentEvent.actors)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)actor.swimming && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
}
}
foreach (Farmer farmerShadow in this._farmerShadows)
{
- if (!Game1.multiplayer.isDisconnecting(farmerShadow.UniqueMultiplayerID) && !(bool)((NetFieldBase<bool, NetBool>)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5)), SpriteEffects.None, 0.0f);
+ if (!Game1.multiplayer.isDisconnecting(farmerShadow.UniqueMultiplayerID) && !(bool)(NetFieldBase<bool, NetBool>)farmerShadow.swimming && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
+ {
+ SpriteBatch spriteBatch = Game1.spriteBatch;
+ Texture2D shadowTexture = Game1.shadowTexture;
+ Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f));
+ Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
+ Microsoft.Xna.Framework.Color white = Microsoft.Xna.Framework.Color.White;
+ Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds;
+ double x = (double)bounds.Center.X;
+ bounds = Game1.shadowTexture.Bounds;
+ double y = (double)bounds.Center.Y;
+ Vector2 origin = new Vector2((float)x, (float)y);
+ double num = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5);
+ spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num, SpriteEffects.None, 0.0f);
+ }
}
}
- Layer layer = Game1.currentLocation.Map.GetLayer("Buildings");
- layer.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
+ Layer layer1 = Game1.currentLocation.Map.GetLayer("Buildings");
+ layer1.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
Game1.mapDisplayDevice.EndScene();
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@@ -1181,23 +1195,37 @@ namespace StardewModdingAPI.Framework
{
foreach (NPC character in Game1.currentLocation.characters)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && (!(bool)((NetFieldBase<bool, NetBool>)character.isInvisible) && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)character.swimming && !character.HideShadow && (!(bool)(NetFieldBase<bool, NetBool>)character.isInvisible && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
}
}
else
{
foreach (NPC actor in Game1.CurrentEvent.actors)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)actor.swimming && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
}
}
foreach (Farmer farmerShadow in this._farmerShadows)
{
- float layerDepth = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f;
- if (!(bool)((NetFieldBase<bool, NetBool>)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5)), SpriteEffects.None, layerDepth);
+ float num1 = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f;
+ if (!(bool)(NetFieldBase<bool, NetBool>)farmerShadow.swimming && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
+ {
+ SpriteBatch spriteBatch = Game1.spriteBatch;
+ Texture2D shadowTexture = Game1.shadowTexture;
+ Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f));
+ Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
+ Microsoft.Xna.Framework.Color white = Microsoft.Xna.Framework.Color.White;
+ Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds;
+ double x = (double)bounds.Center.X;
+ bounds = Game1.shadowTexture.Bounds;
+ double y = (double)bounds.Center.Y;
+ Vector2 origin = new Vector2((float)x, (float)y);
+ double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5);
+ double num3 = (double)num1;
+ spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num2, SpriteEffects.None, (float)num3);
+ }
}
}
if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null))
@@ -1207,7 +1235,7 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.draw(Game1.spriteBatch);
foreach (Vector2 key in Game1.crabPotOverlayTiles.Keys)
{
- Tile tile = layer.Tiles[(int)key.X, (int)key.Y];
+ Tile tile = layer1.Tiles[(int)key.X, (int)key.Y];
if (tile != null)
{
Vector2 local = Game1.GlobalToLocal(Game1.viewport, key * 64f);
@@ -1237,10 +1265,31 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch);
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)
+ if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)(NetFieldBase<bool, NetBool>)Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)
Game1.drawPlayerHeldObject(Game1.player);
- else if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways") || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")))
+ else if (Game1.displayFarmer && Game1.player.ActiveObject != null)
+ {
+ if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways"))
+ {
+ Layer layer2 = Game1.currentLocation.Map.GetLayer("Front");
+ rectangle = Game1.player.GetBoundingBox();
+ Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38);
+ xTile.Dimensions.Size size1 = Game1.viewport.Size;
+ if (layer2.PickTile(mapDisplayLocation1, size1) != null)
+ {
+ Layer layer3 = Game1.currentLocation.Map.GetLayer("Front");
+ rectangle = Game1.player.GetBoundingBox();
+ Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38);
+ xTile.Dimensions.Size size2 = Game1.viewport.Size;
+ if (layer3.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways"))
+ goto label_139;
+ }
+ else
+ goto label_139;
+ }
Game1.drawPlayerHeldObject(Game1.player);
+ }
+ label_139:
if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)))
Game1.drawTool(Game1.player);
if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null)
@@ -1274,23 +1323,9 @@ namespace StardewModdingAPI.Framework
if (Game1.farmEvent != null)
Game1.farmEvent.draw(Game1.spriteBatch);
if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel;
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel);
if (Game1.screenGlow)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Microsoft.Xna.Framework.Color color = Game1.screenGlowColor * Game1.screenGlowAlpha;
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha);
Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch);
if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)))
Game1.player.CurrentTool.draw(Game1.spriteBatch);
@@ -1317,15 +1352,8 @@ namespace StardewModdingAPI.Framework
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f);
- if (Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D staminaRect = Game1.staminaRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.OrangeRed * 0.45f;
- spriteBatch.Draw(staminaRect, bounds, color);
- }
+ if (Game1.isRaining && (bool)(NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))
+ Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.OrangeRed * 0.45f);
Game1.spriteBatch.End();
}
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@@ -1424,12 +1452,28 @@ namespace StardewModdingAPI.Framework
this.drawDialogueBox();
if (Game1.progressBar && !this.takingMapScreenshot)
{
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Microsoft.Xna.Framework.Color.LightGray);
- Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Microsoft.Xna.Framework.Color.DimGray);
+ SpriteBatch spriteBatch1 = Game1.spriteBatch;
+ Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
+ int x1 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2;
+ rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea();
+ int y1 = rectangle.Bottom - 128;
+ int dialogueWidth = Game1.dialogueWidth;
+ Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, 32);
+ Microsoft.Xna.Framework.Color lightGray = Microsoft.Xna.Framework.Color.LightGray;
+ spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray);
+ SpriteBatch spriteBatch2 = Game1.spriteBatch;
+ Texture2D staminaRect = Game1.staminaRect;
+ int x2 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2;
+ rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea();
+ int y2 = rectangle.Bottom - 128;
+ int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth);
+ Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, 32);
+ Microsoft.Xna.Framework.Color dimGray = Microsoft.Xna.Framework.Color.DimGray;
+ spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray);
}
if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)
Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch);
- if (Game1.isRaining && Game1.currentLocation != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)))
+ if (Game1.isRaining && Game1.currentLocation != null && ((bool)(NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)))
{
SpriteBatch spriteBatch = Game1.spriteBatch;
Texture2D staminaRect = Game1.staminaRect;
diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs
index e04205c8..821c343f 100644
--- a/src/SMAPI/Framework/SMultiplayer.cs
+++ b/src/SMAPI/Framework/SMultiplayer.cs
@@ -338,64 +338,94 @@ namespace StardewModdingAPI.Framework
/// <param name="toPlayerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs)
{
- // validate
+ // validate input
if (message == null)
throw new ArgumentNullException(nameof(message));
if (string.IsNullOrWhiteSpace(messageType))
throw new ArgumentNullException(nameof(messageType));
if (string.IsNullOrWhiteSpace(fromModID))
throw new ArgumentNullException(nameof(fromModID));
- if (!this.Peers.Any())
+
+ // get target players
+ long curPlayerId = Game1.player.UniqueMultiplayerID;
+ bool sendToSelf = false;
+ List<MultiplayerPeer> sendToPeers = new List<MultiplayerPeer>();
+ if (toPlayerIDs == null)
{
- this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players.");
- return;
+ sendToSelf = true;
+ sendToPeers.AddRange(this.Peers.Values);
}
-
- // filter player IDs
- HashSet<long> playerIDs = null;
- if (toPlayerIDs != null && toPlayerIDs.Any())
+ else
{
- playerIDs = new HashSet<long>(toPlayerIDs);
- playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id));
- if (!playerIDs.Any())
+ foreach (long id in toPlayerIDs.Distinct())
{
- this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected.");
- return;
+ if (id == curPlayerId)
+ sendToSelf = true;
+ else if (this.Peers.TryGetValue(id, out MultiplayerPeer peer) && peer.HasSmapi)
+ sendToPeers.Add(peer);
}
}
+ // filter by mod ID
+ if (toModIDs != null)
+ {
+ HashSet<string> sendToMods = new HashSet<string>(toModIDs, StringComparer.InvariantCultureIgnoreCase);
+ if (sendToSelf && toModIDs.All(id => this.ModRegistry.Get(id) == null))
+ sendToSelf = false;
+
+ sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !sendToMods.Contains(mod.ID)));
+ }
+
+ // validate recipients
+ if (!sendToSelf && !sendToPeers.Any())
+ {
+ this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs can receive this message.");
+ return;
+ }
+
// get data to send
ModMessageModel model = new ModMessageModel(
fromPlayerID: Game1.player.UniqueMultiplayerID,
fromModID: fromModID,
toModIDs: toModIDs,
- toPlayerIDs: playerIDs?.ToArray(),
+ toPlayerIDs: sendToPeers.Select(p => p.PlayerID).ToArray(),
type: messageType,
data: JToken.FromObject(message)
);
string data = JsonConvert.SerializeObject(model, Formatting.None);
- // log message
- if (this.LogNetworkTraffic)
- this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace);
+ // send self-message
+ if (sendToSelf)
+ {
+ if (this.LogNetworkTraffic)
+ this.Monitor.Log($"Broadcasting '{messageType}' message to self: {data}.", LogLevel.Trace);
+
+ this.OnModMessageReceived(model);
+ }
- // send message
- if (Context.IsMainPlayer)
+ // send message to peers
+ if (sendToPeers.Any())
{
- foreach (MultiplayerPeer peer in this.Peers.Values)
+ if (Context.IsMainPlayer)
{
- if (playerIDs == null || playerIDs.Contains(peer.PlayerID))
+ foreach (MultiplayerPeer peer in sendToPeers)
{
- model.ToPlayerIDs = new[] { peer.PlayerID };
+ if (this.LogNetworkTraffic)
+ this.Monitor.Log($"Broadcasting '{messageType}' message to farmhand {peer.PlayerID}: {data}.", LogLevel.Trace);
+
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data));
}
}
- }
- else if (this.HostPeer != null && this.HostPeer.HasSmapi)
- this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
- else
- this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
+ else if (this.HostPeer?.HasSmapi == true)
+ {
+ if (this.LogNetworkTraffic)
+ this.Monitor.Log($"Broadcasting '{messageType}' message to host {this.HostPeer.PlayerID}: {data}.", LogLevel.Trace);
+ this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
+ }
+ else
+ this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
+ }
}
diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs
index 1109194f..27ed9267 100644
--- a/src/SMAPI/IAssetDataForImage.cs
+++ b/src/SMAPI/IAssetDataForImage.cs
@@ -19,5 +19,11 @@ namespace StardewModdingAPI
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception>
/// <exception cref="InvalidOperationException">The content being read isn't an image.</exception>
void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace);
+
+ /// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary>
+ /// <param name="minWidth">The minimum texture width.</param>
+ /// <param name="minHeight">The minimum texture height.</param>
+ /// <returns>Whether the texture was resized.</returns>
+ bool ExtendImage(int minWidth, int minHeight);
}
}
diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs
index 328f504b..e9768c24 100644
--- a/src/SMAPI/IInputHelper.cs
+++ b/src/SMAPI/IInputHelper.cs
@@ -17,5 +17,9 @@ namespace StardewModdingAPI
/// <summary>Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event.</summary>
/// <param name="button">The button to suppress.</param>
void Suppress(SButton button);
+
+ /// <summary>Get the state of a button.</summary>
+ /// <param name="button">The button to check.</param>
+ SButtonState GetState(SButton button);
}
}
diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs
index 7a58d52c..8d5ad3ab 100644
--- a/src/SMAPI/Metadata/CoreAssetPropagator.cs
+++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs
@@ -886,10 +886,17 @@ namespace StardewModdingAPI.Metadata
return false;
// update dialogue
+ // Note that marriage dialogue isn't reloaded after reset, but it doesn't need to be
+ // propagated anyway since marriage dialogue keys can't be added/removed and the field
+ // doesn't store the text itself.
foreach (NPC villager in villagers)
{
+ MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray();
+
villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue
villager.resetCurrentDialogue();
+
+ villager.currentMarriageDialogue.Set(marriageDialogue);
}
return true;
diff --git a/src/SMAPI/Framework/Input/InputStatus.cs b/src/SMAPI/SButtonState.cs
index 99b0006c..2b78da27 100644
--- a/src/SMAPI/Framework/Input/InputStatus.cs
+++ b/src/SMAPI/SButtonState.cs
@@ -1,7 +1,7 @@
-namespace StardewModdingAPI.Framework.Input
+namespace StardewModdingAPI
{
- /// <summary>The input status for a button during an update frame.</summary>
- internal enum InputStatus
+ /// <summary>The input state for a button during an update frame.</summary>
+ public enum SButtonState
{
/// <summary>The button was neither pressed, held, nor released.</summary>
None,
@@ -16,14 +16,14 @@ namespace StardewModdingAPI.Framework.Input
Released
}
- /// <summary>Extension methods for <see cref="InputStatus"/>.</summary>
+ /// <summary>Extension methods for <see cref="SButtonState"/>.</summary>
internal static class InputStatusExtensions
{
/// <summary>Whether the button was pressed or held.</summary>
- /// <param name="status">The button status.</param>
- public static bool IsDown(this InputStatus status)
+ /// <param name="state">The button state.</param>
+ public static bool IsDown(this SButtonState state)
{
- return status == InputStatus.Held || status == InputStatus.Pressed;
+ return state == SButtonState.Held || state == SButtonState.Pressed;
}
}
}
diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj
index c5d0f247..c17f13c4 100644
--- a/src/SMAPI/SMAPI.csproj
+++ b/src/SMAPI/SMAPI.csproj
@@ -15,9 +15,9 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="LargeAddressAware" Version="1.0.3" />
+ <PackageReference Include="LargeAddressAware" Version="1.0.4" />
<PackageReference Include="Lib.Harmony" Version="1.2.0.1" />
- <PackageReference Include="Mono.Cecil" Version="0.11.1" />
+ <PackageReference Include="Mono.Cecil" Version="0.11.2" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Platonymous.TMXTile" Version="1.0.2" />
</ItemGroup>
diff --git a/src/SMAPI/i18n/it.json b/src/SMAPI/i18n/it.json
new file mode 100644
index 00000000..43493018
--- /dev/null
+++ b/src/SMAPI/i18n/it.json
@@ -0,0 +1,3 @@
+{
+ "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)."
+}