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') 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 f3acc0b07c1d126b03fbf1ea48f45be1308dce97 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 8 Feb 2020 23:34:43 -0500 Subject: add Italian translations --- src/SMAPI/i18n/it.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/SMAPI/i18n/it.json (limited to 'src/SMAPI') diff --git a/src/SMAPI/i18n/it.json b/src/SMAPI/i18n/it.json new file mode 100644 index 00000000..43493018 --- /dev/null +++ b/src/SMAPI/i18n/it.json @@ -0,0 +1,3 @@ +{ + "warn.invalid-content-removed": "Contenuto non valido rimosso per prevenire un crash (Guarda la console di SMAPI per maggiori informazioni)." +} -- 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 +++++++ src/SMAPI/IInputHelper.cs | 4 ++++ src/SMAPI/InputStatus.cs | 29 +++++++++++++++++++++++++++ 5 files changed, 47 insertions(+), 29 deletions(-) delete mode 100644 src/SMAPI/Framework/Input/InputStatus.cs create mode 100644 src/SMAPI/InputStatus.cs (limited to 'src/SMAPI') 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); + } } } diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs index 328f504b..ba26773d 100644 --- a/src/SMAPI/IInputHelper.cs +++ b/src/SMAPI/IInputHelper.cs @@ -17,5 +17,9 @@ namespace StardewModdingAPI /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. /// The button to suppress. void Suppress(SButton button); + + /// Get the status of a button. + /// The button to check. + InputStatus GetStatus(SButton button); } } diff --git a/src/SMAPI/InputStatus.cs b/src/SMAPI/InputStatus.cs new file mode 100644 index 00000000..d9cdd6b2 --- /dev/null +++ b/src/SMAPI/InputStatus.cs @@ -0,0 +1,29 @@ +namespace StardewModdingAPI +{ + /// The input status for a button during an update frame. + public 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; + } + } +} -- cgit From c649572db8f2f57f5f39ed6d842529b188601206 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 10 Feb 2020 19:37:59 -0500 Subject: fix dialogue propagation clearing marriage dialogue --- src/SMAPI/Metadata/CoreAssetPropagator.cs | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 7a58d52c..8d5ad3ab 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -886,10 +886,17 @@ namespace StardewModdingAPI.Metadata return false; // update dialogue + // Note that marriage dialogue isn't reloaded after reset, but it doesn't need to be + // propagated anyway since marriage dialogue keys can't be added/removed and the field + // doesn't store the text itself. foreach (NPC villager in villagers) { + MarriageDialogueReference[] marriageDialogue = villager.currentMarriageDialogue.ToArray(); + villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue villager.resetCurrentDialogue(); + + villager.currentMarriageDialogue.Set(marriageDialogue); } return true; -- 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') 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 ++-- src/SMAPI/IInputHelper.cs | 4 +-- src/SMAPI/InputStatus.cs | 29 --------------- src/SMAPI/SButtonState.cs | 29 +++++++++++++++ 6 files changed, 63 insertions(+), 63 deletions(-) delete mode 100644 src/SMAPI/InputStatus.cs create mode 100644 src/SMAPI/SButtonState.cs (limited to 'src/SMAPI') 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); diff --git a/src/SMAPI/IInputHelper.cs b/src/SMAPI/IInputHelper.cs index ba26773d..e9768c24 100644 --- a/src/SMAPI/IInputHelper.cs +++ b/src/SMAPI/IInputHelper.cs @@ -18,8 +18,8 @@ namespace StardewModdingAPI /// The button to suppress. void Suppress(SButton button); - /// Get the status of a button. + /// Get the state of a button. /// The button to check. - InputStatus GetStatus(SButton button); + SButtonState GetState(SButton button); } } diff --git a/src/SMAPI/InputStatus.cs b/src/SMAPI/InputStatus.cs deleted file mode 100644 index d9cdd6b2..00000000 --- a/src/SMAPI/InputStatus.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace StardewModdingAPI -{ - /// The input status for a button during an update frame. - public 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/SButtonState.cs b/src/SMAPI/SButtonState.cs new file mode 100644 index 00000000..2b78da27 --- /dev/null +++ b/src/SMAPI/SButtonState.cs @@ -0,0 +1,29 @@ +namespace StardewModdingAPI +{ + /// The input state for a button during an update frame. + public enum SButtonState + { + /// 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 state. + public static bool IsDown(this SButtonState state) + { + return state == SButtonState.Held || state == SButtonState.Pressed; + } + } +} -- 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') 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') 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 +++++++++++++++++ src/SMAPI/IAssetDataForImage.cs | 6 ++++++ 2 files changed, 23 insertions(+) (limited to 'src/SMAPI') 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; + } } } diff --git a/src/SMAPI/IAssetDataForImage.cs b/src/SMAPI/IAssetDataForImage.cs index 1109194f..27ed9267 100644 --- a/src/SMAPI/IAssetDataForImage.cs +++ b/src/SMAPI/IAssetDataForImage.cs @@ -19,5 +19,11 @@ namespace StardewModdingAPI /// The is outside the bounds of the spritesheet. /// The content being read isn't an image. void PatchImage(Texture2D source, Rectangle? sourceArea = null, Rectangle? targetArea = null, PatchMode patchMode = PatchMode.Replace); + + /// 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. + bool ExtendImage(int minWidth, int minHeight); } } -- cgit From 03c3ab3711c18f6b2bedfd02932b47bf368e1b82 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 22 Feb 2020 10:39:54 -0500 Subject: update SMAPI/game version mapping, add older versions --- src/SMAPI/Constants.cs | 53 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 10 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 201d3166..443ca7a6 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -115,26 +115,59 @@ namespace StardewModdingAPI /// Returns the compatible SMAPI version, or null if none was found. internal static ISemanticVersion GetCompatibleApiVersion(ISemanticVersion version) { + // This covers all officially supported public game updates. It might seem like version + // ranges would be better, but the given SMAPI versions may not be compatible with + // intermediate unlisted versions (e.g. private beta updates). + // + // Nonstandard versions are normalized by GameVersion (e.g. 1.07 => 1.0.7). switch (version.ToString()) { + case "1.4.1": + case "1.4.0": + return new SemanticVersion("3.0.1"); + case "1.3.36": - return new SemanticVersion(2, 11, 2); + return new SemanticVersion("2.11.2"); - case "1.3.32": case "1.3.33": - return new SemanticVersion(2, 10, 2); + case "1.3.32": + return new SemanticVersion("2.10.2"); case "1.3.28": - return new SemanticVersion(2, 7, 0); + return new SemanticVersion("2.7.0"); - case "1.2.30": - case "1.2.31": - case "1.2.32": case "1.2.33": - return new SemanticVersion(2, 5, 5); - } + case "1.2.32": + case "1.2.31": + case "1.2.30": + return new SemanticVersion("2.5.5"); + + case "1.2.29": + case "1.2.28": + case "1.2.27": + case "1.2.26": + return new SemanticVersion("1.13.1"); + + case "1.1.1": + case "1.1.0": + return new SemanticVersion("1.9.0"); + + case "1.0.7.1": + case "1.0.7": + case "1.0.6": + case "1.0.5.2": + case "1.0.5.1": + case "1.0.5": + case "1.0.4": + case "1.0.3": + case "1.0.2": + case "1.0.1": + case "1.0.0": + return new SemanticVersion("0.40.0"); - return null; + default: + return null; + } } /// Get metadata for mapping assemblies to the current platform. -- cgit From d3ec98fec8077026d3a97a74d4efc471ab37ad67 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 22 Feb 2020 11:26:05 -0500 Subject: update packages --- src/SMAPI/SMAPI.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/SMAPI') diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index c5d0f247..c17f13c4 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -15,9 +15,9 @@ - + - + -- 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') 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.charact