using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using Pathoschild.Stardew.Common.UI; using StardewModdingAPI; using StardewValley; using StardewValley.Locations; using StardewValley.Menus; namespace Pathoschild.Stardew.Common { /// Provides common utility methods for interacting with the game code shared by my various mods. internal static class CommonHelper { /********* ** Fields *********/ /// A blank pixel which can be colorised and stretched to draw geometric shapes. private static readonly Lazy LazyPixel = new Lazy(() => { Texture2D pixel = new Texture2D(Game1.graphics.GraphicsDevice, 1, 1); pixel.SetData(new[] { Color.White }); return pixel; }); /********* ** Accessors *********/ /// A blank pixel which can be colorised and stretched to draw geometric shapes. public static Texture2D Pixel => CommonHelper.LazyPixel.Value; /// The width of the horizontal and vertical scroll edges (between the origin position and start of content padding). public static readonly Vector2 ScrollEdgeSize = new Vector2(CommonSprites.Scroll.TopLeft.Width * Game1.pixelZoom, CommonSprites.Scroll.TopLeft.Height * Game1.pixelZoom); /********* ** Public methods *********/ /**** ** Game ****/ /// Get all game locations. public static IEnumerable GetLocations() { return Game1.locations .Concat( from location in Game1.locations.OfType() from building in location.buildings where building.indoors.Value != null select building.indoors.Value ); } /**** ** Fonts ****/ /// Get the dimensions of a space character. /// The font to measure. public static float GetSpaceWidth(SpriteFont font) { return font.MeasureString("A B").X - font.MeasureString("AB").X; } /**** ** UI ****/ /// Draw a pretty hover box for the given text. /// The sprite batch being drawn. /// The text to display. /// The position at which to draw the text. /// The maximum width to display. public static Vector2 DrawHoverBox(SpriteBatch spriteBatch, string label, in Vector2 position, float wrapWidth) { const int paddingSize = 27; const int gutterSize = 20; Vector2 labelSize = spriteBatch.DrawTextBlock(Game1.smallFont, label, position + new Vector2(gutterSize), wrapWidth); // draw text to get wrapped text dimensions IClickableMenu.drawTextureBox(spriteBatch, Game1.menuTexture, new Rectangle(0, 256, 60, 60), (int)position.X, (int)position.Y, (int)labelSize.X + paddingSize + gutterSize, (int)labelSize.Y + paddingSize, Color.White); spriteBatch.DrawTextBlock(Game1.smallFont, label, position + new Vector2(gutterSize), wrapWidth); // draw again over texture box return labelSize + new Vector2(paddingSize); } /// Draw a button background. /// The sprite batch to which to draw. /// The top-left pixel coordinate at which to draw the button. /// The button content's pixel size. /// The pixel position at which the content begins. /// The button's outer bounds. /// The padding between the content and border. public static void DrawButton(SpriteBatch spriteBatch, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding = 0) { CommonHelper.DrawContentBox( spriteBatch: spriteBatch, texture: CommonSprites.Button.Sheet, background: CommonSprites.Button.Background, top: CommonSprites.Button.Top, right: CommonSprites.Button.Right, bottom: CommonSprites.Button.Bottom, left: CommonSprites.Button.Left, topLeft: CommonSprites.Button.TopLeft, topRight: CommonSprites.Button.TopRight, bottomRight: CommonSprites.Button.BottomRight, bottomLeft: CommonSprites.Button.BottomLeft, position: position, contentSize: contentSize, contentPos: out contentPos, bounds: out bounds, padding: padding ); } /// Draw a scroll background. /// The sprite batch to which to draw. /// The top-left pixel coordinate at which to draw the scroll. /// The scroll content's pixel size. /// The pixel position at which the content begins. /// The scroll's outer bounds. /// The padding between the content and border. public static void DrawScroll(SpriteBatch spriteBatch, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding = 5) { CommonHelper.DrawContentBox( spriteBatch: spriteBatch, texture: CommonSprites.Scroll.Sheet, background: in CommonSprites.Scroll.Background, top: CommonSprites.Scroll.Top, right: CommonSprites.Scroll.Right, bottom: CommonSprites.Scroll.Bottom, left: CommonSprites.Scroll.Left, topLeft: CommonSprites.Scroll.TopLeft, topRight: CommonSprites.Scroll.TopRight, bottomRight: CommonSprites.Scroll.BottomRight, bottomLeft: CommonSprites.Scroll.BottomLeft, position: position, contentSize: contentSize, contentPos: out contentPos, bounds: out bounds, padding: padding ); } /// Draw a generic content box like a scroll or button. /// The sprite batch to which to draw. /// The texture to draw. /// The source rectangle for the background. /// The source rectangle for the top border. /// The source rectangle for the right border. /// The source rectangle for the bottom border. /// The source rectangle for the left border. /// The source rectangle for the top-left corner. /// The source rectangle for the top-right corner. /// The source rectangle for the bottom-right corner. /// The source rectangle for the bottom-left corner. /// The top-left pixel coordinate at which to draw the button. /// The button content's pixel size. /// The pixel position at which the content begins. /// The box's outer bounds. /// The padding between the content and border. public static void DrawContentBox(SpriteBatch spriteBatch, Texture2D texture, in Rectangle background, in Rectangle top, in Rectangle right, in Rectangle bottom, in Rectangle left, in Rectangle topLeft, in Rectangle topRight, in Rectangle bottomRight, in Rectangle bottomLeft, in Vector2 position, in Vector2 contentSize, out Vector2 contentPos, out Rectangle bounds, int padding) { int cornerWidth = topLeft.Width * Game1.pixelZoom; int cornerHeight = topLeft.Height * Game1.pixelZoom; int innerWidth = (int)(contentSize.X + padding * 2); int innerHeight = (int)(contentSize.Y + padding * 2); int outerWidth = innerWidth + cornerWidth * 2; int outerHeight = innerHeight + cornerHeight * 2; int x = (int)position.X; int y = (int)position.Y; // draw scroll background spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y + cornerHeight, innerWidth, innerHeight), background, Color.White); // draw borders spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y, innerWidth, cornerHeight), top, Color.White); spriteBatch.Draw(texture, new Rectangle(x + cornerWidth, y + cornerHeight + innerHeight, innerWidth, cornerHeight), bottom, Color.White); spriteBatch.Draw(texture, new Rectangle(x, y + cornerHeight, cornerWidth, innerHeight), left, Color.White); spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y + cornerHeight, cornerWidth, innerHeight), right, Color.White); // draw corners spriteBatch.Draw(texture, new Rectangle(x, y, cornerWidth, cornerHeight), topLeft, Color.White); spriteBatch.Draw(texture, new Rectangle(x, y + cornerHeight + innerHeight, cornerWidth, cornerHeight), bottomLeft, Color.White); spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y, cornerWidth, cornerHeight), topRight, Color.White); spriteBatch.Draw(texture, new Rectangle(x + cornerWidth + innerWidth, y + cornerHeight + innerHeight, cornerWidth, cornerHeight), bottomRight, Color.White); // set out params contentPos = new Vector2(x + cornerWidth + padding, y + cornerHeight + padding); bounds = new Rectangle(x, y, outerWidth, outerHeight); } /// Show an informational message to the player. /// The message to show. /// The number of milliseconds during which to keep the message on the screen before it fades (or null for the default time). public static void ShowInfoMessage(string message, int? duration = null) { Game1.addHUDMessage(new HUDMessage(message, 3) { noIcon = true, timeLeft = duration ?? HUDMessage.defaultTime }); } /// Show an error message to the player. /// The message to show. public static void ShowErrorMessage(string message) { Game1.addHUDMessage(new HUDMessage(message, 3)); } /**** ** Drawing ****/ /// Draw a sprite to the screen. /// The sprite batch. /// The X-position at which to start the line. /// The X-position at which to start the line. /// The line dimensions. /// The color to tint the sprite. public static void DrawLine(this SpriteBatch batch, float x, float y, in Vector2 size, in Color? color = null) { batch.Draw(CommonHelper.Pixel, new Rectangle((int)x, (int)y, (int)size.X, (int)size.Y), color ?? Color.White); } /// Draw a block of text to the screen with the specified wrap width. /// The sprite batch. /// The sprite font. /// The block of text to write. /// The position at which to draw the text. /// The width at which to wrap the text. /// The text color. /// Whether to draw bold text. /// The font scale. /// Returns the text dimensions. public static Vector2 DrawTextBlock(this SpriteBatch batch, SpriteFont font, string text, in Vector2 position, float wrapWidth, in Color? color = null, bool bold = false, float scale = 1) { if (text == null) return new Vector2(0, 0); // get word list List words = new List(); foreach (string word in text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)) { // split on newlines string wordPart = word; int newlineIndex; while ((newlineIndex = wordPart.IndexOf(Environment.NewLine, StringComparison.InvariantCulture)) >= 0) { if (newlineIndex == 0) { words.Add(Environment.NewLine); wordPart = wordPart.Substring(Environment.NewLine.Length); } else if (newlineIndex > 0) { words.Add(wordPart.Substring(0, newlineIndex)); words.Add(Environment.NewLine); wordPart = wordPart.Substring(newlineIndex + Environment.NewLine.Length); } } // add remaining word (after newline split) if (wordPart.Length > 0) words.Add(wordPart); } // track draw values float xOffset = 0; float yOffset = 0; float lineHeight = font.MeasureString("ABC").Y * scale; float spaceWidth = CommonHelper.GetSpaceWidth(font) * scale; float blockWidth = 0; float blockHeight = lineHeight; foreach (string word in words) { // check wrap width float wordWidth = font.MeasureString(word).X * scale; if (word == Environment.NewLine || ((wordWidth + xOffset) > wrapWidth && (int)xOffset != 0)) { xOffset = 0; yOffset += lineHeight; blockHeight += lineHeight; } if (word == Environment.NewLine) continue; // draw text Vector2 wordPosition = new Vector2(position.X + xOffset, position.Y + yOffset); if (bold) Utility.drawBoldText(batch, word, font, wordPosition, color ?? Color.Black, scale); else batch.DrawString(font, word, wordPosition, color ?? Color.Black, 0, Vector2.Zero, scale, SpriteEffects.None, 1); // update draw values if (xOffset + wordWidth > blockWidth) blockWidth = xOffset + wordWidth; xOffset += wordWidth + spaceWidth; } // return text position & dimensions return new Vector2(blockWidth, blockHeight); } /**** ** Error handling ****/ /// Intercept errors thrown by the action. /// Encapsulates monitoring and logging. /// The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font. /// The action to invoke. /// A callback invoked if an error is intercepted. public static void InterceptErrors(this IMonitor monitor, string verb, Action action, Action onError = null) { monitor.InterceptErrors(verb, null, action, onError); } /// Intercept errors thrown by the action. /// Encapsulates monitoring and logging. /// The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font. /// A more detailed form of if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font. /// The action to invoke. /// A callback invoked if an error is intercepted. public static void InterceptErrors(this IMonitor monitor, string verb, string detailedVerb, Action action, Action onError = null) { try { action(); } catch (Exception ex) { monitor.InterceptError(ex, verb, detailedVerb); onError?.Invoke(ex); } } /// Log an error and warn the user. /// Encapsulates monitoring and logging. /// The exception to handle. /// The verb describing where the error occurred (e.g. "looking that up"). This is displayed on the screen, so it should be simple and avoid characters that might not be available in the sprite font. /// A more detailed form of if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font. public static void InterceptError(this IMonitor monitor, Exception ex, string verb, string detailedVerb = null) { detailedVerb = detailedVerb ?? verb; monitor.Log($"Something went wrong {detailedVerb}:\n{ex}", LogLevel.Error); CommonHelper.ShowErrorMessage($"Huh. Something went wrong {verb}. The error log has the technical details."); } } }