Merge branch 'develop' into stable

This commit is contained in:
Jesse Plamondon-Willard 2020-02-22 12:03:39 -05:00
commit 66079f2253
No known key found for this signature in database
GPG Key ID: CF8B1456B3E29F49
40 changed files with 604 additions and 290 deletions

View File

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

View File

@ -1,6 +1,40 @@
&larr; [README](README.md) &larr; [README](README.md)
# Release notes # 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 ## 3.2
Released 01 February 2020 for Stardew Valley 1.4.1 or later. 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. * Fixed Android issue where game files were backed up.
* For modders: * 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. * 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. * 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. * Fixed incorrect warning about mods adding invalid schedules in some cases. The validation was unreliable, and has been removed.

View File

@ -7,7 +7,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="2.10.0" /> <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="NUnit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1"> <PackageReference Include="NUnit3TestAdapter" Version="3.16.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@ -1,9 +1,9 @@
{ {
"Name": "Console Commands", "Name": "Console Commands",
"Author": "SMAPI", "Author": "SMAPI",
"Version": "3.2.0", "Version": "3.3.0",
"Description": "Adds SMAPI console commands that let you manipulate the game.", "Description": "Adds SMAPI console commands that let you manipulate the game.",
"UniqueID": "SMAPI.ConsoleCommands", "UniqueID": "SMAPI.ConsoleCommands",
"EntryDll": "ConsoleCommands.dll", "EntryDll": "ConsoleCommands.dll",
"MinimumApiVersion": "3.2.0" "MinimumApiVersion": "3.3.0"
} }

View File

@ -66,29 +66,37 @@ namespace StardewModdingAPI.Mods.SaveBackup
FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName)); FileInfo targetFile = new FileInfo(Path.Combine(backupFolder.FullName, this.FileName));
DirectoryInfo fallbackDir = new DirectoryInfo(Path.Combine(backupFolder.FullName, this.BackupLabel)); DirectoryInfo fallbackDir = new DirectoryInfo(Path.Combine(backupFolder.FullName, this.BackupLabel));
if (targetFile.Exists || fallbackDir.Exists) if (targetFile.Exists || fallbackDir.Exists)
{
this.Monitor.Log("Already backed up today.");
return; return;
}
// copy saves to fallback directory (ignore non-save files/folders) // 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); 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 // compress backup if possible
this.Monitor.Log("Compressing backup if possible...", LogLevel.Trace);
if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError)) if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError))
{ {
if (Constants.TargetPlatform != GamePlatform.Android) // expected to fail on Android this.Monitor.Log(Constants.TargetPlatform != GamePlatform.Android
this.Monitor.Log($"Couldn't compress backup, leaving it uncompressed.\n{compressError}", LogLevel.Trace); ? $"Backed up to {fallbackDir.FullName}." // expected to fail on Android
: $"Backed up to {fallbackDir.FullName}. Couldn't compress backup:\n{compressError}"
);
} }
else else
{
this.Monitor.Log($"Backed up to {targetFile.FullName}.");
fallbackDir.Delete(recursive: true); fallbackDir.Delete(recursive: true);
}
this.Monitor.Log("Backup done!", LogLevel.Trace);
} }
catch (Exception ex) catch (Exception ex)
{ {
this.Monitor.Log("Couldn't back up save files (see log file for details).", LogLevel.Warn); this.Monitor.Log("Couldn't back up saves (see log file for details).", LogLevel.Warn);
this.Monitor.Log(ex.ToString(), LogLevel.Trace); this.Monitor.Log(ex.ToString());
} }
} }
@ -108,7 +116,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
{ {
try try
{ {
this.Monitor.Log($"Deleting {entry.Name}...", LogLevel.Trace); this.Monitor.Log($"Deleting {entry.Name}...");
if (entry is DirectoryInfo folder) if (entry is DirectoryInfo folder)
folder.Delete(recursive: true); folder.Delete(recursive: true);
else else
@ -123,7 +131,7 @@ namespace StardewModdingAPI.Mods.SaveBackup
catch (Exception ex) catch (Exception ex)
{ {
this.Monitor.Log("Couldn't remove old backups (see log file for details).", LogLevel.Warn); 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="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> /// <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> /// <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) if (!source.Exists || filter?.Invoke(source) == false)
targetFolder.Create(); return false;
if (filter?.Invoke(source) == false) bool anyCopied = false;
return;
switch (source) switch (source)
{ {
case FileInfo sourceFile: case FileInfo sourceFile:
targetFolder.Create();
sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name)); sourceFile.CopyTo(Path.Combine(targetFolder.FullName, sourceFile.Name));
anyCopied = true;
break; break;
case DirectoryInfo sourceDir: case DirectoryInfo sourceDir:
DirectoryInfo targetSubfolder = copyRoot ? new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)) : targetFolder; DirectoryInfo targetSubfolder = copyRoot ? new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)) : targetFolder;
foreach (var entry in sourceDir.EnumerateFileSystemInfos()) foreach (var entry in sourceDir.EnumerateFileSystemInfos())
this.RecursiveCopy(entry, targetSubfolder, filter); anyCopied = this.RecursiveCopy(entry, targetSubfolder, filter) || anyCopied;
break; break;
default: default:
throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'."); throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'.");
} }
return anyCopied;
} }
/// <summary>A copy filter which matches save folders.</summary> /// <summary>A copy filter which matches save folders.</summary>

View File

@ -1,9 +1,9 @@
{ {
"Name": "Save Backup", "Name": "Save Backup",
"Author": "SMAPI", "Author": "SMAPI",
"Version": "3.2.0", "Version": "3.3.0",
"Description": "Automatically backs up all your saves once per day into its folder.", "Description": "Automatically backs up all your saves once per day into its folder.",
"UniqueID": "SMAPI.SaveBackup", "UniqueID": "SMAPI.SaveBackup",
"EntryDll": "SaveBackup.dll", "EntryDll": "SaveBackup.dll",
"MinimumApiVersion": "3.2.0" "MinimumApiVersion": "3.3.0"
} }

View File

@ -1,3 +1,6 @@
using Newtonsoft.Json;
using StardewModdingAPI.Toolkit.Serialization.Converters;
namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
{ {
/// <summary>Metadata about a version.</summary> /// <summary>Metadata about a version.</summary>
@ -7,6 +10,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi
** Accessors ** Accessors
*********/ *********/
/// <summary>The version number.</summary> /// <summary>The version number.</summary>
[JsonConverter(typeof(NonStandardSemanticVersionConverter))]
public ISemanticVersion Version { get; set; } public ISemanticVersion Version { get; set; }
/// <summary>The mod page URL.</summary> /// <summary>The mod page URL.</summary>

View File

@ -12,7 +12,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />
<PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" /> <PackageReference Include="System.Management" Version="4.5.0" Condition="'$(OS)' == 'Windows_NT'" />

View File

@ -199,18 +199,19 @@ namespace StardewModdingAPI.Toolkit
/// <returns>Returns whether parsing the version succeeded.</returns> /// <returns>Returns whether parsing the version succeeded.</returns>
public static bool TryParse(string version, out ISemanticVersion parsed) 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="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> /// <param name="parsed">The parsed representation.</param>
/// <returns>Returns whether parsing the version succeeded.</returns> /// <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 try
{ {
parsed = new SemanticVersion(version, true); parsed = new SemanticVersion(version, allowNonStandard);
return true; return true;
} }
catch catch

View File

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

View File

@ -7,6 +7,13 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
/// <summary>Handles deserialization of <see cref="ISemanticVersion"/>.</summary> /// <summary>Handles deserialization of <see cref="ISemanticVersion"/>.</summary>
internal class SemanticVersionConverter : JsonConverter internal class SemanticVersionConverter : JsonConverter
{ {
/*********
** Fields
*********/
/// <summary>Whether to allow non-standard extensions to semantic versioning.</summary>
protected bool AllowNonStandard { get; set; }
/********* /*********
** Accessors ** Accessors
*********/ *********/
@ -78,7 +85,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters
{ {
if (string.IsNullOrWhiteSpace(str)) if (string.IsNullOrWhiteSpace(str))
return null; 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})."); 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; return version;
} }

View File

@ -53,7 +53,19 @@ namespace StardewModdingAPI.Toolkit.Utilities
} }
catch { } catch { }
#endif #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> /// <summary>Get the name of the Stardew Valley executable.</summary>

View File

@ -41,11 +41,8 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>The cache in which to store mod data.</summary> /// <summary>The cache in which to store mod data.</summary>
private readonly IModCacheRepository ModCache; private readonly IModCacheRepository ModCache;
/// <summary>The number of minutes successful update checks should be cached before refetching them.</summary> /// <summary>The config settings for mod update checks.</summary>
private readonly int SuccessCacheMinutes; private readonly IOptions<ModUpdateCheckConfig> Config;
/// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
private readonly int ErrorCacheMinutes;
/// <summary>The internal mod metadata list.</summary> /// <summary>The internal mod metadata list.</summary>
private readonly ModDatabase ModDatabase; private readonly ModDatabase ModDatabase;
@ -58,21 +55,19 @@ namespace StardewModdingAPI.Web.Controllers
/// <param name="environment">The web hosting environment.</param> /// <param name="environment">The web hosting environment.</param>
/// <param name="wikiCache">The cache in which to store wiki data.</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="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="chucklefish">The Chucklefish API client.</param>
/// <param name="curseForge">The CurseForge API client.</param> /// <param name="curseForge">The CurseForge API client.</param>
/// <param name="github">The GitHub API client.</param> /// <param name="github">The GitHub API client.</param>
/// <param name="modDrop">The ModDrop API client.</param> /// <param name="modDrop">The ModDrop API client.</param>
/// <param name="nexus">The Nexus 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")); this.ModDatabase = new ModToolkit().GetModDatabase(Path.Combine(environment.WebRootPath, "SMAPI.metadata.json"));
ModUpdateCheckConfig config = configProvider.Value;
this.WikiCache = wikiCache; this.WikiCache = wikiCache;
this.ModCache = modCache; this.ModCache = modCache;
this.SuccessCacheMinutes = config.SuccessCacheMinutes; this.Config = config;
this.ErrorCacheMinutes = config.ErrorCacheMinutes;
this.Repositories = this.Repositories =
new IModRepository[] new IModRepository[]
{ {
@ -133,6 +128,8 @@ namespace StardewModdingAPI.Web.Controllers
ModDataRecord record = this.ModDatabase.Get(search.ID); ModDataRecord record = this.ModDatabase.Get(search.ID);
WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase)); WikiModEntry wikiEntry = wikiData.FirstOrDefault(entry => entry.ID.Contains(search.ID.Trim(), StringComparer.InvariantCultureIgnoreCase));
UpdateKey[] updateKeys = this.GetUpdateKeys(search.UpdateKeys, record, wikiEntry).ToArray(); 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 // get latest versions
ModEntryModel result = new ModEntryModel { ID = search.ID }; ModEntryModel result = new ModEntryModel { ID = search.ID };
@ -151,7 +148,7 @@ namespace StardewModdingAPI.Web.Controllers
} }
// fetch data // fetch data
ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey); ModInfoModel data = await this.GetInfoForUpdateKeyAsync(updateKey, allowNonStandardVersions);
if (data.Error != null) if (data.Error != null)
{ {
errors.Add(data.Error); errors.Add(data.Error);
@ -161,7 +158,7 @@ namespace StardewModdingAPI.Web.Controllers
// handle main version // handle main version
if (data.Version != null) if (data.Version != null)
{ {
ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions); ISemanticVersion version = this.GetMappedVersion(data.Version, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
if (version == null) if (version == null)
{ {
errors.Add($"The update key '{updateKey}' matches a mod with invalid semantic version '{data.Version}'."); 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 // handle optional version
if (data.PreviewVersion != null) if (data.PreviewVersion != null)
{ {
ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions); ISemanticVersion version = this.GetMappedVersion(data.PreviewVersion, wikiEntry?.MapRemoteVersions, allowNonStandardVersions);
if (version == null) if (version == null)
{ {
errors.Add($"The update key '{updateKey}' matches a mod with invalid optional semantic version '{data.PreviewVersion}'."); 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 // special cases
if (result.ID == "Pathoschild.SMAPI") if (overrides?.SetUrl != null)
{ {
if (main != null) if (main != null)
main.Url = "https://smapi.io/"; main.Url = overrides.SetUrl;
if (optional != null) if (optional != null)
optional.Url = "https://smapi.io/"; optional.Url = overrides.SetUrl;
} }
// get recommended update (if any) // 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) if (apiVersion != null && installedVersion != null)
{ {
// get newer versions // get newer versions
@ -283,10 +280,11 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get the mod info for an update key.</summary> /// <summary>Get the mod info for an update key.</summary>
/// <param name="updateKey">The namespaced update key.</param> /// <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 // 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 // get site
if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository)) if (!this.Repositories.TryGetValue(updateKey.Repository, out IModRepository repository))
@ -298,7 +296,7 @@ namespace StardewModdingAPI.Web.Controllers
{ {
if (result.Version == null) if (result.Version == null)
result.SetError(RemoteModStatus.InvalidData, $"The update key '{updateKey}' matches a mod with no version number."); 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}'."); 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> /// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to parse.</param> /// <param name="version">The version to parse.</param>
/// <param name="map">A map of version replacements.</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 // try mapped version
string rawNewVersion = this.GetRawMappedVersion(version, map); string rawNewVersion = this.GetRawMappedVersion(version, map, allowNonStandard);
if (SemanticVersion.TryParse(rawNewVersion, out ISemanticVersion parsedNew)) if (SemanticVersion.TryParse(rawNewVersion, allowNonStandard, out ISemanticVersion parsedNew))
return parsedNew; return parsedNew;
// return original version // return original version
return SemanticVersion.TryParse(version, out ISemanticVersion parsedOld) return SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsedOld)
? parsedOld ? parsedOld
: null; : null;
} }
@ -373,7 +372,8 @@ namespace StardewModdingAPI.Web.Controllers
/// <summary>Get a semantic local version for update checks.</summary> /// <summary>Get a semantic local version for update checks.</summary>
/// <param name="version">The version to map.</param> /// <param name="version">The version to map.</param>
/// <param name="map">A map of version replacements.</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()) if (version == null || map == null || !map.Any())
return version; return version;
@ -383,14 +383,14 @@ namespace StardewModdingAPI.Web.Controllers
return map[version]; return map[version];
// match parsed version // match parsed version
if (SemanticVersion.TryParse(version, out ISemanticVersion parsed)) if (SemanticVersion.TryParse(version, allowNonStandard, out ISemanticVersion parsed))
{ {
if (map.ContainsKey(parsed.ToString())) if (map.ContainsKey(parsed.ToString()))
return map[parsed.ToString()]; return map[parsed.ToString()];
foreach (var pair in map) 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(); return newVersion.ToString();
} }
} }

View File

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

View File

@ -11,5 +11,8 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The number of minutes failed update checks should be cached before refetching them.</summary> /// <summary>The number of minutes failed update checks should be cached before refetching them.</summary>
public int ErrorCacheMinutes { get; set; } public int ErrorCacheMinutes { get; set; }
/// <summary>Update-check metadata to override.</summary>
public ModOverrideConfig[] ModOverrides { get; set; }
} }
} }

View File

@ -28,7 +28,7 @@ namespace StardewModdingAPI.Web.Framework
return return
values.TryGetValue(routeKey, out object routeValue) values.TryGetValue(routeKey, out object routeValue)
&& routeValue is string routeStr && routeValue is string routeStr
&& SemanticVersion.TryParseNonStandard(routeStr, out _); && SemanticVersion.TryParse(routeStr, allowNonStandard: true, out _);
} }
} }
} }

View File

@ -12,11 +12,11 @@
</ItemGroup> </ItemGroup>
<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.AspNetCore" Version="1.7.9" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.6.3" /> <PackageReference Include="Hangfire.MemoryStorage" Version="1.6.3" />
<PackageReference Include="Hangfire.Mongo" Version="0.6.6" /> <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="Humanizer.Core" Version="2.7.9" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" /> <PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="Markdig" Version="0.18.1" /> <PackageReference Include="Markdig" Version="0.18.1" />
@ -25,7 +25,7 @@
<PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Rewrite" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.2.0" />
<PackageReference Include="Mongo2Go" Version="2.2.12" /> <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="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Pathoschild.FluentNexus" Version="0.8.0" /> <PackageReference Include="Pathoschild.FluentNexus" Version="0.8.0" />
<PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" /> <PackageReference Include="Pathoschild.Http.FluentClient" Version="3.3.1" />

View File

@ -8,11 +8,11 @@
TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated; TimeSpan staleAge = DateTimeOffset.UtcNow - Model.LastUpdated;
} }
@section Head { @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/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/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="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> <script>
$(function() { $(function() {
var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None }); var data = @Json.Serialize(Model.Mods, new JsonSerializerSettings { Formatting = Formatting.None });

View File

@ -64,6 +64,17 @@
"ModUpdateCheck": { "ModUpdateCheck": {
"SuccessCacheMinutes": 60, "SuccessCacheMinutes": 60,
"ErrorCacheMinutes": 5 "ErrorCacheMinutes": 5,
"ModOverrides": [
{
"ID": "Pathoschild.SMAPI",
"AllowNonStandardVersions": true,
"SetUrl": "https://smapi.io"
},
{
"ID": "MartyrPher.SMAPI-Android-Installer",
"AllowNonStandardVersions": true
}
]
} }
} }

View File

@ -86,6 +86,11 @@ table.wikitable > caption {
font-size: 0.9em; font-size: 0.9em;
} }
#mod-list thead tr {
position: sticky;
top: 0;
}
#mod-list th.header { #mod-list th.header {
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: center right; background-position: center right;

View File

@ -102,7 +102,7 @@ smapi.modList = function (mods, enableBeta) {
app = new Vue({ app = new Vue({
el: "#app", el: "#app",
data: data, data: data,
mounted: function() { mounted: function () {
// enable table sorting // enable table sorting
$("#mod-list").tablesorter({ $("#mod-list").tablesorter({
cssHeader: "header", cssHeader: "header",
@ -115,11 +115,7 @@ smapi.modList = function (mods, enableBeta) {
$("#search-box").focus(); $("#search-box").focus();
// jump to anchor (since table is added after page load) // jump to anchor (since table is added after page load)
if (location.hash) { this.fixHashPosition();
var row = $(location.hash).get(0);
if (row)
row.scrollIntoView();
}
}, },
methods: { 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. * 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. * @param {string[]} searchWords The search words to match.
* @returns {bool} Whether the mod matches the filters. * @returns {bool} Whether the mod matches the filters.
*/ */
matchesFilters: function(mod, searchWords) { matchesFilters: function (mod, searchWords) {
var filters = data.filters; var filters = data.filters;
// check hash // check hash
@ -249,7 +257,9 @@ smapi.modList = function (mods, enableBeta) {
} }
}); });
app.applyFilters(); app.applyFilters();
app.fixHashPosition();
window.addEventListener("hashchange", function () { window.addEventListener("hashchange", function () {
app.applyFilters(); app.applyFilters();
app.fixHashPosition();
}); });
}; };

View File

@ -112,7 +112,7 @@
"Default | UpdateKey": "Nexus:2341" "Default | UpdateKey": "Nexus:2341"
}, },
"TMX Loader": { "TMXL Map Toolkit": {
"ID": "Platonymous.TMXLoader", "ID": "Platonymous.TMXLoader",
"Default | UpdateKey": "Nexus:1820" "Default | UpdateKey": "Nexus:1820"
}, },
@ -153,9 +153,9 @@
/********* /*********
** Broke in SDV 1.4 ** Broke in SDV 1.4
*********/ *********/
"Fix Dice": { "Auto Quality Patch": {
"ID": "ashley.fixdice", "ID": "SilentOak.AutoQualityPatch",
"~1.1.2 | Status": "AssumeBroken" // crashes game on startup "~2.1.3-unofficial.7 | Status": "AssumeBroken" // runtime errors
}, },
"Fix Dice": { "Fix Dice": {

View File

@ -142,7 +142,7 @@
}, },
"FromFile": { "FromFile": {
"title": "Source file", "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", "type": "string",
"allOf": [ "allOf": [
{ {
@ -151,12 +151,12 @@
} }
}, },
{ {
"pattern": "\\.(json|png|tbin|xnb) *$" "pattern": "\\.(json|png|tbin|tmx|xnb) *$"
} }
], ],
"@errorMessages": { "@errorMessages": {
"allOf:indexes: 0": "Invalid value; must not contain directory climbing (like '../').", "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": { "FromArea": {
@ -325,7 +325,7 @@
"then": { "then": {
"properties": { "properties": {
"FromFile": { "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": { "FromArea": {
"description": "The part of the source map to copy. Defaults to the whole source map." "description": "The part of the source map to copy. Defaults to the whole source map."

View File

@ -20,7 +20,7 @@ namespace StardewModdingAPI
** Public ** Public
****/ ****/
/// <summary>SMAPI's current semantic version.</summary> /// <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> /// <summary>The minimum supported version of Stardew Valley.</summary>
public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1");
@ -115,27 +115,60 @@ namespace StardewModdingAPI
/// <returns>Returns the compatible SMAPI version, or <c>null</c> if none was found.</returns> /// <returns>Returns the compatible SMAPI version, or <c>null</c> if none was found.</returns>
internal static ISemanticVersion GetCompatibleApiVersion(ISemanticVersion version) 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()) switch (version.ToString())
{ {
case "1.3.36": case "1.4.1":
return new SemanticVersion(2, 11, 2); case "1.4.0":
return new SemanticVersion("3.0.1");
case "1.3.36":
return new SemanticVersion("2.11.2");
case "1.3.32":
case "1.3.33": case "1.3.33":
return new SemanticVersion(2, 10, 2); case "1.3.32":
return new SemanticVersion("2.10.2");
case "1.3.28": 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": 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");
default:
return null; return null;
} }
}
/// <summary>Get metadata for mapping assemblies to the current platform.</summary> /// <summary>Get metadata for mapping assemblies to the current platform.</summary>
/// <param name="targetPlatform">The target game platform.</param> /// <param name="targetPlatform">The target game platform.</param>

View File

@ -1,6 +1,7 @@
using System; using System;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Graphics;
using StardewValley;
namespace StardewModdingAPI.Framework.Content namespace StardewModdingAPI.Framework.Content
{ {
@ -102,5 +103,21 @@ namespace StardewModdingAPI.Framework.Content
// patch target texture // patch target texture
target.SetData(0, targetArea, sourceData, 0, pixelCount); 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;
}
} }
} }

View File

@ -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> /// <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="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="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> /// <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(() => return this.ContentManagerLock.InWriteLock(() =>
{ {
@ -123,6 +124,7 @@ namespace StardewModdingAPI.Framework
gameContentManager: gameContentManager, gameContentManager: gameContentManager,
serviceProvider: this.MainContentManager.ServiceProvider, serviceProvider: this.MainContentManager.ServiceProvider,
rootDirectory: rootDirectory, rootDirectory: rootDirectory,
modName: modName,
currentCulture: this.MainContentManager.CurrentCulture, currentCulture: this.MainContentManager.CurrentCulture,
coordinator: this, coordinator: this,
monitor: this.Monitor, monitor: this.Monitor,

View File

@ -2,12 +2,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Framework.Utilities;
using StardewValley; using StardewValley;
using xTile;
namespace StardewModdingAPI.Framework.ContentManagers namespace StardewModdingAPI.Framework.ContentManagers
{ {
@ -337,6 +340,20 @@ namespace StardewModdingAPI.Framework.ContentManagers
{ {
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); 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 // edit asset
foreach (var entry in this.Editors) foreach (var entry in this.Editors)
{ {

View File

@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary> /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper; 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> /// <summary>The game content manager used for map tilesheets not provided by the mod.</summary>
private readonly IContentManager GameContentManager; 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="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="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="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="rootDirectory">The root directory to search for content.</param>
/// <param name="currentCulture">The current culture for which to localize 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> /// <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="reflection">Simplifies access to private code.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</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> /// <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) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
{ {
this.GameContentManager = gameContentManager; this.GameContentManager = gameContentManager;
this.JsonHelper = jsonHelper; this.JsonHelper = jsonHelper;
this.ModName = modName;
} }
/// <summary>Load an asset that has been processed by the content pipeline.</summary> /// <summary>Load an asset that has been processed by the content pipeline.</summary>
@ -248,8 +253,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
texture.GetData(data); texture.GetData(data);
for (int i = 0; i < data.Length; i++) for (int i = 0; i < data.Length; i++)
{ {
if (data[i].A == 0) if (data[i].A == byte.MinValue || data[i].A == byte.MaxValue)
continue; // no need to change fully transparent pixels continue; // no need to change fully transparent/opaque pixels
data[i] = Color.FromNonPremultiplied(data[i].ToVector4()); data[i] = Color.FromNonPremultiplied(data[i].ToVector4());
} }
@ -297,80 +302,81 @@ namespace StardewModdingAPI.Framework.ContentManagers
foreach (TileSheet tilesheet in map.TileSheets) foreach (TileSheet tilesheet in map.TileSheets)
{ {
string imageSource = tilesheet.ImageSource; string imageSource = tilesheet.ImageSource;
string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
// validate tilesheet path // validate tilesheet path
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains("..")) 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 (../)."); throw new SContentLoadException($"{errorPrefix} 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)}";
}
}
// load best match // load best match
try try
{ {
string key = if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, isOutdoors, out string assetName, out string error))
this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource) throw new SContentLoadException($"{errorPrefix} {error}");
?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
if (key != null)
{
tilesheet.ImageSource = key;
continue;
}
}
catch (Exception ex)
{
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex);
}
// none found tilesheet.ImageSource = assetName;
throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder."); }
catch (Exception ex) when (!(ex is SContentLoadException))
{
throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex);
}
} }
} }
/// <summary>Get the actual asset name for a tilesheet.</summary> /// <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="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
/// <param name="imageSource">The tilesheet image source to load.</param> /// <param name="originalPath">The tilesheet path to load.</param>
/// <returns>Returns the asset name.</returns> /// <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> /// <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) assetName = null;
return null; error = null;
// check relative to map file // nothing to do
if (string.IsNullOrWhiteSpace(originalPath))
{ {
string localKey = Path.Combine(modRelativeMapFolder, imageSource); assetName = originalPath;
FileInfo localFile = this.GetModFile(localKey); return true;
if (localFile.Exists)
return this.GetInternalAssetKey(localKey);
} }
// check relative to content folder // 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)
{ {
foreach (string candidateKey in new[] { imageSource, Path.Combine("Maps", imageSource) }) string dirPath = Path.GetDirectoryName(originalPath);
relativePath = Path.Combine(dirPath, $"{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}");
}
// get relative to map file
{ {
string contentKey = candidateKey.EndsWith(".png") string localKey = Path.Combine(modRelativeMapFolder, relativePath);
? candidateKey.Substring(0, candidateKey.Length - 4) if (this.GetModFile(localKey).Exists)
: candidateKey; {
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 try
{ {
this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
return contentKey; assetName = contentKey;
return true;
} }
catch catch
{ {
@ -385,10 +391,10 @@ namespace StardewModdingAPI.Framework.ContentManagers
throw; throw;
} }
} }
}
// not found // 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> /// <summary>Get whether a file from the game's content folder exists.</summary>

View File

@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.Input
public ICursorPosition CursorPosition => this.CursorPositionImpl; public ICursorPosition CursorPosition => this.CursorPositionImpl;
/// <summary>The buttons which were pressed, held, or released.</summary> /// <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> /// <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>(); 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.")] [Obsolete("This method should only be called by the game itself.")]
public override void Update() { } 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() public void TrueUpdate()
{ {
try try
@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Input
GamePadState realController = GamePad.GetState(PlayerIndex.One); GamePadState realController = GamePad.GetState(PlayerIndex.One);
KeyboardState realKeyboard = Keyboard.GetState(); KeyboardState realKeyboard = Keyboard.GetState();
MouseState realMouse = Mouse.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 cursorAbsolutePos = new Vector2((realMouse.X * zoomMultiplier) + Game1.viewport.X, (realMouse.Y * zoomMultiplier) + Game1.viewport.Y);
Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
@ -102,7 +102,7 @@ namespace StardewModdingAPI.Framework.Input
} }
// update suppressed states // update suppressed states
this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown()); this.SuppressButtons.RemoveWhere(p => !this.GetState(activeButtons, p).IsDown());
this.UpdateSuppression(); this.UpdateSuppression();
} }
catch (InvalidOperationException) catch (InvalidOperationException)
@ -159,7 +159,7 @@ namespace StardewModdingAPI.Framework.Input
/// <param name="button">The button to check.</param> /// <param name="button">The button to check.</param>
public bool IsDown(SButton button) 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> /// <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())); 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 ** 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="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="mouseState">The game's mouse state for the current tick.</param>
/// <param name="gamePadState">The game's controller 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) if (this.SuppressButtons.Count == 0)
return; 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> /// <summary>Get the state of all pressed or released buttons relative to their previous state.</summary>
/// <param name="previousStatuses">The previous button statuses.</param> /// <param name="previousStates">The previous button states.</param>
/// <param name="keyboard">The keyboard state.</param> /// <param name="keyboard">The keyboard state.</param>
/// <param name="mouse">The mouse state.</param> /// <param name="mouse">The mouse state.</param>
/// <param name="controller">The controller 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 // handle pressed keys
SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray(); SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray();
foreach (SButton button in down) 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 // 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)) if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key))
activeButtons[prev.Key] = InputStatus.Released; activeButtons[prev.Key] = SButtonState.Released;
} }
return activeButtons; return activeButtons;
} }
/// <summary>Get the status of a button relative to its previous status.</summary> /// <summary>Get the state of a button relative to its previous state.</summary>
/// <param name="oldStatus">The previous button status.</param> /// <param name="oldState">The previous button state.</param>
/// <param name="isDown">Whether the button is currently down.</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()) if (isDown && oldState.IsDown())
return InputStatus.Held; return SButtonState.Held;
if (isDown) if (isDown)
return InputStatus.Pressed; return SButtonState.Pressed;
return InputStatus.Released; 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="activeButtons">The current button states to check.</param>
/// <param name="button">The button 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> /// <summary>Get the buttons pressed in the given stats.</summary>

View File

@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The friendly mod name for use in errors.</summary> /// <summary>The friendly mod name for use in errors.</summary>
private readonly string ModName; private readonly string ModName;
/// <summary>Encapsulates monitoring and logging for a given module.</summary> /// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor; private readonly IMonitor Monitor;
@ -70,9 +70,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor) public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor)
: base(modID) : base(modID)
{ {
string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
this.ContentCore = contentCore; this.ContentCore = contentCore;
this.GameContentManager = contentCore.CreateGameContentManager(this.ContentCore.GetManagedAssetPrefix(modID) + ".content"); this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content");
this.ModContentManager = contentCore.CreateModContentManager(this.ContentCore.GetManagedAssetPrefix(modID), modFolderPath, this.GameContentManager); this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, this.GameContentManager);
this.ModName = modName; this.ModName = modName;
this.Monitor = monitor; this.Monitor = monitor;
} }

View File

@ -50,5 +50,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
{ {
this.InputState.SuppressButtons.Add(button); 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);
}
} }
} }

View File

@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Networking
/**** /****
** Destination ** 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; } public long[] ToPlayerIDs { get; set; }
/// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary> /// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary>

View File

@ -635,16 +635,16 @@ namespace StardewModdingAPI.Framework
foreach (var pair in inputState.ActiveButtons) foreach (var pair in inputState.ActiveButtons)
{ {
SButton button = pair.Key; SButton button = pair.Key;
InputStatus status = pair.Value; SButtonState status = pair.Value;
if (status == InputStatus.Pressed) if (status == SButtonState.Pressed)
{ {
if (this.Monitor.IsVerbose) if (this.Monitor.IsVerbose)
this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace); this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace);
events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState)); events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState));
} }
else if (status == InputStatus.Released) else if (status == SButtonState.Released)
{ {
if (this.Monitor.IsVerbose) if (this.Monitor.IsVerbose)
this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace); this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace);
@ -893,6 +893,7 @@ namespace StardewModdingAPI.Framework
{ {
var events = this.Events; var events = this.Events;
Game1.showingHealthBar = false;
if (Game1._newDayTask != null) if (Game1._newDayTask != null)
{ {
this.GraphicsDevice.Clear(Game1.bgColor); this.GraphicsDevice.Clear(Game1.bgColor);
@ -934,7 +935,7 @@ namespace StardewModdingAPI.Framework
else else
{ {
this.GraphicsDevice.Clear(Game1.bgColor); 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); 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 byte batchOpens = 0; // used for rendering event
Microsoft.Xna.Framework.Rectangle rectangle;
Viewport viewport; Viewport viewport;
if (Game1.gameMode == (byte)0) 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); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
if (++batchOpens == 1) if (++batchOpens == 1)
events.Rendering.RaiseEmpty(); 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); 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() || currentLightSource.lightContext.Value != LightSource.LightContext.WindowLight)
if (!Game1.isRaining && !Game1.isDarkOut() || lightSource.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); Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(currentLightSource.PlayerID);
if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)((NetFieldBase<bool, NetBool>)farmerMaybeOffline.hidden)) if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)(NetFieldBase<bool, NetBool>)farmerMaybeOffline.hidden)
continue; 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))) if (Utility.isOnScreen((Vector2)(NetFieldBase<Vector2, NetVector2>)currentLightSource.position, (int)((double)(float)(NetFieldBase<float, NetFloat>)currentLightSource.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); 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(); Game1.spriteBatch.End();
@ -1134,7 +1135,7 @@ namespace StardewModdingAPI.Framework
{ {
foreach (Farmer farmerActor in Game1.currentLocation.currentEvent.farmerActors) 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); this._farmerShadows.Add(farmerActor);
} }
} }
@ -1142,7 +1143,7 @@ namespace StardewModdingAPI.Framework
{ {
foreach (Farmer farmer in Game1.currentLocation.farmers) 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); this._farmerShadows.Add(farmer);
} }
} }
@ -1152,26 +1153,39 @@ namespace StardewModdingAPI.Framework
{ {
foreach (NPC character in Game1.currentLocation.characters) foreach (NPC character in Game1.currentLocation.characters)
{ {
if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation()))) 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); 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 else
{ {
foreach (NPC actor in Game1.CurrentEvent.actors) foreach (NPC actor in Game1.CurrentEvent.actors)
{ {
if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) 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); 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) 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()))) 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); {
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.mapDisplayDevice.EndScene();
Game1.spriteBatch.End(); Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); 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) 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()))) 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); 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 else
{ {
foreach (NPC actor in Game1.CurrentEvent.actors) foreach (NPC actor in Game1.CurrentEvent.actors)
{ {
if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation())) 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); 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) foreach (Farmer farmerShadow in this._farmerShadows)
{ {
float layerDepth = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f; 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()))) 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); {
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)) if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null))
@ -1207,7 +1235,7 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.draw(Game1.spriteBatch); Game1.currentLocation.draw(Game1.spriteBatch);
foreach (Vector2 key in Game1.crabPotOverlayTiles.Keys) 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) if (tile != null)
{ {
Vector2 local = Game1.GlobalToLocal(Game1.viewport, key * 64f); Vector2 local = Game1.GlobalToLocal(Game1.viewport, key * 64f);
@ -1237,10 +1265,31 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch); Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch);
Game1.spriteBatch.End(); Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); 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); 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); 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))) 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); Game1.drawTool(Game1.player);
if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null) if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null)
@ -1274,23 +1323,9 @@ namespace StardewModdingAPI.Framework
if (Game1.farmEvent != null) if (Game1.farmEvent != null)
Game1.farmEvent.draw(Game1.spriteBatch); Game1.farmEvent.draw(Game1.spriteBatch);
if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000) if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000)
{ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel);
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);
}
if (Game1.screenGlow) if (Game1.screenGlow)
{ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha);
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.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch); 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))) 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); 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.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); 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)) 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);
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);
}
Game1.spriteBatch.End(); Game1.spriteBatch.End();
} }
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null); Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@ -1424,12 +1452,28 @@ namespace StardewModdingAPI.Framework
this.drawDialogueBox(); this.drawDialogueBox();
if (Game1.progressBar && !this.takingMapScreenshot) 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); SpriteBatch spriteBatch1 = Game1.spriteBatch;
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); 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) if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)
Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch); 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; SpriteBatch spriteBatch = Game1.spriteBatch;
Texture2D staminaRect = Game1.staminaRect; Texture2D staminaRect = Game1.staminaRect;

View File

@ -338,30 +338,49 @@ 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> /// <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) public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs)
{ {
// validate // validate input
if (message == null) if (message == null)
throw new ArgumentNullException(nameof(message)); throw new ArgumentNullException(nameof(message));
if (string.IsNullOrWhiteSpace(messageType)) if (string.IsNullOrWhiteSpace(messageType))
throw new ArgumentNullException(nameof(messageType)); throw new ArgumentNullException(nameof(messageType));
if (string.IsNullOrWhiteSpace(fromModID)) if (string.IsNullOrWhiteSpace(fromModID))
throw new ArgumentNullException(nameof(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."); sendToSelf = true;
return; sendToPeers.AddRange(this.Peers.Values);
}
else
{
foreach (long id in toPlayerIDs.Distinct())
{
if (id == curPlayerId)
sendToSelf = true;
else if (this.Peers.TryGetValue(id, out MultiplayerPeer peer) && peer.HasSmapi)
sendToPeers.Add(peer);
}
} }
// filter player IDs // filter by mod ID
HashSet<long> playerIDs = null; if (toModIDs != null)
if (toPlayerIDs != null && toPlayerIDs.Any())
{ {
playerIDs = new HashSet<long>(toPlayerIDs); HashSet<string> sendToMods = new HashSet<string>(toModIDs, StringComparer.InvariantCultureIgnoreCase);
playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id)); if (sendToSelf && toModIDs.All(id => this.ModRegistry.Get(id) == null))
if (!playerIDs.Any()) sendToSelf = false;
{
this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected."); sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !sendToMods.Contains(mod.ID)));
return;
} }
// 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 // get data to send
@ -369,33 +388,44 @@ namespace StardewModdingAPI.Framework
fromPlayerID: Game1.player.UniqueMultiplayerID, fromPlayerID: Game1.player.UniqueMultiplayerID,
fromModID: fromModID, fromModID: fromModID,
toModIDs: toModIDs, toModIDs: toModIDs,
toPlayerIDs: playerIDs?.ToArray(), toPlayerIDs: sendToPeers.Select(p => p.PlayerID).ToArray(),
type: messageType, type: messageType,
data: JToken.FromObject(message) data: JToken.FromObject(message)
); );
string data = JsonConvert.SerializeObject(model, Formatting.None); string data = JsonConvert.SerializeObject(model, Formatting.None);
// log message // send self-message
if (sendToSelf)
{
if (this.LogNetworkTraffic) if (this.LogNetworkTraffic)
this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace); this.Monitor.Log($"Broadcasting '{messageType}' message to self: {data}.", LogLevel.Trace);
// send message this.OnModMessageReceived(model);
}
// send message to peers
if (sendToPeers.Any())
{
if (Context.IsMainPlayer) if (Context.IsMainPlayer)
{ {
foreach (MultiplayerPeer peer in this.Peers.Values) foreach (MultiplayerPeer peer in sendToPeers)
{ {
if (playerIDs == null || playerIDs.Contains(peer.PlayerID)) if (this.LogNetworkTraffic)
{ this.Monitor.Log($"Broadcasting '{messageType}' message to farmhand {peer.PlayerID}: {data}.", LogLevel.Trace);
model.ToPlayerIDs = new[] { peer.PlayerID };
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data)); peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data));
} }
} }
} else if (this.HostPeer?.HasSmapi == true)
else if (this.HostPeer != null && this.HostPeer.HasSmapi) {
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)); this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
}
else else
this.Monitor.VerboseLog(" Can't send message because no valid connections were found."); this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
}
} }

View File

@ -19,5 +19,11 @@ namespace StardewModdingAPI
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception> /// <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> /// <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); 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);
} }
} }

View File

@ -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> /// <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> /// <param name="button">The button to suppress.</param>
void Suppress(SButton button); void Suppress(SButton button);
/// <summary>Get the state of a button.</summary>
/// <param name="button">The button to check.</param>
SButtonState GetState(SButton button);
} }
} }

View File

@ -886,10 +886,17 @@ namespace StardewModdingAPI.Metadata
return false; return false;
// update dialogue // 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) foreach (NPC villager in villagers)
{ {
MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray();
villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue
villager.resetCurrentDialogue(); villager.resetCurrentDialogue();
villager.currentMarriageDialogue.Set(marriageDialogue);
} }
return true; return true;

View File

@ -1,7 +1,7 @@
namespace StardewModdingAPI.Framework.Input namespace StardewModdingAPI
{ {
/// <summary>The input status for a button during an update frame.</summary> /// <summary>The input state for a button during an update frame.</summary>
internal enum InputStatus public enum SButtonState
{ {
/// <summary>The button was neither pressed, held, nor released.</summary> /// <summary>The button was neither pressed, held, nor released.</summary>
None, None,
@ -16,14 +16,14 @@ namespace StardewModdingAPI.Framework.Input
Released Released
} }
/// <summary>Extension methods for <see cref="InputStatus"/>.</summary> /// <summary>Extension methods for <see cref="SButtonState"/>.</summary>
internal static class InputStatusExtensions internal static class InputStatusExtensions
{ {
/// <summary>Whether the button was pressed or held.</summary> /// <summary>Whether the button was pressed or held.</summary>
/// <param name="status">The button status.</param> /// <param name="state">The button state.</param>
public static bool IsDown(this InputStatus status) public static bool IsDown(this SButtonState state)
{ {
return status == InputStatus.Held || status == InputStatus.Pressed; return state == SButtonState.Held || state == SButtonState.Pressed;
} }
} }
} }

View File

@ -15,9 +15,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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="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="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="Platonymous.TMXTile" Version="1.0.2" /> <PackageReference Include="Platonymous.TMXTile" Version="1.0.2" />
</ItemGroup> </ItemGroup>

3
src/SMAPI/i18n/it.json Normal file
View File

@ -0,0 +1,3 @@
{
"warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)."
}