summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework')
-rw-r--r--src/SMAPI/Framework/Content/AssetDataForImage.cs17
-rw-r--r--src/SMAPI/Framework/ContentCoordinator.cs4
-rw-r--r--src/SMAPI/Framework/ContentManagers/GameContentManager.cs17
-rw-r--r--src/SMAPI/Framework/ContentManagers/ModContentManager.cs142
-rw-r--r--src/SMAPI/Framework/Input/InputStatus.cs29
-rw-r--r--src/SMAPI/Framework/Input/SInputState.cs53
-rw-r--r--src/SMAPI/Framework/ModHelpers/ContentHelper.cs8
-rw-r--r--src/SMAPI/Framework/ModHelpers/InputHelper.cs7
-rw-r--r--src/SMAPI/Framework/Networking/ModMessageModel.cs2
-rw-r--r--src/SMAPI/Framework/SGame.cs166
-rw-r--r--src/SMAPI/Framework/SMultiplayer.cs84
11 files changed, 316 insertions, 213 deletions
diff --git a/src/SMAPI/Framework/Content/AssetDataForImage.cs b/src/SMAPI/Framework/Content/AssetDataForImage.cs
index aa615a0b..44a97136 100644
--- a/src/SMAPI/Framework/Content/AssetDataForImage.cs
+++ b/src/SMAPI/Framework/Content/AssetDataForImage.cs
@@ -1,6 +1,7 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
+using StardewValley;
namespace StardewModdingAPI.Framework.Content
{
@@ -102,5 +103,21 @@ namespace StardewModdingAPI.Framework.Content
// patch target texture
target.SetData(0, targetArea, sourceData, 0, pixelCount);
}
+
+ /// <summary>Extend the image if needed to fit the given size. Note that this is an expensive operation, creates a new texture instance, and that extending a spritesheet horizontally may cause game errors or bugs.</summary>
+ /// <param name="minWidth">The minimum texture width.</param>
+ /// <param name="minHeight">The minimum texture height.</param>
+ /// <returns>Whether the texture was resized.</returns>
+ public bool ExtendImage(int minWidth, int minHeight)
+ {
+ if (this.Data.Width >= minWidth && this.Data.Height >= minHeight)
+ return false;
+
+ Texture2D original = this.Data;
+ Texture2D texture = new Texture2D(Game1.graphics.GraphicsDevice, Math.Max(original.Width, minWidth), Math.Max(original.Height, minHeight));
+ this.ReplaceWith(texture);
+ this.PatchImage(original);
+ return true;
+ }
}
}
diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs
index 2fd31263..0b1ccc3c 100644
--- a/src/SMAPI/Framework/ContentCoordinator.cs
+++ b/src/SMAPI/Framework/ContentCoordinator.cs
@@ -112,9 +112,10 @@ namespace StardewModdingAPI.Framework
/// <summary>Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files.</summary>
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
+ /// <param name="modName">The mod display name to show in errors.</param>
/// <param name="rootDirectory">The root directory to search for content (or <c>null</c> for the default).</param>
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
- public ModContentManager CreateModContentManager(string name, string rootDirectory, IContentManager gameContentManager)
+ public ModContentManager CreateModContentManager(string name, string modName, string rootDirectory, IContentManager gameContentManager)
{
return this.ContentManagerLock.InWriteLock(() =>
{
@@ -123,6 +124,7 @@ namespace StardewModdingAPI.Framework
gameContentManager: gameContentManager,
serviceProvider: this.MainContentManager.ServiceProvider,
rootDirectory: rootDirectory,
+ modName: modName,
currentCulture: this.MainContentManager.CurrentCulture,
coordinator: this,
monitor: this.Monitor,
diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
index eecdda74..eaaf0e6f 100644
--- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs
@@ -2,12 +2,15 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
+using System.Reflection;
using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI.Framework.Content;
using StardewModdingAPI.Framework.Exceptions;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Framework.Utilities;
using StardewValley;
+using xTile;
namespace StardewModdingAPI.Framework.ContentManagers
{
@@ -337,6 +340,20 @@ namespace StardewModdingAPI.Framework.ContentManagers
{
IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName);
+ // special case: if the asset was loaded with a more general type like 'object', call editors with the actual type instead.
+ {
+ Type actualType = asset.Data.GetType();
+ Type actualOpenType = actualType.IsGenericType ? actualType.GetGenericTypeDefinition() : null;
+
+ if (typeof(T) != actualType && (actualOpenType == typeof(Dictionary<,>) || actualOpenType == typeof(List<>) || actualType == typeof(Texture2D) || actualType == typeof(Map)))
+ {
+ return (IAssetData)this.GetType()
+ .GetMethod(nameof(this.ApplyEditors), BindingFlags.NonPublic | BindingFlags.Instance)
+ .MakeGenericMethod(actualType)
+ .Invoke(this, new object[] { info, asset });
+ }
+ }
+
// edit asset
foreach (var entry in this.Editors)
{
diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
index 0a526fc8..7d274eb7 100644
--- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
+++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs
@@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
private readonly JsonHelper JsonHelper;
+ /// <summary>The mod display name to show in errors.</summary>
+ private readonly string ModName;
+
/// <summary>The game content manager used for map tilesheets not provided by the mod.</summary>
private readonly IContentManager GameContentManager;
@@ -40,6 +43,7 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="name">A name for the mod manager. Not guaranteed to be unique.</param>
/// <param name="gameContentManager">The game content manager used for map tilesheets not provided by the mod.</param>
/// <param name="serviceProvider">The service provider to use to locate services.</param>
+ /// <param name="modName">The mod display name to show in errors.</param>
/// <param name="rootDirectory">The root directory to search for content.</param>
/// <param name="currentCulture">The current culture for which to localize content.</param>
/// <param name="coordinator">The central coordinator which manages content managers.</param>
@@ -47,11 +51,12 @@ namespace StardewModdingAPI.Framework.ContentManagers
/// <param name="reflection">Simplifies access to private code.</param>
/// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
/// <param name="onDisposing">A callback to invoke when the content manager is being disposed.</param>
- public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
+ public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action<BaseContentManager> onDisposing)
: base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true)
{
this.GameContentManager = gameContentManager;
this.JsonHelper = jsonHelper;
+ this.ModName = modName;
}
/// <summary>Load an asset that has been processed by the content pipeline.</summary>
@@ -248,8 +253,8 @@ namespace StardewModdingAPI.Framework.ContentManagers
texture.GetData(data);
for (int i = 0; i < data.Length; i++)
{
- if (data[i].A == 0)
- continue; // no need to change fully transparent pixels
+ if (data[i].A == byte.MinValue || data[i].A == byte.MaxValue)
+ continue; // no need to change fully transparent/opaque pixels
data[i] = Color.FromNonPremultiplied(data[i].ToVector4());
}
@@ -297,98 +302,99 @@ namespace StardewModdingAPI.Framework.ContentManagers
foreach (TileSheet tilesheet in map.TileSheets)
{
string imageSource = tilesheet.ImageSource;
+ string errorPrefix = $"{this.ModName} loaded map '{relativeMapPath}' with invalid tilesheet path '{imageSource}'.";
// validate tilesheet path
if (Path.IsPathRooted(imageSource) || PathUtilities.GetSegments(imageSource).Contains(".."))
- throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded. Tilesheet paths must be a relative path without directory climbing (../).");
-
- // get seasonal name (if applicable)
- string seasonalImageSource = null;
- if (isOutdoors && Context.IsSaveLoaded && Game1.currentSeason != null)
- {
- string filename = Path.GetFileName(imageSource) ?? throw new InvalidOperationException($"The '{imageSource}' tilesheet couldn't be loaded: filename is unexpectedly null.");
- bool hasSeasonalPrefix =
- filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase)
- || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase);
- if (hasSeasonalPrefix && !filename.StartsWith(Game1.currentSeason + "_"))
- {
- string dirPath = imageSource.Substring(0, imageSource.LastIndexOf(filename, StringComparison.CurrentCultureIgnoreCase));
- seasonalImageSource = $"{dirPath}{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}";
- }
- }
+ throw new SContentLoadException($"{errorPrefix} Tilesheet paths must be a relative path without directory climbing (../).");
// load best match
try
{
- string key =
- this.GetTilesheetAssetName(relativeMapFolder, seasonalImageSource)
- ?? this.GetTilesheetAssetName(relativeMapFolder, imageSource);
- if (key != null)
- {
- tilesheet.ImageSource = key;
- continue;
- }
+ if (!this.TryGetTilesheetAssetName(relativeMapFolder, imageSource, isOutdoors, out string assetName, out string error))
+ throw new SContentLoadException($"{errorPrefix} {error}");
+
+ tilesheet.ImageSource = assetName;
}
- catch (Exception ex)
+ catch (Exception ex) when (!(ex is SContentLoadException))
{
- throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.", ex);
+ throw new SContentLoadException($"{errorPrefix} The tilesheet couldn't be loaded.", ex);
}
-
- // none found
- throw new ContentLoadException($"The '{imageSource}' tilesheet couldn't be loaded relative to either map file or the game's content folder.");
}
}
/// <summary>Get the actual asset name for a tilesheet.</summary>
/// <param name="modRelativeMapFolder">The folder path containing the map, relative to the mod folder.</param>
- /// <param name="imageSource">The tilesheet image source to load.</param>
- /// <returns>Returns the asset name.</returns>
+ /// <param name="originalPath">The tilesheet path to load.</param>
+ /// <param name="willSeasonalize">Whether the game will apply seasonal logic to the tilesheet.</param>
+ /// <param name="assetName">The found asset name.</param>
+ /// <param name="error">A message indicating why the file couldn't be loaded.</param>
+ /// <returns>Returns whether the asset name was found.</returns>
/// <remarks>See remarks on <see cref="FixCustomTilesheetPaths"/>.</remarks>
- private string GetTilesheetAssetName(string modRelativeMapFolder, string imageSource)
+ private bool TryGetTilesheetAssetName(string modRelativeMapFolder, string originalPath, bool willSeasonalize, out string assetName, out string error)
{
- if (imageSource == null)
- return null;
+ assetName = null;
+ error = null;
+
+ // nothing to do
+ if (string.IsNullOrWhiteSpace(originalPath))
+ {
+ assetName = originalPath;
+ return true;
+ }
- // check relative to map file
+ // parse path
+ string filename = Path.GetFileName(originalPath);
+ bool isSeasonal = filename.StartsWith("spring_", StringComparison.CurrentCultureIgnoreCase)
+ || filename.StartsWith("summer_", StringComparison.CurrentCultureIgnoreCase)
+ || filename.StartsWith("fall_", StringComparison.CurrentCultureIgnoreCase)
+ || filename.StartsWith("winter_", StringComparison.CurrentCultureIgnoreCase);
+ string relativePath = originalPath;
+ if (willSeasonalize && isSeasonal)
{
- string localKey = Path.Combine(modRelativeMapFolder, imageSource);
- FileInfo localFile = this.GetModFile(localKey);
- if (localFile.Exists)
- return this.GetInternalAssetKey(localKey);
+ string dirPath = Path.GetDirectoryName(originalPath);
+ relativePath = Path.Combine(dirPath, $"{Game1.currentSeason}_{filename.Substring(filename.IndexOf("_", StringComparison.CurrentCultureIgnoreCase) + 1)}");
}
- // check relative to content folder
+ // get relative to map file
{
- foreach (string candidateKey in new[] { imageSource, Path.Combine("Maps", imageSource) })
+ string localKey = Path.Combine(modRelativeMapFolder, relativePath);
+ if (this.GetModFile(localKey).Exists)
+ {
+ assetName = this.GetInternalAssetKey(localKey);
+ return true;
+ }
+ }
+
+ // get from game assets
+ {
+ string contentKey = Path.Combine("Maps", relativePath);
+ if (contentKey.EndsWith(".png"))
+ contentKey = contentKey.Substring(0, contentKey.Length - 4);
+
+ try
+ {
+ this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
+ assetName = contentKey;
+ return true;
+ }
+ catch
{
- string contentKey = candidateKey.EndsWith(".png")
- ? candidateKey.Substring(0, candidateKey.Length - 4)
- : candidateKey;
-
- try
- {
- this.GameContentManager.Load<Texture2D>(contentKey, this.Language, useCache: true); // no need to bypass cache here, since we're not storing the asset
- return contentKey;
- }
- catch
- {
- // ignore file-not-found errors
- // TODO: while it's useful to suppress an asset-not-found error here to avoid
- // confusion, this is a pretty naive approach. Even if the file doesn't exist,
- // the file may have been loaded through an IAssetLoader which failed. So even
- // if the content file doesn't exist, that doesn't mean the error here is a
- // content-not-found error. Unfortunately XNA doesn't provide a good way to
- // detect the error type.
- if (this.GetContentFolderFileExists(contentKey))
- throw;
- }
+ // ignore file-not-found errors
+ // TODO: while it's useful to suppress an asset-not-found error here to avoid
+ // confusion, this is a pretty naive approach. Even if the file doesn't exist,
+ // the file may have been loaded through an IAssetLoader which failed. So even
+ // if the content file doesn't exist, that doesn't mean the error here is a
+ // content-not-found error. Unfortunately XNA doesn't provide a good way to
+ // detect the error type.
+ if (this.GetContentFolderFileExists(contentKey))
+ throw;
}
}
// not found
- return null;
+ error = "The tilesheet couldn't be found relative to either map file or the game's content folder.";
+ return false;
}
/// <summary>Get whether a file from the game's content folder exists.</summary>
diff --git a/src/SMAPI/Framework/Input/InputStatus.cs b/src/SMAPI/Framework/Input/InputStatus.cs
deleted file mode 100644
index 99b0006c..00000000
--- a/src/SMAPI/Framework/Input/InputStatus.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-namespace StardewModdingAPI.Framework.Input
-{
- /// <summary>The input status for a button during an update frame.</summary>
- internal enum InputStatus
- {
- /// <summary>The button was neither pressed, held, nor released.</summary>
- None,
-
- /// <summary>The button was pressed in this frame.</summary>
- Pressed,
-
- /// <summary>The button has been held since the last frame.</summary>
- Held,
-
- /// <summary>The button was released in this frame.</summary>
- Released
- }
-
- /// <summary>Extension methods for <see cref="InputStatus"/>.</summary>
- internal static class InputStatusExtensions
- {
- /// <summary>Whether the button was pressed or held.</summary>
- /// <param name="status">The button status.</param>
- public static bool IsDown(this InputStatus status)
- {
- return status == InputStatus.Held || status == InputStatus.Pressed;
- }
- }
-}
diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs
index 84cea36c..4eaa9ca6 100644
--- a/src/SMAPI/Framework/Input/SInputState.cs
+++ b/src/SMAPI/Framework/Input/SInputState.cs
@@ -49,7 +49,7 @@ namespace StardewModdingAPI.Framework.Input
public ICursorPosition CursorPosition => this.CursorPositionImpl;
/// <summary>The buttons which were pressed, held, or released.</summary>
- public IDictionary<SButton, InputStatus> ActiveButtons { get; private set; } = new Dictionary<SButton, InputStatus>();
+ public IDictionary<SButton, SButtonState> ActiveButtons { get; private set; } = new Dictionary<SButton, SButtonState>();
/// <summary>The buttons to suppress when the game next handles input. Each button is suppressed until it's released.</summary>
public HashSet<SButton> SuppressButtons { get; } = new HashSet<SButton>();
@@ -75,7 +75,7 @@ namespace StardewModdingAPI.Framework.Input
[Obsolete("This method should only be called by the game itself.")]
public override void Update() { }
- /// <summary>Update the current button statuses for the given tick.</summary>
+ /// <summary>Update the current button states for the given tick.</summary>
public void TrueUpdate()
{
try
@@ -86,7 +86,7 @@ namespace StardewModdingAPI.Framework.Input
GamePadState realController = GamePad.GetState(PlayerIndex.One);
KeyboardState realKeyboard = Keyboard.GetState();
MouseState realMouse = Mouse.GetState();
- var activeButtons = this.DeriveStatuses(this.ActiveButtons, realKeyboard, realMouse, realController);
+ var activeButtons = this.DeriveStates(this.ActiveButtons, realKeyboard, realMouse, realController);
Vector2 cursorAbsolutePos = new Vector2((realMouse.X * zoomMultiplier) + Game1.viewport.X, (realMouse.Y * zoomMultiplier) + Game1.viewport.Y);
Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null;
@@ -102,7 +102,7 @@ namespace StardewModdingAPI.Framework.Input
}
// update suppressed states
- this.SuppressButtons.RemoveWhere(p => !this.GetStatus(activeButtons, p).IsDown());
+ this.SuppressButtons.RemoveWhere(p => !this.GetState(activeButtons, p).IsDown());
this.UpdateSuppression();
}
catch (InvalidOperationException)
@@ -159,7 +159,7 @@ namespace StardewModdingAPI.Framework.Input
/// <param name="button">The button to check.</param>
public bool IsDown(SButton button)
{
- return this.GetStatus(this.ActiveButtons, button).IsDown();
+ return this.GetState(this.ActiveButtons, button).IsDown();
}
/// <summary>Get whether any of the given buttons were pressed or held.</summary>
@@ -169,6 +169,13 @@ namespace StardewModdingAPI.Framework.Input
return buttons.Any(button => this.IsDown(button.ToSButton()));
}
+ /// <summary>Get the state of a button.</summary>
+ /// <param name="button">The button to check.</param>
+ public SButtonState GetState(SButton button)
+ {
+ return this.GetState(this.ActiveButtons, button);
+ }
+
/*********
** Private methods
@@ -198,7 +205,7 @@ namespace StardewModdingAPI.Framework.Input
/// <param name="keyboardState">The game's keyboard state for the current tick.</param>
/// <param name="mouseState">The game's mouse state for the current tick.</param>
/// <param name="gamePadState">The game's controller state for the current tick.</param>
- private void SuppressGivenStates(IDictionary<SButton, InputStatus> activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
+ private void SuppressGivenStates(IDictionary<SButton, SButtonState> activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState)
{
if (this.SuppressButtons.Count == 0)
return;
@@ -245,48 +252,48 @@ namespace StardewModdingAPI.Framework.Input
}
}
- /// <summary>Get the status of all pressed or released buttons relative to their previous status.</summary>
- /// <param name="previousStatuses">The previous button statuses.</param>
+ /// <summary>Get the state of all pressed or released buttons relative to their previous state.</summary>
+ /// <param name="previousStates">The previous button states.</param>
/// <param name="keyboard">The keyboard state.</param>
/// <param name="mouse">The mouse state.</param>
/// <param name="controller">The controller state.</param>
- private IDictionary<SButton, InputStatus> DeriveStatuses(IDictionary<SButton, InputStatus> previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller)
+ private IDictionary<SButton, SButtonState> DeriveStates(IDictionary<SButton, SButtonState> previousStates, KeyboardState keyboard, MouseState mouse, GamePadState controller)
{
- IDictionary<SButton, InputStatus> activeButtons = new Dictionary<SButton, InputStatus>();
+ IDictionary<SButton, SButtonState> activeButtons = new Dictionary<SButton, SButtonState>();
// handle pressed keys
SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray();
foreach (SButton button in down)
- activeButtons[button] = this.DeriveStatus(this.GetStatus(previousStatuses, button), isDown: true);
+ activeButtons[button] = this.DeriveState(this.GetState(previousStates, button), isDown: true);
// handle released keys
- foreach (KeyValuePair<SButton, InputStatus> prev in previousStatuses)
+ foreach (KeyValuePair<SButton, SButtonState> prev in previousStates)
{
if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key))
- activeButtons[prev.Key] = InputStatus.Released;
+ activeButtons[prev.Key] = SButtonState.Released;
}
return activeButtons;
}
- /// <summary>Get the status of a button relative to its previous status.</summary>
- /// <param name="oldStatus">The previous button status.</param>
+ /// <summary>Get the state of a button relative to its previous state.</summary>
+ /// <param name="oldState">The previous button state.</param>
/// <param name="isDown">Whether the button is currently down.</param>
- private InputStatus DeriveStatus(InputStatus oldStatus, bool isDown)
+ private SButtonState DeriveState(SButtonState oldState, bool isDown)
{
- if (isDown && oldStatus.IsDown())
- return InputStatus.Held;
+ if (isDown && oldState.IsDown())
+ return SButtonState.Held;
if (isDown)
- return InputStatus.Pressed;
- return InputStatus.Released;
+ return SButtonState.Pressed;
+ return SButtonState.Released;
}
- /// <summary>Get the status of a button.</summary>
+ /// <summary>Get the state of a button.</summary>
/// <param name="activeButtons">The current button states to check.</param>
/// <param name="button">The button to check.</param>
- private InputStatus GetStatus(IDictionary<SButton, InputStatus> activeButtons, SButton button)
+ private SButtonState GetState(IDictionary<SButton, SButtonState> activeButtons, SButton button)
{
- return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None;
+ return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.None;
}
/// <summary>Get the buttons pressed in the given stats.</summary>
diff --git a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
index 043ae376..e9b70845 100644
--- a/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/ContentHelper.cs
@@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.ModHelpers
/// <summary>The friendly mod name for use in errors.</summary>
private readonly string ModName;
- /// <summary>Encapsulates monitoring and logging for a given module.</summary>
+ /// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
@@ -70,9 +70,11 @@ namespace StardewModdingAPI.Framework.ModHelpers
public ContentHelper(ContentCoordinator contentCore, string modFolderPath, string modID, string modName, IMonitor monitor)
: base(modID)
{
+ string managedAssetPrefix = contentCore.GetManagedAssetPrefix(modID);
+
this.ContentCore = contentCore;
- this.GameContentManager = contentCore.CreateGameContentManager(this.ContentCore.GetManagedAssetPrefix(modID) + ".content");
- this.ModContentManager = contentCore.CreateModContentManager(this.ContentCore.GetManagedAssetPrefix(modID), modFolderPath, this.GameContentManager);
+ this.GameContentManager = contentCore.CreateGameContentManager(managedAssetPrefix + ".content");
+ this.ModContentManager = contentCore.CreateModContentManager(managedAssetPrefix, modName, modFolderPath, this.GameContentManager);
this.ModName = modName;
this.Monitor = monitor;
}
diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs
index f4cd12b6..f8ff0355 100644
--- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs
+++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs
@@ -50,5 +50,12 @@ namespace StardewModdingAPI.Framework.ModHelpers
{
this.InputState.SuppressButtons.Add(button);
}
+
+ /// <summary>Get the state of a button.</summary>
+ /// <param name="button">The button to check.</param>
+ public SButtonState GetState(SButton button)
+ {
+ return this.InputState.GetState(button);
+ }
}
}
diff --git a/src/SMAPI/Framework/Networking/ModMessageModel.cs b/src/SMAPI/Framework/Networking/ModMessageModel.cs
index 7ee39863..4f694f9c 100644
--- a/src/SMAPI/Framework/Networking/ModMessageModel.cs
+++ b/src/SMAPI/Framework/Networking/ModMessageModel.cs
@@ -21,7 +21,7 @@ namespace StardewModdingAPI.Framework.Networking
/****
** Destination
****/
- /// <summary>The players who should receive the message, or <c>null</c> for all players.</summary>
+ /// <summary>The players who should receive the message.</summary>
public long[] ToPlayerIDs { get; set; }
/// <summary>The mods which should receive the message, or <c>null</c> for all mods.</summary>
diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs
index 4b346059..6b9c1365 100644
--- a/src/SMAPI/Framework/SGame.cs
+++ b/src/SMAPI/Framework/SGame.cs
@@ -635,16 +635,16 @@ namespace StardewModdingAPI.Framework
foreach (var pair in inputState.ActiveButtons)
{
SButton button = pair.Key;
- InputStatus status = pair.Value;
+ SButtonState status = pair.Value;
- if (status == InputStatus.Pressed)
+ if (status == SButtonState.Pressed)
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Events: button {button} pressed.", LogLevel.Trace);
events.ButtonPressed.Raise(new ButtonPressedEventArgs(button, cursor, inputState));
}
- else if (status == InputStatus.Released)
+ else if (status == SButtonState.Released)
{
if (this.Monitor.IsVerbose)
this.Monitor.Log($"Events: button {button} released.", LogLevel.Trace);
@@ -893,6 +893,7 @@ namespace StardewModdingAPI.Framework
{
var events = this.Events;
+ Game1.showingHealthBar = false;
if (Game1._newDayTask != null)
{
this.GraphicsDevice.Clear(Game1.bgColor);
@@ -934,7 +935,7 @@ namespace StardewModdingAPI.Framework
else
{
this.GraphicsDevice.Clear(Game1.bgColor);
- if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet())
+ if (Game1.activeClickableMenu != null && Game1.options.showMenuBackground && (Game1.activeClickableMenu.showWithoutTransparencyIfOptionIsSet() && !this.takingMapScreenshot))
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@@ -1081,6 +1082,7 @@ namespace StardewModdingAPI.Framework
{
byte batchOpens = 0; // used for rendering event
+ Microsoft.Xna.Framework.Rectangle rectangle;
Viewport viewport;
if (Game1.gameMode == (byte)0)
{
@@ -1097,21 +1099,20 @@ namespace StardewModdingAPI.Framework
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
if (++batchOpens == 1)
events.Rendering.RaiseEmpty();
- Microsoft.Xna.Framework.Color color = !Game1.currentLocation.Name.StartsWith("UndergroundMine") || !(Game1.currentLocation is MineShaft) ? (Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) ? Game1.outdoorLight : Game1.ambientLight) : (Game1.currentLocation as MineShaft).getLightingColor(gameTime);
+ Microsoft.Xna.Framework.Color color = !Game1.currentLocation.Name.StartsWith("UndergroundMine") || !(Game1.currentLocation is MineShaft) ? (Game1.ambientLight.Equals(Microsoft.Xna.Framework.Color.White) || Game1.isRaining && (bool)(NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors ? Game1.outdoorLight : Game1.ambientLight) : (Game1.currentLocation as MineShaft).getLightingColor(gameTime);
Game1.spriteBatch.Draw(Game1.staminaRect, Game1.lightmap.Bounds, color);
- for (int index = 0; index < Game1.currentLightSources.Count; ++index)
+ foreach (LightSource currentLightSource in Game1.currentLightSources)
{
- LightSource lightSource = Game1.currentLightSources.ElementAt<LightSource>(index);
- if (!Game1.isRaining && !Game1.isDarkOut() || lightSource.lightContext.Value != LightSource.LightContext.WindowLight)
+ if (!Game1.isRaining && !Game1.isDarkOut() || currentLightSource.lightContext.Value != LightSource.LightContext.WindowLight)
{
- if (lightSource.PlayerID != 0L && lightSource.PlayerID != Game1.player.UniqueMultiplayerID)
+ if (currentLightSource.PlayerID != 0L && currentLightSource.PlayerID != Game1.player.UniqueMultiplayerID)
{
- Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(lightSource.PlayerID);
- if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)((NetFieldBase<bool, NetBool>)farmerMaybeOffline.hidden))
+ Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(currentLightSource.PlayerID);
+ if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)(NetFieldBase<bool, NetBool>)farmerMaybeOffline.hidden)
continue;
}
- if (Utility.isOnScreen((Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position), (int)((double)(float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) * 64.0 * 4.0)))
- Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase<Vector2, NetVector2>)Game1.currentLightSources.ElementAt<LightSource>(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase<Microsoft.Xna.Framework.Color, NetColor>)Game1.currentLightSources.ElementAt<LightSource>(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt<LightSource>(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase<float, NetFloat>)Game1.currentLightSources.ElementAt<LightSource>(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f);
+ if (Utility.isOnScreen((Vector2)(NetFieldBase<Vector2, NetVector2>)currentLightSource.position, (int)((double)(float)(NetFieldBase<float, NetFloat>)currentLightSource.radius * 64.0 * 4.0)))
+ Game1.spriteBatch.Draw(currentLightSource.lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)(NetFieldBase<Vector2, NetVector2>)currentLightSource.position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(currentLightSource.lightTexture.Bounds), (Microsoft.Xna.Framework.Color)(NetFieldBase<Microsoft.Xna.Framework.Color, NetColor>)currentLightSource.color, 0.0f, new Vector2((float)currentLightSource.lightTexture.Bounds.Center.X, (float)currentLightSource.lightTexture.Bounds.Center.Y), (float)(NetFieldBase<float, NetFloat>)currentLightSource.radius / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f);
}
}
Game1.spriteBatch.End();
@@ -1134,7 +1135,7 @@ namespace StardewModdingAPI.Framework
{
foreach (Farmer farmerActor in Game1.currentLocation.currentEvent.farmerActors)
{
- if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase<bool, NetBool>)farmerActor.hidden))
+ if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase<bool, NetBool>)farmerActor.hidden)
this._farmerShadows.Add(farmerActor);
}
}
@@ -1142,7 +1143,7 @@ namespace StardewModdingAPI.Framework
{
foreach (Farmer farmer in Game1.currentLocation.farmers)
{
- if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)((NetFieldBase<bool, NetBool>)farmer.hidden))
+ if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase<bool, NetBool>)farmer.hidden)
this._farmerShadows.Add(farmer);
}
}
@@ -1152,26 +1153,39 @@ namespace StardewModdingAPI.Framework
{
foreach (NPC character in Game1.currentLocation.characters)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)character.swimming && !character.HideShadow && (!character.IsInvisible && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
}
}
else
{
foreach (NPC actor in Game1.CurrentEvent.actors)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)actor.swimming && !actor.HideShadow && !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : (actor.Sprite.SpriteHeight <= 16 ? -4 : 12))))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
}
}
foreach (Farmer farmerShadow in this._farmerShadows)
{
- if (!Game1.multiplayer.isDisconnecting(farmerShadow.UniqueMultiplayerID) && !(bool)((NetFieldBase<bool, NetBool>)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5)), SpriteEffects.None, 0.0f);
+ if (!Game1.multiplayer.isDisconnecting(farmerShadow.UniqueMultiplayerID) && !(bool)(NetFieldBase<bool, NetBool>)farmerShadow.swimming && !farmerShadow.isRidingHorse() && (Game1.currentLocation == null || !Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
+ {
+ SpriteBatch spriteBatch = Game1.spriteBatch;
+ Texture2D shadowTexture = Game1.shadowTexture;
+ Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f));
+ Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
+ Microsoft.Xna.Framework.Color white = Microsoft.Xna.Framework.Color.White;
+ Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds;
+ double x = (double)bounds.Center.X;
+ bounds = Game1.shadowTexture.Bounds;
+ double y = (double)bounds.Center.Y;
+ Vector2 origin = new Vector2((float)x, (float)y);
+ double num = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5);
+ spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num, SpriteEffects.None, 0.0f);
+ }
}
}
- Layer layer = Game1.currentLocation.Map.GetLayer("Buildings");
- layer.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
+ Layer layer1 = Game1.currentLocation.Map.GetLayer("Buildings");
+ layer1.Draw(Game1.mapDisplayDevice, Game1.viewport, Location.Origin, false, 4);
Game1.mapDisplayDevice.EndScene();
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.FrontToBack, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@@ -1181,23 +1195,37 @@ namespace StardewModdingAPI.Framework
{
foreach (NPC character in Game1.currentLocation.characters)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)character.swimming) && !character.HideShadow && (!(bool)((NetFieldBase<bool, NetBool>)character.isInvisible) && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)character.swimming && !character.HideShadow && (!(bool)(NetFieldBase<bool, NetBool>)character.isInvisible && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(character.getTileLocation())))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, character.Position + new Vector2((float)(character.Sprite.SpriteWidth * 4) / 2f, (float)(character.GetBoundingBox().Height + (character.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)character.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)character.scale, SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f);
}
}
else
{
foreach (NPC actor in Game1.CurrentEvent.actors)
{
- if (!(bool)((NetFieldBase<bool, NetBool>)actor.swimming) && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)((NetFieldBase<float, NetFloat>)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
+ if (!(bool)(NetFieldBase<bool, NetBool>)actor.swimming && !actor.HideShadow && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(actor.getTileLocation()))
+ Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(Game1.viewport, actor.Position + new Vector2((float)(actor.Sprite.SpriteWidth * 4) / 2f, (float)(actor.GetBoundingBox().Height + (actor.IsMonster ? 0 : 12)))), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 + (double)actor.yJumpOffset / 40.0) * (float)(NetFieldBase<float, NetFloat>)actor.scale, SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f);
}
}
foreach (Farmer farmerShadow in this._farmerShadows)
{
- float layerDepth = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f;
- if (!(bool)((NetFieldBase<bool, NetBool>)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
- Game1.spriteBatch.Draw(Game1.shadowTexture, Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f)), new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, new Vector2((float)Game1.shadowTexture.Bounds.Center.X, (float)Game1.shadowTexture.Bounds.Center.Y), (float)(4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5)), SpriteEffects.None, layerDepth);
+ float num1 = Math.Max(0.0001f, farmerShadow.getDrawLayer() + 0.00011f) - 0.0001f;
+ if (!(bool)(NetFieldBase<bool, NetBool>)farmerShadow.swimming && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShadowBeDrawnAboveBuildingsLayer(farmerShadow.getTileLocation())))
+ {
+ SpriteBatch spriteBatch = Game1.spriteBatch;
+ Texture2D shadowTexture = Game1.shadowTexture;
+ Vector2 local = Game1.GlobalToLocal(farmerShadow.Position + new Vector2(32f, 24f));
+ Microsoft.Xna.Framework.Rectangle? sourceRectangle = new Microsoft.Xna.Framework.Rectangle?(Game1.shadowTexture.Bounds);
+ Microsoft.Xna.Framework.Color white = Microsoft.Xna.Framework.Color.White;
+ Microsoft.Xna.Framework.Rectangle bounds = Game1.shadowTexture.Bounds;
+ double x = (double)bounds.Center.X;
+ bounds = Game1.shadowTexture.Bounds;
+ double y = (double)bounds.Center.Y;
+ Vector2 origin = new Vector2((float)x, (float)y);
+ double num2 = 4.0 - (!farmerShadow.running && !farmerShadow.UsingTool || farmerShadow.FarmerSprite.currentAnimationIndex <= 1 ? 0.0 : (double)Math.Abs(FarmerRenderer.featureYOffsetPerFrame[farmerShadow.FarmerSprite.CurrentFrame]) * 0.5);
+ double num3 = (double)num1;
+ spriteBatch.Draw(shadowTexture, local, sourceRectangle, white, 0.0f, origin, (float)num2, SpriteEffects.None, (float)num3);
+ }
}
}
if ((Game1.eventUp || Game1.killScreen) && (!Game1.killScreen && Game1.currentLocation.currentEvent != null))
@@ -1207,7 +1235,7 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.draw(Game1.spriteBatch);
foreach (Vector2 key in Game1.crabPotOverlayTiles.Keys)
{
- Tile tile = layer.Tiles[(int)key.X, (int)key.Y];
+ Tile tile = layer1.Tiles[(int)key.X, (int)key.Y];
if (tile != null)
{
Vector2 local = Game1.GlobalToLocal(Game1.viewport, key * 64f);
@@ -1237,10 +1265,31 @@ namespace StardewModdingAPI.Framework
Game1.currentLocation.drawAboveFrontLayer(Game1.spriteBatch);
Game1.spriteBatch.End();
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
- if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.player.ActiveObject.bigCraftable) && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)
+ if (Game1.displayFarmer && Game1.player.ActiveObject != null && ((bool)(NetFieldBase<bool, NetBool>)Game1.player.ActiveObject.bigCraftable && this.checkBigCraftableBoundariesForFrontLayer()) && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)
Game1.drawPlayerHeldObject(Game1.player);
- else if (Game1.displayFarmer && Game1.player.ActiveObject != null && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways") || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && !Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.GetBoundingBox().Right, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways")))
+ else if (Game1.displayFarmer && Game1.player.ActiveObject != null)
+ {
+ if (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size) == null || Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location((int)Game1.player.Position.X, (int)Game1.player.Position.Y - 38), Game1.viewport.Size).TileIndexProperties.ContainsKey("FrontAlways"))
+ {
+ Layer layer2 = Game1.currentLocation.Map.GetLayer("Front");
+ rectangle = Game1.player.GetBoundingBox();
+ Location mapDisplayLocation1 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38);
+ xTile.Dimensions.Size size1 = Game1.viewport.Size;
+ if (layer2.PickTile(mapDisplayLocation1, size1) != null)
+ {
+ Layer layer3 = Game1.currentLocation.Map.GetLayer("Front");
+ rectangle = Game1.player.GetBoundingBox();
+ Location mapDisplayLocation2 = new Location(rectangle.Right, (int)Game1.player.Position.Y - 38);
+ xTile.Dimensions.Size size2 = Game1.viewport.Size;
+ if (layer3.PickTile(mapDisplayLocation2, size2).TileIndexProperties.ContainsKey("FrontAlways"))
+ goto label_139;
+ }
+ else
+ goto label_139;
+ }
Game1.drawPlayerHeldObject(Game1.player);
+ }
+ label_139:
if ((Game1.player.UsingTool || Game1.pickingTool) && Game1.player.CurrentTool != null && ((!Game1.player.CurrentTool.Name.Equals("Seeds") || Game1.pickingTool) && (Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), (int)Game1.player.Position.Y - 38), Game1.viewport.Size) != null && Game1.currentLocation.Map.GetLayer("Front").PickTile(new Location(Game1.player.getStandingX(), Game1.player.getStandingY()), Game1.viewport.Size) == null)))
Game1.drawTool(Game1.player);
if (Game1.currentLocation.Map.GetLayer("AlwaysFront") != null)
@@ -1274,23 +1323,9 @@ namespace StardewModdingAPI.Framework
if (Game1.farmEvent != null)
Game1.farmEvent.draw(Game1.spriteBatch);
if ((double)Game1.currentLocation.LightLevel > 0.0 && Game1.timeOfDay < 2000)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel;
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.Black * Game1.currentLocation.LightLevel);
if (Game1.screenGlow)
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Microsoft.Xna.Framework.Color color = Game1.screenGlowColor * Game1.screenGlowAlpha;
- spriteBatch.Draw(fadeToBlackRect, bounds, color);
- }
+ Game1.spriteBatch.Draw(Game1.fadeToBlackRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Game1.screenGlowColor * Game1.screenGlowAlpha);
Game1.currentLocation.drawAboveAlwaysFrontLayer(Game1.spriteBatch);
if (Game1.player.CurrentTool != null && Game1.player.CurrentTool is FishingRod && ((Game1.player.CurrentTool as FishingRod).isTimingCast || (double)(Game1.player.CurrentTool as FishingRod).castingChosenCountdown > 0.0 || ((Game1.player.CurrentTool as FishingRod).fishCaught || (Game1.player.CurrentTool as FishingRod).showingTreasure)))
Game1.player.CurrentTool.draw(Game1.spriteBatch);
@@ -1317,15 +1352,8 @@ namespace StardewModdingAPI.Framework
{
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, this.lightingBlend, SamplerState.LinearClamp, (DepthStencilState)null, (RasterizerState)null);
Game1.spriteBatch.Draw((Texture2D)Game1.lightmap, Vector2.Zero, new Microsoft.Xna.Framework.Rectangle?(Game1.lightmap.Bounds), Microsoft.Xna.Framework.Color.White, 0.0f, Vector2.Zero, (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 1f);
- if (Game1.isRaining && (bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert))
- {
- SpriteBatch spriteBatch = Game1.spriteBatch;
- Texture2D staminaRect = Game1.staminaRect;
- viewport = Game1.graphics.GraphicsDevice.Viewport;
- Microsoft.Xna.Framework.Rectangle bounds = viewport.Bounds;
- Microsoft.Xna.Framework.Color color = Microsoft.Xna.Framework.Color.OrangeRed * 0.45f;
- spriteBatch.Draw(staminaRect, bounds, color);
- }
+ if (Game1.isRaining && (bool)(NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert))
+ Game1.spriteBatch.Draw(Game1.staminaRect, Game1.graphics.GraphicsDevice.Viewport.Bounds, Microsoft.Xna.Framework.Color.OrangeRed * 0.45f);
Game1.spriteBatch.End();
}
Game1.spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend, SamplerState.PointClamp, (DepthStencilState)null, (RasterizerState)null);
@@ -1424,12 +1452,28 @@ namespace StardewModdingAPI.Framework
this.drawDialogueBox();
if (Game1.progressBar && !this.takingMapScreenshot)
{
- Game1.spriteBatch.Draw(Game1.fadeToBlackRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, Game1.dialogueWidth, 32), Microsoft.Xna.Framework.Color.LightGray);
- Game1.spriteBatch.Draw(Game1.staminaRect, new Microsoft.Xna.Framework.Rectangle((Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2, Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Bottom - 128, (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth), 32), Microsoft.Xna.Framework.Color.DimGray);
+ SpriteBatch spriteBatch1 = Game1.spriteBatch;
+ Texture2D fadeToBlackRect = Game1.fadeToBlackRect;
+ int x1 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2;
+ rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea();
+ int y1 = rectangle.Bottom - 128;
+ int dialogueWidth = Game1.dialogueWidth;
+ Microsoft.Xna.Framework.Rectangle destinationRectangle1 = new Microsoft.Xna.Framework.Rectangle(x1, y1, dialogueWidth, 32);
+ Microsoft.Xna.Framework.Color lightGray = Microsoft.Xna.Framework.Color.LightGray;
+ spriteBatch1.Draw(fadeToBlackRect, destinationRectangle1, lightGray);
+ SpriteBatch spriteBatch2 = Game1.spriteBatch;
+ Texture2D staminaRect = Game1.staminaRect;
+ int x2 = (Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea().Width - Game1.dialogueWidth) / 2;
+ rectangle = Game1.graphics.GraphicsDevice.Viewport.GetTitleSafeArea();
+ int y2 = rectangle.Bottom - 128;
+ int width = (int)((double)Game1.pauseAccumulator / (double)Game1.pauseTime * (double)Game1.dialogueWidth);
+ Microsoft.Xna.Framework.Rectangle destinationRectangle2 = new Microsoft.Xna.Framework.Rectangle(x2, y2, width, 32);
+ Microsoft.Xna.Framework.Color dimGray = Microsoft.Xna.Framework.Color.DimGray;
+ spriteBatch2.Draw(staminaRect, destinationRectangle2, dimGray);
}
if (Game1.eventUp && Game1.currentLocation != null && Game1.currentLocation.currentEvent != null)
Game1.currentLocation.currentEvent.drawAfterMap(Game1.spriteBatch);
- if (Game1.isRaining && Game1.currentLocation != null && ((bool)((NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors) && !(Game1.currentLocation is Desert)))
+ if (Game1.isRaining && Game1.currentLocation != null && ((bool)(NetFieldBase<bool, NetBool>)Game1.currentLocation.isOutdoors && !(Game1.currentLocation is Desert)))
{
SpriteBatch spriteBatch = Game1.spriteBatch;
Texture2D staminaRect = Game1.staminaRect;
diff --git a/src/SMAPI/Framework/SMultiplayer.cs b/src/SMAPI/Framework/SMultiplayer.cs
index e04205c8..821c343f 100644
--- a/src/SMAPI/Framework/SMultiplayer.cs
+++ b/src/SMAPI/Framework/SMultiplayer.cs
@@ -338,64 +338,94 @@ namespace StardewModdingAPI.Framework
/// <param name="toPlayerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs)
{
- // validate
+ // validate input
if (message == null)
throw new ArgumentNullException(nameof(message));
if (string.IsNullOrWhiteSpace(messageType))
throw new ArgumentNullException(nameof(messageType));
if (string.IsNullOrWhiteSpace(fromModID))
throw new ArgumentNullException(nameof(fromModID));
- if (!this.Peers.Any())
+
+ // get target players
+ long curPlayerId = Game1.player.UniqueMultiplayerID;
+ bool sendToSelf = false;
+ List<MultiplayerPeer> sendToPeers = new List<MultiplayerPeer>();
+ if (toPlayerIDs == null)
{
- this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players.");
- return;
+ sendToSelf = true;
+ sendToPeers.AddRange(this.Peers.Values);
}
-
- // filter player IDs
- HashSet<long> playerIDs = null;
- if (toPlayerIDs != null && toPlayerIDs.Any())
+ else
{
- playerIDs = new HashSet<long>(toPlayerIDs);
- playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id));
- if (!playerIDs.Any())
+ foreach (long id in toPlayerIDs.Distinct())
{
- this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected.");
- return;
+ if (id == curPlayerId)
+ sendToSelf = true;
+ else if (this.Peers.TryGetValue(id, out MultiplayerPeer peer) && peer.HasSmapi)
+ sendToPeers.Add(peer);
}
}
+ // filter by mod ID
+ if (toModIDs != null)
+ {
+ HashSet<string> sendToMods = new HashSet<string>(toModIDs, StringComparer.InvariantCultureIgnoreCase);
+ if (sendToSelf && toModIDs.All(id => this.ModRegistry.Get(id) == null))
+ sendToSelf = false;
+
+ sendToPeers.RemoveAll(peer => peer.Mods.All(mod => !sendToMods.Contains(mod.ID)));
+ }
+
+ // validate recipients
+ if (!sendToSelf && !sendToPeers.Any())
+ {
+ this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs can receive this message.");
+ return;
+ }
+
// get data to send
ModMessageModel model = new ModMessageModel(
fromPlayerID: Game1.player.UniqueMultiplayerID,
fromModID: fromModID,
toModIDs: toModIDs,
- toPlayerIDs: playerIDs?.ToArray(),
+ toPlayerIDs: sendToPeers.Select(p => p.PlayerID).ToArray(),
type: messageType,
data: JToken.FromObject(message)
);
string data = JsonConvert.SerializeObject(model, Formatting.None);
- // log message
- if (this.LogNetworkTraffic)
- this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace);
+ // send self-message
+ if (sendToSelf)
+ {
+ if (this.LogNetworkTraffic)
+ this.Monitor.Log($"Broadcasting '{messageType}' message to self: {data}.", LogLevel.Trace);
+
+ this.OnModMessageReceived(model);
+ }
- // send message
- if (Context.IsMainPlayer)
+ // send message to peers
+ if (sendToPeers.Any())
{
- foreach (MultiplayerPeer peer in this.Peers.Values)
+ if (Context.IsMainPlayer)
{
- if (playerIDs == null || playerIDs.Contains(peer.PlayerID))
+ foreach (MultiplayerPeer peer in sendToPeers)
{
- model.ToPlayerIDs = new[] { peer.PlayerID };
+ if (this.LogNetworkTraffic)
+ this.Monitor.Log($"Broadcasting '{messageType}' message to farmhand {peer.PlayerID}: {data}.", LogLevel.Trace);
+
peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data));
}
}
- }
- else if (this.HostPeer != null && this.HostPeer.HasSmapi)
- this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
- else
- this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
+ else if (this.HostPeer?.HasSmapi == true)
+ {
+ if (this.LogNetworkTraffic)
+ this.Monitor.Log($"Broadcasting '{messageType}' message to host {this.HostPeer.PlayerID}: {data}.", LogLevel.Trace);
+ this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
+ }
+ else
+ this.Monitor.VerboseLog(" Can't send message because no valid connections were found.");
+ }
}