From a6b11035961a0e3cc31653a888554915f7d3c747 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Mon, 24 Feb 2020 20:29:03 -0500 Subject: add file pickers to web UI for mobile users --- docs/release-notes.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 50c6f639..42316629 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,10 @@ ← [README](README.md) # Release notes +## Upcoming release +* For the web UI: + * Added option to upload files using a file picker. + ## 3.3.2 Released 22 February 2020 for Stardew Valley 1.4.1 or later. @@ -27,6 +31,10 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later. * Fixed warning on MacOS when you have no saves yet. * Reduced log messages. +* For the web UI: + * Updated the JSON validator and Content Patcher schema for `.tmx` support. + * The mod compatibility page now has a sticky table header. + * For modders: * Added support for [message sending](https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Integrations#Message_sending) to mods on the current computer (in addition to remote computers). * Added `ExtendImage` method to content API when editing files to resize textures. @@ -37,10 +45,6 @@ Released 22 February 2020 for Stardew Valley 1.4.1 or later. * Updated dependencies (including Mono.Cecil 0.11.1 → 0.11.2). * Fixed dialogue propagation clearing marriage dialogue. -* For the web UI: - * Updated the JSON validator and Content Patcher schema for `.tmx` support. - * The mod compatibility page now has a sticky table header. - * For SMAPI/tool developers: * Improved support for four-part versions to support SMAPI on Android. * The SMAPI log now prefixes the OS name with `Android` on Android. -- cgit From c6947682b050f4ed8c070a3ca021c7f10ec50009 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 6 Mar 2020 21:41:20 -0500 Subject: update packages --- docs/release-notes.md | 3 +++ src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 6 +++--- src/SMAPI/SMAPI.csproj | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 42316629..4a1bb96f 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming release +* For modders: + * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). + * For the web UI: * Added option to upload files using a file picker. diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index a7de7166..4fd6c475 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 97bea0fb..05200c94 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -14,9 +14,9 @@ - + - + @@ -27,7 +27,7 @@ - + diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index c17f13c4..d728ca2b 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -19,7 +19,7 @@ - + -- cgit From db4254513ef9fca49dfe4da00448a057813cf842 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Mar 2020 12:56:48 -0500 Subject: add support for flipped and rotated map tiles --- docs/release-notes.md | 1 + src/SMAPI/Framework/Rendering/SDisplayDevice.cs | 89 +++++++++++++++ src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs | 121 +++++++++++++++++++++ src/SMAPI/Framework/SGame.cs | 21 +++- src/SMAPI/SMAPI.csproj | 2 +- 5 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 src/SMAPI/Framework/Rendering/SDisplayDevice.cs create mode 100644 src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4a1bb96f..692480c4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming release * For modders: + * Added support for flipped and rotated map tiles (in collaboration with Platonymous). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). * For the web UI: diff --git a/src/SMAPI/Framework/Rendering/SDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs new file mode 100644 index 00000000..382949bf --- /dev/null +++ b/src/SMAPI/Framework/Rendering/SDisplayDevice.cs @@ -0,0 +1,89 @@ +using System; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using StardewValley; +using xTile.Dimensions; +using xTile.Layers; +using xTile.ObjectModel; +using xTile.Tiles; + +namespace StardewModdingAPI.Framework.Rendering +{ + /// A map display device which overrides the draw logic to support tile rotation. + internal class SDisplayDevice : SXnaDisplayDevice + { + /********* + ** Fields + *********/ + /// The origin to use when rotating tiles. + private readonly Vector2 RotationOrigin; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content manager through which to load tiles. + /// The graphics device with which to render tiles. + public SDisplayDevice(ContentManager contentManager, GraphicsDevice graphicsDevice) + : base(contentManager, graphicsDevice) + { + this.RotationOrigin = new Vector2((Game1.tileSize * Game1.pixelZoom) / 2f); + } + + /// Draw a tile to the screen. + /// The tile to draw. + /// The tile position to draw. + /// The layer depth at which to draw. + public override void DrawTile(Tile tile, Location location, float layerDepth) + { + // identical to XnaDisplayDevice + if (tile == null) + return; + xTile.Dimensions.Rectangle tileImageBounds = tile.TileSheet.GetTileImageBounds(tile.TileIndex); + Texture2D tileSheetTexture = this.m_tileSheetTextures[tile.TileSheet]; + if (tileSheetTexture.IsDisposed) + return; + this.m_tilePosition.X = location.X; + this.m_tilePosition.Y = location.Y; + this.m_sourceRectangle.X = tileImageBounds.X; + this.m_sourceRectangle.Y = tileImageBounds.Y; + this.m_sourceRectangle.Width = tileImageBounds.Width; + this.m_sourceRectangle.Height = tileImageBounds.Height; + + // get rotation and effects + float rotation = this.GetRotation(tile); + SpriteEffects effects = this.GetSpriteEffects(tile); + var origin = new Vector2(tileImageBounds.Width / 2f, tileImageBounds.Height / 2f); + this.m_tilePosition.X += origin.X * Layer.zoom; + this.m_tilePosition.Y += origin.X * Layer.zoom; + + // apply + this.m_spriteBatchAlpha.Draw(tileSheetTexture, this.m_tilePosition, this.m_sourceRectangle, this.m_modulationColour, rotation, origin, Layer.zoom, effects, layerDepth); + } + + /// Get the sprite effects to apply for a tile. + /// The tile being drawn. + private SpriteEffects GetSpriteEffects(Tile tile) + { + return tile.Properties.TryGetValue("@Flip", out PropertyValue propertyValue) && int.TryParse(propertyValue, out int value) + ? (SpriteEffects)value + : SpriteEffects.None; + } + + /// Get the draw rotation to apply for a tile. + /// The tile being drawn. + private float GetRotation(Tile tile) + { + if (!tile.Properties.TryGetValue("@Rotation", out PropertyValue propertyValue) || !int.TryParse(propertyValue, out int value)) + return 0; + + value %= 360; + if (value == 0) + return 0; + + return (float)(Math.PI / (180.0 / value)); + } + } +} diff --git a/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs new file mode 100644 index 00000000..d4f62b4f --- /dev/null +++ b/src/SMAPI/Framework/Rendering/SXnaDisplayDevice.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; +using xTile.Dimensions; +using xTile.Display; +using xTile.Layers; +using xTile.Tiles; +using Rectangle = xTile.Dimensions.Rectangle; + +namespace StardewModdingAPI.Framework +{ + /// A map display device which reimplements the default logic. + /// This is an exact copy of , except that private fields are protected and all methods are virtual. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Field naming deliberately matches " + nameof(XnaDisplayDevice) + " to minimize differences.")] + internal class SXnaDisplayDevice : IDisplayDevice + { + /********* + ** Fields + *********/ + protected readonly ContentManager m_contentManager; + protected readonly GraphicsDevice m_graphicsDevice; + protected SpriteBatch m_spriteBatchAlpha; + protected SpriteBatch m_spriteBatchAdditive; + protected readonly Dictionary m_tileSheetTextures; + protected Vector2 m_tilePosition; + protected Microsoft.Xna.Framework.Rectangle m_sourceRectangle; + protected readonly Color m_modulationColour; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The content manager through which to load tiles. + /// The graphics device with which to render tiles. + public SXnaDisplayDevice(ContentManager contentManager, GraphicsDevice graphicsDevice) + { + this.m_contentManager = contentManager; + this.m_graphicsDevice = graphicsDevice; + this.m_spriteBatchAlpha = new SpriteBatch(graphicsDevice); + this.m_spriteBatchAdditive = new SpriteBatch(graphicsDevice); + this.m_tileSheetTextures = new Dictionary(); + this.m_tilePosition = new Vector2(); + this.m_sourceRectangle = new Microsoft.Xna.Framework.Rectangle(); + this.m_modulationColour = Color.White; + } + + /// Load a tilesheet texture. + /// The tilesheet instance. + public virtual void LoadTileSheet(TileSheet tileSheet) + { + Texture2D texture2D = this.m_contentManager.Load(tileSheet.ImageSource); + this.m_tileSheetTextures[tileSheet] = texture2D; + } + + /// Unload a tilesheet texture. + /// The tilesheet instance. + public virtual void DisposeTileSheet(TileSheet tileSheet) + { + this.m_tileSheetTextures.Remove(tileSheet); + } + + /// Prepare to render to the screen. + /// The sprite batch being rendered. + public virtual void BeginScene(SpriteBatch b) + { + this.m_spriteBatchAlpha = b; + } + + /// Set the clipping region. + /// The clipping region. + public virtual void SetClippingRegion(Rectangle clippingRegion) + { + int backBufferWidth = this.m_graphicsDevice.PresentationParameters.BackBufferWidth; + int backBufferHeight = this.m_graphicsDevice.PresentationParameters.BackBufferHeight; + int x = this.Clamp(clippingRegion.X, 0, backBufferWidth); + int y = this.Clamp(clippingRegion.Y, 0, backBufferHeight); + int num1 = this.Clamp(clippingRegion.X + clippingRegion.Width, 0, backBufferWidth); + int num2 = this.Clamp(clippingRegion.Y + clippingRegion.Height, 0, backBufferHeight); + int width = num1 - x; + int height = num2 - y; + this.m_graphicsDevice.Viewport = new Viewport(x, y, width, height); + } + + /// Draw a tile to the screen. + /// The tile to draw. + /// The tile position to draw. + /// The layer depth at which to draw. + public virtual void DrawTile(Tile tile, Location location, float layerDepth) + { + if (tile == null) + return; + xTile.Dimensions.Rectangle tileImageBounds = tile.TileSheet.GetTileImageBounds(tile.TileIndex); + Texture2D tileSheetTexture = this.m_tileSheetTextures[tile.TileSheet]; + if (tileSheetTexture.IsDisposed) + return; + this.m_tilePosition.X = (float)location.X; + this.m_tilePosition.Y = (float)location.Y; + this.m_sourceRectangle.X = tileImageBounds.X; + this.m_sourceRectangle.Y = tileImageBounds.Y; + this.m_sourceRectangle.Width = tileImageBounds.Width; + this.m_sourceRectangle.Height = tileImageBounds.Height; + this.m_spriteBatchAlpha.Draw(tileSheetTexture, this.m_tilePosition, new Microsoft.Xna.Framework.Rectangle?(this.m_sourceRectangle), this.m_modulationColour, 0.0f, Vector2.Zero, (float)Layer.zoom, SpriteEffects.None, layerDepth); + } + + /// Finish drawing to the screen. + public virtual void EndScene() { } + + /// Snap a value to the given range. + /// The value to normalize. + /// The minimum value. + /// The maximum value. + protected int Clamp(int nValue, int nMin, int nMax) + { + return Math.Min(Math.Max(nValue, nMin), nMax); + } + } +} diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index b2d92ce8..d09e1e93 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -19,6 +19,7 @@ using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; +using StardewModdingAPI.Framework.Rendering; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.Snapshots; using StardewModdingAPI.Framework.Utilities; @@ -193,6 +194,13 @@ namespace StardewModdingAPI.Framework Game1.locations = new ObservableCollection(); } + /// Load content when the game is launched. + protected override void LoadContent() + { + base.LoadContent(); + Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, this.GraphicsDevice); + } + /// Initialize just before the game's first update tick. private void InitializeAfterGameStarted() { @@ -252,12 +260,12 @@ namespace StardewModdingAPI.Framework // update data LoadStage oldStage = Context.LoadStage; Context.LoadStage = newStage; + this.Monitor.VerboseLog($"Context: load stage changed to {newStage}"); if (newStage == LoadStage.None) { this.Monitor.Log("Context: returned to title", LogLevel.Trace); - this.Multiplayer.CleanupOnMultiplayerExit(); + this.OnReturnedToTitle(); } - this.Monitor.VerboseLog($"Context: load stage changed to {newStage}"); // raise events this.Events.LoadStageChanged.Raise(new LoadStageChangedEventArgs(oldStage, newStage)); @@ -283,6 +291,15 @@ namespace StardewModdingAPI.Framework } } + /// Perform cleanup when the game returns to the title screen. + private void OnReturnedToTitle() + { + this.Multiplayer.CleanupOnMultiplayerExit(); + + if (!(Game1.mapDisplayDevice is SDisplayDevice)) + Game1.mapDisplayDevice = new SDisplayDevice(Game1.content, this.GraphicsDevice); + } + /// Constructor a content manager to read XNB files. /// The service provider to use to locate services. /// The root directory to search for content. diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index d728ca2b..73c6670b 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -19,7 +19,7 @@ - + -- cgit From 1b282f950ad068fef581fbebba493ca9f952a5c7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 7 Mar 2020 13:03:06 -0500 Subject: update translation docs --- docs/README.md | 2 +- docs/release-notes.md | 3 +++ src/SMAPI/i18n/hu.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) (limited to 'docs') diff --git a/docs/README.md b/docs/README.md index 50478b52..f45e950c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -66,7 +66,7 @@ default | ✓ [fully translated](../src/SMAPI/i18n/default.json) Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json) French | ✓ [fully translated](../src/SMAPI/i18n/fr.json) German | ✓ [fully translated](../src/SMAPI/i18n/de.json) -Hungarian | ❑ not translated +Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json) Italian | ❑ not translated Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json) Korean | ❑ not translated diff --git a/docs/release-notes.md b/docs/release-notes.md index 692480c4..e62852be 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -2,6 +2,9 @@ # Release notes ## Upcoming release +* For players: + * Updated translations. Thanks to Annosz (added Hungarian)! + * For modders: * Added support for flipped and rotated map tiles (in collaboration with Platonymous). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). diff --git a/src/SMAPI/i18n/hu.json b/src/SMAPI/i18n/hu.json index 2171a2b7..aa0c7546 100644 --- a/src/SMAPI/i18n/hu.json +++ b/src/SMAPI/i18n/hu.json @@ -1,3 +1,3 @@ { "warn.invalid-content-removed": "Érvénytelen elemek kerültek eltávolításra, hogy a játék ne omoljon össze (további információk a SMAPI konzolon)." -} \ No newline at end of file +} -- cgit From 29fdf9ae4a91131f758035ab79fbfe0595ff1eca Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Mar 2020 11:45:55 -0400 Subject: rework input handling to allow sending custom input to the game/mods That will let Virtual Keyboard on Android work with the future multi-key binding API, and with mods that check input state directly (e.g. Pathoschild/StardewMods#520). It might also be useful as a public API in future versions. --- docs/release-notes.md | 4 + src/SMAPI/Events/ButtonPressedEventArgs.cs | 6 +- src/SMAPI/Events/ButtonReleasedEventArgs.cs | 6 +- src/SMAPI/Framework/Input/GamePadStateBuilder.cs | 120 +++++------ src/SMAPI/Framework/Input/KeyboardStateBuilder.cs | 51 +++++ src/SMAPI/Framework/Input/MouseStateBuilder.cs | 74 +++++++ src/SMAPI/Framework/Input/SInputState.cs | 232 ++++++++++++---------- src/SMAPI/Framework/ModHelpers/InputHelper.cs | 4 +- src/SMAPI/Framework/SGame.cs | 4 +- src/SMAPI/Framework/WatcherCore.cs | 2 +- 10 files changed, 320 insertions(+), 183 deletions(-) create mode 100644 src/SMAPI/Framework/Input/KeyboardStateBuilder.cs create mode 100644 src/SMAPI/Framework/Input/MouseStateBuilder.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index e62852be..8f9f651c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,10 +8,14 @@ * For modders: * Added support for flipped and rotated map tiles (in collaboration with Platonymous). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). + * Mods are no longer prevented from suppressing key presses in the chatbox. Use this power wisely. * For the web UI: * Added option to upload files using a file picker. +* For SMAPI developers: + * Added internal API to send custom input to the game/mods. This is mainly meant to support Virtual Keyboard on Android, but might be exposed as a public API in future versions. + ## 3.3.2 Released 22 February 2020 for Stardew Valley 1.4.1 or later. diff --git a/src/SMAPI/Events/ButtonPressedEventArgs.cs b/src/SMAPI/Events/ButtonPressedEventArgs.cs index 5d922666..1b30fd23 100644 --- a/src/SMAPI/Events/ButtonPressedEventArgs.cs +++ b/src/SMAPI/Events/ButtonPressedEventArgs.cs @@ -37,17 +37,17 @@ namespace StardewModdingAPI.Events this.InputState = inputState; } - /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. public bool IsSuppressed() { return this.IsSuppressed(this.Button); } - /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. /// The button to check. public bool IsSuppressed(SButton button) { - return this.InputState.SuppressButtons.Contains(button); + return this.InputState.IsSuppressed(button); } /// Get whether a given button was pressed or held. diff --git a/src/SMAPI/Events/ButtonReleasedEventArgs.cs b/src/SMAPI/Events/ButtonReleasedEventArgs.cs index f5282230..40ec1cc1 100644 --- a/src/SMAPI/Events/ButtonReleasedEventArgs.cs +++ b/src/SMAPI/Events/ButtonReleasedEventArgs.cs @@ -37,17 +37,17 @@ namespace StardewModdingAPI.Events this.InputState = inputState; } - /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. public bool IsSuppressed() { return this.IsSuppressed(this.Button); } - /// Whether a mod has indicated the key was already handled, so the game should handle it. + /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. /// The button to check. public bool IsSuppressed(SButton button) { - return this.InputState.SuppressButtons.Contains(button); + return this.InputState.IsSuppressed(button); } /// Get whether a given button was pressed or held. diff --git a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs index a20e1248..315aa920 100644 --- a/src/SMAPI/Framework/Input/GamePadStateBuilder.cs +++ b/src/SMAPI/Framework/Input/GamePadStateBuilder.cs @@ -4,7 +4,7 @@ using Microsoft.Xna.Framework.Input; namespace StardewModdingAPI.Framework.Input { - /// An abstraction for manipulating controller state. + /// Manipulates controller state. internal class GamePadStateBuilder { /********* @@ -30,7 +30,7 @@ namespace StardewModdingAPI.Framework.Input ** Public methods *********/ /// Construct an instance. - /// The initial controller state. + /// The initial state. public GamePadStateBuilder(GamePadState state) { this.ButtonStates = new Dictionary @@ -58,74 +58,64 @@ namespace StardewModdingAPI.Framework.Input this.RightStickPos = state.ThumbSticks.Right; } - /// Mark all matching buttons unpressed. - /// The buttons. - public void SuppressButtons(IEnumerable buttons) + /// Override the states for a set of buttons. + /// The button state overrides. + public GamePadStateBuilder OverrideButtons(IDictionary overrides) { - foreach (SButton button in buttons) - this.SuppressButton(button); - } - - /// Mark a button unpressed. - /// The button. - public void SuppressButton(SButton button) - { - switch (button) + foreach (var pair in overrides) { - // left thumbstick - case SButton.LeftThumbstickUp: - if (this.LeftStickPos.Y > 0) - this.LeftStickPos.Y = 0; - break; - case SButton.LeftThumbstickDown: - if (this.LeftStickPos.Y < 0) - this.LeftStickPos.Y = 0; - break; - case SButton.LeftThumbstickLeft: - if (this.LeftStickPos.X < 0) - this.LeftStickPos.X = 0; - break; - case SButton.LeftThumbstickRight: - if (this.LeftStickPos.X > 0) - this.LeftStickPos.X = 0; - break; - - // right thumbstick - case SButton.RightThumbstickUp: - if (this.RightStickPos.Y > 0) - this.RightStickPos.Y = 0; - break; - case SButton.RightThumbstickDown: - if (this.RightStickPos.Y < 0) - this.RightStickPos.Y = 0; - break; - case SButton.RightThumbstickLeft: - if (this.RightStickPos.X < 0) - this.RightStickPos.X = 0; - break; - case SButton.RightThumbstickRight: - if (this.RightStickPos.X > 0) - this.RightStickPos.X = 0; - break; - - // triggers - case SButton.LeftTrigger: - this.LeftTrigger = 0; - break; - case SButton.RightTrigger: - this.RightTrigger = 0; - break; - - // buttons - default: - if (this.ButtonStates.ContainsKey(button)) - this.ButtonStates[button] = ButtonState.Released; - break; + bool isDown = pair.Value.IsDown(); + switch (pair.Key) + { + // left thumbstick + case SButton.LeftThumbstickUp: + this.LeftStickPos.Y = isDown ? 1 : 0; + break; + case SButton.LeftThumbstickDown: + this.LeftStickPos.Y = isDown ? 1 : 0; + break; + case SButton.LeftThumbstickLeft: + this.LeftStickPos.X = isDown ? 1 : 0; + break; + case SButton.LeftThumbstickRight: + this.LeftStickPos.X = isDown ? 1 : 0; + break; + + // right thumbstick + case SButton.RightThumbstickUp: + this.RightStickPos.Y = isDown ? 1 : 0; + break; + case SButton.RightThumbstickDown: + this.RightStickPos.Y = isDown ? 1 : 0; + break; + case SButton.RightThumbstickLeft: + this.RightStickPos.X = isDown ? 1 : 0; + break; + case SButton.RightThumbstickRight: + this.RightStickPos.X = isDown ? 1 : 0; + break; + + // triggers + case SButton.LeftTrigger: + this.LeftTrigger = isDown ? 1 : 0; + break; + case SButton.RightTrigger: + this.RightTrigger = isDown ? 1 : 0; + break; + + // buttons + default: + if (this.ButtonStates.ContainsKey(pair.Key)) + this.ButtonStates[pair.Key] = isDown ? ButtonState.Pressed : ButtonState.Released; + break; + } } + + return this; } - /// Construct an equivalent gamepad state. - public GamePadState ToGamePadState() + /// Construct an equivalent state. + public GamePadState ToState() { return new GamePadState( leftThumbStick: this.LeftStickPos, diff --git a/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs new file mode 100644 index 00000000..12d780f6 --- /dev/null +++ b/src/SMAPI/Framework/Input/KeyboardStateBuilder.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Framework.Input +{ + /// Manipulates keyboard state. + internal class KeyboardStateBuilder + { + /********* + ** Fields + *********/ + /// The pressed buttons. + private readonly HashSet PressedButtons; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial state. + public KeyboardStateBuilder(KeyboardState state) + { + this.PressedButtons = new HashSet(state.GetPressedKeys()); + } + + /// Override the states for a set of buttons. + /// The button state overrides. + public KeyboardStateBuilder OverrideButtons(IDictionary overrides) + { + foreach (var pair in overrides) + { + if (pair.Key.TryGetKeyboard(out Keys key)) + { + if (pair.Value.IsDown()) + this.PressedButtons.Add(key); + else + this.PressedButtons.Remove(key); + } + } + + return this; + } + + /// Build an equivalent state. + public KeyboardState ToState() + { + return new KeyboardState(this.PressedButtons.ToArray()); + } + } +} diff --git a/src/SMAPI/Framework/Input/MouseStateBuilder.cs b/src/SMAPI/Framework/Input/MouseStateBuilder.cs new file mode 100644 index 00000000..9c6f6f95 --- /dev/null +++ b/src/SMAPI/Framework/Input/MouseStateBuilder.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework.Input; + +namespace StardewModdingAPI.Framework.Input +{ + /// Manipulates mouse state. + internal class MouseStateBuilder + { + /********* + ** Fields + *********/ + /// The current button states. + private readonly IDictionary ButtonStates; + + /// The X cursor position. + private readonly int X; + + /// The Y cursor position. + private readonly int Y; + + /// The mouse wheel scroll value. + private readonly int ScrollWheelValue; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The initial state. + public MouseStateBuilder(MouseState state) + { + this.ButtonStates = new Dictionary + { + [SButton.MouseLeft] = state.LeftButton, + [SButton.MouseMiddle] = state.MiddleButton, + [SButton.MouseRight] = state.RightButton, + [SButton.MouseX1] = state.XButton1, + [SButton.MouseX2] = state.XButton2 + }; + this.X = state.X; + this.Y = state.Y; + this.ScrollWheelValue = state.ScrollWheelValue; + } + + /// Override the states for a set of buttons. + /// The button state overrides. + public MouseStateBuilder OverrideButtons(IDictionary overrides) + { + foreach (var pair in overrides) + { + bool isDown = pair.Value.IsDown(); + if (this.ButtonStates.ContainsKey(pair.Key)) + this.ButtonStates[pair.Key] = isDown ? ButtonState.Pressed : ButtonState.Released; + } + + return this; + } + + /// Construct an equivalent mouse state. + public MouseState ToMouseState() + { + return new MouseState( + x: this.X, + y: this.Y, + scrollWheel: this.ScrollWheelValue, + leftButton: this.ButtonStates[SButton.MouseLeft], + middleButton: this.ButtonStates[SButton.MouseMiddle], + rightButton: this.ButtonStates[SButton.MouseRight], + xButton1: this.ButtonStates[SButton.MouseX1], + xButton2: this.ButtonStates[SButton.MouseX2] + ); + } + } +} diff --git a/src/SMAPI/Framework/Input/SInputState.cs b/src/SMAPI/Framework/Input/SInputState.cs index 4eaa9ca6..06a7ac3b 100644 --- a/src/SMAPI/Framework/Input/SInputState.cs +++ b/src/SMAPI/Framework/Input/SInputState.cs @@ -23,37 +23,34 @@ namespace StardewModdingAPI.Framework.Input /// The player's last known tile position. private Vector2? LastPlayerTile; + /// The buttons to press until the game next handles input. + private readonly HashSet CustomPressedKeys = new HashSet(); + + /// The buttons to consider released until the actual button is released. + private readonly HashSet CustomReleasedKeys = new HashSet(); + + /// The buttons which were actually down as of the last update, ignoring overrides. + private HashSet LastRealButtonPresses = new HashSet(); + /********* ** Accessors *********/ /// The controller state as of the last update. - public GamePadState RealController { get; private set; } + public GamePadState LastController { get; private set; } /// The keyboard state as of the last update. - public KeyboardState RealKeyboard { get; private set; } + public KeyboardState LastKeyboard { get; private set; } /// The mouse state as of the last update. - public MouseState RealMouse { get; private set; } - - /// A derivative of which suppresses the buttons in . - public GamePadState SuppressedController { get; private set; } - - /// A derivative of which suppresses the buttons in . - public KeyboardState SuppressedKeyboard { get; private set; } + public MouseState LastMouse { get; private set; } - /// A derivative of which suppresses the buttons in . - public MouseState SuppressedMouse { get; private set; } + /// The buttons which were pressed, held, or released as of the last update. + public IDictionary LastButtonStates { get; private set; } = new Dictionary(); /// The cursor position on the screen adjusted for the zoom level. public ICursorPosition CursorPosition => this.CursorPositionImpl; - /// The buttons which were pressed, held, or released. - 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(); - /********* ** Public methods @@ -63,14 +60,38 @@ namespace StardewModdingAPI.Framework.Input { return new SInputState { - ActiveButtons = this.ActiveButtons, - RealController = this.RealController, - RealKeyboard = this.RealKeyboard, - RealMouse = this.RealMouse, + LastButtonStates = this.LastButtonStates, + LastController = this.LastController, + LastKeyboard = this.LastKeyboard, + LastMouse = this.LastMouse, CursorPositionImpl = this.CursorPositionImpl }; } + /// Override the state for a button. + /// The button to override. + /// Whether to mark it pressed; else mark it released. + public void OverrideButton(SButton button, bool setDown) + { + if (setDown) + { + this.CustomPressedKeys.Add(button); + this.CustomReleasedKeys.Remove(button); + } + else + { + this.CustomPressedKeys.Remove(button); + this.CustomReleasedKeys.Add(button); + } + } + + /// Get whether a mod has indicated the key was already handled, so the game shouldn't handle it. + /// The button to check. + public bool IsSuppressed(SButton button) + { + return this.CustomReleasedKeys.Contains(button); + } + /// This method is called by the game, and does nothing since SMAPI will already have updated by that point. [Obsolete("This method should only be called by the game itself.")] public override void Update() { } @@ -82,28 +103,47 @@ namespace StardewModdingAPI.Framework.Input { float zoomMultiplier = (1f / Game1.options.zoomLevel); - // get new states - GamePadState realController = GamePad.GetState(PlayerIndex.One); - KeyboardState realKeyboard = Keyboard.GetState(); - MouseState realMouse = Mouse.GetState(); - 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); + // get real values + GamePadState controller = GamePad.GetState(PlayerIndex.One); + KeyboardState keyboard = Keyboard.GetState(); + MouseState mouse = Mouse.GetState(); + Vector2 cursorAbsolutePos = new Vector2((mouse.X * zoomMultiplier) + Game1.viewport.X, (mouse.Y * zoomMultiplier) + Game1.viewport.Y); Vector2? playerTilePos = Context.IsPlayerFree ? Game1.player.getTileLocation() : (Vector2?)null; + HashSet reallyDown = new HashSet(this.GetPressedButtons(keyboard, mouse, controller)); - // update real states - this.ActiveButtons = activeButtons; - this.RealController = realController; - this.RealKeyboard = realKeyboard; - this.RealMouse = realMouse; + // apply overrides + bool hasOverrides = false; + if (this.CustomPressedKeys.Count > 0 || this.CustomReleasedKeys.Count > 0) + { + // reset overrides that no longer apply + this.CustomPressedKeys.RemoveWhere(key => reallyDown.Contains(key)); + this.CustomReleasedKeys.RemoveWhere(key => !reallyDown.Contains(key)); + + // apply overrides + if (this.ApplyOverrides(this.CustomPressedKeys, this.CustomReleasedKeys, ref keyboard, ref mouse, ref controller)) + hasOverrides = true; + + // remove pressed keys + this.CustomPressedKeys.Clear(); + } + + // get button states + var pressedButtons = hasOverrides + ? new HashSet(this.GetPressedButtons(keyboard, mouse, controller)) + : reallyDown; + var activeButtons = this.DeriveStates(this.LastButtonStates, pressedButtons, keyboard, mouse, controller); + + // update + this.LastController = controller; + this.LastKeyboard = keyboard; + this.LastMouse = mouse; + this.LastButtonStates = activeButtons; + this.LastRealButtonPresses = reallyDown; if (cursorAbsolutePos != this.CursorPositionImpl?.AbsolutePixels || playerTilePos != this.LastPlayerTile) { this.LastPlayerTile = playerTilePos; - this.CursorPositionImpl = this.GetCursorPosition(realMouse, cursorAbsolutePos, zoomMultiplier); + this.CursorPositionImpl = this.GetCursorPosition(mouse, cursorAbsolutePos, zoomMultiplier); } - - // update suppressed states - this.SuppressButtons.RemoveWhere(p => !this.GetState(activeButtons, p).IsDown()); - this.UpdateSuppression(); } catch (InvalidOperationException) { @@ -111,18 +151,18 @@ namespace StardewModdingAPI.Framework.Input } } - /// Apply input suppression to current input. - public void UpdateSuppression() + /// Apply input overrides to the current state. + public void ApplyOverrides() { - GamePadState suppressedController = this.RealController; - KeyboardState suppressedKeyboard = this.RealKeyboard; - MouseState suppressedMouse = this.RealMouse; + GamePadState newController = this.LastController; + KeyboardState newKeyboard = this.LastKeyboard; + MouseState newMouse = this.LastMouse; - this.SuppressGivenStates(this.ActiveButtons, ref suppressedKeyboard, ref suppressedMouse, ref suppressedController); + this.ApplyOverrides(pressed: this.CustomPressedKeys, released: this.CustomReleasedKeys, ref newKeyboard, ref newMouse, ref newController); - this.SuppressedController = suppressedController; - this.SuppressedKeyboard = suppressedKeyboard; - this.SuppressedMouse = suppressedMouse; + this.LastController = newController; + this.LastKeyboard = newKeyboard; + this.LastMouse = newMouse; } /// Get the gamepad state visible to the game. @@ -130,36 +170,30 @@ namespace StardewModdingAPI.Framework.Input public override GamePadState GetGamePadState() { if (Game1.options.gamepadMode == Options.GamepadModes.ForceOff) - return base.GetGamePadState(); + return new GamePadState(); - return this.ShouldSuppressNow() - ? this.SuppressedController - : this.RealController; + return this.LastController; } /// Get the keyboard state visible to the game. [Obsolete("This method should only be called by the game itself.")] public override KeyboardState GetKeyboardState() { - return this.ShouldSuppressNow() - ? this.SuppressedKeyboard - : this.RealKeyboard; + return this.LastKeyboard; } /// Get the keyboard state visible to the game. [Obsolete("This method should only be called by the game itself.")] public override MouseState GetMouseState() { - return this.ShouldSuppressNow() - ? this.SuppressedMouse - : this.RealMouse; + return this.LastMouse; } /// Get whether a given button was pressed or held. /// The button to check. public bool IsDown(SButton button) { - return this.GetState(this.ActiveButtons, button).IsDown(); + return this.GetState(this.LastButtonStates, button).IsDown(); } /// Get whether any of the given buttons were pressed or held. @@ -173,7 +207,7 @@ namespace StardewModdingAPI.Framework.Input /// The button to check. public SButtonState GetState(SButton button) { - return this.GetState(this.ActiveButtons, button); + return this.GetState(this.LastButtonStates, button); } @@ -194,76 +228,60 @@ namespace StardewModdingAPI.Framework.Input return new CursorPosition(absolutePixels, screenPixels, tile, grabTile); } - /// Whether input should be suppressed in the current context. - private bool ShouldSuppressNow() - { - return Game1.chatBox == null || !Game1.chatBox.isActive(); - } - - /// Apply input suppression to the given input states. - /// The current button states to check. + /// Apply input overrides to the given states. + /// The buttons to mark pressed. + /// The buttons to mark released. /// 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) + /// Returns whether any overrides were applied. + private bool ApplyOverrides(ISet pressed, ISet released, ref KeyboardState keyboardState, ref MouseState mouseState, ref GamePadState gamePadState) { - if (this.SuppressButtons.Count == 0) - return; - - // gather info - HashSet suppressKeys = new HashSet(); - HashSet suppressButtons = new HashSet(); - HashSet suppressMouse = new HashSet(); - foreach (SButton button in this.SuppressButtons) + if (pressed.Count == 0 && released.Count == 0) + return false; + + // group keys by type + IDictionary keyboardOverrides = new Dictionary(); + IDictionary controllerOverrides = new Dictionary(); + IDictionary mouseOverrides = new Dictionary(); + foreach (var button in pressed.Concat(released)) { + var newState = this.DeriveState( + oldState: this.GetState(button), + isDown: pressed.Contains(button) + ); + if (button == SButton.MouseLeft || button == SButton.MouseMiddle || button == SButton.MouseRight || button == SButton.MouseX1 || button == SButton.MouseX2) - suppressMouse.Add(button); - else if (button.TryGetKeyboard(out Keys key)) - suppressKeys.Add(key); + mouseOverrides[button] = newState; + else if (button.TryGetKeyboard(out Keys _)) + keyboardOverrides[button] = newState; else if (gamePadState.IsConnected && button.TryGetController(out Buttons _)) - suppressButtons.Add(button); + controllerOverrides[button] = newState; } - // suppress keyboard keys - if (keyboardState.GetPressedKeys().Any() && suppressKeys.Any()) - keyboardState = new KeyboardState(keyboardState.GetPressedKeys().Except(suppressKeys).ToArray()); + // override states + if (keyboardOverrides.Any()) + keyboardState = new KeyboardStateBuilder(keyboardState).OverrideButtons(keyboardOverrides).ToState(); + if (gamePadState.IsConnected && controllerOverrides.Any()) + gamePadState = new GamePadStateBuilder(gamePadState).OverrideButtons(controllerOverrides).ToState(); + if (mouseOverrides.Any()) + mouseState = new MouseStateBuilder(mouseState).OverrideButtons(mouseOverrides).ToMouseState(); - // suppress controller keys - if (gamePadState.IsConnected && suppressButtons.Any()) - { - GamePadStateBuilder builder = new GamePadStateBuilder(gamePadState); - builder.SuppressButtons(suppressButtons); - gamePadState = builder.ToGamePadState(); - } - - // suppress mouse buttons - if (suppressMouse.Any()) - { - mouseState = new MouseState( - x: mouseState.X, - y: mouseState.Y, - scrollWheel: mouseState.ScrollWheelValue, - leftButton: suppressMouse.Contains(SButton.MouseLeft) ? ButtonState.Released : mouseState.LeftButton, - middleButton: suppressMouse.Contains(SButton.MouseMiddle) ? ButtonState.Released : mouseState.MiddleButton, - rightButton: suppressMouse.Contains(SButton.MouseRight) ? ButtonState.Released : mouseState.RightButton, - xButton1: suppressMouse.Contains(SButton.MouseX1) ? ButtonState.Released : mouseState.XButton1, - xButton2: suppressMouse.Contains(SButton.MouseX2) ? ButtonState.Released : mouseState.XButton2 - ); - } + return true; } /// Get the state of all pressed or released buttons relative to their previous state. /// The previous button states. + /// The currently pressed buttons. /// The keyboard state. /// The mouse state. /// The controller state. - private IDictionary DeriveStates(IDictionary previousStates, KeyboardState keyboard, MouseState mouse, GamePadState controller) + private IDictionary DeriveStates(IDictionary previousStates, HashSet pressedButtons, KeyboardState keyboard, MouseState mouse, GamePadState controller) { IDictionary activeButtons = new Dictionary(); // handle pressed keys - SButton[] down = this.GetPressedButtons(keyboard, mouse, controller).ToArray(); - foreach (SButton button in down) + foreach (SButton button in pressedButtons) activeButtons[button] = this.DeriveState(this.GetState(previousStates, button), isDown: true); // handle released keys diff --git a/src/SMAPI/Framework/ModHelpers/InputHelper.cs b/src/SMAPI/Framework/ModHelpers/InputHelper.cs index f8ff0355..134ba8d1 100644 --- a/src/SMAPI/Framework/ModHelpers/InputHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/InputHelper.cs @@ -41,14 +41,14 @@ namespace StardewModdingAPI.Framework.ModHelpers /// The button. public bool IsSuppressed(SButton button) { - return this.InputState.SuppressButtons.Contains(button); + return this.InputState.IsSuppressed(button); } /// Prevent the game from handling a button press. This doesn't prevent other mods from receiving the event. /// The button to suppress. public void Suppress(SButton button) { - this.InputState.SuppressButtons.Add(button); + this.InputState.OverrideButton(button, setDown: false); } /// Get the state of a button. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d09e1e93..2a30b595 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -653,7 +653,7 @@ namespace StardewModdingAPI.Framework } // raise input button events - foreach (var pair in inputState.ActiveButtons) + foreach (var pair in inputState.LastButtonStates) { SButton button = pair.Key; SButtonState status = pair.Value; @@ -824,7 +824,7 @@ namespace StardewModdingAPI.Framework events.OneSecondUpdateTicking.RaiseEmpty(); try { - this.Input.UpdateSuppression(); + this.Input.ApplyOverrides(); // if mods added any new overrides since the update, process them now SGame.TicksElapsed++; base.Update(gameTime); } diff --git a/src/SMAPI/Framework/WatcherCore.cs b/src/SMAPI/Framework/WatcherCore.cs index 32b7fdc6..c89efa44 100644 --- a/src/SMAPI/Framework/WatcherCore.cs +++ b/src/SMAPI/Framework/WatcherCore.cs @@ -60,7 +60,7 @@ namespace StardewModdingAPI.Framework { // init watchers this.CursorWatcher = WatcherFactory.ForEquatable(() => inputState.CursorPosition); - this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.RealMouse.ScrollWheelValue); + this.MouseWheelScrollWatcher = WatcherFactory.ForEquatable(() => inputState.LastMouse.ScrollWheelValue); this.SaveIdWatcher = WatcherFactory.ForEquatable(() => Game1.hasLoadedGame ? Game1.uniqueIDForThisGame : 0); this.WindowSizeWatcher = WatcherFactory.ForEquatable(() => new Point(Game1.viewport.Width, Game1.viewport.Height)); this.TimeWatcher = WatcherFactory.ForEquatable(() => Game1.timeOfDay); -- cgit From 5ba53cb390e06094587b90bb4e00f000725bfc05 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Mar 2020 14:07:42 -0400 Subject: optimize log parser for very long multi-line messages --- docs/release-notes.md | 1 + .../Framework/LogParsing/LogMessageBuilder.cs | 88 ++++++++++++++++++++++ src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 64 ++++++++-------- 3 files changed, 123 insertions(+), 30 deletions(-) create mode 100644 src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8f9f651c..fe4349c9 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,6 +11,7 @@ * Mods are no longer prevented from suppressing key presses in the chatbox. Use this power wisely. * For the web UI: + * Optimized log parser for very long multi-line log messages. * Added option to upload files using a file picker. * For SMAPI developers: diff --git a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs new file mode 100644 index 00000000..371acfd3 --- /dev/null +++ b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs @@ -0,0 +1,88 @@ +using System; +using System.Text; +using StardewModdingAPI.Web.Framework.LogParsing.Models; + +namespace StardewModdingAPI.Web.Framework.LogParsing +{ + /// Handles constructing log message instances with minimal memory allocation. + internal class LogMessageBuilder + { + /********* + ** Fields + *********/ + /// The local time when the next log was posted. + public string Time { get; set; } + + /// The log level for the next log message. + public LogLevel Level { get; set; } + + /// The mod name for the next log message. + public string Mod { get; set; } + + /// The text for the next log message. + private readonly StringBuilder Text = new StringBuilder(); + + + /********* + ** Accessors + *********/ + /// Whether the next log message has been started. + public bool Started { get; private set; } + + + /********* + ** Public methods + *********/ + /// Start accumulating values for a new log message. + /// The local time when the log was posted. + /// The log level. + /// The mod name. + /// The initial log text. + /// A log message is already started; call before starting a new message. + public void Start(string time, LogLevel level, string mod, string text) + { + if (this.Started) + throw new InvalidOperationException("Can't start new message, previous log message isn't done yet."); + + this.Started = true; + + this.Time = time; + this.Level = level; + this.Mod = mod; + this.Text.AppendLine(text); + } + + /// Add a new line to the next log message being built. + /// The line to add. + /// A log message hasn't been started yet. + public void AddLine(string text) + { + if (!this.Started) + throw new InvalidOperationException("Can't add text, no log message started yet."); + + this.Text.AppendLine(text); + } + + /// Get a log message for the accumulated values. + public LogMessage Build() + { + if (!this.Started) + return null; + + return new LogMessage + { + Time = this.Time, + Level = this.Level, + Mod = this.Mod, + Text = this.Text.ToString() + }; + } + + /// Reset to start a new log message. + public void Clear() + { + this.Started = false; + this.Text.Clear(); + } + } +} diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index cc91ec51..9ef1c53f 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -282,43 +282,47 @@ namespace StardewModdingAPI.Web.Framework.LogParsing /// The log text can't be parsed successfully. private IEnumerable GetMessages(string logText) { - LogMessage message = new LogMessage(); - using (StringReader reader = new StringReader(logText)) + LogMessageBuilder builder = new LogMessageBuilder(); + using StringReader reader = new StringReader(logText); + while (true) { - while (true) - { - // read data - string line = reader.ReadLine(); - if (line == null) - break; - Match header = this.MessageHeaderPattern.Match(line); - - // validate - if (message.Text == null && !header.Success) - throw new LogParseException("Found a log message with no SMAPI metadata. Is this a SMAPI log file?"); + // read line + string line = reader.ReadLine(); + if (line == null) + break; - // start or continue message - if (header.Success) - { - if (message.Text != null) - yield return message; + // match header + Match header = this.MessageHeaderPattern.Match(line); + bool isNewMessage = header.Success; - message = new LogMessage - { - Time = header.Groups["time"].Value, - Level = Enum.Parse(header.Groups["level"].Value, ignoreCase: true), - Mod = header.Groups["modName"].Value, - Text = line.Substring(header.Length) - }; + // start/continue message + if (isNewMessage) + { + if (builder.Started) + { + yield return builder.Build(); + builder.Clear(); } - else - message.Text += "\n" + line; + + builder.Start( + time: header.Groups["time"].Value, + level: Enum.Parse(header.Groups["level"].Value, ignoreCase: true), + mod: header.Groups["modName"].Value, + text: line.Substring(header.Length) + ); } + else + { + if (!builder.Started) + throw new LogParseException("Found a log message with no SMAPI metadata. Is this a SMAPI log file?"); - // end last message - if (message.Text != null) - yield return message; + builder.AddLine(line); + } } + + // end last message + if (builder.Started) + yield return builder.Build(); } } } -- cgit From e39b9e0d699079edfbcf8595d7499aff894578b6 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 8 Mar 2020 14:38:24 -0400 Subject: fix log parse issues --- docs/release-notes.md | 3 ++- src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs | 5 +++-- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index fe4349c9..4a136df5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,8 +11,9 @@ * Mods are no longer prevented from suppressing key presses in the chatbox. Use this power wisely. * For the web UI: - * Optimized log parser for very long multi-line log messages. * Added option to upload files using a file picker. + * Optimized log parser for very long multi-line log messages. + * Fixed log parser not detecting folder path in recent versions of SMAPI. * For SMAPI developers: * Added internal API to send custom input to the game/mods. This is mainly meant to support Virtual Keyboard on Android, but might be exposed as a public API in future versions. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs index 371acfd3..42e283a9 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogMessageBuilder.cs @@ -49,7 +49,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing this.Time = time; this.Level = level; this.Mod = mod; - this.Text.AppendLine(text); + this.Text.Append(text); } /// Add a new line to the next log message being built. @@ -60,7 +60,8 @@ namespace StardewModdingAPI.Web.Framework.LogParsing if (!this.Started) throw new InvalidOperationException("Can't add text, no log message started yet."); - this.Text.AppendLine(text); + this.Text.Append("\n"); + this.Text.Append(text); } /// Get a log message for the accumulated values. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 9ef1c53f..cce80816 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -201,7 +201,7 @@ namespace StardewModdingAPI.Web.Framework.LogParsing } // mod path line - else if (message.Level == LogLevel.Debug && this.ModPathPattern.IsMatch(message.Text)) + else if (message.Level == LogLevel.Info && this.ModPathPattern.IsMatch(message.Text)) { Match match = this.ModPathPattern.Match(message.Text); log.ModPath = match.Groups["path"].Value; -- cgit From dfb12351333dc3deb47f5c33212d7be7d2ac94d7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 14 Mar 2020 18:47:19 -0400 Subject: fix semi-transparency issues on Linux/Mac Apparently Mono no longer premultiplies loaded PNGs by default. --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentManagers/ModContentManager.cs | 7 ------- 2 files changed, 1 insertion(+), 7 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 4a136df5..df931eb3 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -3,6 +3,7 @@ # Release notes ## Upcoming release * For players: + * Fixed semi-transparency issues on Linux/Mac in recent versions of Mono (e.g. pink shadows). * Updated translations. Thanks to Annosz (added Hungarian)! * For modders: diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index 4ffe3acd..fda80a83 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -241,13 +241,6 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Based on code by David Gouveia. private Texture2D PremultiplyTransparency(Texture2D texture) { - // Textures loaded by Texture2D.FromStream are already premultiplied on Linux/Mac, even - // though the XNA documentation explicitly says otherwise. That's a glitch in MonoGame - // fixed in newer versions, but the game uses a bundled version that will always be - // affected. See https://github.com/MonoGame/MonoGame/issues/4820 for more info. - if (Constants.TargetPlatform != GamePlatform.Windows) - return texture; - // premultiply pixels Color[] data = new Color[texture.Width * texture.Height]; texture.GetData(data); -- cgit From 8d88ce8a169643e36531cc0db5cc6863db35e91f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Mar 2020 14:22:16 -0400 Subject: fix player_add error if the player has broken XNB mods --- docs/release-notes.md | 3 +++ .../Framework/ItemRepository.cs | 28 ++++++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index df931eb3..67d043a1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,9 @@ * Fixed semi-transparency issues on Linux/Mac in recent versions of Mono (e.g. pink shadows). * Updated translations. Thanks to Annosz (added Hungarian)! +* For the Console Commands mod: + * fixed `player_add` error if you have broken XNB mods. + * For modders: * Added support for flipped and rotated map tiles (in collaboration with Platonymous). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs index 08dd8eed..6a17213c 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/ItemRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData; using StardewValley; using StardewValley.Menus; @@ -59,13 +60,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework yield return this.TryCreate(ItemType.Flooring, id, () => new Wallpaper(id, isFloor: true) { Category = SObject.furnitureCategory }); // equipment - foreach (int id in Game1.content.Load>("Data\\Boots").Keys) + foreach (int id in this.TryLoad("Data\\Boots").Keys) yield return this.TryCreate(ItemType.Boots, id, () => new Boots(id)); - foreach (int id in Game1.content.Load>("Data\\hats").Keys) + foreach (int id in this.TryLoad("Data\\hats").Keys) yield return this.TryCreate(ItemType.Hat, id, () => new Hat(id)); // weapons - foreach (int id in Game1.content.Load>("Data\\weapons").Keys) + foreach (int id in this.TryLoad("Data\\weapons").Keys) { yield return this.TryCreate(ItemType.Weapon, id, () => (id >= 32 && id <= 34) ? (Item)new Slingshot(id) @@ -74,7 +75,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework } // furniture - foreach (int id in Game1.content.Load>("Data\\Furniture").Keys) + foreach (int id in this.TryLoad("Data\\Furniture").Keys) { if (id == 1466 || id == 1468) yield return this.TryCreate(ItemType.Furniture, id, () => new TV(id, Vector2.Zero)); @@ -94,7 +95,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework // secret notes if (id == 79) { - foreach (int secretNoteId in Game1.content.Load>("Data\\SecretNotes").Keys) + foreach (int secretNoteId in this.TryLoad("Data\\SecretNotes").Keys) { yield return this.TryCreate(ItemType.Object, this.CustomIDOffset + secretNoteId, () => { @@ -233,6 +234,23 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework /********* ** Private methods *********/ + /// Try to load a data file, and return empty data if it's invalid. + /// The asset key type. + /// The asset value type. + /// The data asset name. + private Dictionary TryLoad(string assetName) + { + try + { + return Game1.content.Load>(assetName); + } + catch (ContentLoadException) + { + // generally due to a player incorrectly replacing a data file with an XNB mod + return new Dictionary(); + } + } + /// Create a searchable item if valid. /// The item type. /// The unique ID (if different from the item's parent sheet index). -- cgit From a50e78efd827bfeab32dac37749c001af0c5a2d3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Mar 2020 17:40:31 -0400 Subject: add monitor.LogOnce method --- docs/release-notes.md | 3 ++- src/SMAPI/Framework/Monitor.cs | 13 +++++++++++++ src/SMAPI/IMonitor.cs | 5 +++++ 3 files changed, 20 insertions(+), 1 deletion(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 67d043a1..73fa8ed1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -12,7 +12,8 @@ * For modders: * Added support for flipped and rotated map tiles (in collaboration with Platonymous). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). - * Mods are no longer prevented from suppressing key presses in the chatbox. Use this power wisely. + * Added `this.Monitor.LogOnce` method. + * Mods are no longer prevented from suppressing key presses in the chatbox. * For the web UI: * Added option to upload files using a file picker. diff --git a/src/SMAPI/Framework/Monitor.cs b/src/SMAPI/Framework/Monitor.cs index 06cf1b46..f630c7fe 100644 --- a/src/SMAPI/Framework/Monitor.cs +++ b/src/SMAPI/Framework/Monitor.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Framework.Logging; using StardewModdingAPI.Internal.ConsoleWriting; @@ -26,6 +27,9 @@ namespace StardewModdingAPI.Framework /// The maximum length of the values. private static readonly int MaxLevelLength = (from level in Enum.GetValues(typeof(LogLevel)).Cast() select level.ToString().Length).Max(); + /// A cache of messages that should only be logged once. + private readonly HashSet LogOnceCache = new HashSet(); + /********* ** Accessors @@ -74,6 +78,15 @@ namespace StardewModdingAPI.Framework this.LogImpl(this.Source, message, (ConsoleLogLevel)level); } + /// Log a message for the player or developer, but only if it hasn't already been logged since the last game launch. + /// The message to log. + /// The log severity level. + public void LogOnce(string message, LogLevel level = LogLevel.Trace) + { + if (this.LogOnceCache.Add($"{message}|{level}")) + this.LogImpl(this.Source, message, (ConsoleLogLevel)level); + } + /// Log a message that only appears when is enabled. /// The message to log. public void VerboseLog(string message) diff --git a/src/SMAPI/IMonitor.cs b/src/SMAPI/IMonitor.cs index f2d110b8..c400a211 100644 --- a/src/SMAPI/IMonitor.cs +++ b/src/SMAPI/IMonitor.cs @@ -18,6 +18,11 @@ namespace StardewModdingAPI /// The log severity level. void Log(string message, LogLevel level = LogLevel.Trace); + /// Log a message for the player or developer, but only if it hasn't already been logged since the last game launch. + /// The message to log. + /// The log severity level. + void LogOnce(string message, LogLevel level = LogLevel.Trace); + /// Log a message that only appears when is enabled. /// The message to log. void VerboseLog(string message); -- cgit From 737e31b531e2064643e4c790673d5053afe683ae Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Mar 2020 19:27:08 -0400 Subject: remove invalid-location check now handled by the game --- docs/release-notes.md | 1 + src/SMAPI/Patches/LoadErrorPatch.cs | 25 +------------------------ 2 files changed, 2 insertions(+), 24 deletions(-) (limited to 'docs') diff --git a/docs/release-notes.md b/docs/release-notes.md index 73fa8ed1..12ba056d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -4,6 +4,7 @@ ## Upcoming release * For players: * Fixed semi-transparency issues on Linux/Mac in recent versions of Mono (e.g. pink shadows). + * Removed invalid location check. This is now handled by the game itself. * Updated translations. Thanks to Annosz (added Hungarian)! * For the Console Commands mod: diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index c16ca7cc..77415ff2 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -67,8 +67,7 @@ namespace StardewModdingAPI.Patches private static bool Before_SaveGame_LoadDataToLocations(List gamelocations) { bool removedAny = - LoadErrorPatch.RemoveInvalidLocations(gamelocations) - | LoadErrorPatch.RemoveBrokenBuildings(gamelocations) + LoadErrorPatch.RemoveBrokenBuildings(gamelocations) | LoadErrorPatch.RemoveInvalidNpcs(gamelocations); if (removedAny) @@ -77,28 +76,6 @@ namespace StardewModdingAPI.Patches return true; } - /// Remove locations which don't exist in-game. - /// The current game locations. - private static bool RemoveInvalidLocations(List locations) - { - bool removedAny = false; - - foreach (GameLocation location in locations.ToArray()) - { - if (location is Cellar) - continue; // missing cellars will be added by the game code - - if (Game1.getLocationFromName(location.name) == null) - { - LoadErrorPatch.Monitor.Log($"Removed invalid location '{location.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom location mod?)", LogLevel.Warn); - locations.Remove(location); - removedAny = true; - } - } - - return removedAny; - } - /// Remove buildings which don't exist in the game data. /// The current game locations. private static bool RemoveBrokenBuildings(IEnumerable locations) -- cgit From 6d1494a56c5d04e7bc1ee406810a5a53dea2229a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 22 Mar 2020 19:36:49 -0400 Subject: prepare for release --- build/common.targets | 2 +- docs/README.md | 2 +- docs/release-notes.md | 10 +++++----- src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) (limited to 'docs') diff --git a/build/common.targets b/build/common.targets index c1617375..b3211aba 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ - 3.3.2 + 3.4.0 SMAPI $(AssemblySearchPaths);{GAC} diff --git a/docs/README.md b/docs/README.md index f45e950c..546ee6b3 100644 --- a/docs/README.md +++ b/docs/README.md @@ -67,7 +67,7 @@ Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json) French | ✓ [fully translated](../src/SMAPI/i18n/fr.json) German | ✓ [fully translated](../src/SMAPI/i18n/de.json) Hungarian | ✓ [fully translated](../src/SMAPI/i18n/hu.json) -Italian | ❑ not translated +Italian | ✓ [fully translated](../src/SMAPI/i18n/it.json) Japanese | ✓ [fully translated](../src/SMAPI/i18n/ja.json) Korean | ❑ not translated Portuguese | ✓ [fully translated](../src/SMAPI/i18n/pt.json) diff --git a/docs/release-notes.md b/docs/release-notes.md index 12ba056d..5a5e24d4 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,15 +1,15 @@ ← [README](README.md) # Release notes -## Upcoming release +## 3.4 +Released 22 March 2020 for Stardew Valley 1.4.1 or later. + * For players: * Fixed semi-transparency issues on Linux/Mac in recent versions of Mono (e.g. pink shadows). - * Removed invalid location check. This is now handled by the game itself. + * Fixed `player_add` command error if you have broken XNB mods. + * Removed invalid-location check now handled by the game. * Updated translations. Thanks to Annosz (added Hungarian)! -* For the Console Commands mod: - * fixed `player_add` error if you have broken XNB mods. - * For modders: * Added support for flipped and rotated map tiles (in collaboration with Platonymous). * Added support for `.tmx` maps using zlib compression (thanks to Platonymous!). diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 0e6805dc..dbed84eb 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.3.2", + "Version": "3.4.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.3.2" + "MinimumApiVersion": "3.4.0" } diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 5165d2b2..dc8bc8d4 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.3.2", + "Version": "3.4.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.3.2" + "MinimumApiVersion": "3.4.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 3242a12c..d66e9d6b 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.3.2"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.4.0"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); -- cgit