From 0a2b15d3c36a0a5db4d8fb6e48b592507dc74c14 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 2 Feb 2020 14:20:41 -0500 Subject: add support for self-broadcasts, optimize network messages --- src/SMAPI/Framework/Networking/ModMessageModel.cs | 2 +- src/SMAPI/Framework/SMultiplayer.cs | 84 +++++++++++++++-------- 2 files changed, 58 insertions(+), 28 deletions(-) (limited to 'src/SMAPI/Framework') 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 ****/ - /// The players who should receive the message, or null for all players. + /// The players who should receive the message. public long[] ToPlayerIDs { get; set; } /// The mods which should receive the message, or null for all mods. 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 /// The values for the players who should receive the message, or null for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency. public void BroadcastModMessage(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 sendToPeers = new List(); + 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 playerIDs = null; - if (toPlayerIDs != null && toPlayerIDs.Any()) + else { - playerIDs = new HashSet(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 sendToMods = new HashSet(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."); + } } -- cgit From 136773678e1ce623bd23f170dca265d31030d200 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 9 Feb 2020 01:04:55 -0500 Subject: add helper.Input.GetStatus method --- src/SMAPI/Framework/Input/InputStatus.cs | 29 --------------------------- src/SMAPI/Framework/Input/SInputState.cs | 7 +++++++ src/SMAPI/Framework/ModHelpers/InputHelper.cs | 7 +++++++ 3 files changed, 14 insertions(+), 29 deletions(-) delete mode 100644 src/SMAPI/Framework/Input/InputStatus.cs (limited to 'src/SMAPI/Framework') 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 -{ - /// The input status for a button during an update frame. - internal enum InputStatus - { - /// The button was neither pressed, held, nor released. - None, - - /// The button was pressed in this frame. - Pressed, - - /// The button has been held since the last frame. - Held, - - /// The button was released in this frame. - Released - } - - /// Extension methods for . - internal static class InputStatusExtensions - { - /// Whether the button was pressed or held. - /// The button status. - 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..f54d3124 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -169,6 +169,13 @@ namespace StardewModdingAPI.Framework.Input return buttons.Any(button => this.IsDown(button.ToSButton())); } + /// Get the status of a button. + /// The button to check. + public InputStatus GetStatus(SButton button) + { + return this.GetStatus(this.ActiveButtons, button); + } + /********* ** Private methods diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index f4cd12b6..5858cddd 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); } + + /// Get the status of a button. + /// The button to check. + public InputStatus GetStatus(SButton button) + { + return this.InputState.GetStatus(button); + } } } -- cgit From 2e9807a034a84d7e1ce821e92671a655ce13b199 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Feb 2020 23:20:55 -0500 Subject: rework tilesheet loading to improve errors, allow future validation, and drop support for legacy content files --- src/SMAPI/Framework/ContentCoordinator.cs | 4 +- .../Framework/ContentManagers/ModContentManager.cs | 138 +++++++++++---------- src/SMAPI/Framework/ModHelpers/ContentHelper.cs | 8 +- 3 files changed, 80 insertions(+), 70 deletions(-) (limited to 'src/SMAPI/Framework') 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 /// Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files. /// A name for the mod manager. Not guaranteed to be unique. + /// The mod display name to show in errors. /// The root directory to search for content (or null for the default). /// The game content manager used for map tilesheets not provided by the mod. - 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/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 0a526fc8..6b463424 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -26,6 +26,9 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Encapsulates SMAPI's JSON file parsing. private readonly JsonHelper JsonHelper; + /// The mod display name to show in errors. + private readonly string ModName; + /// The game content manager used for map tilesheets not provided by the mod. private readonly IContentManager GameContentManager; @@ -40,6 +43,7 @@ namespace StardewModdingAPI.Framework.ContentManagers /// A name for the mod manager. Not guaranteed to be unique. /// The game content manager used for map tilesheets not provided by the mod. /// The service provider to use to locate services. + /// The mod display name to show in errors. /// The root directory to search for content. /// The current culture for which to localize content. /// The central coordinator which manages content managers. @@ -47,11 +51,12 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Simplifies access to private code. /// Encapsulates SMAPI's JSON file parsing. /// A callback to invoke when the content manager is being disposed. - public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing) + public ModContentManager(string name, IContentManager gameContentManager, IServiceProvider serviceProvider, string modName, string rootDirectory, CultureInfo currentCulture, ContentCoordinator coordinator, IMonitor monitor, Reflector reflection, JsonHelper jsonHelper, Action onDisposing) : base(name, serviceProvider, rootDirectory, currentCulture, coordinator, monitor, reflection, onDisposing, isNamespaced: true) { this.GameContentManager = gameContentManager; this.JsonHelper = jsonHelper; + this.ModName = modName; } /// Load an asset that has been processed by the content pipeline. @@ -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."); } } /// Get the actual asset name for a tilesheet. /// The folder path containing the map, relative to the mod folder. - /// The tilesheet image source to load. - /// Returns the asset name. + /// The tilesheet path to load. + /// Whether the game will apply seasonal logic to the tilesheet. + /// The found asset name. + /// A message indicating why the file couldn't be loaded. + /// Returns whether the asset name was found. /// See remarks on . - 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(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(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; } /// Get whether a file from the game's content folder exists. 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 /// The friendly mod name for use in errors. private readonly string ModName; - /// Encapsulates monitoring and logging for a given module. + /// Encapsulates monitoring and logging. 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; } -- cgit From ab90e2c890bf16674e0f5699729f114877a6f7a4 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Feb 2020 23:28:37 -0500 Subject: rename InputStatus to SButtonState for consistency --- src/SMAPI/Framework/Input/SInputState.cs | 52 +++++++++++++-------------- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 6 ++-- src/SMAPI/Framework/SGame.cs | 6 ++-- 3 files changed, 32 insertions(+), 32 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index f54d3124..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; /// The buttons which were pressed, held, or released. - public IDictionary ActiveButtons { get; private set; } = new Dictionary(); + public IDictionary ActiveButtons { get; private set; } = new Dictionary(); /// The buttons to suppress when the game next handles input. Each button is suppressed until it's released. public HashSet SuppressButtons { get; } = new HashSet(); @@ -75,7 +75,7 @@ namespace StardewModdingAPI.Framework.Input [Obsolete("This method should only be called by the game itself.")] public override void Update() { } - /// Update the current button statuses for the given tick. + /// Update the current button states for the given tick. 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 /// The button to check. public bool IsDown(SButton button) { - return this.GetStatus(this.ActiveButtons, button).IsDown(); + return this.GetState(this.ActiveButtons, button).IsDown(); } /// Get whether any of the given buttons were pressed or held. @@ -169,11 +169,11 @@ namespace StardewModdingAPI.Framework.Input return buttons.Any(button => this.IsDown(button.ToSButton())); } - /// Get the status of a button. + /// Get the state of a button. /// The button to check. - public InputStatus GetStatus(SButton button) + public SButtonState GetState(SButton button) { - return this.GetStatus(this.ActiveButtons, button); + return this.GetState(this.ActiveButtons, button); } @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework.Input /// The game's keyboard state for the current tick. /// The game's mouse state for the current tick. /// The game's controller state for the current tick. - private void SuppressGivenStates(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) + private void SuppressGivenStates(IDictionary activeButtons, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) { if (this.SuppressButtons.Count == 0) return; @@ -252,48 +252,48 @@ namespace StardewModdingAPI.Framework.Input } } - /// Get the status of all pressed or released buttons relative to their previous status. - /// The previous button statuses. + /// Get the state of all pressed or released buttons relative to their previous state. + /// The previous button states. /// The keyboard state. /// The mouse state. /// The controller state. - private IDictionary DeriveStatuses(IDictionary previousStatuses, KeyboardState keyboard, MouseState mouse, GamePadState controller) + private IDictionary DeriveStates(IDictionary previousStates, KeyboardState keyboard, MouseState mouse, GamePadState controller) { - IDictionary activeButtons = new Dictionary(); + IDictionary activeButtons = new Dictionary(); // 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 prev in previousStatuses) + foreach (KeyValuePair prev in previousStates) { if (prev.Value.IsDown() && !activeButtons.ContainsKey(prev.Key)) - activeButtons[prev.Key] = InputStatus.Released; + activeButtons[prev.Key] = SButtonState.Released; } return activeButtons; } - /// Get the status of a button relative to its previous status. - /// The previous button status. + /// Get the state of a button relative to its previous state. + /// The previous button state. /// Whether the button is currently down. - 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; } - /// Get the status of a button. + /// Get the state of a button. /// The current button states to check. /// The button to check. - private InputStatus GetStatus(IDictionary activeButtons, SButton button) + private SButtonState GetState(IDictionary activeButtons, SButton button) { - return activeButtons.TryGetValue(button, out InputStatus status) ? status : InputStatus.None; + return activeButtons.TryGetValue(button, out SButtonState state) ? state : SButtonState.None; } /// Get the buttons pressed in the given stats. diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index 5858cddd..f8ff0355 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -51,11 +51,11 @@ namespace StardewModdingAPI.Framework.ModHelpers this.InputState.SuppressButtons.Add(button); } - /// Get the status of a button. + /// Get the state of a button. /// The button to check. - public InputStatus GetStatus(SButton button) + public SButtonState GetState(SButton button) { - return this.InputState.GetStatus(button); + return this.InputState.GetState(button); } } } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 4b346059..6a472e3c 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); -- cgit From 2cc786907b2a6cd62691ba2d6514f45a7b46c03c Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 19 Feb 2020 23:42:44 -0500 Subject: call IAssetEditor with actual type if applicable --- .../Framework/ContentManagers/GameContentManager.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/SMAPI/Framework') 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) { -- cgit From eff29d94fbf41f0992ffac7bb5a04a08b71f3453 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Feb 2020 19:52:52 -0500 Subject: don't premultiply fully opaque pixels --- src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 6b463424..7d274eb7 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -253,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()); } -- cgit From 6a9bf10a81f4557d44668666117d99440d99d873 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 20 Feb 2020 21:12:00 -0500 Subject: migrate to new method in SMAPI 3.3 --- src/SMAPI/Framework/Content/AssetDataForImage.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) (limited to 'src/SMAPI/Framework') 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); } + + /// 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. + /// The minimum texture width. + /// The minimum texture height. + /// Whether the texture was resized. + 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; + } } } -- cgit From b8636fdf2fb341029e16f0b9b51c07735d2a71d8 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 22 Feb 2020 11:59:10 -0500 Subject: update draw logic for recent game updates --- src/SMAPI/Framework/SGame.cs | 160 +++++++++++++++++++++++++++---------------- 1 file changed, 102 insertions(+), 58 deletions(-) (limited to 'src/SMAPI/Framework') diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 6a472e3c..6b9c1365 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -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)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)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(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)farmerMaybeOffline.hidden)) + Farmer farmerMaybeOffline = Game1.getFarmerMaybeOffline(currentLightSource.PlayerID); + if (farmerMaybeOffline == null || farmerMaybeOffline.currentLocation != null && farmerMaybeOffline.currentLocation.Name != Game1.currentLocation.Name || (bool)(NetFieldBase)farmerMaybeOffline.hidden) continue; } - if (Utility.isOnScreen((Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position), (int)((double)(float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) * 64.0 * 4.0))) - Game1.spriteBatch.Draw(Game1.currentLightSources.ElementAt(index).lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)((NetFieldBase)Game1.currentLightSources.ElementAt(index).position)) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(Game1.currentLightSources.ElementAt(index).lightTexture.Bounds), (Microsoft.Xna.Framework.Color)((NetFieldBase)Game1.currentLightSources.ElementAt(index).color), 0.0f, new Vector2((float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.X, (float)Game1.currentLightSources.ElementAt(index).lightTexture.Bounds.Center.Y), (float)((NetFieldBase)Game1.currentLightSources.ElementAt(index).radius) / (float)(Game1.options.lightingQuality / 2), SpriteEffects.None, 0.9f); + if (Utility.isOnScreen((Vector2)(NetFieldBase)currentLightSource.position, (int)((double)(float)(NetFieldBase)currentLightSource.radius * 64.0 * 4.0))) + Game1.spriteBatch.Draw(currentLightSource.lightTexture, Game1.GlobalToLocal(Game1.viewport, (Vector2)(NetFieldBase)currentLightSource.position) / (float)(Game1.options.lightingQuality / 2), new Microsoft.Xna.Framework.Rectangle?(currentLightSource.lightTexture.Bounds), (Microsoft.Xna.Framework.Color)(NetFieldBase)currentLightSource.color, 0.0f, new Vector2((float)currentLightSource.lightTexture.Bounds.Center.X, (float)currentLightSource.lightTexture.Bounds.Center.Y), (float)(NetFieldBase)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)farmerActor.hidden)) + if (farmerActor.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase)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)farmer.hidden)) + if (farmer.IsLocalPlayer && Game1.displayFarmer || !(bool)(NetFieldBase)farmer.hidden) this._farmerShadows.Add(farmer); } } @@ -1152,26 +1153,39 @@ namespace StardewModdingAPI.Framework { foreach (NPC character in Game1.currentLocation.characters) { - if (!(bool)((NetFieldBase)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)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); + if (!(bool)(NetFieldBase)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)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)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)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); + if (!(bool)(NetFieldBase)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)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)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)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)character.swimming) && !character.HideShadow && (!(bool)((NetFieldBase)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)character.scale), SpriteEffects.None, Math.Max(0.0f, (float)character.getStandingY() / 10000f) - 1E-06f); + if (!(bool)(NetFieldBase)character.swimming && !character.HideShadow && (!(bool)(NetFieldBase)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)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)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)actor.scale), SpriteEffects.None, Math.Max(0.0f, (float)actor.getStandingY() / 10000f) - 1E-06f); + if (!(bool)(NetFieldBase)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)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)farmerShadow.swimming) && !farmerShadow.isRidingHorse() && (Game1.currentLocation != null && Game1.currentLocation.shouldShado