OnDraw Hook Added.

LookupAnything and UI Info Suite MOD
This commit is contained in:
yangzhi 2019-04-12 13:20:42 +08:00
parent 0c2c5fdcf6
commit a22263b70a
161 changed files with 18632 additions and 775 deletions

View File

@ -6,6 +6,7 @@ stsfld StardewValley.ModHooks StardewValley.Game1::hooks
3.Modify class StardewValley.ModHooks, inject method:
public virtual void OnGame1_Update(GameTime time);
public virtual void OnGame1_Draw(GameTime time, RenderTarget2D toBuffer);
public virtual LocalizedContentManager OnGame1_CreateContentManager(IServiceProvider serviceProvider, string rootDirectory) => null;
4.Modify class StardewValley.Game1, modify method Update(GameTime gameTime), insert Instructions at beginning:
@ -18,4 +19,10 @@ ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
ldarg.1
ldarg.2
callvirt StardewValley.LocalizedContentManager StardewValley.ModHooks::OnGame1_CreateContentManager(System.IServiceProvider,System.String)
ret
ret
6.Modify class StardewValley.Game1, modify method Draw(GameTime gameTime, RenderTarget2D toBuffer), modify Instructions at beginning:
ldsfld StardewValley.ModHooks StardewValley.Game1::hooks
ldarg.1
ldnull
callvirt System.Void StardewValley.ModHooks::OnGame1_Draw(Microsoft.Xna.Framework.GameTime,Microsoft.Xna.Framework.Graphics.RenderTarget2D)

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>b9e9edfc-e98a-4370-994f-40a9f39a0284</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="Common.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>b9e9edfc-e98a-4370-994f-40a9f39a0284</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>Common</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)DataParsers\CropDataParser.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\BaseIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\Automate\AutomateIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\Automate\IAutomateApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\LineSprinklers\LineSprinklersIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\LineSprinklers\ILineSprinklersApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\PrismaticTools\PrismaticToolsIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\PrismaticTools\IPrismaticToolsApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\BetterJunimos\BetterJunimosIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\BetterJunimos\IBetterJunimosApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PathUtilities.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\BaseOverlay.cs" />
<Compile Include="$(MSBuildThisFileDirectory)CommonHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\FarmExpansion\FarmExpansionIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\FarmExpansion\IFarmExpansionApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\BetterSprinklers\BetterSprinklersIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\BetterSprinklers\IBetterSprinklersApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\Cobalt\CobaltIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\Cobalt\ICobaltApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\CustomFarmingRedux\CustomFarmingReduxIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\CustomFarmingRedux\ICustomFarmingApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\IModIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\PelicanFiber\PelicanFiberIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\SimpleSprinkler\ISimplerSprinklerApi.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Integrations\SimpleSprinkler\SimpleSprinklerIntegration.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SpriteInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)StringEnumArrayConverter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)TileHelper.cs" />
<Compile Include="$(MSBuildThisFileDirectory)UI\CommonSprites.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utilities\ConstraintSet.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utilities\InvariantDictionary.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utilities\InvariantHashSet.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Utilities\ObjectReferenceComparer.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,345 @@
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
{
/// <summary>Provides common utility methods for interacting with the game code shared by my various mods.</summary>
internal static class CommonHelper
{
/*********
** Fields
*********/
/// <summary>A blank pixel which can be colorised and stretched to draw geometric shapes.</summary>
private static readonly Lazy<Texture2D> LazyPixel = new Lazy<Texture2D>(() =>
{
Texture2D pixel = new Texture2D(Game1.graphics.GraphicsDevice, 1, 1);
pixel.SetData(new[] { Color.White });
return pixel;
});
/*********
** Accessors
*********/
/// <summary>A blank pixel which can be colorised and stretched to draw geometric shapes.</summary>
public static Texture2D Pixel => CommonHelper.LazyPixel.Value;
/// <summary>The width of the horizontal and vertical scroll edges (between the origin position and start of content padding).</summary>
public static readonly Vector2 ScrollEdgeSize = new Vector2(CommonSprites.Scroll.TopLeft.Width * Game1.pixelZoom, CommonSprites.Scroll.TopLeft.Height * Game1.pixelZoom);
/*********
** Public methods
*********/
/****
** Game
****/
/// <summary>Get all game locations.</summary>
public static IEnumerable<GameLocation> GetLocations()
{
return Game1.locations
.Concat(
from location in Game1.locations.OfType<BuildableGameLocation>()
from building in location.buildings
where building.indoors.Value != null
select building.indoors.Value
);
}
/****
** Fonts
****/
/// <summary>Get the dimensions of a space character.</summary>
/// <param name="font">The font to measure.</param>
public static float GetSpaceWidth(SpriteFont font)
{
return font.MeasureString("A B").X - font.MeasureString("AB").X;
}
/****
** UI
****/
/// <summary>Draw a pretty hover box for the given text.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="label">The text to display.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The maximum width to display.</param>
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);
}
/// <summary>Draw a button background.</summary>
/// <param name="spriteBatch">The sprite batch to which to draw.</param>
/// <param name="position">The top-left pixel coordinate at which to draw the button.</param>
/// <param name="contentSize">The button content's pixel size.</param>
/// <param name="contentPos">The pixel position at which the content begins.</param>
/// <param name="bounds">The button's outer bounds.</param>
/// <param name="padding">The padding between the content and border.</param>
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
);
}
/// <summary>Draw a scroll background.</summary>
/// <param name="spriteBatch">The sprite batch to which to draw.</param>
/// <param name="position">The top-left pixel coordinate at which to draw the scroll.</param>
/// <param name="contentSize">The scroll content's pixel size.</param>
/// <param name="contentPos">The pixel position at which the content begins.</param>
/// <param name="bounds">The scroll's outer bounds.</param>
/// <param name="padding">The padding between the content and border.</param>
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
);
}
/// <summary>Draw a generic content box like a scroll or button.</summary>
/// <param name="spriteBatch">The sprite batch to which to draw.</param>
/// <param name="texture">The texture to draw.</param>
/// <param name="background">The source rectangle for the background.</param>
/// <param name="top">The source rectangle for the top border.</param>
/// <param name="right">The source rectangle for the right border.</param>
/// <param name="bottom">The source rectangle for the bottom border.</param>
/// <param name="left">The source rectangle for the left border.</param>
/// <param name="topLeft">The source rectangle for the top-left corner.</param>
/// <param name="topRight">The source rectangle for the top-right corner.</param>
/// <param name="bottomRight">The source rectangle for the bottom-right corner.</param>
/// <param name="bottomLeft">The source rectangle for the bottom-left corner.</param>
/// <param name="position">The top-left pixel coordinate at which to draw the button.</param>
/// <param name="contentSize">The button content's pixel size.</param>
/// <param name="contentPos">The pixel position at which the content begins.</param>
/// <param name="bounds">The box's outer bounds.</param>
/// <param name="padding">The padding between the content and border.</param>
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);
}
/// <summary>Show an informational message to the player.</summary>
/// <param name="message">The message to show.</param>
/// <param name="duration">The number of milliseconds during which to keep the message on the screen before it fades (or <c>null</c> for the default time).</param>
public static void ShowInfoMessage(string message, int? duration = null)
{
Game1.addHUDMessage(new HUDMessage(message, 3) { noIcon = true, timeLeft = duration ?? HUDMessage.defaultTime });
}
/// <summary>Show an error message to the player.</summary>
/// <param name="message">The message to show.</param>
public static void ShowErrorMessage(string message)
{
Game1.addHUDMessage(new HUDMessage(message, 3));
}
/****
** Drawing
****/
/// <summary>Draw a sprite to the screen.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="x">The X-position at which to start the line.</param>
/// <param name="y">The X-position at which to start the line.</param>
/// <param name="size">The line dimensions.</param>
/// <param name="color">The color to tint the sprite.</param>
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);
}
/// <summary>Draw a block of text to the screen with the specified wrap width.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="font">The sprite font.</param>
/// <param name="text">The block of text to write.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The width at which to wrap the text.</param>
/// <param name="color">The text color.</param>
/// <param name="bold">Whether to draw bold text.</param>
/// <param name="scale">The font scale.</param>
/// <returns>Returns the text dimensions.</returns>
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<string> words = new List<string>();
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
****/
/// <summary>Intercept errors thrown by the action.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="verb">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.</param>
/// <param name="action">The action to invoke.</param>
/// <param name="onError">A callback invoked if an error is intercepted.</param>
public static void InterceptErrors(this IMonitor monitor, string verb, Action action, Action<Exception> onError = null)
{
monitor.InterceptErrors(verb, null, action, onError);
}
/// <summary>Intercept errors thrown by the action.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="verb">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.</param>
/// <param name="detailedVerb">A more detailed form of <see cref="verb"/> if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font.</param>
/// <param name="action">The action to invoke.</param>
/// <param name="onError">A callback invoked if an error is intercepted.</param>
public static void InterceptErrors(this IMonitor monitor, string verb, string detailedVerb, Action action, Action<Exception> onError = null)
{
try
{
action();
}
catch (Exception ex)
{
monitor.InterceptError(ex, verb, detailedVerb);
onError?.Invoke(ex);
}
}
/// <summary>Log an error and warn the user.</summary>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
/// <param name="ex">The exception to handle.</param>
/// <param name="verb">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.</param>
/// <param name="detailedVerb">A more detailed form of <see cref="verb"/> if applicable. This is displayed in the log, so it can be more technical and isn't constrained by the sprite font.</param>
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.");
}
}
}

View File

@ -0,0 +1,93 @@
using System;
using System.Linq;
using StardewModdingAPI.Utilities;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Common.DataParsers
{
/// <summary>Analyses crop data for a tile.</summary>
internal class CropDataParser
{
/*********
** Accessors
*********/
/// <summary>The crop.</summary>
public Crop Crop { get; }
/// <summary>The seasons in which the crop grows.</summary>
public string[] Seasons { get; }
/// <summary>The phase index in <see cref="StardewValley.Crop.phaseDays"/> when the crop can be harvested.</summary>
public int HarvestablePhase { get; }
/// <summary>The number of days needed between planting and first harvest.</summary>
public int DaysToFirstHarvest { get; }
/// <summary>The number of days needed between harvests, after the first harvest.</summary>
public int DaysToSubsequentHarvest { get; }
/// <summary>Whether the crop can be harvested multiple times.</summary>
public bool HasMultipleHarvests { get; }
/// <summary>Whether the crop is ready to harvest now.</summary>
public bool CanHarvestNow { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="crop">The crop.</param>
public CropDataParser(Crop crop)
{
this.Crop = crop;
if (crop != null)
{
this.Seasons = crop.seasonsToGrowIn.ToArray();
this.HasMultipleHarvests = crop.regrowAfterHarvest.Value == -1;
this.HarvestablePhase = crop.phaseDays.Count - 1;
this.CanHarvestNow = (crop.currentPhase.Value >= this.HarvestablePhase) && (!crop.fullyGrown.Value || crop.dayOfCurrentPhase.Value <= 0);
this.DaysToFirstHarvest = crop.phaseDays.Take(crop.phaseDays.Count - 1).Sum(); // ignore harvestable phase
this.DaysToSubsequentHarvest = crop.regrowAfterHarvest.Value;
}
}
/// <summary>Get the date when the crop will next be ready to harvest.</summary>
public SDate GetNextHarvest()
{
// get crop
Crop crop = this.Crop;
if (crop == null)
throw new InvalidOperationException("Can't get the harvest date because there's no crop.");
// ready now
if (this.CanHarvestNow)
return SDate.Now();
// growing: days until next harvest
if (!crop.fullyGrown.Value)
{
int daysUntilLastPhase = this.DaysToFirstHarvest - this.Crop.dayOfCurrentPhase.Value - crop.phaseDays.Take(crop.currentPhase.Value).Sum();
return SDate.Now().AddDays(daysUntilLastPhase);
}
// regrowable crop harvested today
if (crop.dayOfCurrentPhase.Value >= crop.regrowAfterHarvest.Value)
return SDate.Now().AddDays(crop.regrowAfterHarvest.Value);
// regrowable crop
// dayOfCurrentPhase decreases to 0 when fully grown, where <=0 is harvestable
return SDate.Now().AddDays(crop.dayOfCurrentPhase.Value);
}
/// <summary>Get a sample item acquired by harvesting the crop.</summary>
public Item GetSampleDrop()
{
if (this.Crop == null)
throw new InvalidOperationException("Can't get a sample drop because there's no crop.");
return new SObject(this.Crop.indexOfHarvest.Value, 1);
}
}
}

View File

@ -0,0 +1,44 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.Automate
{
/// <summary>Handles the logic for integrating with the Automate mod.</summary>
internal class AutomateIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IAutomateApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public AutomateIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Automate", "Pathoschild.Automate", "1.11.0", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IAutomateApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
public IDictionary<Vector2, int> GetMachineStates(GameLocation location, Rectangle tileArea)
{
this.AssertLoaded();
return this.ModApi.GetMachineStates(location, tileArea);
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.Automate
{
/// <summary>The API provided by the Automate mod.</summary>
public interface IAutomateApi
{
/// <summary>Get the status of machines in a tile area. This is a specialised API for Data Layers and similar mods.</summary>
/// <param name="location">The location for which to display data.</param>
/// <param name="tileArea">The tile area for which to display data.</param>
IDictionary<Vector2, int> GetMachineStates(GameLocation location, Rectangle tileArea);
}
}

View File

@ -0,0 +1,82 @@
using System;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations
{
/// <summary>The base implementation for a mod integration.</summary>
internal abstract class BaseIntegration : IModIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's unique ID.</summary>
protected string ModID { get; }
/// <summary>An API for fetching metadata about loaded mods.</summary>
protected IModRegistry ModRegistry { get; }
/// <summary>Encapsulates monitoring and logging.</summary>
protected IMonitor Monitor { get; }
/*********
** Accessors
*********/
/// <summary>A human-readable name for the mod.</summary>
public string Label { get; }
/// <summary>Whether the mod is available.</summary>
public bool IsLoaded { get; protected set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="label">A human-readable name for the mod.</param>
/// <param name="modID">The mod's unique ID.</param>
/// <param name="minVersion">The minimum version of the mod that's supported.</param>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
protected BaseIntegration(string label, string modID, string minVersion, IModRegistry modRegistry, IMonitor monitor)
{
// init
this.Label = label;
this.ModID = modID;
this.ModRegistry = modRegistry;
this.Monitor = monitor;
// validate mod
IManifest manifest = modRegistry.Get(this.ModID)?.Manifest;
if (manifest == null)
return;
if (manifest.Version.IsOlderThan(minVersion))
{
monitor.Log($"Detected {label} {manifest.Version}, but need {minVersion} or later. Disabled integration with this mod.", LogLevel.Warn);
return;
}
this.IsLoaded = true;
}
/// <summary>Get an API for the mod, and show a message if it can't be loaded.</summary>
/// <typeparam name="TInterface">The API type.</typeparam>
protected TInterface GetValidatedApi<TInterface>() where TInterface : class
{
TInterface api = this.ModRegistry.GetApi<TInterface>(this.ModID);
if (api == null)
{
this.Monitor.Log($"Detected {this.Label}, but couldn't fetch its API. Disabled integration with this mod.", LogLevel.Warn);
return null;
}
return api;
}
/// <summary>Assert that the integration is loaded.</summary>
/// <exception cref="InvalidOperationException">The integration isn't loaded.</exception>
protected void AssertLoaded()
{
if (!this.IsLoaded)
throw new InvalidOperationException($"The {this.Label} integration isn't loaded.");
}
}
}

View File

@ -0,0 +1,40 @@
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.BetterJunimos
{
/// <summary>Handles the logic for integrating with the Better Junimos mod.</summary>
internal class BetterJunimosIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IBetterJunimosApi ModApi;
/*********
** Accessors
*********/
/// <summary>The Junimo Hut coverage radius.</summary>
public int MaxRadius { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public BetterJunimosIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Better Junimos", "hawkfalcon.BetterJunimos", "0.5.0", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IBetterJunimosApi>();
this.IsLoaded = this.ModApi != null;
this.MaxRadius = this.ModApi?.GetJunimoHutMaxRadius() ?? 0;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Pathoschild.Stardew.Common.Integrations.BetterJunimos
{
/// <summary>The API provided by the Better Junimos mod.</summary>
public interface IBetterJunimosApi
{
/// <summary>Get the maximum radius for Junimo Huts.</summary>
int GetJunimoHutMaxRadius();
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.BetterSprinklers
{
/// <summary>Handles the logic for integrating with the Better Sprinklers mod.</summary>
internal class BetterSprinklersIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IBetterSprinklersApi ModApi;
/*********
** Accessors
*********/
/// <summary>The maximum possible sprinkler radius.</summary>
public int MaxRadius { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public BetterSprinklersIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Better Sprinklers", "Speeder.BetterSprinklers", "2.3.1-unofficial.6-pathoschild", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IBetterSprinklersApi>();
this.IsLoaded = this.ModApi != null;
this.MaxRadius = this.ModApi?.GetMaxGridSize() ?? 0;
}
/// <summary>Get the configured Sprinkler tiles relative to (0, 0).</summary>
public IDictionary<int, Vector2[]> GetSprinklerTiles()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerCoverage();
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.BetterSprinklers
{
/// <summary>The API provided by the Better Sprinklers mod.</summary>
public interface IBetterSprinklersApi
{
/// <summary>Get the maximum supported coverage width or height.</summary>
int GetMaxGridSize();
/// <summary>Get the relative tile coverage by supported sprinkler ID.</summary>
IDictionary<int, Vector2[]> GetSprinklerCoverage();
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.Cobalt
{
/// <summary>Handles the logic for integrating with the Cobalt mod.</summary>
internal class CobaltIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly ICobaltApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public CobaltIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Cobalt", "spacechase0.Cobalt", "1.1", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<ICobaltApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get the cobalt sprinkler's object ID.</summary>
public int GetSprinklerId()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerId();
}
/// <summary>Get the configured Sprinkler tiles relative to (0, 0).</summary>
public IEnumerable<Vector2> GetSprinklerTiles()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerCoverage(Vector2.Zero);
}
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.Cobalt
{
/// <summary>The API provided by the Cobalt mod.</summary>
public interface ICobaltApi
{
/*********
** Public methods
*********/
/// <summary>Get the cobalt sprinkler's object ID.</summary>
int GetSprinklerId();
/// <summary>Get the cobalt sprinkler coverage.</summary>
/// <param name="origin">The tile position containing the sprinkler.</param>
IEnumerable<Vector2> GetSprinklerCoverage(Vector2 origin);
}
}

View File

@ -0,0 +1,49 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewModdingAPI;
using StardewValley;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.Common.Integrations.CustomFarmingRedux
{
/// <summary>Handles the logic for integrating with the Custom Farming Redux mod.</summary>
internal class CustomFarmingReduxIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly ICustomFarmingApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public CustomFarmingReduxIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Custom Farming Redux", "Platonymous.CustomFarming", "2.8.5", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<ICustomFarmingApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get the sprite info for a custom object, or <c>null</c> if the object isn't custom.</summary>
/// <param name="obj">The custom object.</param>
public SpriteInfo GetSprite(SObject obj)
{
this.AssertLoaded();
Tuple<Item, Texture2D, Rectangle, Color> data = this.ModApi.getRealItemAndTexture(obj);
return data != null
? new SpriteInfo(data.Item2, data.Item3)
: null;
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.CustomFarmingRedux
{
/// <summary>The API provided by the Custom Farming Redux mod.</summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "The naming convention is defined by the Custom Farming Redux mod.")]
public interface ICustomFarmingApi
{
/*********
** Public methods
*********/
/// <summary>Get metadata for a custom machine and draw metadata for an object.</summary>
/// <param name="dummy">The item that would be replaced by the custom item.</param>
Tuple<Item, Texture2D, Rectangle, Color> getRealItemAndTexture(StardewValley.Object dummy);
}
}

View File

@ -0,0 +1,49 @@
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.FarmExpansion
{
/// <summary>Handles the logic for integrating with the Farm Expansion mod.</summary>
internal class FarmExpansionIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IFarmExpansionApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public FarmExpansionIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Farm Expansion", "Advize.FarmExpansion", "3.3", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IFarmExpansionApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Add a blueprint to all future carpenter menus for the farm area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
public void AddFarmBluePrint(BluePrint blueprint)
{
this.AssertLoaded();
this.ModApi.AddFarmBluePrint(blueprint);
}
/// <summary>Add a blueprint to all future carpenter menus for the expansion area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
public void AddExpansionBluePrint(BluePrint blueprint)
{
this.AssertLoaded();
this.ModApi.AddExpansionBluePrint(blueprint);
}
}
}

View File

@ -0,0 +1,16 @@
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.FarmExpansion
{
/// <summary>The API provided by the Farm Expansion mod.</summary>
public interface IFarmExpansionApi
{
/// <summary>Add a blueprint to all future carpenter menus for the farm area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
void AddFarmBluePrint(BluePrint blueprint);
/// <summary>Add a blueprint to all future carpenter menus for the expansion area.</summary>
/// <param name="blueprint">The blueprint to add.</param>
void AddExpansionBluePrint(BluePrint blueprint);
}
}

View File

@ -0,0 +1,15 @@
namespace Pathoschild.Stardew.Common.Integrations
{
/// <summary>Handles integration with a given mod.</summary>
internal interface IModIntegration
{
/*********
** Accessors
*********/
/// <summary>A human-readable name for the mod.</summary>
string Label { get; }
/// <summary>Whether the mod is available.</summary>
bool IsLoaded { get; }
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.LineSprinklers
{
/// <summary>The API provided by the Line Sprinklers mod.</summary>
public interface ILineSprinklersApi
{
/// <summary>Get the maximum supported coverage width or height.</summary>
int GetMaxGridSize();
/// <summary>Get the relative tile coverage by supported sprinkler ID.</summary>
IDictionary<int, Vector2[]> GetSprinklerCoverage();
}
}

View File

@ -0,0 +1,49 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.LineSprinklers
{
/// <summary>Handles the logic for integrating with the Line Sprinklers mod.</summary>
internal class LineSprinklersIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly ILineSprinklersApi ModApi;
/*********
** Accessors
*********/
/// <summary>The maximum possible sprinkler radius.</summary>
public int MaxRadius { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public LineSprinklersIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Line Sprinklers", "hootless.LineSprinklers", "1.1.0", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<ILineSprinklersApi>();
this.IsLoaded = this.ModApi != null;
this.MaxRadius = this.ModApi?.GetMaxGridSize() ?? 0;
}
/// <summary>Get the configured Sprinkler tiles relative to (0, 0).</summary>
public IDictionary<int, Vector2[]> GetSprinklerTiles()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerCoverage();
}
}
}

View File

@ -0,0 +1,49 @@
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.Common.Integrations.PelicanFiber
{
/// <summary>Handles the logic for integrating with the Pelican Fiber mod.</summary>
internal class PelicanFiberIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The full type name of the Pelican Fiber mod's build menu.</summary>
private readonly string MenuTypeName = "PelicanFiber.Framework.ConstructionMenu";
/// <summary>An API for accessing private code.</summary>
private readonly IReflectionHelper Reflection;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="reflection">An API for accessing private code.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public PelicanFiberIntegration(IModRegistry modRegistry, IReflectionHelper reflection, IMonitor monitor)
: base("Pelican Fiber", "jwdred.PelicanFiber", "3.0.2", modRegistry, monitor)
{
this.Reflection = reflection;
}
/// <summary>Get whether the Pelican Fiber build menu is open.</summary>
public bool IsBuildMenuOpen()
{
this.AssertLoaded();
return Game1.activeClickableMenu?.GetType().FullName == this.MenuTypeName;
}
/// <summary>Get the selected blueprint from the Pelican Fiber build menu, if it's open.</summary>
public BluePrint GetBuildMenuBlueprint()
{
this.AssertLoaded();
if (!this.IsBuildMenuOpen())
return null;
return this.Reflection.GetProperty<BluePrint>(Game1.activeClickableMenu, "CurrentBlueprint").GetValue();
}
}
}

View File

@ -0,0 +1,19 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.PrismaticTools
{
/// <summary>The API provided by the Prismatic Tools mod.</summary>
public interface IPrismaticToolsApi
{
/// <summary>Whether prismatic sprinklers also act as scarecrows.</summary>
bool ArePrismaticSprinklersScarecrows { get; }
/// <summary>The prismatic sprinkler object ID.</summary>
int SprinklerIndex { get; }
/// <summary>Get the relative tile coverage for a prismatic sprinkler.</summary>
/// <param name="origin">The sprinkler tile.</param>
IEnumerable<Vector2> GetSprinklerCoverage(Vector2 origin);
}
}

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.PrismaticTools
{
/// <summary>Handles the logic for integrating with the Prismatic Tools mod.</summary>
internal class PrismaticToolsIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly IPrismaticToolsApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public PrismaticToolsIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Prismatic Tools", "stokastic.PrismaticTools", "1.3.0", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<IPrismaticToolsApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get whether prismatic sprinklers also act as scarecrows.</summary>
public bool ArePrismaticSprinklersScarecrows()
{
this.AssertLoaded();
return this.ModApi.ArePrismaticSprinklersScarecrows;
}
/// <summary>Get the prismatic sprinkler object ID.</summary>
public int GetSprinklerID()
{
this.AssertLoaded();
return this.ModApi.SprinklerIndex;
}
/// <summary>Get the relative tile coverage for a prismatic sprinkler.</summary>
public IEnumerable<Vector2> GetSprinklerCoverage()
{
this.AssertLoaded();
return this.ModApi.GetSprinklerCoverage(Vector2.Zero);
}
}
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.Common.Integrations.SimpleSprinkler
{
/// <summary>The API provided by the Simple Sprinkler mod.</summary>
public interface ISimplerSprinklerApi
{
/// <summary>Get the relative tile coverage for supported sprinkler IDs (additive to the game's default coverage).</summary>
IDictionary<int, Vector2[]> GetNewSprinklerCoverage();
}
}

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using StardewModdingAPI;
namespace Pathoschild.Stardew.Common.Integrations.SimpleSprinkler
{
/// <summary>Handles the logic for integrating with the Simple Sprinkler mod.</summary>
internal class SimpleSprinklerIntegration : BaseIntegration
{
/*********
** Fields
*********/
/// <summary>The mod's public API.</summary>
private readonly ISimplerSprinklerApi ModApi;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="modRegistry">An API for fetching metadata about loaded mods.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public SimpleSprinklerIntegration(IModRegistry modRegistry, IMonitor monitor)
: base("Simple Sprinklers", "tZed.SimpleSprinkler", "1.6.0", modRegistry, monitor)
{
if (!this.IsLoaded)
return;
// get mod API
this.ModApi = this.GetValidatedApi<ISimplerSprinklerApi>();
this.IsLoaded = this.ModApi != null;
}
/// <summary>Get the Sprinkler tiles relative to (0, 0), additive to the game's default sprinkler coverage.</summary>
public IDictionary<int, Vector2[]> GetNewSprinklerTiles()
{
this.AssertLoaded();
return this.ModApi.GetNewSprinklerCoverage();
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
namespace Pathoschild.Stardew.Common
{
/// <summary>Provides utilities for normalising file paths.</summary>
/// <remarks>This class is duplicated from <c>StardewModdingAPI.Toolkit.Utilities</c>.</remarks>
internal static class PathUtilities
{
/*********
** Fields
*********/
/// <summary>The possible directory separator characters in a file path.</summary>
private static readonly char[] PossiblePathSeparators = new[] { '/', '\\', Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.Distinct().ToArray();
/// <summary>The preferred directory separator chaeacter in an asset key.</summary>
private static readonly string PreferredPathSeparator = Path.DirectorySeparatorChar.ToString();
/*********
** Public methods
*********/
/// <summary>Get the segments from a path (e.g. <c>/usr/bin/boop</c> => <c>usr</c>, <c>bin</c>, and <c>boop</c>).</summary>
/// <param name="path">The path to split.</param>
/// <param name="limit">The number of segments to match. Any additional segments will be merged into the last returned part.</param>
public static string[] GetSegments(string path, int? limit = null)
{
return limit.HasValue
? path.Split(PathUtilities.PossiblePathSeparators, limit.Value, StringSplitOptions.RemoveEmptyEntries)
: path.Split(PathUtilities.PossiblePathSeparators, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>Normalise path separators in a file path.</summary>
/// <param name="path">The file path to normalise.</param>
[Pure]
public static string NormalisePathSeparators(string path)
{
string[] parts = PathUtilities.GetSegments(path);
string normalised = string.Join(PathUtilities.PreferredPathSeparator, parts);
if (path.StartsWith(PathUtilities.PreferredPathSeparator))
normalised = PathUtilities.PreferredPathSeparator + normalised; // keep root slash
return normalised;
}
/// <summary>Get a directory or file path relative to a given source path.</summary>
/// <param name="sourceDir">The source folder path.</param>
/// <param name="targetPath">The target folder or file path.</param>
[Pure]
public static string GetRelativePath(string sourceDir, string targetPath)
{
// convert to URIs
Uri from = new Uri(sourceDir.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
Uri to = new Uri(targetPath.TrimEnd(PathUtilities.PossiblePathSeparators) + "/");
if (from.Scheme != to.Scheme)
throw new InvalidOperationException($"Can't get path for '{targetPath}' relative to '{sourceDir}'.");
// get relative path
string relative = PathUtilities.NormalisePathSeparators(Uri.UnescapeDataString(from.MakeRelativeUri(to).ToString()));
if (relative == "")
relative = "./";
return relative;
}
/// <summary>Get whether a path is relative and doesn't try to climb out of its containing folder (e.g. doesn't contain <c>../</c>).</summary>
/// <param name="path">The path to check.</param>
public static bool IsSafeRelativePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
return true;
return
!Path.IsPathRooted(path)
&& PathUtilities.GetSegments(path).All(segment => segment.Trim() != "..");
}
/// <summary>Get whether a string is a valid 'slug', containing only basic characters that are safe in all contexts (e.g. filenames, URLs, etc).</summary>
/// <param name="str">The string to check.</param>
public static bool IsSlug(string str)
{
return !Regex.IsMatch(str, "[^a-z0-9_.-]", RegexOptions.IgnoreCase);
}
}
}

View File

@ -0,0 +1,31 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Pathoschild.Stardew.Common
{
/// <summary>Represents a single sprite in a spritesheet.</summary>
internal class SpriteInfo
{
/*********
** Accessors
*********/
/// <summary>The spritesheet texture.</summary>
public Texture2D Spritesheet { get; }
/// <summary>The area in the spritesheet containing the sprite.</summary>
public Rectangle SourceRectangle { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="spritesheet">The spritesheet texture.</param>
/// <param name="sourceRectangle">The area in the spritesheet containing the sprite.</param>
public SpriteInfo(Texture2D spritesheet, Rectangle sourceRectangle)
{
this.Spritesheet = spritesheet;
this.SourceRectangle = sourceRectangle;
}
}
}

View File

@ -0,0 +1,153 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
namespace Pathoschild.Stardew.Common
{
/// <summary>A variant of <see cref="StringEnumConverter"/> which represents arrays in JSON as a comma-delimited string.</summary>
internal class StringEnumArrayConverter : StringEnumConverter
{
/*********
** Fields
*********/
/// <summary>Whether to return null values for missing data instead of an empty array.</summary>
public bool AllowNull { get; set; }
/*********
** Public methods
*********/
/// <summary>Get whether this instance can convert the specified object type.</summary>
/// <param name="type">The object type.</param>
public override bool CanConvert(Type type)
{
if (!type.IsArray)
return false;
Type elementType = this.GetElementType(type);
return elementType != null && base.CanConvert(elementType);
}
/// <summary>Read a JSON representation.</summary>
/// <param name="reader">The JSON reader from which to read.</param>
/// <param name="valueType">The value type.</param>
/// <param name="rawValue">The raw value of the object being read.</param>
/// <param name="serializer">The calling serializer.</param>
public override object ReadJson(JsonReader reader, Type valueType, object rawValue, JsonSerializer serializer)
{
// get element type
Type elementType = this.GetElementType(valueType);
if (elementType == null)
throw new InvalidOperationException("Couldn't extract enum array element type."); // should never happen since we validate in CanConvert
// parse
switch (reader.TokenType)
{
case JsonToken.Null:
return this.GetNullOrEmptyArray(elementType);
case JsonToken.StartArray:
{
string[] elements = JArray.Load(reader).Values<string>().ToArray();
object[] parsed = elements.Select(raw => this.ParseOne(raw, elementType)).ToArray();
return this.Cast(parsed, elementType);
}
case JsonToken.String:
{
string value = (string)JToken.Load(reader);
if (string.IsNullOrWhiteSpace(value))
return this.GetNullOrEmptyArray(elementType);
object[] parsed = this.ParseMany(value, elementType).ToArray();
return this.Cast(parsed, elementType);
}
default:
return base.ReadJson(reader, valueType, rawValue, serializer);
}
}
/// <summary>Write a JSON representation.</summary>
/// <param name="writer">The JSON writer to which to write.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
if (value == null)
writer.WriteNull();
else if (value is IEnumerable list)
{
string[] array = (from object element in list where element != null select element.ToString()).ToArray();
writer.WriteValue(string.Join(", ", array));
}
else
base.WriteJson(writer, value, serializer);
}
/*********
** Private methods
*********/
/// <summary>Get the underlying array element type (bypassing <see cref="Nullable"/> if necessary).</summary>
/// <param name="type">The array type.</param>
private Type GetElementType(Type type)
{
if (!type.IsArray)
return null;
type = type.GetElementType();
if (type == null)
return null;
type = Nullable.GetUnderlyingType(type) ?? type;
return type;
}
/// <summary>Parse a string into individual values.</summary>
/// <param name="input">The input string.</param>
/// <param name="elementType">The enum type.</param>
private IEnumerable<object> ParseMany(string input, Type elementType)
{
string[] values = input.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string value in values)
yield return this.ParseOne(value, elementType);
}
/// <summary>Parse a string into one value.</summary>
/// <param name="input">The input string.</param>
/// <param name="elementType">The enum type.</param>
private object ParseOne(string input, Type elementType)
{
return Enum.Parse(elementType, input, ignoreCase: true);
}
/// <summary>Get <c>null</c> or an empty array, depending on the value of <see cref="AllowNull"/>.</summary>
/// <param name="elementType">The enum type.</param>
private Array GetNullOrEmptyArray(Type elementType)
{
return this.AllowNull
? null
: Array.CreateInstance(elementType, 0);
}
/// <summary>Create an array of elements with the given type.</summary>
/// <param name="elements">The array elements.</param>
/// <param name="elementType">The array element type.</param>
private Array Cast(object[] elements, Type elementType)
{
if (elements == null)
return null;
Array result = Array.CreateInstance(elementType, elements.Length);
Array.Copy(elements, result, result.Length);
return result;
}
}
}

View File

@ -0,0 +1,126 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using StardewValley;
using xTile.Layers;
namespace Pathoschild.Stardew.Common
{
/// <summary>Provides extension methods for working with tiles.</summary>
internal static class TileHelper
{
/*********
** Public methods
*********/
/****
** Location
****/
/// <summary>Get the tile coordinates in the game location.</summary>
/// <param name="location">The game location to search.</param>
public static IEnumerable<Vector2> GetTiles(this GameLocation location)
{
if (location?.Map?.Layers == null)
return Enumerable.Empty<Vector2>();
Layer layer = location.Map.Layers[0];
return TileHelper.GetTiles(0, 0, layer.LayerWidth, layer.LayerHeight);
}
/****
** Rectangle
****/
/// <summary>Get the tile coordinates in the tile area.</summary>
/// <param name="area">The tile area to search.</param>
public static IEnumerable<Vector2> GetTiles(this Rectangle area)
{
return TileHelper.GetTiles(area.X, area.Y, area.Width, area.Height);
}
/// <summary>Expand a rectangle equally in all directions.</summary>
/// <param name="area">The rectangle to expand.</param>
/// <param name="distance">The number of tiles to add in each direction.</param>
public static Rectangle Expand(this Rectangle area, int distance)
{
return new Rectangle(area.X - distance, area.Y - distance, area.Width + distance * 2, area.Height + distance * 2);
}
/****
** Tiles
****/
/// <summary>Get the eight tiles surrounding the given tile.</summary>
/// <param name="tile">The center tile.</param>
public static IEnumerable<Vector2> GetSurroundingTiles(this Vector2 tile)
{
return Utility.getSurroundingTileLocationsArray(tile);
}
/// <summary>Get the tiles surrounding the given tile area.</summary>
/// <param name="area">The center tile area.</param>
public static IEnumerable<Vector2> GetSurroundingTiles(this Rectangle area)
{
for (int x = area.X - 1; x <= area.X + area.Width; x++)
{
for (int y = area.Y - 1; y <= area.Y + area.Height; y++)
{
if (!area.Contains(x, y))
yield return new Vector2(x, y);
}
}
}
/// <summary>Get the four tiles adjacent to the given tile.</summary>
/// <param name="tile">The center tile.</param>
public static IEnumerable<Vector2> GetAdjacentTiles(this Vector2 tile)
{
return Utility.getAdjacentTileLocationsArray(tile);
}
/// <summary>Get a rectangular grid of tiles.</summary>
/// <param name="x">The X coordinate of the top-left tile.</param>
/// <param name="y">The Y coordinate of the top-left tile.</param>
/// <param name="width">The grid width.</param>
/// <param name="height">The grid height.</param>
public static IEnumerable<Vector2> GetTiles(int x, int y, int width, int height)
{
for (int curX = x, maxX = x + width - 1; curX <= maxX; curX++)
{
for (int curY = y, maxY = y + height - 1; curY <= maxY; curY++)
yield return new Vector2(curX, curY);
}
}
/// <summary>Get all tiles which are on-screen.</summary>
public static IEnumerable<Vector2> GetVisibleTiles()
{
return TileHelper.GetVisibleArea().GetTiles();
}
/// <summary>Get the tile area visible on-screen.</summary>
public static Rectangle GetVisibleArea()
{
return new Rectangle(
x: Game1.viewport.X / Game1.tileSize,
y: Game1.viewport.Y / Game1.tileSize,
width: (int)(Game1.viewport.Width / (decimal)Game1.tileSize) + 2, // extend off-screen slightly to avoid edges popping in
height: (int)(Game1.viewport.Height / (decimal)Game1.tileSize) + 2
);
}
/****
** Cursor
****/
/// <summary>Get the tile under the player's cursor (not restricted to the player's grab tile range).</summary>
public static Vector2 GetTileFromCursor()
{
return TileHelper.GetTileFromScreenPosition(Game1.getMouseX(), Game1.getMouseY());
}
/// <summary>Get the tile at the pixel coordinate relative to the top-left corner of the screen.</summary>
/// <param name="x">The pixel X coordinate.</param>
/// <param name="y">The pixel Y coordinate.</param>
public static Vector2 GetTileFromScreenPosition(float x, float y)
{
return new Vector2((int)((Game1.viewport.X + x) / Game1.tileSize), (int)((Game1.viewport.Y + y) / Game1.tileSize));
}
}
}

View File

@ -0,0 +1,214 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using StardewModdingAPI;
using StardewModdingAPI.Events;
using StardewValley;
using Rectangle = xTile.Dimensions.Rectangle;
namespace Pathoschild.Stardew.Common.UI
{
/// <summary>An interface which supports user interaction and overlays the active menu (if any).</summary>
internal abstract class BaseOverlay : IDisposable
{
/*********
** Fields
*********/
/// <summary>The SMAPI events available for mods.</summary>
private readonly IModEvents Events;
/// <summary>An API for checking and changing input state.</summary>
protected readonly IInputHelper InputHelper;
/// <summary>The last viewport bounds.</summary>
private Rectangle LastViewport;
/// <summary>Indicates whether to keep the overlay active. If <c>null</c>, the overlay is kept until explicitly disposed.</summary>
private readonly Func<bool> KeepAliveCheck;
/*********
** Public methods
*********/
/// <summary>Release all resources.</summary>
public virtual void Dispose()
{
this.Events.Display.Rendered -= this.OnRendered;
this.Events.GameLoop.UpdateTicked -= this.OnUpdateTicked;
this.Events.Input.ButtonPressed -= this.OnButtonPressed;
this.Events.Input.CursorMoved -= this.OnCursorMoved;
this.Events.Input.MouseWheelScrolled -= this.OnMouseWheelScrolled;
}
/*********
** Protected methods
*********/
/****
** Implementation
****/
/// <summary>Construct an instance.</summary>
/// <param name="events">The SMAPI events available for mods.</param>
/// <param name="inputHelper">An API for checking and changing input state.</param>
/// <param name="keepAlive">Indicates whether to keep the overlay active. If <c>null</c>, the overlay is kept until explicitly disposed.</param>
protected BaseOverlay(IModEvents events, IInputHelper inputHelper, Func<bool> keepAlive = null)
{
this.Events = events;
this.InputHelper = inputHelper;
this.KeepAliveCheck = keepAlive;
this.LastViewport = new Rectangle(Game1.viewport.X, Game1.viewport.Y, Game1.viewport.Width, Game1.viewport.Height);
events.Display.Rendered += this.OnRendered;
events.GameLoop.UpdateTicked += this.OnUpdateTicked;
events.Input.ButtonPressed += this.OnButtonPressed;
events.Input.CursorMoved += this.OnCursorMoved;
events.Input.MouseWheelScrolled += this.OnMouseWheelScrolled;
}
/// <summary>Draw the overlay to the screen.</summary>
/// <param name="batch">The sprite batch being drawn.</param>
protected virtual void Draw(SpriteBatch batch) { }
/// <summary>The method invoked when the player left-clicks.</summary>
/// <param name="x">The X-position of the cursor.</param>
/// <param name="y">The Y-position of the cursor.</param>
/// <returns>Whether the event has been handled and shouldn't be propagated further.</returns>
protected virtual bool ReceiveLeftClick(int x, int y)
{
return false;
}
/// <summary>The method invoked when the player presses a button.</summary>
/// <param name="input">The button that was pressed.</param>
/// <returns>Whether the event has been handled and shouldn't be propagated further.</returns>
protected virtual bool ReceiveButtonPress(SButton input)
{
return false;
}
/// <summary>The method invoked when the player uses the mouse scroll wheel.</summary>
/// <param name="amount">The scroll amount.</param>
/// <returns>Whether the event has been handled and shouldn't be propagated further.</returns>
protected virtual bool ReceiveScrollWheelAction(int amount)
{
return false;
}
/// <summary>The method invoked when the cursor is hovered.</summary>
/// <param name="x">The cursor's X position.</param>
/// <param name="y">The cursor's Y position.</param>
/// <returns>Whether the event has been handled and shouldn't be propagated further.</returns>
protected virtual bool ReceiveCursorHover(int x, int y)
{
return false;
}
/// <summary>The method invoked when the player resizes the game windoww.</summary>
/// <param name="oldBounds">The previous game window bounds.</param>
/// <param name="newBounds">The new game window bounds.</param>
protected virtual void ReceiveGameWindowResized(Rectangle oldBounds, Rectangle newBounds) { }
/// <summary>Draw the mouse cursor.</summary>
/// <remarks>Derived from <see cref="StardewValley.Menus.IClickableMenu.drawMouse"/>.</remarks>
protected void DrawCursor()
{
if (Game1.options.hardwareCursor)
return;
Game1.spriteBatch.Draw(Game1.mouseCursors, new Vector2(Game1.getMouseX(), Game1.getMouseY()), Game1.getSourceRectForStandardTileSheet(Game1.mouseCursors, Game1.options.SnappyMenus ? 44 : 0, 16, 16), Color.White * Game1.mouseCursorTransparency, 0.0f, Vector2.Zero, Game1.pixelZoom + Game1.dialogueButtonScale / 150f, SpriteEffects.None, 1f);
}
/****
** Event listeners
****/
/// <summary>The method called when the game finishes drawing components to the screen.</summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void OnRendered(object sender, RenderedEventArgs e)
{
this.Draw(Game1.spriteBatch);
}
/// <summary>The method called once per event tick.</summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void OnUpdateTicked(object sender, UpdateTickedEventArgs e)
{
// detect end of life
if (this.KeepAliveCheck != null && !this.KeepAliveCheck())
{
this.Dispose();
return;
}
// trigger window resize event
Rectangle newViewport = Game1.viewport;
if (this.LastViewport.Width != newViewport.Width || this.LastViewport.Height != newViewport.Height)
{
newViewport = new Rectangle(newViewport.X, newViewport.Y, newViewport.Width, newViewport.Height);
this.ReceiveGameWindowResized(this.LastViewport, newViewport);
this.LastViewport = newViewport;
}
}
/// <summary>The method invoked when the player presses a key.</summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void OnButtonPressed(object sender, ButtonPressedEventArgs e)
{
bool handled = e.Button == SButton.MouseLeft || e.Button.IsUseToolButton()
? this.ReceiveLeftClick(Game1.getMouseX(), Game1.getMouseY())
: this.ReceiveButtonPress(e.Button);
if (handled)
this.InputHelper.Suppress(e.Button);
}
/// <summary>The method invoked when the mouse wheel is scrolled.</summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void OnMouseWheelScrolled(object sender, MouseWheelScrolledEventArgs e)
{
bool scrollHandled = this.ReceiveScrollWheelAction(e.Delta);
if (scrollHandled)
{
MouseState cur = Game1.oldMouseState;
Game1.oldMouseState = new MouseState(
x: cur.X,
y: cur.Y,
scrollWheel: e.NewValue,
leftButton: cur.LeftButton,
middleButton: cur.MiddleButton,
rightButton: cur.RightButton,
xButton1: cur.XButton1,
xButton2: cur.XButton2
);
}
}
/// <summary>The method invoked when the in-game cursor is moved.</summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The event arguments.</param>
private void OnCursorMoved(object sender, CursorMovedEventArgs e)
{
int x = (int)e.NewPosition.ScreenPixels.X;
int y = (int)e.NewPosition.ScreenPixels.Y;
bool hoverHandled = this.ReceiveCursorHover(x, y);
if (hoverHandled)
{
MouseState cur = Game1.oldMouseState;
Game1.oldMouseState = new MouseState(
x: x,
y: y,
scrollWheel: cur.ScrollWheelValue,
leftButton: cur.LeftButton,
middleButton: cur.MiddleButton,
rightButton: cur.RightButton,
xButton1: cur.XButton1,
xButton2: cur.XButton2
);
}
}
}
}

View File

@ -0,0 +1,79 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using StardewValley;
namespace Pathoschild.Stardew.Common.UI
{
/// <summary>Simplifies access to the game's sprite sheets.</summary>
/// <remarks>Each sprite is represented by a rectangle, which specifies the coordinates and dimensions of the image in the sprite sheet.</remarks>
internal static class CommonSprites
{
/// <summary>Sprites used to draw a button.</summary>
public static class Button
{
/// <summary>The sprite sheet containing the icon sprites.</summary>
public static Texture2D Sheet => Game1.mouseCursors;
/// <summary>The legend background.</summary>
public static readonly Rectangle Background = new Rectangle(297, 364, 1, 1);
/// <summary>The top border.</summary>
public static readonly Rectangle Top = new Rectangle(279, 284, 1, 4);
/// <summary>The bottom border.</summary>
public static readonly Rectangle Bottom = new Rectangle(279, 296, 1, 4);
/// <summary>The left border.</summary>
public static readonly Rectangle Left = new Rectangle(274, 289, 4, 1);
/// <summary>The right border.</summary>
public static readonly Rectangle Right = new Rectangle(286, 289, 4, 1);
/// <summary>The top-left corner.</summary>
public static readonly Rectangle TopLeft = new Rectangle(274, 284, 4, 4);
/// <summary>The top-right corner.</summary>
public static readonly Rectangle TopRight = new Rectangle(286, 284, 4, 4);
/// <summary>The bottom-left corner.</summary>
public static readonly Rectangle BottomLeft = new Rectangle(274, 296, 4, 4);
/// <summary>The bottom-right corner.</summary>
public static readonly Rectangle BottomRight = new Rectangle(286, 296, 4, 4);
}
/// <summary>Sprites used to draw a scroll.</summary>
public static class Scroll
{
/// <summary>The sprite sheet containing the icon sprites.</summary>
public static Texture2D Sheet => Game1.mouseCursors;
/// <summary>The legend background.</summary>
public static readonly Rectangle Background = new Rectangle(334, 321, 1, 1);
/// <summary>The top border.</summary>
public static readonly Rectangle Top = new Rectangle(331, 318, 1, 2);
/// <summary>The bottom border.</summary>
public static readonly Rectangle Bottom = new Rectangle(327, 334, 1, 2);
/// <summary>The left border.</summary>
public static readonly Rectangle Left = new Rectangle(325, 320, 6, 1);
/// <summary>The right border.</summary>
public static readonly Rectangle Right = new Rectangle(344, 320, 6, 1);
/// <summary>The top-left corner.</summary>
public static readonly Rectangle TopLeft = new Rectangle(325, 318, 6, 2);
/// <summary>The top-right corner.</summary>
public static readonly Rectangle TopRight = new Rectangle(344, 318, 6, 2);
/// <summary>The bottom-left corner.</summary>
public static readonly Rectangle BottomLeft = new Rectangle(325, 334, 6, 2);
/// <summary>The bottom-right corner.</summary>
public static readonly Rectangle BottomRight = new Rectangle(344, 334, 6, 2);
}
}
}

View File

@ -0,0 +1,141 @@
using System.Collections.Generic;
namespace Pathoschild.Stardew.Common.Utilities
{
/// <summary>A logical collection of values defined by restriction and exclusion values which may be infinite.</summary>
/// <remarks>
/// <para>
/// Unlike a typical collection, a constraint set doesn't necessarily track the values it contains. For
/// example, a constraint set of <see cref="uint"/> values with one exclusion only stores one number but
/// logically contains <see cref="uint.MaxValue"/> elements.
/// </para>
///
/// <para>
/// A constraint set is defined by two inner sets: <see cref="ExcludeValues"/> contains values which are
/// explicitly not part of the set, and <see cref="RestrictToValues"/> contains values which are explicitly
/// part of the set. Crucially, an empty <see cref="RestrictToValues"/> means an unbounded set (i.e. it
/// contains all possible values). If a value is part of both <see cref="ExcludeValues"/> and
/// <see cref="RestrictToValues"/>, the exclusion takes priority.
/// </para>
/// </remarks>
internal class ConstraintSet<T>
{
/*********
** Accessors
*********/
/// <summary>The specific values to contain (or empty to match any value).</summary>
public HashSet<T> RestrictToValues { get; }
/// <summary>The specific values to exclude.</summary>
public HashSet<T> ExcludeValues { get; }
/// <summary>Whether the constraint set matches a finite set of values.</summary>
public bool IsBounded => this.RestrictToValues.Count != 0;
/// <summary>Get whether the constraint set logically matches an infinite set of values.</summary>
public bool IsInfinite => !this.IsBounded;
/// <summary>Whether there are any constraints placed on the set of values.</summary>
public bool IsConstrained => this.RestrictToValues.Count != 0 || this.ExcludeValues.Count != 0;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public ConstraintSet()
: this(EqualityComparer<T>.Default) { }
/// <summary>Construct an instance.</summary>
/// <param name="comparer">The equality comparer to use when comparing values in the set, or <see langword="null" /> to use the default implementation for the set type.</param>
public ConstraintSet(IEqualityComparer<T> comparer)
{
this.RestrictToValues = new HashSet<T>(comparer);
this.ExcludeValues = new HashSet<T>(comparer);
}
/// <summary>Bound the constraint set by adding the given value to the set of allowed values. If the constraint set is unbounded, this makes it bounded.</summary>
/// <param name="value">The value.</param>
/// <returns>Returns <c>true</c> if the value was added; else <c>false</c> if it was already present.</returns>
public bool AddBound(T value)
{
return this.RestrictToValues.Add(value);
}
/// <summary>Bound the constraint set by adding the given values to the set of allowed values. If the constraint set is unbounded, this makes it bounded.</summary>
/// <param name="values">The values.</param>
/// <returns>Returns <c>true</c> if any value was added; else <c>false</c> if all values were already present.</returns>
public bool AddBound(IEnumerable<T> values)
{
bool anyAdded = false;
foreach (T value in values)
{
if (this.RestrictToValues.Add(value))
anyAdded = true;
}
return anyAdded;
}
/// <summary>Add values to exclude.</summary>
/// <param name="value">The value to exclude.</param>
/// <returns>Returns <c>true</c> if the value was added; else <c>false</c> if it was already present.</returns>
public bool Exclude(T value)
{
return this.ExcludeValues.Add(value);
}
/// <summary>Add values to exclude.</summary>
/// <param name="values">The values to exclude.</param>
/// <returns>Returns <c>true</c> if any value was added; else <c>false</c> if all values were already present.</returns>
public bool Exclude(IEnumerable<T> values)
{
bool anyAdded = false;
foreach (T value in values)
{
if (this.ExcludeValues.Add(value))
anyAdded = true;
}
return anyAdded;
}
/// <summary>Get whether this constraint allows some values that would be allowed by another.</summary>
/// <param name="other">The other </param>
public bool Intersects(ConstraintSet<T> other)
{
// If both sets are unbounded, they're guaranteed to intersect since exclude can't be unbounded.
if (this.IsInfinite && other.IsInfinite)
return true;
// if either set is bounded, they can only intersect in the included subset.
if (this.IsBounded)
{
foreach (T value in this.RestrictToValues)
{
if (this.Allows(value) && other.Allows(value))
return true;
}
}
if (other.IsBounded)
{
foreach (T value in other.RestrictToValues)
{
if (other.Allows(value) && this.Allows(value))
return true;
}
}
// else no intersection
return false;
}
/// <summary>Get whether the constraints allow the given value.</summary>
/// <param name="value">The value to match.</param>
public bool Allows(T value)
{
if (this.ExcludeValues.Contains(value))
return false;
return this.IsInfinite || this.RestrictToValues.Contains(value);
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
namespace Pathoschild.Stardew.Common.Utilities
{
/// <summary>An implementation of <see cref="Dictionary{TKey,TValue}"/> whose keys are guaranteed to use <see cref="StringComparer.InvariantCultureIgnoreCase"/>.</summary>
internal class InvariantDictionary<TValue> : Dictionary<string, TValue>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public InvariantDictionary()
: base(StringComparer.InvariantCultureIgnoreCase) { }
/// <summary>Construct an instance.</summary>
/// <param name="values">The values to add.</param>
public InvariantDictionary(IDictionary<string, TValue> values)
: base(values, StringComparer.InvariantCultureIgnoreCase) { }
/// <summary>Construct an instance.</summary>
/// <param name="values">The values to add.</param>
public InvariantDictionary(IEnumerable<KeyValuePair<string, TValue>> values)
: base(StringComparer.InvariantCultureIgnoreCase)
{
foreach (var entry in values)
this.Add(entry.Key, entry.Value);
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace Pathoschild.Stardew.Common.Utilities
{
/// <summary>An implementation of <see cref="HashSet{T}"/> for strings which always uses <see cref="StringComparer.InvariantCultureIgnoreCase"/>.</summary>
internal class InvariantHashSet : HashSet<string>
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
public InvariantHashSet()
: base(StringComparer.InvariantCultureIgnoreCase) { }
/// <summary>Construct an instance.</summary>
/// <param name="values">The values to add.</param>
public InvariantHashSet(IEnumerable<string> values)
: base(values, StringComparer.InvariantCultureIgnoreCase) { }
/// <summary>Construct an instance.</summary>
/// <param name="value">The single value to add.</param>
public InvariantHashSet(string value)
: base(new[] { value }, StringComparer.InvariantCultureIgnoreCase) { }
/// <summary>Get a hashset for boolean true/false.</summary>
public static InvariantHashSet Boolean()
{
return new InvariantHashSet(new[] { "true", "false" });
}
}
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Pathoschild.Stardew.Common.Utilities
{
/// <summary>A comparer which considers two references equal if they point to the same instance.</summary>
/// <typeparam name="T">The value type.</typeparam>
internal class ObjectReferenceComparer<T> : IEqualityComparer<T>
{
/*********
** Public methods
*********/
/// <summary>Determines whether the specified objects are equal.</summary>
/// <returns>true if the specified objects are equal; otherwise, false.</returns>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
public bool Equals(T x, T y)
{
return object.ReferenceEquals(x, y);
}
/// <summary>Get a hash code for the specified object.</summary>
/// <param name="obj">The value.</param>
public int GetHashCode(T obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
}

View File

@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.LookupAnything.Framework;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Subjects;
using Pathoschild.Stardew.LookupAnything.Framework.Targets;
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Components
{
/// <summary>Draws debug information to the screen.</summary>
internal class DebugInterface
{
/*********
** Fields
*********/
/// <summary>Provides utility methods for interacting with the game code.</summary>
private readonly GameHelper GameHelper;
/// <summary>Finds and analyses lookup targets in the world.</summary>
private readonly TargetFactory TargetFactory;
/// <summary>Encapsulates monitoring and logging.</summary>
private readonly IMonitor Monitor;
/// <summary>The warning text to display when debug mode is enabled.</summary>
private readonly string WarningText;
/*********
** Accessors
*********/
/// <summary>Whether the debug interface is enabled.</summary>
public bool Enabled { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="targetFactory">Finds and analyses lookup targets in the world.</param>
/// <param name="config">The mod configuration.</param>
/// <param name="monitor">Encapsulates monitoring and logging.</param>
public DebugInterface(GameHelper gameHelper, TargetFactory targetFactory, ModConfig config, IMonitor monitor)
{
// save fields
this.GameHelper = gameHelper;
this.TargetFactory = targetFactory;
this.Monitor = monitor;
// generate warning text
this.WarningText = $"Debug info enabled; press {string.Join(" or ", config.Controls.ToggleDebug)} to disable.";
}
/// <summary>Draw debug metadata to the screen.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
public void Draw(SpriteBatch spriteBatch)
{
if (!this.Enabled)
return;
this.Monitor.InterceptErrors("drawing debug info", () =>
{
// get location info
GameLocation currentLocation = Game1.currentLocation;
Vector2 cursorTile = Game1.currentCursorTile;
Vector2 cursorPosition = this.GameHelper.GetScreenCoordinatesFromCursor();
// show 'debug enabled' warning + cursor position
{
string metadata = $"{this.WarningText} Cursor tile ({cursorTile.X}, {cursorTile.Y}), position ({cursorPosition.X}, {cursorPosition.Y}).";
this.GameHelper.DrawHoverBox(spriteBatch, metadata, Vector2.Zero, Game1.viewport.Width);
}
// show cursor pixel
spriteBatch.DrawLine(cursorPosition.X - 1, cursorPosition.Y - 1, new Vector2(Game1.pixelZoom, Game1.pixelZoom), Color.DarkRed);
// show targets within detection radius
Rectangle tileArea = this.GameHelper.GetScreenCoordinatesFromTile(Game1.currentCursorTile);
IEnumerable<ITarget> targets = this.TargetFactory
.GetNearbyTargets(currentLocation, cursorTile, includeMapTile: false)
.OrderBy(p => p.Type == TargetType.Unknown ? 0 : 1);
// if targets overlap, prioritise info on known targets
foreach (ITarget target in targets)
{
// get metadata
bool spriteAreaIntersects = target.GetWorldArea().Intersects(tileArea);
ISubject subject = this.TargetFactory.GetSubjectFrom(target);
// draw tile
{
Rectangle tile = this.GameHelper.GetScreenCoordinatesFromTile(target.GetTile());
Color color = (subject != null ? Color.Green : Color.Red) * .5f;
spriteBatch.DrawLine(tile.X, tile.Y, new Vector2(tile.Width, tile.Height), color);
}
// draw sprite box
if (subject != null)
{
int borderSize = 3;
Color borderColor = Color.Green;
if (!spriteAreaIntersects)
{
borderSize = 1;
borderColor *= 0.5f;
}
Rectangle spriteBox = target.GetWorldArea();
spriteBatch.DrawLine(spriteBox.X, spriteBox.Y, new Vector2(spriteBox.Width, borderSize), borderColor); // top
spriteBatch.DrawLine(spriteBox.X, spriteBox.Y, new Vector2(borderSize, spriteBox.Height), borderColor); // left
spriteBatch.DrawLine(spriteBox.X + spriteBox.Width, spriteBox.Y, new Vector2(borderSize, spriteBox.Height), borderColor); // right
spriteBatch.DrawLine(spriteBox.X, spriteBox.Y + spriteBox.Height, new Vector2(spriteBox.Width, borderSize), borderColor); // bottom
}
}
// show current target name (if any)
{
ISubject subject = this.TargetFactory.GetSubjectFrom(Game1.player, currentLocation, LookupMode.Cursor, includeMapTile: false);
if (subject != null)
this.GameHelper.DrawHoverBox(spriteBatch, subject.Name, new Vector2(Game1.getMouseX(), Game1.getMouseY()) + new Vector2(Game1.tileSize / 2f), Game1.viewport.Width / 4f);
}
}, this.OnDrawError);
}
/*********
** Public methods
*********/
/// <summary>The method invoked when an unhandled exception is intercepted.</summary>
/// <param name="ex">The intercepted exception.</param>
private void OnDrawError(Exception ex)
{
this.Monitor.InterceptErrors("handling an error in the debug code", () =>
{
this.Enabled = false;
});
}
}
}

View File

@ -0,0 +1,418 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.LookupAnything.Framework;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using Pathoschild.Stardew.LookupAnything.Framework.Subjects;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Menus;
namespace Pathoschild.Stardew.LookupAnything.Components
{
/// <summary>A UI which shows information about an item.</summary>
internal class LookupMenu : IClickableMenu
{
/*********
** Fields
*********/
/// <summary>The subject metadata.</summary>
private readonly ISubject Subject;
/// <summary>Encapsulates logging and monitoring.</summary>
private readonly IMonitor Monitor;
/// <summary>A callback which shows a new lookup for a given subject.</summary>
private readonly Action<ISubject> ShowNewPage;
/// <summary>The data to display for this subject.</summary>
private readonly ICustomField[] Fields;
/// <summary>The aspect ratio of the page background.</summary>
private readonly Vector2 AspectRatio = new Vector2(Sprites.Letter.Sprite.Width, Sprites.Letter.Sprite.Height);
/// <summary>Simplifies access to private game code.</summary>
private readonly IReflectionHelper Reflection;
/// <summary>The amount to scroll long content on each up/down scroll.</summary>
private readonly int ScrollAmount;
/// <summary>The clickable 'scroll up' icon.</summary>
private readonly ClickableTextureComponent ScrollUpButton;
/// <summary>The clickable 'scroll down' icon.</summary>
private readonly ClickableTextureComponent ScrollDownButton;
/// <summary>The spacing around the scroll buttons.</summary>
private readonly int ScrollButtonGutter = 15;
/// <summary>The maximum pixels to scroll.</summary>
private int MaxScroll;
/// <summary>The number of pixels to scroll.</summary>
private int CurrentScroll;
/// <summary>Whether the game's draw mode has been validated for compatibility.</summary>
private bool ValidatedDrawMode;
/// <summary>Click areas for link fields that open a new subject.</summary>
private readonly IDictionary<ILinkField, Rectangle> LinkFieldAreas = new Dictionary<ILinkField, Rectangle>();
/*********
** Accessors
*********/
/// <summary>Whether the lookup is showing information for a tile.</summary>
public bool IsTileLookup { get; set; }
/*********
** Public methods
*********/
/****
** Constructors
****/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="subject">The metadata to display.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
/// <param name="monitor">Encapsulates logging and monitoring.</param>
/// <param name="reflectionHelper">Simplifies access to private game code.</param>
/// <param name="scroll">The amount to scroll long content on each up/down scroll.</param>
/// <param name="showDebugFields">Whether to display debug fields.</param>
/// <param name="showNewPage">A callback which shows a new lookup for a given subject.</param>
public LookupMenu(GameHelper gameHelper, ISubject subject, Metadata metadata, IMonitor monitor, IReflectionHelper reflectionHelper, int scroll, bool showDebugFields, Action<ISubject> showNewPage)
{
// save data
this.Subject = subject;
this.Fields = subject.GetData(metadata).Where(p => p.HasValue).ToArray();
this.Monitor = monitor;
this.Reflection = reflectionHelper;
this.ScrollAmount = scroll;
this.ShowNewPage = showNewPage;
// save debug fields
if (showDebugFields)
{
IDebugField[] debugFields = subject.GetDebugFields(metadata).ToArray();
this.Fields = this.Fields
.Concat(new[]
{
new DataMiningField(gameHelper, "debug (pinned)", debugFields.Where(p => p.IsPinned)),
new DataMiningField(gameHelper, "debug (raw)", debugFields.Where(p => !p.IsPinned))
})
.ToArray();
}
// add scroll buttons
this.ScrollUpButton = new ClickableTextureComponent(Rectangle.Empty, Sprites.Icons.Sheet, Sprites.Icons.UpArrow, 1);
this.ScrollDownButton = new ClickableTextureComponent(Rectangle.Empty, Sprites.Icons.Sheet, Sprites.Icons.DownArrow, 1);
// update layout
this.UpdateLayout();
}
/****
** Events
****/
/// <summary>The method invoked when the player left-clicks on the lookup UI.</summary>
/// <param name="x">The X-position of the cursor.</param>
/// <param name="y">The Y-position of the cursor.</param>
/// <param name="playSound">Whether to enable sound.</param>
public override void receiveLeftClick(int x, int y, bool playSound = true)
{
this.HandleLeftClick(x, y);
}
/// <summary>The method invoked when the player right-clicks on the lookup UI.</summary>
/// <param name="x">The X-position of the cursor.</param>
/// <param name="y">The Y-position of the cursor.</param>
/// <param name="playSound">Whether to enable sound.</param>
public override void receiveRightClick(int x, int y, bool playSound = true) { }
/// <summary>The method invoked when the player scrolls the mouse wheel on the lookup UI.</summary>
/// <param name="direction">The scroll direction.</param>
public override void receiveScrollWheelAction(int direction)
{
if (direction > 0) // positive number scrolls content up
this.ScrollUp();
else
this.ScrollDown();
}
/// <summary>The method called when the game window changes size.</summary>
/// <param name="oldBounds">The former viewport.</param>
/// <param name="newBounds">The new viewport.</param>
public override void gameWindowSizeChanged(Rectangle oldBounds, Rectangle newBounds)
{
this.UpdateLayout();
}
/// <summary>The method called when the player presses a controller button.</summary>
/// <param name="button">The controller button pressed.</param>
public override void receiveGamePadButton(Buttons button)
{
switch (button)
{
// left click
case Buttons.A:
Point p = Game1.getMousePosition();
this.HandleLeftClick(p.X, p.Y);
break;
// exit
case Buttons.B:
this.exitThisMenu();
break;
// scroll up
case Buttons.RightThumbstickUp:
this.ScrollUp();
break;
// scroll down
case Buttons.RightThumbstickDown:
this.ScrollDown();
break;
}
}
/****
** Methods
****/
/// <summary>Scroll up the menu content by the specified amount (if possible).</summary>
public void ScrollUp()
{
this.CurrentScroll -= this.ScrollAmount;
}
/// <summary>Scroll down the menu content by the specified amount (if possible).</summary>
public void ScrollDown()
{
this.CurrentScroll += this.ScrollAmount;
}
/// <summary>Handle a left-click from the player's mouse or controller.</summary>
/// <param name="x">The x-position of the cursor.</param>
/// <param name="y">The y-position of the cursor.</param>
public void HandleLeftClick(int x, int y)
{
// close menu when clicked outside
if (!this.isWithinBounds(x, y))
this.exitThisMenu();
// scroll up or down
else if (this.ScrollUpButton.containsPoint(x, y))
this.ScrollUp();
else if (this.ScrollDownButton.containsPoint(x, y))
this.ScrollDown();
// custom link fields
else
{
foreach (var area in this.LinkFieldAreas)
{
if (area.Value.Contains(x, y))
{
ISubject subject = area.Key.GetLinkSubject();
if (subject != null)
this.ShowNewPage(subject);
break;
}
}
}
}
/// <summary>Render the UI.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
public override void draw(SpriteBatch spriteBatch)
{
this.Monitor.InterceptErrors("drawing the lookup info", () =>
{
ISubject subject = this.Subject;
// disable when game is using immediate sprite sorting
// (This prevents Lookup Anything from creating new sprite batches, which breaks its core rendering logic.
// Fortunately this very rarely happens; the only known case is the Stardew Valley Fair, when the only thing
// you can look up anyway is the farmer.)
if (!this.ValidatedDrawMode)
{
IReflectedField<SpriteSortMode> sortModeField =
this.Reflection.GetField<SpriteSortMode>(Game1.spriteBatch, "spriteSortMode", required: false) // XNA
?? this.Reflection.GetField<SpriteSortMode>(Game1.spriteBatch, "_sortMode"); // MonoGame
if (sortModeField.GetValue() == SpriteSortMode.Immediate)
{
this.Monitor.Log("Aborted the lookup because the game's current rendering mode isn't compatible with the mod's UI. This only happens in rare cases (e.g. the Stardew Valley Fair).", LogLevel.Warn);
this.exitThisMenu(playSound: false);
return;
}
this.ValidatedDrawMode = true;
}
// calculate dimensions
int x = this.xPositionOnScreen;
int y = this.yPositionOnScreen;
const int gutter = 15;
float leftOffset = gutter;
float topOffset = gutter;
float contentWidth = this.width - gutter * 2;
float contentHeight = this.height - gutter * 2;
int tableBorderWidth = 1;
// get font
SpriteFont font = Game1.smallFont;
float lineHeight = font.MeasureString("ABC").Y;
float spaceWidth = DrawHelper.GetSpaceWidth(font);
// draw background
// (This uses a separate sprite batch because it needs to be drawn before the
// foreground batch, and we can't use the foreground batch because the background is
// outside the clipping area.)
using (SpriteBatch backgroundBatch = new SpriteBatch(Game1.graphics.GraphicsDevice))
{
backgroundBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, null);
backgroundBatch.DrawSprite(Sprites.Letter.Sheet, Sprites.Letter.Sprite, x, y, scale: this.width / (float)Sprites.Letter.Sprite.Width);
backgroundBatch.End();
}
// draw foreground
// (This uses a separate sprite batch to set a clipping area for scrolling.)
using (SpriteBatch contentBatch = new SpriteBatch(Game1.graphics.GraphicsDevice))
{
GraphicsDevice device = Game1.graphics.GraphicsDevice;
Rectangle prevScissorRectangle = device.ScissorRectangle;
try
{
// begin draw
device.ScissorRectangle = new Rectangle(x + gutter, y + gutter, (int)contentWidth, (int)contentHeight);
contentBatch.Begin(SpriteSortMode.Deferred, BlendState.NonPremultiplied, SamplerState.PointClamp, null, new RasterizerState { ScissorTestEnable = true });
// scroll view
this.CurrentScroll = Math.Max(0, this.CurrentScroll); // don't scroll past top
this.CurrentScroll = Math.Min(this.MaxScroll, this.CurrentScroll); // don't scroll past bottom
topOffset -= this.CurrentScroll; // scrolled down == move text up
// draw portrait
if (subject.DrawPortrait(contentBatch, new Vector2(x + leftOffset, y + topOffset), new Vector2(70, 70)))
leftOffset += 72;
// draw fields
float wrapWidth = this.width - leftOffset - gutter;
{
// draw name & item type
{
Vector2 nameSize = contentBatch.DrawTextBlock(font, $"{subject.Name}.", new Vector2(x + leftOffset, y + topOffset), wrapWidth, bold: Constant.AllowBold);
Vector2 typeSize = contentBatch.DrawTextBlock(font, $"{subject.Type}.", new Vector2(x + leftOffset + nameSize.X + spaceWidth, y + topOffset), wrapWidth);
topOffset += Math.Max(nameSize.Y, typeSize.Y);
}
// draw description
if (subject.Description != null)
{
Vector2 size = contentBatch.DrawTextBlock(font, subject.Description?.Replace(Environment.NewLine, " "), new Vector2(x + leftOffset, y + topOffset), wrapWidth);
topOffset += size.Y;
}
// draw spacer
topOffset += lineHeight;
// draw custom fields
if (this.Fields.Any())
{
ICustomField[] fields = this.Fields;
float cellPadding = 3;
float labelWidth = fields.Where(p => p.HasValue).Max(p => font.MeasureString(p.Label).X);
float valueWidth = wrapWidth - labelWidth - cellPadding * 4 - tableBorderWidth;
foreach (ICustomField field in fields)
{
if (!field.HasValue)
continue;
// draw label & value
Vector2 labelSize = contentBatch.DrawTextBlock(font, field.Label, new Vector2(x + leftOffset + cellPadding, y + topOffset + cellPadding), wrapWidth);
Vector2 valuePosition = new Vector2(x + leftOffset + labelWidth + cellPadding * 3, y + topOffset + cellPadding);
Vector2 valueSize =
field.DrawValue(contentBatch, font, valuePosition, valueWidth)
?? contentBatch.DrawTextBlock(font, field.Value, valuePosition, valueWidth);
Vector2 rowSize = new Vector2(labelWidth + valueWidth + cellPadding * 4, Math.Max(labelSize.Y, valueSize.Y));
// draw table row
Color lineColor = Color.Gray;
contentBatch.DrawLine(x + leftOffset, y + topOffset, new Vector2(rowSize.X, tableBorderWidth), lineColor); // top
contentBatch.DrawLine(x + leftOffset, y + topOffset + rowSize.Y, new Vector2(rowSize.X, tableBorderWidth), lineColor); // bottom
contentBatch.DrawLine(x + leftOffset, y + topOffset, new Vector2(tableBorderWidth, rowSize.Y), lineColor); // left
contentBatch.DrawLine(x + leftOffset + labelWidth + cellPadding * 2, y + topOffset, new Vector2(tableBorderWidth, rowSize.Y), lineColor); // middle
contentBatch.DrawLine(x + leftOffset + rowSize.X, y + topOffset, new Vector2(tableBorderWidth, rowSize.Y), lineColor); // right
// track link area
if (field is ILinkField linkField)
this.LinkFieldAreas[linkField] = new Rectangle((int)valuePosition.X, (int)valuePosition.Y, (int)valueSize.X, (int)valueSize.Y);
// update offset
topOffset += Math.Max(labelSize.Y, valueSize.Y);
}
}
}
// update max scroll
this.MaxScroll = Math.Max(0, (int)(topOffset - contentHeight + this.CurrentScroll));
// draw scroll icons
if (this.MaxScroll > 0 && this.CurrentScroll > 0)
this.ScrollUpButton.draw(contentBatch);
if (this.MaxScroll > 0 && this.CurrentScroll < this.MaxScroll)
this.ScrollDownButton.draw(spriteBatch);
// end draw
contentBatch.End();
}
finally
{
device.ScissorRectangle = prevScissorRectangle;
}
}
// draw cursor
this.drawMouse(Game1.spriteBatch);
}, this.OnDrawError);
}
/*********
** Private methods
*********/
/// <summary>Update the layout dimensions based on the current game scale.</summary>
private void UpdateLayout()
{
// update size
this.width = Math.Min(Game1.tileSize * 14, Game1.viewport.Width);
this.height = Math.Min((int)(this.AspectRatio.Y / this.AspectRatio.X * this.width), Game1.viewport.Height);
// update position
Vector2 origin = Utility.getTopLeftPositionForCenteringOnScreen(this.width, this.height);
this.xPositionOnScreen = (int)origin.X;
this.yPositionOnScreen = (int)origin.Y;
// update up/down buttons
int x = this.xPositionOnScreen;
int y = this.yPositionOnScreen;
int gutter = this.ScrollButtonGutter;
float contentHeight = this.height - gutter * 2;
this.ScrollUpButton.bounds = new Rectangle(x + gutter, (int)(y + contentHeight - Sprites.Icons.UpArrow.Height - gutter - Sprites.Icons.DownArrow.Height), Sprites.Icons.UpArrow.Height, Sprites.Icons.UpArrow.Width);
this.ScrollDownButton.bounds = new Rectangle(x + gutter, (int)(y + contentHeight - Sprites.Icons.DownArrow.Height), Sprites.Icons.DownArrow.Height, Sprites.Icons.DownArrow.Width);
}
/// <summary>The method invoked when an unhandled exception is intercepted.</summary>
/// <param name="ex">The intercepted exception.</param>
private void OnDrawError(Exception ex)
{
this.Monitor.InterceptErrors("handling an error in the lookup code", () => this.exitThisMenu());
}
}
}

View File

@ -0,0 +1,56 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Components
{
/// <summary>Simplifies access to the game's sprite sheets.</summary>
/// <remarks>Each sprite is represented by a rectangle, which specifies the coordinates and dimensions of the image in the sprite sheet.</remarks>
internal static class Sprites
{
/*********
** Accessors
*********/
/// <summary>Sprites used to draw a letter.</summary>
public static class Letter
{
/// <summary>The sprite sheet containing the letter sprites.</summary>
public static Texture2D Sheet => Game1.content.Load<Texture2D>("LooseSprites\\letterBG");
/// <summary>The letter background (including edges and corners).</summary>
public static readonly Rectangle Sprite = new Rectangle(0, 0, 320, 180);
}
/// <summary>Sprites used to draw icons.</summary>
public static class Icons
{
/// <summary>The sprite sheet containing the icon sprites.</summary>
public static Texture2D Sheet => Game1.mouseCursors;
/// <summary>An empty checkbox icon.</summary>
public static readonly Rectangle EmptyCheckbox = new Rectangle(227, 425, 9, 9);
/// <summary>A filled checkbox icon.</summary>
public static readonly Rectangle FilledCheckbox = new Rectangle(236, 425, 9, 9);
/// <summary>A filled heart indicating a friendship level.</summary>
public static readonly Rectangle FilledHeart = new Rectangle(211, 428, 7, 6);
/// <summary>An empty heart indicating a missing friendship level.</summary>
public static readonly Rectangle EmptyHeart = new Rectangle(218, 428, 7, 6);
/// <summary>A down arrow for scrolling content.</summary>
public static readonly Rectangle DownArrow = new Rectangle(12, 76, 40, 44);
/// <summary>An up arrow for scrolling content.</summary>
public static readonly Rectangle UpArrow = new Rectangle(76, 72, 40, 44);
/// <summary>A stardrop icon.</summary>
public static readonly Rectangle Stardrop = new Rectangle(346, 392, 8, 8);
}
/// <summary>A blank pixel which can be colorised and stretched to draw geometric shapes.</summary>
public static readonly Texture2D Pixel = CommonHelper.Pixel;
}
}

View File

@ -0,0 +1,379 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.LookupAnything.Framework;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Data;
using Pathoschild.Stardew.LookupAnything.Framework.Models;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Characters;
using StardewValley.Objects;
using SFarmer = StardewValley.Farmer;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.LookupAnything
{
/// <summary>Parses the raw game data into usable models. These may be expensive operations and should be cached.</summary>
internal class DataParser
{
/*********
** Fields
*********/
/// <summary>Provides utility methods for interacting with the game code.</summary>
private readonly GameHelper GameHelper;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
public DataParser(GameHelper gameHelper)
{
this.GameHelper = gameHelper;
}
/// <summary>Read parsed data about the Community Center bundles.</summary>
/// <remarks>Derived from the <see cref="StardewValley.Locations.CommunityCenter"/> constructor and <see cref="StardewValley.Menus.JunimoNoteMenu.openRewardsMenu"/>.</remarks>
public IEnumerable<BundleModel> GetBundles()
{
IDictionary<string, string> data = Game1.content.Load<Dictionary<string, string>>("Data\\Bundles");
foreach (var entry in data)
{
// parse key
string[] keyParts = entry.Key.Split('/');
string area = keyParts[0];
int id = int.Parse(keyParts[1]);
// parse bundle info
string[] valueParts = entry.Value.Split('/');
string name = valueParts[0];
string reward = valueParts[1];
string displayName = LocalizedContentManager.CurrentLanguageCode == LocalizedContentManager.LanguageCode.en
? name // field isn't present in English
: valueParts.Last(); // number of fields varies, but display name is always last
// parse ingredients
List<BundleIngredientModel> ingredients = new List<BundleIngredientModel>();
string[] ingredientData = valueParts[2].Split(' ');
for (int i = 0; i < ingredientData.Length; i += 3)
{
int index = i / 3;
int itemID = int.Parse(ingredientData[i]);
int stack = int.Parse(ingredientData[i + 1]);
ItemQuality quality = (ItemQuality)int.Parse(ingredientData[i + 2]);
ingredients.Add(new BundleIngredientModel(index, itemID, stack, quality));
}
// create bundle
yield return new BundleModel(id, name, displayName, area, reward, ingredients);
}
}
/// <summary>Get parsed data about the friendship between a player and NPC.</summary>
/// <param name="player">The player.</param>
/// <param name="npc">The NPC.</param>
/// <param name="friendship">The current friendship data.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public FriendshipModel GetFriendshipForVillager(SFarmer player, NPC npc, Friendship friendship, Metadata metadata)
{
return new FriendshipModel(player, npc, friendship, metadata.Constants);
}
/// <summary>Get parsed data about the friendship between a player and NPC.</summary>
/// <param name="player">The player.</param>
/// <param name="pet">The pet.</param>
public FriendshipModel GetFriendshipForPet(SFarmer player, Pet pet)
{
return new FriendshipModel(pet.friendshipTowardFarmer, Pet.maxFriendship / 10, Pet.maxFriendship);
}
/// <summary>Get parsed data about the friendship between a player and NPC.</summary>
/// <param name="player">The player.</param>
/// <param name="animal">The farm animal.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public FriendshipModel GetFriendshipForAnimal(SFarmer player, FarmAnimal animal, Metadata metadata)
{
return new FriendshipModel(animal.friendshipTowardFarmer.Value, metadata.Constants.AnimalFriendshipPointsPerLevel, metadata.Constants.AnimalFriendshipMaxPoints);
}
/// <summary>Get the raw gift tastes from the underlying data.</summary>
/// <param name="objects">The game's object data.</param>
/// <remarks>Reverse engineered from <c>Data\NPCGiftTastes</c> and <see cref="StardewValley.NPC.getGiftTasteForThisItem"/>.</remarks>
public IEnumerable<GiftTasteModel> GetGiftTastes(ObjectModel[] objects)
{
// extract raw values
var tastes = new List<GiftTasteModel>();
{
// define data schema
var universal = new Dictionary<string, GiftTaste>
{
["Universal_Love"] = GiftTaste.Love,
["Universal_Like"] = GiftTaste.Like,
["Universal_Neutral"] = GiftTaste.Neutral,
["Universal_Dislike"] = GiftTaste.Dislike,
["Universal_Hate"] = GiftTaste.Hate
};
var personalMetadataKeys = new Dictionary<int, GiftTaste>
{
// metadata is paired: odd values contain a list of item references, even values contain the reaction dialogue
[1] = GiftTaste.Love,
[3] = GiftTaste.Like,
[5] = GiftTaste.Dislike,
[7] = GiftTaste.Hate,
[9] = GiftTaste.Neutral
};
// read data
IDictionary<string, string> data = Game1.NPCGiftTastes;
foreach (string villager in data.Keys)
{
string tasteStr = data[villager];
if (universal.ContainsKey(villager))
{
GiftTaste taste = universal[villager];
tastes.AddRange(
from refID in tasteStr.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
select new GiftTasteModel(taste, "*", int.Parse(refID), isUniversal: true)
);
}
else
{
string[] personalData = tasteStr.Split('/');
foreach (KeyValuePair<int, GiftTaste> taste in personalMetadataKeys)
{
tastes.AddRange(
from refID in
personalData[taste.Key].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
select new GiftTasteModel(taste.Value, villager, int.Parse(refID))
);
}
}
}
}
// get sanitised data
HashSet<int> validItemIDs = new HashSet<int>(objects.Select(p => p.ParentSpriteIndex));
HashSet<int> validCategories = new HashSet<int>(objects.Where(p => p.Category != 0).Select(p => p.Category));
return tastes
.Where(model => validCategories.Contains(model.RefID) || validItemIDs.Contains(model.RefID)); // ignore invalid entries
}
/// <summary>Parse monster data.</summary>
/// <remarks>Reverse engineered from <see cref="StardewValley.Monsters.Monster.parseMonsterInfo"/>, <see cref="GameLocation.monsterDrop"/>, and the <see cref="Debris"/> constructor.</remarks>
public IEnumerable<MonsterData> GetMonsters()
{
Dictionary<string, string> data = Game1.content.Load<Dictionary<string, string>>("Data\\Monsters");
foreach (var entry in data)
{
// monster fields
string[] fields = entry.Value.Split('/');
string name = entry.Key;
int health = int.Parse(fields[0]);
int damageToFarmer = int.Parse(fields[1]);
//int minCoins = int.Parse(fields[2]);
//int maxCoins = int.Parse(fields[3]) + 1;
bool isGlider = bool.Parse(fields[4]);
int durationOfRandomMovements = int.Parse(fields[5]);
int resilience = int.Parse(fields[7]);
double jitteriness = double.Parse(fields[8]);
int moveTowardsPlayerThreshold = int.Parse(fields[9]);
int speed = int.Parse(fields[10]);
double missChance = double.Parse(fields[11]);
bool isMineMonster = bool.Parse(fields[12]);
// drops
var drops = new List<ItemDropData>();
string[] dropFields = fields[6].Split(' ');
for (int i = 0; i < dropFields.Length; i += 2)
{
// get drop info
int itemID = int.Parse(dropFields[i]);
float chance = float.Parse(dropFields[i + 1]);
int maxDrops = 1;
// if itemID is negative, game randomly drops 1-3
if (itemID < 0)
{
itemID = -itemID;
maxDrops = 3;
}
// some item IDs have special meaning
if (itemID == Debris.copperDebris)
itemID = SObject.copper;
else if (itemID == Debris.ironDebris)
itemID = SObject.iron;
else if (itemID == Debris.coalDebris)
itemID = SObject.coal;
else if (itemID == Debris.goldDebris)
itemID = SObject.gold;
else if (itemID == Debris.coinsDebris)
continue; // no drop
else if (itemID == Debris.iridiumDebris)
itemID = SObject.iridium;
else if (itemID == Debris.woodDebris)
itemID = SObject.wood;
else if (itemID == Debris.stoneDebris)
itemID = SObject.stone;
// add drop
drops.Add(new ItemDropData(itemID, maxDrops, chance));
}
if (isMineMonster && Game1.player.timesReachedMineBottom >= 1)
{
drops.Add(new ItemDropData(SObject.diamondIndex, 1, 0.008f));
drops.Add(new ItemDropData(SObject.prismaticShardIndex, 1, 0.008f));
}
// yield data
yield return new MonsterData(
name: name,
health: health,
damageToFarmer: damageToFarmer,
isGlider: isGlider,
durationOfRandomMovements: durationOfRandomMovements,
resilience: resilience,
jitteriness: jitteriness,
moveTowardsPlayerThreshold: moveTowardsPlayerThreshold,
speed: speed,
missChance: missChance,
isMineMonster: isMineMonster,
drops: drops
);
}
}
/// <summary>Parse gift tastes.</summary>
/// <param name="monitor">The monitor with which to log errors.</param>
/// <remarks>Derived from the <see cref="CraftingRecipe.createItem"/>.</remarks>
public IEnumerable<ObjectModel> GetObjects(IMonitor monitor)
{
IDictionary<int, string> data = Game1.objectInformation;
foreach (var pair in data)
{
int parentSpriteIndex = pair.Key;
ObjectModel model;
try
{
string[] fields = pair.Value.Split('/');
// ring
if (parentSpriteIndex >= Ring.ringLowerIndexRange && parentSpriteIndex <= Ring.ringUpperIndexRange)
{
model = new ObjectModel(
parentSpriteIndex: parentSpriteIndex,
name: fields[0],
description: fields[1],
price: int.Parse(fields[2]),
edibility: -300,
type: fields[3],
category: SObject.ringCategory
);
}
// any other object
else
{
string name = fields[SObject.objectInfoNameIndex];
int price = int.Parse(fields[SObject.objectInfoPriceIndex]);
int edibility = int.Parse(fields[SObject.objectInfoEdibilityIndex]);
string description = fields[SObject.objectInfoDescriptionIndex];
// type & category
string[] typeParts = fields[SObject.objectInfoTypeIndex].Split(' ');
string typeName = typeParts[0];
int category = 0;
if (typeParts.Length > 1)
category = int.Parse(typeParts[1]);
model = new ObjectModel(parentSpriteIndex, name, description, price, edibility, typeName, category);
}
}
catch (Exception ex)
{
monitor.Log($"Couldn't parse object #{parentSpriteIndex} from Content\\Data\\ObjectInformation.xnb due to an invalid format.\nObject data: {pair.Value}\nError: {ex}", LogLevel.Warn);
continue;
}
yield return model;
}
}
/// <summary>Get the recipe ingredients.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
/// <param name="reflectionHelper">Simplifies access to private game code.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
public RecipeModel[] GetRecipes(Metadata metadata, IReflectionHelper reflectionHelper, ITranslationHelper translations)
{
List<RecipeModel> recipes = new List<RecipeModel>();
// cooking recipes
recipes.AddRange(
from entry in CraftingRecipe.cookingRecipes
let recipe = new CraftingRecipe(entry.Key, isCookingRecipe: true)
select new RecipeModel(recipe, reflectionHelper, translations)
);
// crafting recipes
recipes.AddRange(
from entry in CraftingRecipe.craftingRecipes
let recipe = new CraftingRecipe(entry.Key, isCookingRecipe: false)
select new RecipeModel(recipe, reflectionHelper, translations)
);
// machine recipes
recipes.AddRange(
from entry in metadata.MachineRecipes
let machine = new SObject(Vector2.Zero, entry.MachineID)
select new RecipeModel(null, RecipeType.MachineInput, machine.DisplayName, entry.Ingredients, ingredient => this.CreateRecipeItem(ingredient.ParentSheetIndex, entry.Output), false, entry.ExceptIngredients, outputItemIndex: entry.Output)
);
// building recipes
recipes.AddRange(
from entry in metadata.BuildingRecipes
let building = new BluePrint(entry.BuildingKey)
select new RecipeModel(null, RecipeType.BuildingBlueprint, building.displayName, entry.Ingredients, ingredient => this.CreateRecipeItem(ingredient.ParentSheetIndex, entry.Output), false, entry.ExceptIngredients)
);
return recipes.ToArray();
}
/*********
** Private methods
*********/
/// <summary>Create a custom recipe output.</summary>
/// <param name="inputID">The input ingredient ID.</param>
/// <param name="outputID">The output item ID.</param>
private SObject CreateRecipeItem(int inputID, int outputID)
{
SObject item = this.GameHelper.GetObjectBySpriteIndex(outputID);
switch (outputID)
{
case 342:
item.preserve.Value = SObject.PreserveType.Pickle;
item.preservedParentSheetIndex.Value = inputID;
break;
case 344:
item.preserve.Value = SObject.PreserveType.Jelly;
item.preservedParentSheetIndex.Value = inputID;
break;
case 348:
item.preserve.Value = SObject.PreserveType.Wine;
item.preservedParentSheetIndex.Value = inputID;
break;
case 350:
item.preserve.Value = SObject.PreserveType.Juice;
item.preservedParentSheetIndex.Value = inputID;
break;
}
return item;
}
}
}

View File

@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.LookupAnything.Components;
using Pathoschild.Stardew.LookupAnything.Framework;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything
{
/// <summary>Provides utility methods for drawing to the screen.</summary>
internal static class DrawHelper
{
/*********
** Public methods
*********/
/****
** Fonts
****/
/// <summary>Get the dimensions of a space character.</summary>
/// <param name="font">The font to measure.</param>
public static float GetSpaceWidth(SpriteFont font)
{
return CommonHelper.GetSpaceWidth(font);
}
/****
** Drawing
****/
/// <summary>Draw a sprite to the screen.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="sheet">The sprite sheet containing the sprite.</param>
/// <param name="sprite">The sprite coordinates and dimensions in the sprite sheet.</param>
/// <param name="x">The X-position at which to draw the sprite.</param>
/// <param name="y">The X-position at which to draw the sprite.</param>
/// <param name="color">The color to tint the sprite.</param>
/// <param name="scale">The scale to draw.</param>
public static void DrawSprite(this SpriteBatch spriteBatch, Texture2D sheet, Rectangle sprite, float x, float y, Color? color = null, float scale = 1)
{
spriteBatch.Draw(sheet, new Vector2(x, y), sprite, color ?? Color.White, 0, Vector2.Zero, scale, SpriteEffects.None, 0);
}
/// <summary>Draw a sprite to the screen scaled and centered to fit the given dimensions.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="sprite">The sprite to draw.</param>
/// <param name="x">The X-position at which to draw the sprite.</param>
/// <param name="y">The X-position at which to draw the sprite.</param>
/// <param name="size">The size to draw.</param>
/// <param name="color">The color to tint the sprite.</param>
public static void DrawSpriteWithin(this SpriteBatch spriteBatch, SpriteInfo sprite, float x, float y, Vector2 size, Color? color = null)
{
spriteBatch.DrawSpriteWithin(sprite.Spritesheet, sprite.SourceRectangle, x, y, size, color ?? Color.White);
}
/// <summary>Draw a sprite to the screen scaled and centered to fit the given dimensions.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="sheet">The sprite sheet containing the sprite.</param>
/// <param name="sprite">The sprite coordinates and dimensions in the sprite sheet.</param>
/// <param name="x">The X-position at which to draw the sprite.</param>
/// <param name="y">The X-position at which to draw the sprite.</param>
/// <param name="size">The size to draw.</param>
/// <param name="color">The color to tint the sprite.</param>
public static void DrawSpriteWithin(this SpriteBatch spriteBatch, Texture2D sheet, Rectangle sprite, float x, float y, Vector2 size, Color? color = null)
{
// calculate dimensions
float largestDimension = Math.Max(sprite.Width, sprite.Height);
float scale = size.X / largestDimension;
float leftOffset = Math.Max((size.X - (sprite.Width * scale)) / 2, 0);
float topOffset = Math.Max((size.Y - (sprite.Height * scale)) / 2, 0);
// draw
spriteBatch.DrawSprite(sheet, sprite, x + leftOffset, y + topOffset, color ?? Color.White, scale);
}
/// <summary>Draw a sprite to the screen.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="x">The X-position at which to start the line.</param>
/// <param name="y">The X-position at which to start the line.</param>
/// <param name="size">The line dimensions.</param>
/// <param name="color">The color to tint the sprite.</param>
public static void DrawLine(this SpriteBatch batch, float x, float y, Vector2 size, Color? color = null)
{
batch.Draw(Sprites.Pixel, new Rectangle((int)x, (int)y, (int)size.X, (int)size.Y), color ?? Color.White);
}
/// <summary>Draw a block of text to the screen with the specified wrap width.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="font">The sprite font.</param>
/// <param name="text">The block of text to write.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The width at which to wrap the text.</param>
/// <param name="color">The text color.</param>
/// <param name="bold">Whether to draw bold text.</param>
/// <param name="scale">The font scale.</param>
/// <returns>Returns the text dimensions.</returns>
public static Vector2 DrawTextBlock(this SpriteBatch batch, SpriteFont font, string text, Vector2 position, float wrapWidth, Color? color = null, bool bold = false, float scale = 1)
{
return batch.DrawTextBlock(font, new IFormattedText[] { new FormattedText(text, color, bold) }, position, wrapWidth, scale);
}
/// <summary>Draw a block of text to the screen with the specified wrap width.</summary>
/// <param name="batch">The sprite batch.</param>
/// <param name="font">The sprite font.</param>
/// <param name="text">The block of text to write.</param>
/// <param name="position">The position at which to draw the text.</param>
/// <param name="wrapWidth">The width at which to wrap the text.</param>
/// <param name="scale">The font scale.</param>
/// <returns>Returns the text dimensions.</returns>
public static Vector2 DrawTextBlock(this SpriteBatch batch, SpriteFont font, IEnumerable<IFormattedText> text, Vector2 position, float wrapWidth, float scale = 1)
{
if (text == null)
return new Vector2(0, 0);
// track draw values
float xOffset = 0;
float yOffset = 0;
float lineHeight = font.MeasureString("ABC").Y * scale;
float spaceWidth = DrawHelper.GetSpaceWidth(font) * scale;
float blockWidth = 0;
float blockHeight = lineHeight;
// draw text snippets
foreach (IFormattedText snippet in text)
{
if (snippet?.Text == null)
continue;
// get word list
List<string> words = new List<string>();
foreach (string word in snippet.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);
}
// draw words to screen
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 (snippet.Bold)
Utility.drawBoldText(batch, word, font, wordPosition, snippet.Color ?? Color.Black, scale);
else
batch.DrawString(font, word, wordPosition, snippet.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);
}
}
}

View File

@ -0,0 +1,20 @@
using StardewValley.Characters;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>The growth stage for a player's child.</summary>
internal enum ChildAge
{
/// <summary>The child was born days ago.</summary>
Newborn = Child.newborn,
/// <summary>The child is older than newborn, and can sit on its own.</summary>
Baby = Child.baby,
/// <summary>The child is older than baby, and can crawl around.</summary>
Crawler = Child.crawler,
/// <summary>The child is older than crawler, and can toddle around.</summary>
Toddler = Child.toddler
}
}

View File

@ -0,0 +1,73 @@
using Microsoft.Xna.Framework;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Constant mod values.</summary>
internal static class Constant
{
/*********
** Accessors
*********/
/// <summary>The maximum stack size for which to calculate a stack price (e.g. to avoid showing a stack size for infinite store inventory).</summary>
public static readonly int MaxStackSizeForPricing = 999;
/// <summary>Whether bold text should be enabled where needed.</summary>
/// <remarks>This is disabled for languages like Chinese which are difficult to read in bold.</remarks>
public static bool AllowBold => Game1.content.GetCurrentLanguage() != LocalizedContentManager.LanguageCode.zh;
/// <summary>The largest expected sprite size (measured in tiles).</summary>
/// <remarks>This is used to account for sprites that extend beyond their tile when searching for targets. These values should be large enough to cover the largest target sprites, but small enough to minimise expensive cursor collision checks.</remarks>
public static readonly Vector2 MaxTargetSpriteSize = new Vector2(3, 5);
/// <summary>Equivalent to <see cref="MaxTargetSpriteSize"/>, but for building targets.</summary>
public static readonly Vector2 MaxBuildingTargetSpriteSize = new Vector2(10, 10);
/// <summary>The <see cref="StardewValley.Farmer.mailReceived"/> keys referenced by the mod.</summary>
public static class MailLetters
{
/// <summary>Set when the spouse gives the player a stardrop.</summary>
public const string ReceivedSpouseStardrop = "CF_Spouse";
/// <summary>Set when the player buys a Joja membership, which demolishes the community center.</summary>
public const string JojaMember = "JojaMember";
}
/// <summary>The season names.</summary>
public static class SeasonNames
{
/// <summary>The internal name for Spring.</summary>
public const string Spring = "spring";
/// <summary>The internal name for Summer.</summary>
public const string Summer = "summer";
/// <summary>The internal name for Fall.</summary>
public const string Fall = "fall";
/// <summary>The internal name for Winter.</summary>
public const string Winter = "winter";
}
/// <summary>The names of items referenced by the mod.</summary>
public static class ItemNames
{
/// <summary>The internal name for the heater object.</summary>
public static string Heater = "Heater";
}
/// <summary>The names of buildings referenced by the mod.</summary>
public static class BuildingNames
{
/// <summary>The internal name for the Gold Clock.</summary>
public static string GoldClock = "Gold Clock";
}
/// <summary>The parent sheet indexes referenced by the mod.</summary>
public static class ObjectIndexes
{
/// <summary>The parent sheet index for the auto-grabber.</summary>
public static int AutoGrabber = 165;
}
}
}

View File

@ -0,0 +1,18 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>The direction a player is facing.</summary>
internal enum FacingDirection
{
/// <summary>The player is facing the top of the screen.</summary>
Up = 0,
/// <summary>The player is facing the right side of the screen.</summary>
Right = 1,
/// <summary>The player is facing the bottom of the screen.</summary>
Down = 2,
/// <summary>The player is facing the left side of the screen.</summary>
Left = 3
}
}

View File

@ -0,0 +1,13 @@
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates a fence type. Better fences last longer.</summary>
internal enum FenceType
{
Wood = Fence.wood,
Stone = Fence.stone,
Iron = Fence.steel, // sic
Hardwood = Fence.gold // sic
}
}

View File

@ -0,0 +1,14 @@
using StardewValley.TerrainFeatures;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates a tree's growth stage.</summary>
internal enum FruitTreeGrowthStage
{
Seed = FruitTree.seedStage,
Sprout = FruitTree.sproutStage,
Sapling = FruitTree.saplingStage,
Bush = FruitTree.bushStage,
Tree = FruitTree.treeStage
}
}

View File

@ -0,0 +1,14 @@
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates how much an NPC likes a particular gift.</summary>
internal enum GiftTaste
{
Hate = NPC.gift_taste_hate,
Dislike = NPC.gift_taste_dislike,
Neutral = NPC.gift_taste_neutral,
Like = NPC.gift_taste_like,
Love = NPC.gift_taste_love
}
}

View File

@ -0,0 +1,43 @@
using System;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates an item quality. (Higher-quality items are sold at a higher price.)</summary>
internal enum ItemQuality
{
Normal = SObject.lowQuality,
Silver = SObject.medQuality,
Gold = SObject.highQuality,
Iridium = SObject.bestQuality
}
/// <summary>Extension methods for <see cref="ItemQuality"/>.</summary>
internal static class ItemQualityExtensions
{
/// <summary>Get the quality name.</summary>
/// <param name="current">The quality.</param>
public static string GetName(this ItemQuality current)
{
return current.ToString().ToLower();
}
/// <summary>Get the next better quality.</summary>
/// <param name="current">The current quality.</param>
public static ItemQuality GetNext(this ItemQuality current)
{
switch (current)
{
case ItemQuality.Normal:
return ItemQuality.Silver;
case ItemQuality.Silver:
return ItemQuality.Gold;
case ItemQuality.Gold:
case ItemQuality.Iridium:
return ItemQuality.Iridium;
default:
throw new NotSupportedException($"Unknown quality '{current}'.");
}
}
}
}

View File

@ -0,0 +1,30 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates the sprite sheet used to draw an object. A given sprite ID can be duplicated between two sprite sheets.</summary>
internal enum ItemSpriteType
{
/// <summary>The <c>Data\ObjectInformation.xnb</c> (<see cref="StardewValley.Game1.objectSpriteSheet"/>) sprite sheet used to draw most inventory items and some placeable objects.</summary>
Object,
/// <summary>The <c>Data\BigCraftablesInformation.xnb</c> (<see cref="StardewValley.Game1.bigCraftableSpriteSheet"/>) sprite sheet used to draw furniture, scarecrows, tappers, crafting stations, and similar placeable objects.</summary>
BigCraftable,
/// <summary>The <c>Data\Boots.xnb</c> sprite sheet used to draw boot equipment.</summary>
Boots,
/// <summary>The <c>Data\hats.xnb</c> sprite sheet used to draw boot equipment.</summary>
Hat,
/// <summary>The <c>TileSheets\furniture.xnb</c> sprite sheet used to draw furniture.</summary>
Furniture,
/// <summary>The <c>TileSheets\weapons.xnb</c> sprite sheet used to draw tools and weapons.</summary>
Tool,
/// <summary>The <c>Maps\walls_and_floors</c> sprite sheet used to draw wallpapers and flooring.</summary>
Wallpaper,
/// <summary>The item isn't covered by one of the known types.</summary>
Unknown
}
}

View File

@ -0,0 +1,814 @@
using System.Diagnostics.CodeAnalysis;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Localisation Keys matching the mod's <c>i18n</c> schema.</summary>
[SuppressMessage("ReSharper", "MemberHidesStaticFromOuterClass", Justification = "Irrelevant in this context.")]
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Deliberately named to keep translation keys short.")]
internal static class L10n
{
/*********
** Accessors
*********/
/// <summary>Generic field value translations.</summary>
public static class Generic
{
/// <summary>A value like <c>{{seasonName}} {{dayNumber}}</c>. Expected tokens: <c>{{seasonName}}, {{seasonNumber}}, {{dayNumber}}, {{year}}</c>.</summary>
public const string Date = "generic.date";
/// <summary>A value like <c>{{seasonName}} {{dayNumber}} in year {{Year}}</c>. Expected tokens: <c>{{seasonName}}, {{seasonNumber}}, {{dayNumber}}, {{year}}</c>.</summary>
public const string DateWithYear = "generic.date-with-year";
/// <summary>A value like <c>{{percent}}%</c>.</summary>
public const string Percent = "generic.percent";
/// <summary>A value like <c>{{percent}}% chance of {{label}}</c>.</summary>
public const string PercentChanceOf = "generic.percent-chance-of";
/// <summary>A value like <c>{{percent}}% ({{value}} of {{max}})</c>.</summary>
public const string PercentRatio = "generic.percent-ratio";
/// <summary>A value like <c>{{value}} of {{max}}</c>.</summary>
public const string Ratio = "generic.ratio";
/// <summary>A value like <c>{{min}} to {{max}}</c>.</summary>
public const string Range = "generic.range";
/// <summary>A value like <c>yes</c>.</summary>
public const string Yes = "generic.yes";
/// <summary>A value like <c>no</c>.</summary>
public const string No = "generic.no";
/// <summary>A value like <c>{{count}} seconds</c>.</summary>
public const string Seconds = "generic.seconds";
/// <summary>A value like <c>{{count}} minutes</c>.</summary>
public const string Minutes = "generic.minutes";
/// <summary>A value like <c>{{count}} hours</c>.</summary>
public const string Hours = "generic.hours";
/// <summary>A value like <c>{{count}} days</c>.</summary>
public const string Days = "generic.days";
/// <summary>A value like <c>in {{count}} days</c>.</summary>
public const string InXDays = "generic.in-x-days";
/// <summary>A value like <c>tomorrow</c>.</summary>
public const string Tomorrow = "generic.tomorrow";
/// <summary>A value like <c>{{price}}g</c>.</summary>
public const string Price = "generic.price";
/// <summary>A value like <c>{{price}}g ({{quality}})</c>.</summary>
public const string PriceForQuality = "generic.price-for-quality";
/// <summary>A value like <c>{{price}}g for stack of {{count}}</c>.</summary>
public const string PriceForStack = "generic.price-for-stack";
}
/// <summary>Lookup subject types.</summary>
public static class Types
{
/// <summary>A value like <c>Building</c>.</summary>
public const string Building = "type.building";
/// <summary>A value like <c>{{fruitName}} Tree</c>.</summary>
public const string FruitTree = "type.fruit-tree";
/// <summary>A value like <c>Monster</c>.</summary>
public const string Monster = "type.monster";
/// <summary>A value like <c>Player</c>.</summary>
public const string Player = "type.player";
/// <summary>A value like <c>Map tile</c>.</summary>
public const string Tile = "type.map-tile";
/// <summary>A value like <c>Tree</c>.</summary>
public const string Tree = "type.tree";
/// <summary>A value like <c>Villager</c>.</summary>
public const string Villager = "type.villager";
/// <summary>A value like <c>Other</c>.</summary>
public const string Other = "type.other";
}
/// <summary>Community Center bundle areas.</summary>
public static class BundleAreas
{
/// <summary>A value like <c>Pantry</c>.</summary>
public const string Pantry = "bundle-area.pantry";
/// <summary>A value like <c>Crafts Room</c>.</summary>
public const string CraftsRoom = "bundle-area.crafts-room";
/// <summary>A value like <c>Fish Tank</c>.</summary>
public const string FishTank = "bundle-area.fish-tank";
/// <summary>A value like <c>Boiler Room</c>.</summary>
public const string BoilerRoom = "bundle-area.boiler-room";
/// <summary>A value like <c>Vault</c>.</summary>
public const string Vault = "bundle-area.vault";
/// <summary>A value like <c>Bulletin Board</c>.</summary>
public const string BulletinBoard = "bundle-area.bulletin-board";
}
/// <summary>Recipe types.</summary>
public static class RecipeTypes
{
/// <summary>A value like <c>Cooking</c>.</summary>
public const string Cooking = "recipe-type.cooking";
/// <summary>A value like <c>Crafting</c>.</summary>
public const string Crafting = "recipe-type.crafting";
}
/// <summary>Animal lookup translations.</summary>
public static class Animal
{
/****
** Labels
****/
/// <summary>A value like <c>Love</c>.</summary>
public const string Love = "animal.love";
/// <summary>A value like <c>Happiness</c>.</summary>
public const string Happiness = "animal.happiness";
/// <summary>A value like <c>Mood today</c>.</summary>
public const string Mood = "animal.mood";
/// <summary>A value like <c>Complaints</c>.</summary>
public const string Complaints = "animal.complaints";
/// <summary>A value like <c>Produce ready</c>.</summary>
public const string ProduceReady = "animal.produce-ready";
/// <summary>A value like <c>Growth</c>.</summary>
public const string Growth = "animal.growth";
/// <summary>A value like <c>Sells for</c>.</summary>
public const string SellsFor = "animal.sells-for";
/****
** Values
****/
/// <summary>A value like <c>was disturbed by {{name}}</c>.</summary>
public const string ComplaintsWildAnimalAttack = "animal.complaints.wild-animal-attack";
/// <summary>A value like <c>wasn't fed yesterday</c>.</summary>
public const string ComplaintsHungry = "animal.complaints.hungry";
/// <summary>A value like <c>was left outside last night</c>.</summary>
public const string ComplaintsLeftOut = "animal.complaints.left-out";
/// <summary>A value like <c>moved into new home</c>.</summary>
public const string ComplaintsNewHome = "animal.complaints.new-home";
/// <summary>A value like <c>no heater in winter</c>.</summary>
public const string ComplaintsNoHeater = "animal.complaints.no-heater";
/// <summary>A value like <c>hasn't been petted today</c>.</summary>
public const string ComplaintsNotPetted = "animal.complaints.not-petted";
}
/// <summary>building lookup translations.</summary>
public static class Building
{
/****
** Labels
****/
/// <summary>A value like <c>Animals</c>.</summary>
public const string Animals = "building.animals";
/// <summary>A value like <c>Construction</c>.</summary>
public const string Construction = "building.construction";
/// <summary>A value like <c>Feed trough</c>.</summary>
public const string FeedTrough = "building.feed-trough";
/// <summary>A value like <c>Horse</c>.</summary>
public const string Horse = "building.horse";
/// <summary>A value like <c>Horse</c>.</summary>
public const string HorseLocation = "building.horse-location";
/// <summary>A value like <c>Harvesting enabled</c>.</summary>
public const string JunimoHarvestingEnabled = "building.junimo-harvesting-enabled";
/// <summary>A value like <c>Owner</c>.</summary>
public const string Owner = "building.owner";
/// <summary>A value like <c>Produce ready</c>.</summary>
public const string OutputProcessing = "building.output-processing";
/// <summary>A value like <c>Produce ready</c>.</summary>
public const string OutputReady = "building.output-ready";
/// <summary>A value like <c>Slimes</c>.</summary>
public const string Slimes = "building.slimes";
/// <summary>A value like <c>Stored hay</c>.</summary>
public const string StoredHay = "building.stored-hay";
/// <summary>A value like <c>Upgrades</c>.</summary>
public const string Upgrades = "building.upgrades";
/// <summary>A value like <c>Water trough</c>.</summary>
public const string WaterTrough = "building.water-trough";
/****
** Values
****/
/// <summary>A value like <c>{{count}} of max {{max}} animals</c>.</summary>
public const string AnimalsSummary = "building.animals.summary";
/// <summary>A value like <c>ready on {{date}}</c>.</summary>
public const string ConstructionSummary = "building.construction.summary";
/// <summary>A value like <c>automated</c>.</summary>
public const string FeedTroughAutomated = "building.feed-trough.automated";
/// <summary>A value like <c>{{filled}} of {{max}} feed slots filled</c>.</summary>
public const string FeedTroughSummary = "building.feed-trough.summary";
/// <summary>A value like <c>{{location}} ({{x}}, {{y}})</c>.</summary>
public const string HorseLocationSummary = "building.horse-location.summary";
/// <summary>A value like <c>no owner</c>.</summary>
public const string OwnerNone = "building.owner.none";
/// <summary>A value like <c>{{count}} of max {{max}} slimes</c>.</summary>
public const string SlimesSummary = "building.slimes.summary";
/// <summary>A value like <c>{{hayCount}} hay (max capacity: {{maxHay}})</c>.</summary>
public const string StoredHaySummaryOneSilo = "building.stored-hay.summary-one-silo";
/// <summary>A value like <c>{{hayCount}} hay in {{siloCount}} silos (max capacity: {{maxHay}})</c>.</summary>
public const string StoredHaySummaryMultipleSilos = "building.stored-hay.summary-multiple-silos";
/// <summary>A value like <c>up to 4 animals, add cows</c>.</summary>
public const string UpgradesBarn0 = "building.upgrades.barn.0";
/// <summary>A value like <c>up to 8 animals, add pregnancy and goats</c>.</summary>
public const string UpgradesBarn1 = "building.upgrades.barn.1";
/// <summary>A value like <c>up to 12 animals, add autofeed, pigs, and sheep"</c>.</summary>
public const string UpgradesBarn2 = "building.upgrades.barn.2";
/// <summary>A value like <c>initial cabin</c>.</summary>
public const string UpgradesCabin0 = "building.upgrades.cabin.0";
/// <summary>A value like <c>add kitchen, enable marriage</c>.</summary>
public const string UpgradesCabin1 = "building.upgrades.cabin.1";
/// <summary>A value like <c>enable children</c>.</summary>
public const string UpgradesCabin2 = "building.upgrades.cabin.2";
/// <summary>A value like <c>up to 4 animals; add chickens</c>.</summary>
public const string UpgradesCoop0 = "building.upgrades.coop.0";
/// <summary>A value like <c>up to 8 animals; add incubator, dinosaurs, and ducks</c>.</summary>
public const string UpgradesCoop1 = "building.upgrades.coop.1";
/// <summary>A value like <c>up to 12 animals; add autofeed and rabbits</c>.</summary>
public const string UpgradesCoop2 = "building.upgrades.coop.2";
/// <summary>A value like <c>{{filled}} of {{max}} water troughs filled</c>.</summary>
public const string WaterTroughSummary = "building.water-trough.summary";
}
/// <summary>Fruit tree lookup translations.</summary>
public static class FruitTree
{
/****
** Labels
****/
/// <summary>A value like <c>Complaints</c>.</summary>
public const string Complaints = "fruit-tree.complaints";
/// <summary>A value like <c>Growth</c>.</summary>
public const string Growth = "fruit-tree.growth";
/// <summary>A value like <c>{{fruitName}} Tree</c>.</summary>
public const string Name = "fruit-tree.name";
/// <summary>A value like <c>Next fruit</c>.</summary>
public const string NextFruit = "fruit-tree.next-fruit";
/// <summary>A value like <c>Season</c>.</summary>
public const string Season = "fruit-tree.season";
/// <summary>A value like <c>Quality</c>.</summary>
public const string Quality = "fruit-tree.quality";
/****
** Values
****/
/// <summary>A value like <c>can't grow because there are adjacent objects</c>.</summary>
public const string ComplaintsAdjacentObjects = "fruit-tree.complaints.adjacent-objects";
/// <summary>A value like <c>mature on {{date}}</c>.</summary>
public const string GrowthSummary = "fruit-tree.growth.summary";
/// <summary>A value like <c>struck by lightning! Will recover in {{count}} days.</c>.</summary>
public const string NextFruitStruckByLightning = "fruit-tree.next-fruit.struck-by-lightning";
/// <summary>A value like <c>out of season</c>.</summary>
public const string NextFruitOutOfSeason = "fruit-tree.next-fruit.out-of-season";
/// <summary>A value like <c>won't grow any more fruit until you harvest those it has</c>.</summary>
public const string NextFruitMaxFruit = "fruit-tree.next-fruit.max-fruit";
/// <summary>A value like <c>too young to bear fruit</c>.</summary>
public const string NextFruitTooYoung = "fruit-tree.next-fruit.too-young";
/// <summary>A value like <c>{{quality}} now</c>.</summary>
public const string QualityNow = "fruit-tree.quality.now";
/// <summary>A value like <c>{{quality}} on {{date}}</c>.</summary>
public const string QualityOnDate = "fruit-tree.quality.on-date";
/// <summary>A value like <c>{{quality}} on {{date}} next year</c>.</summary>
public const string QualityOnDateNextYear = "fruit-tree.quality.on-date-next-year";
/// <summary>A value like <c>{{season}} (or anytime in greenhouse)</c>.</summary>
public const string SeasonSummary = "fruit-tree.season.summary";
}
/// <summary>Crop lookup translations.</summary>
public static class Crop
{
/****
** Labels
****/
/// <summary>A value like <c>Crop</c>.</summary>
public const string Summary = "crop.summary";
/// <summary>A value like <c>Harvest</c>.</summary>
public const string Harvest = "crop.harvest";
/****
** Values
****/
/// <summary>A value like <c>This crop is dead.</c>.</summary>
public const string SummaryDead = "crop.summary.dead";
/// <summary>A value like <c>drops {{count}}</c>.</summary>
public const string SummaryDropsX = "crop.summary.drops-x";
/// <summary>A value like <c>drops {{min}} to {{max}} ({{percent}}% chance of extra crops)</c>.</summary>
public const string SummaryDropsXToY = "crop.summary.drops-x-to-y";
/// <summary>A value like <c>harvest after {{daysToFirstHarvest}} days</c>.</summary>
public const string SummaryHarvestOnce = "crop.summary.harvest-once";
/// <summary>A value like <c>harvest after {{daysToFirstHarvest}} days, then every {{daysToNextHarvests}} days</c>.</summary>
public const string SummaryHarvestMulti = "crop.summary.harvest-multi";
/// <summary>A value like <c>grows in {{seasons}}</c>.</summary>
public const string SummarySeasons = "crop.summary.seasons";
/// <summary>A value like <c>sells for {{price}}</c>.</summary>
public const string SummarySellsFor = "crop.summary.sells-for";
/// <summary>A value like <c>now</c>.</summary>
public const string HarvestNow = "crop.harvest.now";
/// <summary>A value like <c>too late in the season for the next harvest (would be on {{date}})</c>.</summary>
public const string HarvestTooLate = "crop.harvest.too-late";
}
/// <summary>Item lookup translations.</summary>
public static class Item
{
/// <summary>A value like <c>Aging</c>.</summary>
public const string CaskSchedule = "item.cask-schedule";
/// <summary>A value like <c>Bait</c>.</summary>
public const string CrabpotBait = "item.crabpot-bait";
/// <summary>A value like <c>Needs bait!</c>.</summary>
public const string CrabpotBaitNeeded = "item.crabpot-bait-needed";
/// <summary>A value like <c>Not needed due to Luremaster profession.</c>.</summary>
public const string CrabpotBaitNotNeeded = "item.crabpot-bait-not-needed";
/// <summary>A value like <c>Contents</c>.</summary>
public const string Contents = "item.contents";
/// <summary>A value like <c>Needed for</c>.</summary>
public const string NeededFor = "item.needed-for";
/// <summary>A value like <c>Sells for</c>.</summary>
public const string SellsFor = "item.sells-for";
/// <summary>A value like <c>Sells to</c>.</summary>
public const string SellsTo = "item.sells-to";
/// <summary>A value like <c>Likes this</c>.</summary>
public const string LikesThis = "item.likes-this";
/// <summary>A value like <c>Loves this</c>.</summary>
public const string LovesThis = "item.loves-this";
/// <summary>A value like <c>Health</c>.</summary>
public const string FenceHealth = "item.fence-health";
/// <summary>A value like <c>Recipes</c>.</summary>
public const string Recipes = "item.recipes";
/// <summary>A value like <c>Owned</c>.</summary>
public const string Owned = "item.number-owned";
/// <summary>A value like <c>Cooked</c>.</summary>
public const string Cooked = "item.number-cooked";
/// <summary>A value like <c>Crafted</c>.</summary>
public const string Crafted = "item.number-crafted";
/// <summary>A value like <c>See also</c>.</summary>
public const string SeeAlso = "item.see-also";
/****
** Values
****/
/// <summary>A value like <c>{{quality}} ready now</c>.</summary>
public const string CaskScheduleNow = "item.cask-schedule.now";
/// <summary>A value like <c>{{quality}} now (use pickaxe to stop aging)</c>.</summary>
public const string CaskSchedulePartial = "item.cask-schedule.now-partial";
/// <summary>A value like <c>{{quality}} tomorrow</c>.</summary>
public const string CaskScheduleTomorrow = "item.cask-schedule.tomorrow";
/// <summary>A value like <c>{{quality}} in {{count}} days ({{date}})</c>.</summary>
public const string CaskScheduleInXDays = "item.cask-schedule.in-x-days";
/// <summary>A value like <c>has {{name}}</c>.</summary>
public const string ContentsPlaced = "item.contents.placed";
/// <summary>A value like <c>{{name}} ready</c>.</summary>
public const string ContentsReady = "item.contents.ready";
/// <summary>A value like <c>{{name}} in {{time}}</c>.</summary>
public const string ContentsPartial = "item.contents.partial";
/// <summary>A value like <c>community center ({{bundles}})</c>.</summary>
public const string NeededForCommunityCenter = "item.needed-for.community-center";
/// <summary>A value like <c>full shipment achievement (ship one)</c>.</summary>
public const string NeededForFullShipment = "item.needed-for.full-shipment";
/// <summary>A value like <c>polyculture achievement (ship {{count}} more)</c>.</summary>
public const string NeededForPolyculture = "item.needed-for.polyculture";
/// <summary>A value like <c>full collection achievement (donate one to museum)</c>.</summary>
public const string NeededForFullCollection = "item.needed-for.full-collection";
/// <summary>A value like <c>gourmet chef achievement (cook {{recipes}})</c>.</summary>
public const string NeededForGourmetChef = "item.needed-for.gourmet-chef";
/// <summary>A value like <c>craft master achievement (make {{recipes}})</c>.</summary>
public const string NeededForCraftMaster = "item.needed-for.craft-master";
/// <summary>A value like <c>shipping box</c>.</summary>
public const string SellsToShippingBox = "item.sells-to.shipping-box";
/// <summary>A value like <c>no decay with Gold Clock</c>.</summary>
public const string FenceHealthGoldClock = "item.fence-health.gold-clock";
/// <summary>A value like <c>{{percent}}% (roughly {{count}} days left)</c>.</summary>
public const string FenceHealthSummary = "item.fence-health.summary";
/// <summary>A value like <c>{{name}} (needs {{count}})</c>.</summary>
public const string RecipesEntry = "item.recipes.entry";
/// <summary>A value like <c>you own {{count}} of these</c>.</summary>
public const string OwnedSummary = "item.number-owned.summary";
/// <summary>A value like <c>you made {{count}} of these</c>.</summary>
public const string CraftedSummary = "item.number-crafted.summary";
}
/// <summary>Monster lookup translations.</summary>
public static class Monster
{
/****
** Labels
****/
/// <summary>A value like <c>Invincible</c>.</summary>
public const string Invincible = "monster.invincible";
/// <summary>A value like <c>Health</c>.</summary>
public const string Health = "monster.health";
/// <summary>A value like <c>Drops</c>.</summary>
public const string Drops = "monster.drops";
/// <summary>A value like <c>XP</c>.</summary>
public const string Experience = "monster.experience";
/// <summary>A value like <c>Defence</c>.</summary>
public const string Defence = "monster.defence";
/// <summary>A value like <c>Attack</c>.</summary>
public const string Attack = "monster.attack";
/// <summary>A value like <c>Adventure Guild</c>.</summary>
public const string AdventureGuild = "monster.adventure-guild";
/****
** Values
****/
/// <summary>A value like <c>nothing</c>.</summary>
public const string DropsNothing = "monster.drops.nothing";
/// <summary>A value like <c>complete</c>.</summary>
public const string AdventureGuildComplete = "monster.adventure-guild.complete";
/// <summary>A value like <c>in progress</c>.</summary>
public const string AdventureGuildIncomplete = "monster.adventure-guild.incomplete";
/// <summary>A value like <c>killed {{count}} of {{requiredCount}}</c>.</summary>
public const string AdventureGuildProgress = "monster.adventure-guild.progress";
}
/// <summary>NPC lookup translations.</summary>
public static class Npc
{
/****
** Labels
****/
/// <summary>A value like <c>Birthday</c>.</summary>
public const string Birthday = "npc.birthday";
/// <summary>A value like <c>Can romance</c>.</summary>
public const string CanRomance = "npc.can-romance";
/// <summary>A value like <c>Friendship</c>.</summary>
public const string Friendship = "npc.friendship";
/// <summary>A value like <c>Talked today</c>.</summary>
public const string TalkedToday = "npc.talked-today";
/// <summary>A value like <c>Gifted today</c>.</summary>
public const string GiftedToday = "npc.gifted-today";
/// <summary>A value like <c>Gifted this week</c>.</summary>
public const string GiftedThisWeek = "npc.gifted-this-week";
/// <summary>A value like <c>Likes gifts</c>.</summary>
public const string LikesGifts = "npc.likes-gifts";
/// <summary>A value like <c>Loves gifts</c>.</summary>
public const string LovesGifts = "npc.loves-gifts";
/// <summary>A value like <c>Neutral gifts</c>.</summary>
public const string NeutralGifts = "npc.neutral-gifts";
/****
** Values
****/
/// <summary>A value like <c>You're married! &lt;</c>.</summary>
public const string CanRomanceMarried = "npc.can-romance.married";
/// <summary>A value like <c>You haven't met them yet.</c>.</summary>
public const string FriendshipNotMet = "npc.friendship.not-met";
/// <summary>A value like <c>need bouquet for next</c>.</summary>
public const string FriendshipNeedBouquet = "npc.friendship.need-bouquet";
/// <summary>A value like <c>next in {{count}} pts</c>.</summary>
public const string FriendshipNeedPoints = "npc.friendship.need-points";
}
/// <summary>NPC child lookup translations.</summary>
public static class NpcChild
{
/****
** Labels
****/
/// <summary>A value like <c>Age</c>.</summary>
public const string Age = "npc.child.age";
/****
** Values
****/
/// <summary>A value like <c>{{label}} ({{count}} days to {{nextLabel}})</c>.</summary>
public const string AgeDescriptionPartial = "npc.child.age.description-partial";
/// <summary>A value like <c>{{label}}</c>.</summary>
public const string AgeDescriptionGrown = "npc.child.age.description-grown";
/// <summary>A value like <c>newborn</c>.</summary>
public const string AgeNewborn = "npc.child.age.newborn";
/// <summary>A value like <c>baby</c>.</summary>
public const string AgeBaby = "npc.child.age.baby";
/// <summary>A value like <c>crawler</c>.</summary>
public const string AgeCrawler = "npc.child.age.crawler";
/// <summary>A value like <c>toddler</c>.</summary>
public const string AgeToddler = "npc.child.age.toddler";
}
/// <summary>Pet lookup translations.</summary>
public static class Pet
{
/// <summary>A value like <c>Love</c>.</summary>
public const string Love = "pet.love";
/// <summary>A value like <c>Petted today</c>.</summary>
public const string PettedToday = "pet.petted-today";
}
/// <summary>Player lookup translations.</summary>
public static class Player
{
/****
** Labels
****/
/// <summary>A value like <c>Farm name</c>.</summary>
public const string FarmName = "player.farm-name";
/// <summary>A value like <c>Farm map</c>.</summary>
public const string FarmMap = "player.farm-map";
/// <summary>A value like <c>Favourite thing</c>.</summary>
public const string FavoriteThing = "player.favorite-thing";
/// <summary>A value like <c>Gender</c>.</summary>
public const string Gender = "player.gender";
/// <summary>A value like <c>Spouse</c>.</summary>
public const string Spouse = "player.spouse";
/// <summary>A value like <c>Combat skill</c>.</summary>
public const string CombatSkill = "player.combat-skill";
/// <summary>A value like <c>Farming skill</c>.</summary>
public const string FarmingSkill = "player.farming-skill";
/// <summary>A value like <c>Foraging skill</c>.</summary>
public const string ForagingSkill = "player.foraging-skill";
/// <summary>A value like <c>Fishing skill</c>.</summary>
public const string FishingSkill = "player.fishing-skill";
/// <summary>A value like <c>Mining skill</c>.</summary>
public const string MiningSkill = "player.mining-skill";
/// <summary>A value like <c>Luck</c>.</summary>
public const string Luck = "player.luck";
/****
** Values
****/
/// <summary>A value like <c>Custom</c>.</summary>
public const string FarmMapCustom = "player.farm-map.custom";
/// <summary>A value like <c>male</c>.</summary>
public const string GenderMale = "player.gender.male";
/// <summary>A value like <c>female</c>.</summary>
public const string GenderFemale = "player.gender.female";
/// <summary>A value like <c>({{percent}}% to many random checks)</c>.</summary>
public const string LuckSummary = "player.luck.summary";
/// <summary>A value like <c>level {{level}} ({{expNeeded}} XP to next)</c>.</summary>
public const string SkillProgress = "player.skill.progress";
/// <summary>A value like <c>level {{level}}</c>.</summary>
public const string SkillProgressLast = "player.skill.progress-last";
}
/// <summary>Tile lookup translations.</summary>
public static class Tile
{
/****
** Labels
****/
/// <summary>A value like <c>A tile position on the map. This is displayed because you enabled tile lookups in the configuration.</c>.</summary>
public const string Description = "tile.description";
/// <summary>A value like <c>Map name</c>.</summary>
public const string MapName = "tile.map-name";
/// <summary>A value like <c>Tile</c>.</summary>
public const string TileField = "tile.tile";
/// <summary>A value like <c>{{layerName}}: tile index</c>.</summary>
public const string TileIndex = "tile.tile-index";
/// <summary>A value like <c>{{layerName}}: tilesheet</c>.</summary>
public const string TileSheet = "tile.tilesheet";
/// <summary>A value like <c>{{layerName}}: blend mode</c>.</summary>
public const string BlendMode = "tile.blend-mode";
/// <summary>A value like <c>{{layerName}}: ix props: {{propertyName}}</c>.</summary>
public const string IndexProperty = "tile.index-property";
/// <summary>A value like <c>{{layerName}}: props: {{propertyName}}</c>.</summary>
public const string TileProperty = "tile.tile-property";
/****
** Values
****/
/// <summary>A value like <c>no tile here</c>.</summary>
public const string TileFieldNoneFound = "tile.tile.none-here";
}
/// <summary>Wild tree lookup translations.</summary>
public static class Tree
{
/****
** Labels
****/
/// <summary>A value like <c>Maple Tree</c>.</summary>
public const string NameMaple = "tree.name.maple";
/// <summary>A value like <c>Oak Tree</c>.</summary>
public const string NameOak = "tree.name.oak";
/// <summary>A value like <c>Pine Tree</c>.</summary>
public const string NamePine = "tree.name.pine";
/// <summary>A value like <c>Palm Tree</c>.</summary>
public const string NamePalm = "tree.name.palm";
/// <summary>A value like <c>Big Mushroom</c>.</summary>
public const string NameBigMushroom = "tree.name.big-mushroom";
/// <summary>A value like <c>Unknown Tree</c>.</summary>
public const string NameUnknown = "tree.name.unknown";
/// <summary>A value like <c>Growth stage</c>.</summary>
public const string Stage = "tree.stage";
/// <summary>A value like <c>Next growth</c>.</summary>
public const string NextGrowth = "tree.next-growth";
/// <summary>A value like <c>Has seed</c>.</summary>
public const string HasSeed = "tree.has-seed";
/****
** Values
****/
/// <summary>A value like <c>Fully grown</c>.</summary>
public const string StageDone = "tree.stage.done";
/// <summary>A value like <c>{{stageName}} ({{step}} of {{max}})</c>.</summary>
public const string StagePartial = "tree.stage.partial";
/// <summary>A value like <c>can't grow in winter outside greenhouse</c>.</summary>
public const string NextGrowthWinter = "tree.next-growth.winter";
/// <summary>A value like <c>can't grow because other trees are too close</c>.</summary>
public const string NextGrowthAdjacentTrees = "tree.next-growth.adjacent-trees";
/// <summary>A value like <c>20% chance to grow into {{stage}} tomorrow</c>.</summary>
public const string NextGrowthRandom = "tree.next-growth.random";
}
/*********
** Public methods
*********/
/// <summary>Get a translation key for an enum value.</summary>
/// <param name="stage">The tree growth stage.</param>
public static string For(WildTreeGrowthStage stage)
{
return $"tree.stages.{stage}";
}
/// <summary>Get a translation key for an enum value.</summary>
/// <param name="quality">The item quality.</param>
public static string For(ItemQuality quality)
{
return $"quality.{quality.GetName()}";
}
/// <summary>Get a translation key for an enum value.</summary>
/// <param name="status">The friendship status.</param>
public static string For(FriendshipStatus status)
{
return $"friendship-status.{status.ToString().ToLower()}";
}
/// <summary>Get a translation key for an enum value.</summary>
/// <param name="age">The child age.</param>
public static string For(ChildAge age)
{
return $"npc.child.age.{age.ToString().ToLower()}";
}
}
}

View File

@ -0,0 +1,12 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates how to lookup targets.</summary>
internal enum LookupMode
{
/// <summary>Lookup whatever's under the cursor.</summary>
Cursor,
/// <summary>Lookup whatever's in front of the player.</summary>
FacingPlayer
}
}

View File

@ -0,0 +1,14 @@
using StardewValley.TerrainFeatures;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates a tree type.</summary>
internal enum TreeType
{
Oak = Tree.bushyTree,
Maple = Tree.leafyTree,
Pine = Tree.pineTree,
Palm = Tree.palmTree,
BigMushroom = Tree.mushroomTree
}
}

View File

@ -0,0 +1,15 @@
using StardewTree = StardewValley.TerrainFeatures.Tree;
namespace Pathoschild.Stardew.LookupAnything.Framework.Constants
{
/// <summary>Indicates a tree's growth stage.</summary>
internal enum WildTreeGrowthStage
{
Seed = StardewTree.seedStage,
Sprout = StardewTree.sproutStage,
Sapling = StardewTree.saplingStage,
Bush = StardewTree.bushStage,
SmallTree = StardewTree.treeStage - 1, // an intermediate stage between bush and tree, no constant
Tree = StardewTree.treeStage
}
}

View File

@ -0,0 +1,12 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>Information about an Adventure Guild monster-slaying quest.</summary>
internal class AdventureGuildQuestData
{
/// <summary>The names of the monsters in this category.</summary>
public string[] Targets { get; set; }
/// <summary>The number of kills required for the reward.</summary>
public int RequiredKills { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>Metadata for a building recipe.</summary>
internal class BuildingRecipeData
{
/*********
** Accessors
*********/
/// <summary>The building key.</summary>
public string BuildingKey { get; set; }
/// <summary>The items needed to craft the recipe (item ID => number needed).</summary>
public IDictionary<int, int> Ingredients { get; set; }
/// <summary>The ingredients which can't be used in this recipe (typically exceptions for a category ingredient).</summary>
public int[] ExceptIngredients { get; set; }
/// <summary>The item created by the recipe.</summary>
public int Output { get; set; }
}
}

View File

@ -0,0 +1,22 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>Provides override metadata about a game NPC.</summary>
internal class CharacterData
{
/*********
** Accessors
*********/
/****
** Identify object
****/
/// <summary>The NPC identifier, like "Horse" (any NPCs of type Horse) or "Villager::Gunther" (any NPCs of type Villager with the name "Gunther").</summary>
public string ID { get; set; }
/****
** Overrides
****/
/// <summary>The translation key which should override the NPC description (if any).</summary>
public string DescriptionKey { get; set; }
}
}

View File

@ -0,0 +1,98 @@
using System.Collections.Generic;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>Constant values hardcoded by the game.</summary>
internal class ConstantData
{
/*********
** Accessors
*********/
/****
** Farm animals
****/
/// <summary>The number of friendship points per level for a farm animal.</summary>
/// <remarks>Derived from <see cref="StardewValley.FarmAnimal.dayUpdate"/>.</remarks>
public int AnimalFriendshipPointsPerLevel { get; set; }
/// <summary>The maximum number of friendship points for a farm animal.</summary>
/// <remarks>Derived from <see cref="StardewValley.FarmAnimal.dayUpdate"/>.</remarks>
public int AnimalFriendshipMaxPoints { get; set; }
/// <summary>The maximum happiness points for a farm animal.</summary>
/// <remarks>Derived from <see cref="StardewValley.FarmAnimal.dayUpdate"/>.</remarks>
public int AnimalMaxHappiness { get; set; }
/// <summary>The number of days until a fruit tree produces a better-quality fruit.</summary>
/// <remarks>Derived from <see cref="StardewValley.TerrainFeatures.FruitTree.shake"/>.</remarks>
public int FruitTreeQualityGrowthTime { get; set; }
/****
** NPCs
****/
/// <summary>The names of villagers with social data (e.g. birthdays or gift tastes).</summary>
public string[] AsocialVillagers { get; set; }
/// <summary>The number of hearts for dateable NPCs which are locked until you give them a bouquet.</summary>
public int DatingHearts { get; set; }
/// <summary>The maximum friendship points for a married NPC.</summary>
public int SpouseMaxFriendship { get; set; }
/// <summary>The minimum friendship points with a married NPC before they give the player a stardrop.</summary>
public int SpouseFriendshipForStardrop { get; set; }
/****
** Players
****/
/// <summary>The maximum experience points for a skill.</summary>
/// <remarks>Derived from <see cref="StardewValley.Farmer.checkForLevelGain"/>.</remarks>
public int PlayerMaxSkillPoints { get; set; }
/// <summary>The experience points needed for each skill level.</summary>
/// <remarks>Derived from <see cref="StardewValley.Farmer.checkForLevelGain"/>.</remarks>
public int[] PlayerSkillPointsPerLevel { get; set; }
/****
** Time
****/
/// <summary>The number of days in each season.</summary>
public int DaysInSeason { get; set; }
/// <summary>The fractional rate at which fences decay (calculated as minutes divided by this value).</summary>
/// <remarks>Derived from <see cref="StardewValley.Fence.minutesElapsed"/>.</remarks>
public float FenceDecayRate { get; set; }
/****
** Crafting
****/
/// <summary>The age thresholds for casks.</summary>
/// <remarks>Derived from <see cref="StardewValley.Objects.Cask.checkForMaturity"/>.</remarks>
public IDictionary<ItemQuality, int> CaskAgeSchedule { get; set; }
/****
** Items
****/
/// <summary>Items which can have an iridium quality. This is a list of category IDs (negative) or item IDs (positive).</summary>
/// <remarks>
/// The following can have iridium quality:
/// • animal produce;
/// • fruit tree produce;
/// • artisanal products aged in the cask (derived from <see cref="StardewValley.Objects.Cask.performObjectDropInAction"/>);
/// • forage crops.
/// </remarks>
public int[] ItemsWithIridiumQuality { get; set; }
/****
** Achievements
****/
/// <summary>The crops that must be shipped for the polyculture achievement.</summary>
/// <remarks>Derived from <see cref="StardewValley.Stats.checkForShippingAchievements"/>.</remarks>
public int[] PolycultureCrops { get; set; }
/// <summary>The number of each crop that must be shipped for the polyculture achievement.</summary>
/// <remarks>Derived from <see cref="StardewValley.Stats.checkForShippingAchievements"/>.</remarks>
public int PolycultureCount { get; set; }
}
}

View File

@ -0,0 +1,33 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>A loot entry parsed from the game data.</summary>
internal class ItemDropData
{
/*********
** Accessors
*********/
/// <summary>The item's parent sprite index.</summary>
public int ItemID { get; }
/// <summary>The maximum number to drop.</summary>
public int MaxDrop { get; }
/// <summary>The probability that the item will be dropped.</summary>
public float Probability { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="itemID">The item's parent sprite index.</param>
/// <param name="maxDrop">The maximum number to drop.</param>
/// <param name="probability">The probability that the item will be dropped.</param>
public ItemDropData(int itemID, int maxDrop, float probability)
{
this.ItemID = itemID;
this.MaxDrop = maxDrop;
this.Probability = probability;
}
}
}

View File

@ -0,0 +1,23 @@
using System.Collections.Generic;
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>Metadata for a machine recipe.</summary>
internal class MachineRecipeData
{
/*********
** Accessors
*********/
/// <summary>The machine item ID.</summary>
public int MachineID { get; set; }
/// <summary>The items needed to craft the recipe (item ID => number needed).</summary>
public IDictionary<int, int> Ingredients { get; set; }
/// <summary>The ingredients which can't be used in this recipe (typically exceptions for a category ingredient).</summary>
public int[] ExceptIngredients { get; set; }
/// <summary>The item created by the recipe.</summary>
public int Output { get; set; }
}
}

View File

@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Linq;
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>A monster entry parsed from the game data.</summary>
internal class MonsterData
{
/*********
** Accessors
*********/
/// <summary>The monster name.</summary>
public string Name { get; }
/// <summary>The monster's health points.</summary>
public int Health { get; }
/// <summary>The damage points the monster afflicts on the player.</summary>
public int DamageToFarmer { get; }
/// <summary>Whether the monster can fly.</summary>
public bool IsGlider { get; }
/// <summary>The amount of time between random movement changes (in milliseconds).</summary>
public int DurationOfRandomMovements { get; }
/// <summary>The monster's damage resistance. (This amount is subtracted from received damage points.)</summary>
public int Resilience { get; }
/// <summary>The probability that a monster will randomly change direction when checked.</summary>
public double Jitteriness { get; }
/// <summary>The tile distance within which the monster will begin moving towards the player.</summary>
public int MoveTowardsPlayerThreshold { get; }
/// <summary>The speed at which the monster moves.</summary>
public int Speed { get; }
/// <summary>The probability that the player will miss when attacking this monster.</summary>
public double MissChance { get; }
/// <summary>Whether the monster appears in the mines. If <c>true</c>, the monster's base stats are increased once the player has reached the bottom of the mine at least once.</summary>
public bool IsMineMonster { get; }
/// <summary>The items dropped by this monster and their probability to drop.</summary>
public ItemDropData[] Drops { get; }
/*********
** public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="name">The monster name.</param>
/// <param name="health">The monster's health points.</param>
/// <param name="damageToFarmer">The damage points the monster afflicts on the player.</param>
/// <summary>Whether the monster can fly.</summary>
/// <param name="isGlider">The amount of time between random movement changes (in milliseconds).</param>
/// <param name="durationOfRandomMovements">The amount of time between random movement changes (in milliseconds).</param>
/// <param name="resilience">The monster's damage resistance.</param>
/// <param name="jitteriness">The probability that a monster will randomly change direction when checked.</param>
/// <param name="moveTowardsPlayerThreshold">The tile distance within which the monster will begin moving towards the player.</param>
/// <param name="speed">The speed at which the monster moves.</param>
/// <param name="missChance">The probability that the player will miss when attacking this monster.</param>
/// <param name="isMineMonster">Whether the monster appears in the mines.</param>
/// <param name="drops">The items dropped by this monster and their probability to drop.</param>
public MonsterData(string name, int health, int damageToFarmer, bool isGlider, int durationOfRandomMovements, int resilience, double jitteriness, int moveTowardsPlayerThreshold, int speed, double missChance, bool isMineMonster, IEnumerable<ItemDropData> drops)
{
this.Name = name;
this.Health = health;
this.DamageToFarmer = damageToFarmer;
this.IsGlider = isGlider;
this.DurationOfRandomMovements = durationOfRandomMovements;
this.Resilience = resilience;
this.Jitteriness = jitteriness;
this.MoveTowardsPlayerThreshold = moveTowardsPlayerThreshold;
this.Speed = speed;
this.MissChance = missChance;
this.IsMineMonster = isMineMonster;
this.Drops = drops.ToArray();
}
}
}

View File

@ -0,0 +1,18 @@
using System;
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>The context in which to override an object.</summary>
[Flags]
internal enum ObjectContext
{
/// <summary>Objects in the world.</summary>
World = 1,
/// <summary>Objects in an item inventory.</summary>
Inventory = 2,
/// <summary>Objects in any context.</summary>
Any = ObjectContext.World | ObjectContext.Inventory
}
}

View File

@ -0,0 +1,38 @@
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>Provides override metadata about a game item.</summary>
internal class ObjectData
{
/*********
** Accessors
*********/
/****
** Identify object
****/
/// <summary>The context in which to override the object.</summary>
public ObjectContext Context { get; set; } = ObjectContext.Any;
/// <summary>The sprite sheet used to draw the object. A given sprite ID can be duplicated between two sprite sheets.</summary>
public ItemSpriteType SpriteSheet { get; set; } = ItemSpriteType.Object;
/// <summary>The sprite IDs for this object.</summary>
public int[] SpriteID { get; set; }
/****
** Overrides
****/
/// <summary>The translation key which should override the item name (if any).</summary>
public string NameKey { get; set; }
/// <summary>The translation key which should override the item description (if any).</summary>
public string DescriptionKey { get; set; }
/// <summary>The translation key which should override the item type name (if any).</summary>
public string TypeKey { get; set; }
/// <summary>Whether the player can pick up this item.</summary>
public bool? ShowInventoryFields { get; set; }
}
}

View File

@ -0,0 +1,18 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Data
{
/// <summary>Metadata for a shop that isn't available from the game data directly.</summary>
internal class ShopData
{
/*********
** Accessors
*********/
/// <summary>The internal name of the shop's indoor location.</summary>
public string LocationName { get; set; }
/// <summary>The translation key for the shop name.</summary>
public string DisplayKey { get; set; }
/// <summary>The categories of items that the player can sell to this shop.</summary>
public int[] BuysCategories { get; set; }
}
}

View File

@ -0,0 +1,56 @@
using System.Globalization;
namespace Pathoschild.Stardew.LookupAnything.Framework.DebugFields
{
/// <summary>A generic debug field containing a raw datamining value.</summary>
internal class GenericDebugField : IDebugField
{
/*********
** Accessors
*********/
/// <summary>A short field label.</summary>
public string Label { get; protected set; }
/// <summary>The field value.</summary>
public string Value { get; protected set; }
/// <summary>Whether the field should be displayed.</summary>
public bool HasValue { get; protected set; }
/// <summary>Whether the field should be highlighted for special attention.</summary>
public bool IsPinned { get; protected set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="label">A short field label.</param>
/// <param name="value">The field value.</param>
/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>
/// <param name="pinned">Whether the field should be highlighted for special attention.</param>
public GenericDebugField(string label, string value, bool? hasValue = null, bool pinned = false)
{
this.Label = label;
this.Value = value;
this.HasValue = hasValue ?? !string.IsNullOrWhiteSpace(this.Value);
this.IsPinned = pinned;
}
/// <summary>Construct an instance.</summary>
/// <param name="label">A short field label.</param>
/// <param name="value">The field value.</param>
/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>
/// <param name="pinned">Whether the field should be highlighted for special attention.</param>
public GenericDebugField(string label, int value, bool? hasValue = null, bool pinned = false)
: this(label, value.ToString(CultureInfo.InvariantCulture), hasValue, pinned) { }
/// <summary>Construct an instance.</summary>
/// <param name="label">A short field label.</param>
/// <param name="value">The field value.</param>
/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>
/// <param name="pinned">Whether the field should be highlighted for special attention.</param>
public GenericDebugField(string label, float value, bool? hasValue = null, bool pinned = false)
: this(label, value.ToString(CultureInfo.InvariantCulture), hasValue, pinned) { }
}
}

View File

@ -0,0 +1,21 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.DebugFields
{
/// <summary>A debug field containing a raw datamining value.</summary>
internal interface IDebugField
{
/*********
** Accessors
*********/
/// <summary>A short field label.</summary>
string Label { get; }
/// <summary>The field value.</summary>
string Value { get; }
/// <summary>Whether the field should be displayed.</summary>
bool HasValue { get; }
/// <summary>Whether the field should be highlighted for special attention.</summary>
bool IsPinned { get; }
}
}

View File

@ -0,0 +1,116 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Components;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Models;
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows friendship points.</summary>
internal class CharacterFriendshipField : GenericField
{
/*********
** Fields
*********/
/// <summary>The player's current friendship data with the NPC.</summary>
private readonly FriendshipModel Friendship;
/// <summary>Provides translations stored in the mod folder.</summary>
private readonly ITranslationHelper Translations;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="friendship">The player's current friendship data with the NPC.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
public CharacterFriendshipField(GameHelper gameHelper, string label, FriendshipModel friendship, ITranslationHelper translations)
: base(gameHelper, label, hasValue: true)
{
this.Friendship = friendship;
this.Translations = translations;
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
FriendshipModel friendship = this.Friendship;
// draw status
float leftOffset = 0;
{
string statusText = this.Translations.Get(L10n.For(friendship.Status));
Vector2 textSize = spriteBatch.DrawTextBlock(font, statusText, new Vector2(position.X + leftOffset, position.Y), wrapWidth - leftOffset);
leftOffset += textSize.X + DrawHelper.GetSpaceWidth(font);
}
// draw hearts
for (int i = 0; i < friendship.TotalHearts; i++)
{
// get icon
Color color;
Rectangle icon;
if (friendship.LockedHearts >= friendship.TotalHearts - i)
{
icon = Sprites.Icons.FilledHeart;
color = Color.Black * 0.35f;
}
else if (i >= friendship.FilledHearts)
{
icon = Sprites.Icons.EmptyHeart;
color = Color.White;
}
else
{
icon = Sprites.Icons.FilledHeart;
color = Color.White;
}
// draw
spriteBatch.DrawSprite(Sprites.Icons.Sheet, icon, position.X + leftOffset, position.Y, color, Game1.pixelZoom);
leftOffset += Sprites.Icons.FilledHeart.Width * Game1.pixelZoom;
}
// draw stardrop (if applicable)
if (friendship.HasStardrop)
{
leftOffset += 1;
float zoom = (Sprites.Icons.EmptyHeart.Height / (Sprites.Icons.Stardrop.Height * 1f)) * Game1.pixelZoom;
spriteBatch.DrawSprite(Sprites.Icons.Sheet, Sprites.Icons.Stardrop, position.X + leftOffset, position.Y, Color.White * 0.25f, zoom);
leftOffset += Sprites.Icons.Stardrop.Width * zoom;
}
// get caption text
string caption = null;
if (this.Friendship.EmptyHearts == 0 && this.Friendship.LockedHearts > 0)
caption = $"({this.Translations.Get(L10n.Npc.FriendshipNeedBouquet)})";
else
{
int pointsToNext = this.Friendship.GetPointsToNext();
if (pointsToNext > 0)
caption = $"({this.Translations.Get(L10n.Npc.FriendshipNeedPoints, new { count = pointsToNext })})";
}
// draw caption
{
float spaceSize = DrawHelper.GetSpaceWidth(font);
Vector2 textSize = Vector2.Zero;
if (caption != null)
textSize = spriteBatch.DrawTextBlock(font, caption, new Vector2(position.X + leftOffset + spaceSize, position.Y), wrapWidth - leftOffset);
return new Vector2(Sprites.Icons.FilledHeart.Width * Game1.pixelZoom * this.Friendship.TotalHearts + textSize.X + spaceSize, Math.Max(Sprites.Icons.FilledHeart.Height * Game1.pixelZoom, textSize.Y));
}
}
}
}

View File

@ -0,0 +1,66 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows which items an NPC likes receiving.</summary>
internal class CharacterGiftTastesField : GenericField
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="giftTastes">The items by how much this NPC likes receiving them.</param>
/// <param name="showTaste">The gift taste to show.</param>
public CharacterGiftTastesField(GameHelper gameHelper, string label, IDictionary<GiftTaste, Item[]> giftTastes, GiftTaste showTaste)
: base(gameHelper, label, CharacterGiftTastesField.GetText(gameHelper, giftTastes, showTaste)) { }
/*********
** Private methods
*********/
/// <summary>Get the text to display.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="giftTastes">The items by how much this NPC likes receiving them.</param>
/// <param name="showTaste">The gift taste to show.</param>
private static IEnumerable<IFormattedText> GetText(GameHelper gameHelper, IDictionary<GiftTaste, Item[]> giftTastes, GiftTaste showTaste)
{
if (!giftTastes.ContainsKey(showTaste))
yield break;
// get item data
Item[] ownedItems = gameHelper.GetAllOwnedItems().ToArray();
Item[] inventory = Game1.player.Items.Where(p => p != null).ToArray();
var items =
(
from item in giftTastes[showTaste]
let isInventory = inventory.Any(p => p.ParentSheetIndex == item.ParentSheetIndex && p.Category == item.Category)
let isOwned = ownedItems.Any(p => p.ParentSheetIndex == item.ParentSheetIndex && p.Category == item.Category)
orderby isInventory descending, isOwned descending, item.DisplayName
select new { Item = item, IsInventory = isInventory, IsOwned = isOwned }
)
.ToArray();
// generate text
for (int i = 0, last = items.Length - 1; i <= last; i++)
{
var entry = items[i];
string text = i != last
? entry.Item.DisplayName + ","
: entry.Item.DisplayName;
if (entry.IsInventory)
yield return new FormattedText(text, Color.Green);
else if (entry.IsOwned)
yield return new FormattedText(text, Color.Black);
else
yield return new FormattedText(text, Color.Gray);
}
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Components;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows a list of checkbox values.</summary>
internal class CheckboxListField : GenericField
{
/*********
** Fields
*********/
/// <summary>The checkbox values to display.</summary>
private readonly KeyValuePair<IFormattedText[], bool>[] Checkboxes;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="checkboxes">The checkbox labels and values to display.</param>
public CheckboxListField(GameHelper gameHelper, string label, IEnumerable<KeyValuePair<IFormattedText[], bool>> checkboxes)
: base(gameHelper, label, hasValue: true)
{
this.Checkboxes = checkboxes.ToArray();
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
float topOffset = 0;
float checkboxSize = Sprites.Icons.FilledCheckbox.Width * (Game1.pixelZoom / 2);
float lineHeight = Math.Max(checkboxSize, Game1.smallFont.MeasureString("ABC").Y);
float checkboxOffset = (lineHeight - checkboxSize) / 2;
foreach (KeyValuePair<IFormattedText[], bool> entry in this.Checkboxes)
{
// draw icon
spriteBatch.Draw(
texture: Sprites.Icons.Sheet,
position: new Vector2(position.X, position.Y + topOffset + checkboxOffset),
sourceRectangle: entry.Value ? Sprites.Icons.FilledCheckbox : Sprites.Icons.EmptyCheckbox,
color: Color.White,
rotation: 0,
origin: Vector2.Zero,
scale: checkboxSize / Sprites.Icons.FilledCheckbox.Width,
effects: SpriteEffects.None,
layerDepth: 1f
);
// draw text
Vector2 textSize = spriteBatch.DrawTextBlock(Game1.smallFont, entry.Key, new Vector2(position.X + checkboxSize + 7, position.Y + topOffset), wrapWidth - checkboxSize - 7);
// update offset
topOffset += Math.Max(checkboxSize, textSize.Y);
}
return new Vector2(wrapWidth, topOffset);
}
}
}

View File

@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>Shows a collection of debug fields.</summary>
internal class DataMiningField : GenericField
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="fields">The debug fields to display.</param>
public DataMiningField(GameHelper gameHelper, string label, IEnumerable<IDebugField> fields)
: base(gameHelper, label)
{
IDebugField[] fieldArray = fields?.ToArray() ?? new IDebugField[0];
this.HasValue = fieldArray.Any();
if (this.HasValue)
this.Value = this.GetFormattedText(fieldArray).ToArray();
}
/*********
** Private methods
*********/
/// <summary>Get a formatted representation of a set of debug fields.</summary>
/// <param name="fields">The debug fields to display.</param>
private IEnumerable<IFormattedText> GetFormattedText(IDebugField[] fields)
{
for (int i = 0, last = fields.Length - 1; i <= last; i++)
{
IDebugField field = fields[i];
yield return new FormattedText("*", Color.Red, bold: true);
yield return new FormattedText($"{field.Label}:");
yield return i != last
? new FormattedText($"{field.Value}{Environment.NewLine}")
: new FormattedText(field.Value);
}
}
}
}

View File

@ -0,0 +1,147 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using StardewModdingAPI;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A generic metadata field shown as an extended property in the lookup UI.</summary>
internal class GenericField : ICustomField
{
/*********
** Accessors
*********/
/// <summary>Provides utility methods for interacting with the game code.</summary>
protected GameHelper GameHelper;
/*********
** Accessors
*********/
/// <summary>A short field label.</summary>
public string Label { get; protected set; }
/// <summary>The field value.</summary>
public IFormattedText[] Value { get; protected set; }
/// <summary>Whether the field should be displayed.</summary>
public bool HasValue { get; protected set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="value">The field value.</param>
/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>
public GenericField(GameHelper gameHelper, string label, string value, bool? hasValue = null)
{
this.GameHelper = gameHelper;
this.Label = label;
this.Value = this.FormatValue(value);
this.HasValue = hasValue ?? this.Value?.Any() == true;
}
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="value">The field value.</param>
/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>
public GenericField(GameHelper gameHelper, string label, IFormattedText value, bool? hasValue = null)
: this(gameHelper, label, new[] { value }, hasValue) { }
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="value">The field value.</param>
/// <param name="hasValue">Whether the field should be displayed (or <c>null</c> to check the <paramref name="value"/>).</param>
public GenericField(GameHelper gameHelper, string label, IEnumerable<IFormattedText> value, bool? hasValue = null)
{
this.GameHelper = gameHelper;
this.Label = label;
this.Value = value.ToArray();
this.HasValue = hasValue ?? this.Value?.Any() == true;
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="Value"/> using the default format.</returns>
public virtual Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
return null;
}
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="hasValue">Whether the field should be displayed.</param>
protected GenericField(GameHelper gameHelper, string label, bool hasValue = false)
: this(gameHelper, label, null as string, hasValue) { }
/// <summary>Wrap text into a list of formatted snippets.</summary>
/// <param name="value">The text to wrap.</param>
protected IFormattedText[] FormatValue(string value)
{
return !string.IsNullOrWhiteSpace(value)
? new IFormattedText[] { new FormattedText(value) }
: new IFormattedText[0];
}
/// <summary>Get the display value for sale price data.</summary>
/// <param name="saleValue">The flat sale price.</param>
/// <param name="stackSize">The number of items in the stack.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
public static string GetSaleValueString(int saleValue, int stackSize, ITranslationHelper translations)
{
return GenericField.GetSaleValueString(new Dictionary<ItemQuality, int> { [ItemQuality.Normal] = saleValue }, stackSize, translations);
}
/// <summary>Get the display value for sale price data.</summary>
/// <param name="saleValues">The sale price data.</param>
/// <param name="stackSize">The number of items in the stack.</param>
/// <param name="translations">Provides methods for fetching translations and generating text.</param>
public static string GetSaleValueString(IDictionary<ItemQuality, int> saleValues, int stackSize, ITranslationHelper translations)
{
// can't be sold
if (saleValues == null || !saleValues.Any() || saleValues.Values.All(p => p == 0))
return null;
// one quality
if (saleValues.Count == 1)
{
string result = translations.Get(L10n.Generic.Price, new { price = saleValues.First().Value });
if (stackSize > 1 && stackSize <= Constant.MaxStackSizeForPricing)
result += $" ({translations.Get(L10n.Generic.PriceForStack, new { price = saleValues.First().Value * stackSize, count = stackSize })})";
return result;
}
// prices by quality
List<string> priceStrings = new List<string>();
for (ItemQuality quality = ItemQuality.Normal; ; quality = quality.GetNext())
{
if (saleValues.ContainsKey(quality))
{
priceStrings.Add(quality == ItemQuality.Normal
? translations.Get(L10n.Generic.Price, new { price = saleValues[quality] })
: translations.Get(L10n.Generic.PriceForQuality, new { price = saleValues[quality], quality = translations.Get(L10n.For(quality)) })
);
}
if (quality.GetNext() == quality)
break;
}
return string.Join(", ", priceStrings);
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field shown as an extended property in the lookup UI.</summary>
internal interface ICustomField
{
/*********
** Accessors
*********/
/// <summary>A short field label.</summary>
string Label { get; }
/// <summary>The field value.</summary>
IFormattedText[] Value { get; }
/// <summary>Whether the field should be displayed.</summary>
bool HasValue { get; }
/*********
** Public methods
*********/
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="Value"/> using the default format.</returns>
Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth);
}
}

View File

@ -0,0 +1,14 @@
using Pathoschild.Stardew.LookupAnything.Framework.Subjects;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A field which links to another entry.</summary>
internal interface ILinkField : ICustomField
{
/*********
** Public methods
*********/
/// <summary>Get the subject the link points to.</summary>
ISubject GetLinkSubject();
}
}

View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Data;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Objects;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows a list of item drops.</summary>
internal class ItemDropListField : GenericField
{
/*********
** Fields
*********/
/// <summary>The possible drops.</summary>
private readonly Tuple<ItemDropData, SObject, SpriteInfo>[] Drops;
/// <summary>The text to display if there are no items.</summary>
private readonly string DefaultText;
/// <summary>Provides translations stored in the mod folder.</summary>
private readonly ITranslationHelper Translations;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="drops">The possible drops.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
/// <param name="defaultText">The text to display if there are no items (or <c>null</c> to hide the field).</param>
public ItemDropListField(GameHelper gameHelper, string label, IEnumerable<ItemDropData> drops, ITranslationHelper translations, string defaultText = null)
: base(gameHelper, label)
{
this.Drops = this
.GetEntries(drops, gameHelper)
.OrderByDescending(p => p.Item1.Probability)
.ThenBy(p => p.Item2.DisplayName)
.ToArray();
this.DefaultText = defaultText;
this.HasValue = defaultText != null || this.Drops.Any();
this.Translations = translations;
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
if (!this.Drops.Any())
return spriteBatch.DrawTextBlock(font, this.DefaultText, position, wrapWidth);
// get icon size
Vector2 iconSize = new Vector2(font.MeasureString("ABC").Y);
// list drops
bool canReroll = Game1.player.isWearingRing(Ring.burglarsRing);
float height = 0;
foreach (var entry in this.Drops)
{
// get data
ItemDropData drop = entry.Item1;
SObject item = entry.Item2;
SpriteInfo sprite = entry.Item3;
bool isGuaranteed = drop.Probability > .99f;
// draw icon
spriteBatch.DrawSpriteWithin(sprite, position.X, position.Y + height, iconSize, isGuaranteed ? Color.White : Color.White * 0.5f);
// draw text
string text = isGuaranteed ? item.DisplayName : this.Translations.Get(L10n.Generic.PercentChanceOf, new { percent = Math.Round(drop.Probability, 4) * 100, label = item.DisplayName });
if (drop.MaxDrop > 1)
text += " (" + this.Translations.Get(L10n.Generic.Range, new { min = 1, max = drop.MaxDrop }) + ")";
Vector2 textSize = spriteBatch.DrawTextBlock(font, text, position + new Vector2(iconSize.X + 5, height + 5), wrapWidth, isGuaranteed ? Color.Black : Color.Gray);
// cross out item if it definitely won't drop
if (!isGuaranteed && !canReroll)
spriteBatch.DrawLine(position.X + iconSize.X + 5, position.Y + height + iconSize.Y / 2, new Vector2(textSize.X, 1), Color.Gray);
height += textSize.Y + 5;
}
// return size
return new Vector2(wrapWidth, height);
}
/*********
** Private methods
*********/
/// <summary>Get the internal drop list entries.</summary>
/// <param name="drops">The possible drops.</param>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
private IEnumerable<Tuple<ItemDropData, SObject, SpriteInfo>> GetEntries(IEnumerable<ItemDropData> drops, GameHelper gameHelper)
{
foreach (ItemDropData drop in drops)
{
SObject item = this.GameHelper.GetObjectBySpriteIndex(drop.ItemID);
SpriteInfo sprite = gameHelper.GetSprite(item);
yield return Tuple.Create(drop, item, sprite);
}
}
}
}

View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.Linq;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows how much each NPC likes receiving this item.</summary>
internal class ItemGiftTastesField : GenericField
{
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="giftTastes">NPCs by how much they like receiving this item.</param>
/// <param name="showTaste">The gift taste to show.</param>
public ItemGiftTastesField(GameHelper gameHelper, string label, IDictionary<GiftTaste, string[]> giftTastes, GiftTaste showTaste)
: base(gameHelper, label, ItemGiftTastesField.GetText(giftTastes, showTaste)) { }
/*********
** Private methods
*********/
/// <summary>Get the text to display.</summary>
/// <param name="giftTastes">NPCs by how much they like receiving this item.</param>
/// <param name="showTaste">The gift taste to show.</param>
private static string GetText(IDictionary<GiftTaste, string[]> giftTastes, GiftTaste showTaste)
{
if (!giftTastes.ContainsKey(showTaste))
return null;
string[] names = giftTastes[showTaste].OrderBy(p => p).ToArray();
return string.Join(", ", names);
}
}
}

View File

@ -0,0 +1,58 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows an item icon.</summary>
internal class ItemIconField : GenericField
{
/*********
** Fields
*********/
/// <summary>The item icon to draw.</summary>
private readonly SpriteInfo Sprite;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="item">The item for which to display an icon.</param>
/// <param name="text">The text to display (if not the item name).</param>
public ItemIconField(GameHelper gameHelper, string label, Item item, string text = null)
: base(gameHelper, label, hasValue: item != null)
{
this.Sprite = gameHelper.GetSprite(item);
if (item != null)
{
this.Value = !string.IsNullOrWhiteSpace(text)
? this.FormatValue(text)
: this.FormatValue(item.DisplayName);
}
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
// get icon size
float textHeight = font.MeasureString("ABC").Y;
Vector2 iconSize = new Vector2(textHeight);
// draw icon & text
spriteBatch.DrawSpriteWithin(this.Sprite, position.X, position.Y, iconSize);
Vector2 textSize = spriteBatch.DrawTextBlock(font, this.Value, position + new Vector2(iconSize.X + 5, 5), wrapWidth);
// return size
return new Vector2(wrapWidth, textSize.Y + 5);
}
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows a list of linked item names with icons.</summary>
internal class ItemIconListField : GenericField
{
/*********
** Fields
*********/
/// <summary>The items to draw.</summary>
private readonly Tuple<Item, SpriteInfo>[] Items;
/// <summary>Whether to draw the stack size on the item icon.</summary>
private readonly bool ShowStackSize;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="items">The items to display.</param>
/// <param name="showStackSize">Whether to draw the stack size on the item icon.</param>
public ItemIconListField(GameHelper gameHelper, string label, IEnumerable<Item> items, bool showStackSize)
: base(gameHelper, label, hasValue: items != null)
{
if (items == null)
return;
this.Items = items.Where(p => p != null).Select(item => Tuple.Create(item, gameHelper.GetSprite(item))).ToArray();
this.HasValue = this.Items.Any();
this.ShowStackSize = showStackSize;
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
// get icon size
float textHeight = font.MeasureString("ABC").Y;
Vector2 iconSize = new Vector2(textHeight);
// draw list
const int padding = 5;
int topOffset = 0;
foreach (Tuple<Item, SpriteInfo> entry in this.Items)
{
Item item = entry.Item1;
SpriteInfo sprite = entry.Item2;
// draw icon
spriteBatch.DrawSpriteWithin(sprite, position.X, position.Y + topOffset, iconSize);
if (this.ShowStackSize && item.Stack > 1)
{
float scale = 2f; //sprite.SourceRectangle.Width / iconSize.X;
Vector2 sizePos = position + new Vector2(iconSize.X - Utility.getWidthOfTinyDigitString(item.Stack, scale), iconSize.Y + topOffset - 6f * scale);
Utility.drawTinyDigits(item.Stack, spriteBatch, sizePos, scale: scale, layerDepth: 1f, Color.White);
}
Vector2 textSize = spriteBatch.DrawTextBlock(font, item.DisplayName, position + new Vector2(iconSize.X + padding, topOffset), wrapWidth);
topOffset += (int)Math.Max(iconSize.Y, textSize.Y) + padding;
}
// return size
return new Vector2(wrapWidth, topOffset + padding);
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.LookupAnything.Framework.Subjects;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field containing clickable links.</summary>
internal class LinkField : GenericField, ILinkField
{
/*********
** Fields
*********/
/// <summary>Gets the subject the link points to.</summary>
private readonly Func<ISubject> Subject;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="text">The link text.</param>
/// <param name="subject">Gets the subject the link points to.</param>
public LinkField(GameHelper gameHelper, string label, string text, Func<ISubject> subject)
: base(gameHelper, label, new FormattedText(text, Color.Blue))
{
this.Subject = subject;
}
/// <summary>Get the subject the link points to.</summary>
public ISubject GetLinkSubject()
{
return this.Subject();
}
}
}

View File

@ -0,0 +1,94 @@
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Components;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows a progress bar UI.</summary>
internal class PercentageBarField : GenericField
{
/*********
** Fields
*********/
/// <summary>The current progress value.</summary>
protected readonly int CurrentValue;
/// <summary>The maximum progress value.</summary>
protected readonly int MaxValue;
/// <summary>The text to show next to the progress bar (if any).</summary>
protected readonly string Text;
/// <summary>The color of the filled bar.</summary>
protected readonly Color FilledColor;
/// <summary>The color of the empty bar.</summary>
protected readonly Color EmptyColor;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="currentValue">The current progress value.</param>
/// <param name="maxValue">The maximum progress value.</param>
/// <param name="filledColor">The color of the filled bar.</param>
/// <param name="emptyColor">The color of the empty bar.</param>
/// <param name="text">The text to show next to the progress bar (if any).</param>
public PercentageBarField(GameHelper gameHelper, string label, int currentValue, int maxValue, Color filledColor, Color emptyColor, string text)
: base(gameHelper, label, hasValue: true)
{
this.CurrentValue = currentValue;
this.MaxValue = maxValue;
this.FilledColor = filledColor;
this.EmptyColor = emptyColor;
this.Text = text;
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
Vector2 barSize = this.DrawBar(spriteBatch, position, this.CurrentValue / (this.MaxValue * 1f), this.FilledColor, this.EmptyColor, wrapWidth);
Vector2 textSize = !string.IsNullOrWhiteSpace(this.Text)
? spriteBatch.DrawTextBlock(font, this.Text, new Vector2(position.X + barSize.X + 3, position.Y), wrapWidth)
: Vector2.Zero;
return new Vector2(barSize.X + 3 + textSize.X, Math.Max(barSize.Y, textSize.Y));
}
/*********
** Protected methods
*********/
/// <summary>Draw a percentage bar.</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="ratio">The percentage value (between 0 and 1).</param>
/// <param name="filledColor">The color of the filled bar.</param>
/// <param name="emptyColor">The color of the empty bar.</param>
/// <param name="maxWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
protected Vector2 DrawBar(SpriteBatch spriteBatch, Vector2 position, float ratio, Color filledColor, Color emptyColor, float maxWidth = 100)
{
int barHeight = 22;
ratio = Math.Min(1f, ratio);
float width = Math.Min(100, maxWidth);
float filledWidth = width * ratio;
float emptyWidth = width - filledWidth;
if (filledWidth > 0)
spriteBatch.Draw(Sprites.Pixel, new Rectangle((int)position.X, (int)position.Y, (int)filledWidth, barHeight), filledColor);
if (emptyWidth > 0)
spriteBatch.Draw(Sprites.Pixel, new Rectangle((int)(position.X + filledWidth), (int)position.Y, (int)emptyWidth, barHeight), emptyColor);
return new Vector2(width, barHeight);
}
}
}

View File

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Models;
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows a list of recipes containing an ingredient.</summary>
internal class RecipesForIngredientField : GenericField
{
/*********
** Fields
*********/
/// <summary>Metadata needed to draw a recipe.</summary>
private struct Entry
{
/// <summary>The recipe name.</summary>
public string Name;
/// <summary>The recipe type.</summary>
public string Type;
/// <summary>Whether the player knows the recipe.</summary>
public bool IsKnown;
/// <summary>The number of the item required for the recipe.</summary>
public int NumberRequired;
/// <summary>The sprite to display.</summary>
public SpriteInfo Sprite;
}
/// <summary>The recipe data to list (type => recipe => {player knows recipe, number required for recipe}).</summary>
private readonly Entry[] Recipes;
/// <summary>Provides translations stored in the mod folder.</summary>
private readonly ITranslationHelper Translations;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="ingredient">The ingredient item.</param>
/// <param name="recipes">The recipe to list.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
public RecipesForIngredientField(GameHelper gameHelper, string label, Item ingredient, RecipeModel[] recipes, ITranslationHelper translations)
: base(gameHelper, label, hasValue: true)
{
this.Translations = translations;
this.Recipes = this.GetRecipeEntries(this.GameHelper, ingredient, recipes).OrderBy(p => p.Type).ThenBy(p => p.Name).ToArray();
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
const float leftIndent = 16;
float height = 0;
// get icon size
float textHeight = font.MeasureString("ABC").Y;
Vector2 iconSize = new Vector2(textHeight);
// draw recipes
string lastType = null;
foreach (Entry entry in this.Recipes)
{
// draw type
if (entry.Type != lastType)
{
height += spriteBatch.DrawTextBlock(font, $"{entry.Type}:", position + new Vector2(0, height), wrapWidth).Y;
lastType = entry.Type;
}
// draw icon
Color iconColor = entry.IsKnown ? Color.White : Color.White * .5f;
spriteBatch.DrawSpriteWithin(entry.Sprite, position.X + leftIndent, position.Y + height, iconSize, iconColor);
// draw text
Color color = entry.IsKnown ? Color.Black : Color.Gray;
Vector2 textSize = spriteBatch.DrawTextBlock(font, this.Translations.Get(L10n.Item.RecipesEntry, new { name = entry.Name, count = entry.NumberRequired }), position + new Vector2(leftIndent + iconSize.X + 3, height + 5), wrapWidth - iconSize.X, color);
height += Math.Max(iconSize.Y, textSize.Y) + 5;
}
return new Vector2(wrapWidth, height);
}
/*********
** Private methods
*********/
/// <summary>Get the recipe entries.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="ingredient">The ingredient item.</param>
/// <param name="recipes">The recipe to list.</param>
private IEnumerable<Entry> GetRecipeEntries(GameHelper gameHelper, Item ingredient, IEnumerable<RecipeModel> recipes)
{
foreach (RecipeModel recipe in recipes)
{
Item output = recipe.CreateItem(ingredient);
SpriteInfo customSprite = gameHelper.GetSprite(output);
yield return new Entry
{
Name = output.DisplayName,
Type = recipe.DisplayType,
IsKnown = !recipe.MustBeLearned || recipe.KnowsRecipe(Game1.player),
NumberRequired = recipe.Ingredients.ContainsKey(ingredient.ParentSheetIndex) ? recipe.Ingredients[ingredient.ParentSheetIndex] : recipe.Ingredients[ingredient.Category],
Sprite = customSprite
};
}
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using StardewModdingAPI;
namespace Pathoschild.Stardew.LookupAnything.Framework.Fields
{
/// <summary>A metadata field which shows experience points for a skill.</summary>
/// <remarks>Skill calculations reverse-engineered from <see cref="StardewValley.Farmer.checkForLevelGain"/>.</remarks>
internal class SkillBarField : PercentageBarField
{
/*********
** Fields
*********/
/// <summary>The experience points needed for each skill level.</summary>
private readonly int[] SkillPointsPerLevel;
/// <summary>Provides translations stored in the mod folder.</summary>
private readonly ITranslationHelper Translations;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="label">A short field label.</param>
/// <param name="experience">The current progress value.</param>
/// <param name="maxSkillPoints">The maximum experience points for a skill.</param>
/// <param name="skillPointsPerLevel">The experience points needed for each skill level.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
public SkillBarField(GameHelper gameHelper, string label, int experience, int maxSkillPoints, int[] skillPointsPerLevel, ITranslationHelper translations)
: base(gameHelper, label, experience, maxSkillPoints, Color.Green, Color.Gray, null)
{
this.SkillPointsPerLevel = skillPointsPerLevel;
this.Translations = translations;
}
/// <summary>Draw the value (or return <c>null</c> to render the <see cref="GenericField.Value"/> using the default format).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="font">The recommended font.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="wrapWidth">The maximum width before which content should be wrapped.</param>
/// <returns>Returns the drawn dimensions, or <c>null</c> to draw the <see cref="GenericField.Value"/> using the default format.</returns>
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
int[] pointsPerLevel = this.SkillPointsPerLevel;
// generate text
int nextLevelExp = pointsPerLevel.FirstOrDefault(p => p - this.CurrentValue > 0);
int pointsForNextLevel = nextLevelExp > 0 ? nextLevelExp - this.CurrentValue : 0;
int currentLevel = nextLevelExp > 0 ? Array.IndexOf(pointsPerLevel, nextLevelExp) : pointsPerLevel.Length;
string text = pointsForNextLevel > 0
? this.Translations.Get(L10n.Player.SkillProgress, new { level = currentLevel, expNeeded = pointsForNextLevel })
: this.Translations.Get(L10n.Player.SkillProgressLast, new { level = currentLevel });
// draw bars
const int barWidth = 25;
float leftOffset = 0;
int barHeight = 0;
foreach (int levelExp in pointsPerLevel)
{
float progress = Math.Min(1f, this.CurrentValue / (levelExp * 1f));
Vector2 barSize = this.DrawBar(spriteBatch, position + new Vector2(leftOffset, 0), progress, this.FilledColor, this.EmptyColor, barWidth);
barHeight = (int)barSize.Y;
leftOffset += barSize.X + 2;
}
// draw text
Vector2 textSize = spriteBatch.DrawTextBlock(font, text, position + new Vector2(leftOffset, 0), wrapWidth - leftOffset);
return new Vector2(leftOffset + textSize.X, Math.Max(barHeight, textSize.Y));
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.LookupAnything.Framework
{
/// <summary>A snippet of formatted text.</summary>
internal struct FormattedText : IFormattedText
{
/********
** Accessors
*********/
/// <summary>The text to format.</summary>
public string Text { get; }
/// <summary>The font color (or <c>null</c> for the default color).</summary>
public Color? Color { get; }
/// <summary>Whether to draw bold text.</summary>
public bool Bold { get; }
/********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="text">The text to format.</param>
/// <param name="color">The font color (or <c>null</c> for the default color).</param>
/// <param name="bold">Whether to draw bold text.</param>
public FormattedText(string text, Color? color = null, bool bold = false)
{
this.Text = text;
this.Color = color;
this.Bold = bold;
}
}
}

View File

@ -0,0 +1,17 @@
using Microsoft.Xna.Framework;
namespace Pathoschild.Stardew.LookupAnything.Framework
{
/// <summary>A snippet of formatted text.</summary>
internal interface IFormattedText
{
/// <summary>The font color (or <c>null</c> for the default color).</summary>
Color? Color { get; }
/// <summary>The text to format.</summary>
string Text { get; }
/// <summary>Whether to draw bold text.</summary>
bool Bold { get; }
}
}

View File

@ -0,0 +1,41 @@
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using StardewValley;
using StardewValley.Objects;
using Object = StardewValley.Object;
namespace Pathoschild.Stardew.LookupAnything.Framework
{
/// <summary>Provides utility extension methods.</summary>
internal static class InternalExtensions
{
/*********
** Public methods
*********/
/****
** Items
****/
/// <summary>Get the sprite sheet to which the item's <see cref="Item.parentSheetIndex"/> refers.</summary>
/// <param name="item">The item to check.</param>
public static ItemSpriteType GetSpriteType(this Item item)
{
if (item is Object obj)
{
if (obj is Furniture)
return ItemSpriteType.Furniture;
if (obj is Wallpaper)
return ItemSpriteType.Wallpaper;
return obj.bigCraftable.Value
? ItemSpriteType.BigCraftable
: ItemSpriteType.Object;
}
if (item is Boots)
return ItemSpriteType.Boots;
if (item is Hat)
return ItemSpriteType.Hat;
if (item is Tool)
return ItemSpriteType.Tool;
return ItemSpriteType.Unknown;
}
}
}

View File

@ -0,0 +1,76 @@
using System.Linq;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Data;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework
{
/// <summary>Provides metadata that's not available from the game data directly (e.g. because it's buried in the logic).</summary>
internal class Metadata
{
/*********
** Accessors
*********/
/// <summary>Constant values hardcoded by the game.</summary>
public ConstantData Constants { get; set; }
/// <summary>Metadata for game objects (including inventory items, terrain features, crops, trees, and other map objects).</summary>
public ObjectData[] Objects { get; set; }
/// <summary>Metadata for NPCs in the game.</summary>
public CharacterData[] Characters { get; set; }
/// <summary>Information about Adventure Guild monster-slaying quests.</summary>
/// <remarks>Derived from <see cref="StardewValley.Locations.AdventureGuild.showMonsterKillList"/>.</remarks>
public AdventureGuildQuestData[] AdventureGuildQuests { get; set; }
/// <summary>The building recipes.</summary>
/// <remarks>Derived from <see cref="StardewValley.Buildings.Mill.dayUpdate"/>.</remarks>
public BuildingRecipeData[] BuildingRecipes { get; set; }
/// <summary>The machine recipes.</summary>
/// <remarks>Derived from <see cref="Object.performObjectDropInAction"/>.</remarks>
public MachineRecipeData[] MachineRecipes { get; set; }
/// <summary>The shops that buy items from the player.</summary>
/// <remarks>Derived from <see cref="StardewValley.Menus.ShopMenu"/> constructor.</remarks>
public ShopData[] Shops { get; set; }
/*********
** Public methods
*********/
/// <summary>Get whether the metadata seems to be basically valid.</summary>
public bool LooksValid()
{
return new object[] { this.Constants, this.Objects, this.Characters, this.AdventureGuildQuests, this.BuildingRecipes, this.MachineRecipes, this.Shops }.All(p => p != null);
}
/// <summary>Get overrides for a game object.</summary>
/// <param name="item">The item for which to get overrides.</param>
/// <param name="context">The context for which to get an override.</param>
public ObjectData GetObject(Item item, ObjectContext context)
{
ItemSpriteType sheet = item.GetSpriteType();
return this.Objects
.FirstOrDefault(obj => obj.SpriteSheet == sheet && obj.SpriteID.Contains(item.ParentSheetIndex) && obj.Context.HasFlag(context));
}
/// <summary>Get overrides for a game object.</summary>
/// <param name="character">The character for which to get overrides.</param>
/// <param name="type">The character type.</param>
public CharacterData GetCharacter(NPC character, TargetType type)
{
return
this.Characters?.FirstOrDefault(p => p.ID == $"{type}::{character.Name}") // override by type + name
?? this.Characters?.FirstOrDefault(p => p.ID == type.ToString()); // override by type
}
/// <summary>Get the adventurer guild quest for the specified monster (if any).</summary>
/// <param name="monster">The monster name.</param>
public AdventureGuildQuestData GetAdventurerGuildQuest(string monster)
{
return this.AdventureGuildQuests.FirstOrDefault(p => p.Targets.Contains(monster));
}
}
}

View File

@ -0,0 +1,56 @@
using Newtonsoft.Json;
using Pathoschild.Stardew.Common;
using StardewModdingAPI;
namespace Pathoschild.Stardew.LookupAnything.Framework
{
/// <summary>The parsed mod configuration.</summary>
internal class ModConfig
{
/*********
** Accessors
*********/
/// <summary>Whether to close the lookup UI when the lookup key is release.</summary>
public bool HideOnKeyUp { get; set; }
/// <summary>The amount to scroll long content on each up/down scroll.</summary>
public int ScrollAmount { get; set; } = 160;
/// <summary>Whether to show advanced data mining fields.</summary>
public bool ShowDataMiningFields { get; set; }
/// <summary>Whether to include map tiles as lookup targets.</summary>
public bool EnableTileLookups { get; set; }
/// <summary>The control bindings.</summary>
public ModConfigControls Controls { get; set; } = new ModConfigControls();
/*********
** Nested models
*********/
/// <summary>A set of control bindings.</summary>
internal class ModConfigControls
{
/// <summary>The control which toggles the lookup UI for something under the cursor.</summary>
[JsonConverter(typeof(StringEnumArrayConverter))]
public SButton[] ToggleLookup { get; set; } = { SButton.Help, SButton.VolumeDown };
/// <summary>The control which toggles the lookup UI for something in front of the player.</summary>
[JsonConverter(typeof(StringEnumArrayConverter))]
public SButton[] ToggleLookupInFrontOfPlayer { get; set; } = new SButton[0];
/// <summary>The control which scrolls up long content.</summary>
[JsonConverter(typeof(StringEnumArrayConverter))]
public SButton[] ScrollUp { get; set; } = { SButton.Up };
/// <summary>The control which scrolls down long content.</summary>
[JsonConverter(typeof(StringEnumArrayConverter))]
public SButton[] ScrollDown { get; set; } = { SButton.Down };
/// <summary>Toggle the display of debug information.</summary>
[JsonConverter(typeof(StringEnumArrayConverter))]
public SButton[] ToggleDebug { get; set; } = new SButton[0];
}
}
}

View File

@ -0,0 +1,40 @@
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
namespace Pathoschild.Stardew.LookupAnything.Framework.Models
{
/// <summary>An item slot for a bundle.</summary>
internal class BundleIngredientModel
{
/*********
** Accessors
*********/
/// <summary>The ingredient's index in the bundle.</summary>
public int Index { get; }
/// <summary>The required item's parent sprite index (or -1 for a monetary bundle).</summary>
public int ItemID { get; }
/// <summary>The number of items required.</summary>
public int Stack { get; }
/// <summary>The required item quality.</summary>
public ItemQuality Quality { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="index">The ingredient's index in the bundle.</param>
/// <param name="itemID">The required item's parent sprite index (or -1 for a monetary bundle).</param>
/// <param name="stack">The number of items required.</param>
/// <param name="quality">The required item quality.</param>
public BundleIngredientModel(int index, int itemID, int stack, ItemQuality quality)
{
this.Index = index;
this.ItemID = itemID;
this.Stack = stack;
this.Quality = quality;
}
}
}

View File

@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.Linq;
namespace Pathoschild.Stardew.LookupAnything.Framework.Models
{
/// <summary>A bundle entry parsed from the game's data files.</summary>
internal class BundleModel
{
/*********
** Accessors
*********/
/// <summary>The unique bundle ID.</summary>
public int ID { get; }
/// <summary>The bundle name.</summary>
public string Name { get; }
/// <summary>The translated bundle name.</summary>
public string DisplayName { get; }
/// <summary>The community center area containing the bundle.</summary>
public string Area { get; }
/// <summary>The unparsed reward description, which can be parsed with <see cref="StardewValley.Utility.getItemFromStandardTextDescription"/>.</summary>
public string RewardData { get; }
/// <summary>The required item ingredients.</summary>
public BundleIngredientModel[] Ingredients { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="id">The unique bundle ID.</param>
/// <param name="name">The bundle name.</param>
/// <param name="displayName">The translated bundle name.</param>
/// <param name="area">The community center area containing the bundle.</param>
/// <param name="rewardData">The unparsed reward description.</param>
/// <param name="ingredients">The required item ingredients.</param>
public BundleModel(int id, string name, string displayName, string area, string rewardData, IEnumerable<BundleIngredientModel> ingredients)
{
this.ID = id;
this.Name = name;
this.DisplayName = displayName;
this.Area = area;
this.RewardData = rewardData;
this.Ingredients = ingredients.ToArray();
}
}
}

View File

@ -0,0 +1,128 @@
using Pathoschild.Stardew.LookupAnything.Framework.Data;
using StardewValley;
using SFarmer = StardewValley.Farmer;
namespace Pathoschild.Stardew.LookupAnything.Framework.Models
{
/// <summary>Summarises details about the friendship between an NPC and a player.</summary>
internal class FriendshipModel
{
/*********
** Accessors
*********/
/****
** Flags
****/
/// <summary>Whether the player can date the NPC.</summary>
public bool CanDate { get; set; }
/// <summary>Whether the NPC is dating the player.</summary>
public bool IsDating { get; set; }
/// <summary>Whether the NPC is married to the player.</summary>
public bool IsSpouse { get; set; }
/// <summary>Whether the NPC has a stardrop to give to the player once they reach enough points.</summary>
public bool HasStardrop { get; set; }
/// <summary>Whether the player talked to them today.</summary>
public bool TalkedToday { get; set; }
/// <summary>The number of gifts the player gave the NPC today.</summary>
public int GiftsToday { get; set; }
/// <summary>The number of gifts the player gave the NPC this week.</summary>
public int GiftsThisWeek { get; set; }
/// <summary>The current friendship status.</summary>
public FriendshipStatus Status { get; set; }
/****
** Points
****/
/// <summary>The player's current friendship points with the NPC.</summary>
public int Points { get; }
/// <summary>The number of friendship points needed to obtain a stardrop (if applicable).</summary>
public int? StardropPoints { get; }
/// <summary>The maximum number of points which the player can currently reach with an NPC.</summary>
public int MaxPoints { get; }
/// <summary>The number of points per heart level.</summary>
public int PointsPerLevel { get; }
/****
** Hearts
****/
/// <summary>The number of filled hearts in their friendship meter.</summary>
public int FilledHearts { get; set; }
/// <summary>The number of empty hearts in their friendship meter.</summary>
public int EmptyHearts { get; set; }
/// <summary>The number of locked hearts in their friendship meter.</summary>
public int LockedHearts { get; set; }
/// <summary>The total number of hearts that can be unlocked with this NPC.</summary>
public int TotalHearts => this.FilledHearts + this.EmptyHearts + this.LockedHearts;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="player">The player.</param>
/// <param name="npc">The NPC.</param>
/// <param name="constants">The constant assumptions.</param>
/// <param name="friendship">The current friendship data.</param>
public FriendshipModel(SFarmer player, NPC npc, Friendship friendship, ConstantData constants)
{
// flags
this.CanDate = npc.datable.Value;
this.IsDating = friendship.IsDating();
this.IsSpouse = friendship.IsMarried();
this.Status = friendship.Status;
this.TalkedToday = friendship.TalkedToToday;
this.GiftsToday = friendship.GiftsToday;
this.GiftsThisWeek = friendship.GiftsThisWeek;
// points
this.MaxPoints = this.IsSpouse ? constants.SpouseMaxFriendship : NPC.maxFriendshipPoints;
this.Points = friendship.Points;
this.PointsPerLevel = NPC.friendshipPointsPerHeartLevel;
this.FilledHearts = this.Points / NPC.friendshipPointsPerHeartLevel;
this.LockedHearts = this.CanDate && !this.IsDating ? constants.DatingHearts : 0;
this.EmptyHearts = this.MaxPoints / NPC.friendshipPointsPerHeartLevel - this.FilledHearts - this.LockedHearts;
if (this.IsSpouse)
{
this.StardropPoints = constants.SpouseFriendshipForStardrop;
this.HasStardrop = !player.mailReceived.Contains(Constants.Constant.MailLetters.ReceivedSpouseStardrop);
}
}
/// <summary>Construct an instance.</summary>
/// <param name="points">The player's current friendship points with the NPC.</param>
/// <param name="pointsPerLevel">The number of points per heart level.</param>
/// <param name="maxPoints">The maximum number of points which the player can currently reach with an NPC.</param>
public FriendshipModel(int points, int pointsPerLevel, int maxPoints)
{
this.Points = points;
this.PointsPerLevel = pointsPerLevel;
this.MaxPoints = maxPoints;
this.FilledHearts = this.Points / pointsPerLevel;
this.EmptyHearts = this.MaxPoints / pointsPerLevel - this.FilledHearts;
}
/// <summary>Get the number of points to the next heart level or startdrop.</summary>
public int GetPointsToNext()
{
if (this.Points < this.MaxPoints)
return this.PointsPerLevel - (this.Points % this.PointsPerLevel);
if (this.StardropPoints.HasValue && this.Points < this.StardropPoints)
return this.StardropPoints.Value - this.Points;
return 0;
}
}
}

View File

@ -0,0 +1,53 @@
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
namespace Pathoschild.Stardew.LookupAnything.Framework.Models
{
/// <summary>A raw gift taste entry parsed from the game's data files.</summary>
internal class GiftTasteModel
{
/*********
** Accessors
*********/
/// <summary>How much the target villager likes this item.</summary>
public GiftTaste Taste { get; private set; }
/// <summary>The name of the target villager.</summary>
public string Villager { get; }
/// <summary>The item parent sprite index (if positive) or category (if negative).</summary>
public int RefID { get; set; }
/// <summary>Whether this gift taste applies to all villagers unless otherwise excepted.</summary>
public bool IsUniversal { get; }
/// <summary>Whether the <see cref="RefID"/> refers to a category of items, instead of a specific item ID.</summary>
public bool IsCategory => this.RefID < 0;
/// <summary>The precedence used to resolve conflicting tastes (lower is better).</summary>
public int Precedence { get; set; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="taste">How much the target villager likes this item.</param>
/// <param name="villager">The name of the target villager.</param>
/// <param name="refID">The item parent sprite index (if positive) or category (if negative).</param>
/// <param name="isUniversal">Whether this gift taste applies to all villagers unless otherwise excepted.</param>
public GiftTasteModel(GiftTaste taste, string villager, int refID, bool isUniversal = false)
{
this.Taste = taste;
this.Villager = villager;
this.RefID = refID;
this.IsUniversal = isUniversal;
}
/// <summary>Override the taste value.</summary>
/// <param name="taste">The taste value to set.</param>
public void SetTaste(GiftTaste taste)
{
this.Taste = taste;
}
}
}

View File

@ -0,0 +1,54 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Models
{
/// <summary>An object entry parsed from the game's data files.</summary>
internal class ObjectModel
{
/*********
** Accessors
*********/
/// <summary>The object's index in the object sprite sheet.</summary>
public int ParentSpriteIndex { get; }
/// <summary>The object name.</summary>
public string Name { get; }
/// <summary>The base description. This may be overridden by game logic (e.g. for the Gunther-can-tell-you-more messages).</summary>
public string Description { get; }
/// <summary>The base sale price.</summary>
public int Price { get; }
/// <summary>How edible the item is, where -300 is inedible.</summary>
public int Edibility { get; }
/// <summary>The type name.</summary>
public string Type { get; }
/// <summary>The category ID (or <c>0</c> if there is none).</summary>
public int Category { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="parentSpriteIndex">The object's index in the object sprite sheet.</param>
/// <param name="name">The object name.</param>
/// <param name="description">The base description.</param>
/// <param name="price">The base sale price.</param>
/// <param name="edibility">How edible the item is, where -300 is inedible.</param>
/// <param name="type">The type name.</param>
/// <param name="category">The category ID (or <c>0</c> if there is none).</param>
public ObjectModel(int parentSpriteIndex, string name, string description, int price, int edibility, string type, int category)
{
this.ParentSpriteIndex = parentSpriteIndex;
this.Name = name;
this.Description = description;
this.Price = price;
this.Edibility = edibility;
this.Type = type;
this.Category = category;
}
}
}

View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using StardewModdingAPI;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Models
{
/// <summary>Represents metadata about a recipe.</summary>
internal class RecipeModel
{
/*********
** Fields
*********/
/// <summary>The item that be created by this recipe, given the ingredient.</summary>
private readonly Func<Item, Item> Item;
/*********
** Accessors
*********/
/// <summary>The recipe's lookup name (if any).</summary>
public string Key { get; }
/// <summary>The recipe type.</summary>
public RecipeType Type { get; }
/// <summary>The display name for the machine or building name for which the recipe applies.</summary>
public string DisplayType { get; }
/// <summary>The items needed to craft the recipe (item ID => number needed).</summary>
public IDictionary<int, int> Ingredients { get; }
/// <summary>The item ID produced by this recipe, if applicable.</summary>
public int? OutputItemIndex { get; }
/// <summary>The ingredients which can't be used in this recipe (typically exceptions for a category ingredient).</summary>
public int[] ExceptIngredients { get; }
/// <summary>Whether the recipe must be learned before it can be used.</summary>
public bool MustBeLearned { get; }
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="recipe">The recipe to parse.</param>
/// <param name="reflectionHelper">Simplifies access to private game code.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
public RecipeModel(CraftingRecipe recipe, IReflectionHelper reflectionHelper, ITranslationHelper translations)
: this(
key: recipe.name,
type: recipe.isCookingRecipe ? RecipeType.Cooking : RecipeType.Crafting,
displayType: translations.Get(recipe.isCookingRecipe ? L10n.RecipeTypes.Cooking : L10n.RecipeTypes.Crafting),
ingredients: reflectionHelper.GetField<Dictionary<int, int>>(recipe, "recipeList").GetValue(),
item: item => recipe.createItem(),
mustBeLearned: true,
outputItemIndex: reflectionHelper.GetField<List<int>>(recipe, "itemToProduce").GetValue()[0]
)
{ }
/// <summary>Construct an instance.</summary>
/// <param name="key">The recipe's lookup name (if any).</param>
/// <param name="type">The recipe type.</param>
/// <param name="displayType">The display name for the machine or building name for which the recipe applies.</param>
/// <param name="ingredients">The items needed to craft the recipe (item ID => number needed).</param>
/// <param name="item">The item that be created by this recipe.</param>
/// <param name="mustBeLearned">Whether the recipe must be learned before it can be used.</param>
/// <param name="exceptIngredients">The ingredients which can't be used in this recipe (typically exceptions for a category ingredient).</param>
/// <param name="outputItemIndex">The item ID produced by this recipe, if applicable.</param>
public RecipeModel(string key, RecipeType type, string displayType, IDictionary<int, int> ingredients, Func<Item, Item> item, bool mustBeLearned, int[] exceptIngredients = null, int? outputItemIndex = null)
{
this.Key = key;
this.Type = type;
this.DisplayType = displayType;
this.Ingredients = ingredients;
this.ExceptIngredients = exceptIngredients ?? new int[0];
this.Item = item;
this.MustBeLearned = mustBeLearned;
this.OutputItemIndex = outputItemIndex;
}
/// <summary>Create the item crafted by this recipe.</summary>
/// <param name="ingredient">The ingredient for which to create an item.</param>
public Item CreateItem(Item ingredient)
{
return this.Item(ingredient);
}
/// <summary>Get whether a player knows this recipe.</summary>
/// <param name="farmer">The farmer to check.</param>
public bool KnowsRecipe(Farmer farmer)
{
return this.Key != null && farmer.knowsRecipe(this.Key);
}
/// <summary>Get the number of times this player has crafted the recipe.</summary>
/// <returns>Returns the times crafted, or -1 if unknown (e.g. some recipe types like furnace aren't tracked).</returns>
public int GetTimesCrafted(Farmer player)
{
switch (this.Type)
{
case RecipeType.Cooking:
return this.OutputItemIndex.HasValue && player.recipesCooked.TryGetValue(this.OutputItemIndex.Value, out int timesCooked) ? timesCooked : 0;
case RecipeType.Crafting:
return player.craftingRecipes.TryGetValue(this.Key, out int timesCrafted) ? timesCrafted : 0;
default:
return -1;
}
}
}
}

View File

@ -0,0 +1,18 @@
namespace Pathoschild.Stardew.LookupAnything.Framework.Models
{
/// <summary>Indicates an in-game recipe type.</summary>
internal enum RecipeType
{
/// <summary>The recipe is cooked in the kitchen.</summary>
Cooking,
/// <summary>The recipe is crafted through the game menu.</summary>
Crafting,
/// <summary>The recipe represents the input for a crafting machine like a furnace.</summary>
MachineInput,
/// <summary>The recipe represents the materials needed to construct a building through Robin or the Wizard.</summary>
BuildingBlueprint
}
}

View File

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Netcode;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using StardewModdingAPI;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>The base class for object metadata.</summary>
internal abstract class BaseSubject : ISubject
{
/*********
** Fields
*********/
/// <summary>Provides translations stored in the mod folder.</summary>
protected ITranslationHelper Text { get; }
/// <summary>Provides utility methods for interacting with the game code.</summary>
protected GameHelper GameHelper { get; }
/*********
** Accessors
*********/
/// <summary>The display name.</summary>
public string Name { get; protected set; }
/// <summary>The object description (if applicable).</summary>
public string Description { get; protected set; }
/// <summary>The object type.</summary>
public string Type { get; protected set; }
/*********
** Public methods
*********/
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public abstract IEnumerable<ICustomField> GetData(Metadata metadata);
/// <summary>Get raw debug data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public abstract IEnumerable<IDebugField> GetDebugFields(Metadata metadata);
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
public abstract bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size);
/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
protected BaseSubject(GameHelper gameHelper, ITranslationHelper translations)
{
this.GameHelper = gameHelper;
this.Text = translations;
}
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="name">The display name.</param>
/// <param name="description">The object description (if applicable).</param>
/// <param name="type">The object type.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
protected BaseSubject(GameHelper gameHelper, string name, string description, string type, ITranslationHelper translations)
: this(gameHelper, translations)
{
this.Initialise(name, description, type);
}
/// <summary>Initialise the base values.</summary>
/// <param name="name">The display name.</param>
/// <param name="description">The object description (if applicable).</param>
/// <param name="type">The object type.</param>
protected void Initialise(string name, string description, string type)
{
this.Name = name;
this.Description = description;
this.Type = type;
}
/// <summary>Get all debug fields by reflecting over an instance.</summary>
/// <param name="obj">The object instance over which to reflect.</param>
protected IEnumerable<IDebugField> GetDebugFieldsFrom(object obj)
{
if (obj == null)
yield break;
for (Type type = obj.GetType(); type != null; type = type.BaseType)
{
// get fields & properties
var fields =
(
from field in type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy)
where !field.IsLiteral // exclude constants
select new { field.Name, Type = field.FieldType, Value = this.GetDebugValue(obj, field) }
)
.Concat(
from property in type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy)
where property.CanRead
select new { property.Name, Type = property.PropertyType, Value = this.GetDebugValue(obj, property) }
)
.OrderBy(field => field.Name, StringComparer.InvariantCultureIgnoreCase);
// yield valid values
IDictionary<string, string> seenValues = new Dictionary<string, string>(StringComparer.InvariantCulture);
foreach (var field in fields)
{
if (seenValues.TryGetValue(field.Name, out string value) && value == field.Value)
continue; // key/value pair differs only in the key case
if (field.Value == field.Type.ToString())
continue; // can't be displayed
yield return new GenericDebugField($"{type.Name}::{field.Name}", field.Value);
}
}
}
/// <summary>Get a human-readable representation of a value.</summary>
/// <param name="value">The underlying value.</param>
protected string Stringify(object value)
{
return this.Text.Stringify(value);
}
/// <summary>Get a translation for the current locale.</summary>
/// <param name="key">The translation key.</param>
/// <param name="tokens">An anonymous object containing token key/value pairs, like <c>new { value = 42, name = "Cranberries" }</c>.</param>
/// <exception cref="KeyNotFoundException">The <paramref name="key" /> doesn't match an available translation.</exception>
protected Translation Translate(string key, object tokens = null)
{
return this.Text.Get(key, tokens);
}
/// <summary>Get a human-readable value for a debug value.</summary>
/// <param name="obj">The object whose values to read.</param>
/// <param name="field">The field to read.</param>
private string GetDebugValue(object obj, FieldInfo field)
{
try
{
return this.Stringify(field.GetValue(obj));
}
catch (Exception ex)
{
return $"error reading field: {ex.Message}";
}
}
/// <summary>Get a human-readable value for a debug value.</summary>
/// <param name="obj">The object whose values to read.</param>
/// <param name="property">The property to read.</param>
private string GetDebugValue(object obj, PropertyInfo property)
{
try
{
return this.Stringify(property.GetValue(obj));
}
catch (Exception ex)
{
return $"error reading property: {ex.Message}";
}
}
}
}

View File

@ -0,0 +1,322 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using StardewModdingAPI;
using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Characters;
using StardewValley.Locations;
using StardewValley.Monsters;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>Describes a constructed building.</summary>
internal class BuildingSubject : BaseSubject
{
/*********
** Fields
*********/
/// <summary>Simplifies access to private game code.</summary>
private readonly IReflectionHelper Reflection;
/// <summary>The lookup target.</summary>
private readonly Building Target;
/// <summary>The building's source rectangle in its spritesheet.</summary>
private readonly Rectangle SourceRectangle;
/// <summary>Provides metadata that's not available from the game data directly.</summary>
private readonly Metadata Metadata;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="building">The lookup target.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
/// <param name="sourceRectangle">The building's source rectangle in its spritesheet.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
/// <param name="reflectionHelper">Simplifies access to private game code.</param>
public BuildingSubject(GameHelper gameHelper, Metadata metadata, Building building, Rectangle sourceRectangle, ITranslationHelper translations, IReflectionHelper reflectionHelper)
: base(gameHelper, building.buildingType.Value, null, translations.Get(L10n.Types.Building), translations)
{
// init
this.Metadata = metadata;
this.Reflection = reflectionHelper;
this.Target = building;
this.SourceRectangle = sourceRectangle;
// get name/description from blueprint if available
try
{
BluePrint blueprint = new BluePrint(building.buildingType.Value);
this.Name = blueprint.displayName;
this.Description = blueprint.description;
}
catch (ContentLoadException)
{
// use default values
}
}
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<ICustomField> GetData(Metadata metadata)
{
var text = this.Text;
// get info
Building building = this.Target;
bool built = !building.isUnderConstruction();
int? upgradeLevel = this.GetUpgradeLevel(building);
// construction / upgrade
if (!built || building.daysUntilUpgrade.Value > 0)
{
int daysLeft = building.isUnderConstruction() ? building.daysOfConstructionLeft.Value : building.daysUntilUpgrade.Value;
SDate readyDate = SDate.Now().AddDays(daysLeft);
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.Construction), text.Get(L10n.Building.ConstructionSummary, new { date = readyDate }));
}
// owner
Farmer owner = this.GetOwner();
if (owner != null)
yield return new LinkField(this.GameHelper, text.Get(L10n.Building.Owner), owner.Name, () => new FarmerSubject(this.GameHelper, owner, text, this.Reflection));
else if (building.indoors.Value is Cabin)
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.Owner), text.Get(L10n.Building.OwnerNone));
// stable horse
if (built && building is Stable stable)
{
Horse horse = Utility.findHorse(stable.HorseId);
if (horse != null)
{
yield return new LinkField(this.GameHelper, text.Get(L10n.Building.Horse), horse.Name, () => new CharacterSubject(this.GameHelper, horse, TargetType.Horse, this.Metadata, text, this.Reflection));
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.HorseLocation), text.Get(L10n.Building.HorseLocationSummary, new { location = horse.currentLocation.Name, x = horse.getTileX(), y = horse.getTileY() }));
}
}
// animals
if (built && building.indoors.Value is AnimalHouse animalHouse)
{
// animal counts
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.Animals), text.Get(L10n.Building.AnimalsSummary, new { count = animalHouse.animalsThatLiveHere.Count, max = animalHouse.animalLimit.Value }));
// feed trough
if ((building is Barn || building is Coop) && upgradeLevel >= 2)
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.FeedTrough), text.Get(L10n.Building.FeedTroughAutomated));
else
{
this.GetFeedMetrics(animalHouse, out int totalFeedSpaces, out int filledFeedSpaces);
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.FeedTrough), text.Get(L10n.Building.FeedTroughSummary, new { filled = filledFeedSpaces, max = totalFeedSpaces }));
}
}
// slimes
if (built && building.indoors.Value is SlimeHutch slimeHutch)
{
// slime count
int slimeCount = slimeHutch.characters.OfType<GreenSlime>().Count();
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.Slimes), text.Get(L10n.Building.SlimesSummary, new { count = slimeCount, max = 20 }));
// water trough
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.WaterTrough), text.Get(L10n.Building.WaterTroughSummary, new { filled = slimeHutch.waterSpots.Count(p => p), max = slimeHutch.waterSpots.Count }));
}
// upgrade level
if (built)
{
var upgradeLevelSummary = this.GetUpgradeLevelSummary(building, upgradeLevel).ToArray();
if (upgradeLevelSummary.Any())
yield return new CheckboxListField(this.GameHelper, text.Get(L10n.Building.Upgrades), upgradeLevelSummary);
}
// silo hay
if (built && building.buildingType.Value == "Silo")
{
Farm farm = Game1.getFarm();
int siloCount = Utility.numSilos();
yield return new GenericField(
this.GameHelper,
text.Get(L10n.Building.StoredHay),
text.Get(siloCount == 1 ? L10n.Building.StoredHaySummaryOneSilo : L10n.Building.StoredHaySummaryMultipleSilos, new { hayCount = farm.piecesOfHay, siloCount = siloCount, maxHay = Math.Max(farm.piecesOfHay.Value, siloCount * 240) })
);
}
if (built && building is JunimoHut hut)
{
yield return new GenericField(this.GameHelper, text.Get(L10n.Building.JunimoHarvestingEnabled), text.Stringify(!hut.noHarvest.Value));
yield return new ItemIconListField(this.GameHelper, text.Get(L10n.Building.OutputReady), hut.output.Value?.items, showStackSize: true);
}
// mill output
if (built && building is Mill mill)
{
yield return new ItemIconListField(this.GameHelper, text.Get(L10n.Building.OutputProcessing), mill.input.Value?.items, showStackSize: true);
yield return new ItemIconListField(this.GameHelper, text.Get(L10n.Building.OutputReady), mill.output.Value?.items, showStackSize: true);
}
}
/// <summary>Get raw debug data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<IDebugField> GetDebugFields(Metadata metadata)
{
Building target = this.Target;
// pinned fields
yield return new GenericDebugField("building type", target.buildingType.Value, pinned: true);
yield return new GenericDebugField("days of construction left", target.daysOfConstructionLeft.Value, pinned: true);
yield return new GenericDebugField("name of indoors", target.nameOfIndoors, pinned: true);
// raw fields
foreach (IDebugField field in this.GetDebugFieldsFrom(target))
yield return field;
}
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
/// <remarks>Derived from <see cref="Building.drawInMenu"/>, modified to draw within the target size.</remarks>
public override bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size)
{
Building target = this.Target;
spriteBatch.Draw(target.texture.Value, position, this.SourceRectangle, target.color.Value, 0.0f, Vector2.Zero, size.X / this.SourceRectangle.Width, SpriteEffects.None, 0.89f);
return true;
}
/*********
** Private fields
*********/
/// <summary>Get the building owner, if any.</summary>
private Farmer GetOwner()
{
Building target = this.Target;
// stable
if (target is Stable stable)
{
long ownerID = stable.owner.Value;
return Game1.getFarmerMaybeOffline(ownerID);
}
// cabin
if (this.Target.indoors.Value is Cabin cabin)
return cabin.owner;
return null;
}
/// <summary>Get the upgrade level for a building, if applicable.</summary>
/// <param name="building">The building to check.</param>
private int? GetUpgradeLevel(Building building)
{
// barn
if (building is Barn barn && int.TryParse(barn.nameOfIndoorsWithoutUnique.Substring("Barn".Length), out int barnUpgradeLevel))
return barnUpgradeLevel - 1; // Barn2 is first upgrade
// cabin
if (building.indoors.Value is Cabin cabin)
return cabin.upgradeLevel;
// coop
if (building is Coop coop && int.TryParse(coop.nameOfIndoorsWithoutUnique.Substring("Coop".Length), out int coopUpgradeLevel))
return coopUpgradeLevel - 1; // Coop2 is first upgrade
return null;
}
/// <summary>Get the feed metrics for an animal building.</summary>
/// <param name="building">The animal building to check.</param>
/// <param name="total">The total number of feed trough spaces.</param>
/// <param name="filled">The number of feed trough spaces which contain hay.</param>
private void GetFeedMetrics(AnimalHouse building, out int total, out int filled)
{
var map = building.Map;
total = 0;
filled = 0;
for (int x = 0; x < map.Layers[0].LayerWidth; x++)
{
for (int y = 0; y < map.Layers[0].LayerHeight; y++)
{
if (building.doesTileHaveProperty(x, y, "Trough", "Back") != null)
{
total++;
if (building.objects.TryGetValue(new Vector2(x, y), out SObject obj) && obj.ParentSheetIndex == 178)
filled++;
}
}
}
}
/// <summary>Get the upgrade levels for a building, for use with a checkbox field.</summary>
/// <param name="building">The building to check.</param>
/// <param name="upgradeLevel">The current upgrade level, if applicable.</param>
private IEnumerable<KeyValuePair<IFormattedText[], bool>> GetUpgradeLevelSummary(Building building, int? upgradeLevel)
{
// barn
if (building is Barn)
{
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesBarn0)) },
value: true
);
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesBarn1)) },
value: upgradeLevel >= 1
);
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesBarn2)) },
value: upgradeLevel >= 2
);
}
// cabin
else if (building.indoors.Value is Cabin)
{
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesCabin0)) },
value: true
);
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesCabin1)) },
value: upgradeLevel >= 1
);
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesCabin2)) },
value: upgradeLevel >= 2
);
}
// coop
else if (building is Coop)
{
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesCoop0)) },
value: true
);
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesCoop1)) },
value: upgradeLevel >= 1
);
yield return new KeyValuePair<IFormattedText[], bool>(
key: new IFormattedText[] { new FormattedText(this.Text.Get(L10n.Building.UpgradesCoop2)) },
value: upgradeLevel >= 2
);
}
}
}
}

View File

@ -0,0 +1,272 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Data;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using Pathoschild.Stardew.LookupAnything.Framework.Models;
using StardewModdingAPI;
using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.Characters;
using StardewValley.Menus;
using StardewValley.Monsters;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>Describes an NPC (including villagers, monsters, and pets).</summary>
internal class CharacterSubject : BaseSubject
{
/*********
** Fields
*********/
/// <summary>The NPC type.s</summary>
private readonly TargetType TargetType;
/// <summary>The lookup target.</summary>
private readonly NPC Target;
/// <summary>Simplifies access to private game code.</summary>
private readonly IReflectionHelper Reflection;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="npc">The lookup target.</param>
/// <param name="type">The NPC type.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
/// <param name="reflectionHelper">Simplifies access to private game code.</param>
/// <remarks>Reverse engineered from <see cref="NPC"/>.</remarks>
public CharacterSubject(GameHelper gameHelper, NPC npc, TargetType type, Metadata metadata, ITranslationHelper translations, IReflectionHelper reflectionHelper)
: base(gameHelper, translations)
{
this.Reflection = reflectionHelper;
// get display type
string typeName;
if (type == TargetType.Villager)
typeName = this.Text.Get(L10n.Types.Villager);
else if (type == TargetType.Monster)
typeName = this.Text.Get(L10n.Types.Monster);
else
typeName = npc.GetType().Name;
// initialise
this.Target = npc;
this.TargetType = type;
CharacterData overrides = metadata.GetCharacter(npc, type);
string name = npc.getName();
string description = overrides?.DescriptionKey != null ? translations.Get(overrides.DescriptionKey) : null;
this.Initialise(name, description, typeName);
}
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<ICustomField> GetData(Metadata metadata)
{
NPC npc = this.Target;
switch (this.TargetType)
{
case TargetType.Villager:
// special NPCs like Gunther
if (metadata.Constants.AsocialVillagers.Contains(npc.Name))
{
// no data
}
// children
else if (npc is Child child)
{
// birthday
SDate birthday = SDate.Now().AddDays(-child.daysOld);
yield return new GenericField(this.GameHelper, this.Text.Get(L10n.Npc.Birthday), this.Text.Stringify(birthday, withYear: true));
// age
{
ChildAge stage = (ChildAge)child.Age;
int daysOld = child.daysOld;
int daysToNext = this.GetDaysToNextChildGrowth(stage, daysOld);
bool isGrown = daysToNext == -1;
int daysAtNext = daysOld + (isGrown ? 0 : daysToNext);
string ageLabel = this.Translate(L10n.NpcChild.Age);
string ageName = this.Translate(L10n.For(stage));
string ageDesc = isGrown
? this.Translate(L10n.NpcChild.AgeDescriptionGrown, new { label = ageName })
: this.Translate(L10n.NpcChild.AgeDescriptionPartial, new { label = ageName, count = daysToNext, nextLabel = this.Text.Get(L10n.For(stage + 1)) });
yield return new PercentageBarField(this.GameHelper, ageLabel, child.daysOld, daysAtNext, Color.Green, Color.Gray, ageDesc);
}
// friendship
if (Game1.player.friendshipData.ContainsKey(child.Name))
{
FriendshipModel friendship = this.GameHelper.GetFriendshipForVillager(Game1.player, child, Game1.player.friendshipData[child.Name], metadata);
yield return new CharacterFriendshipField(this.GameHelper, this.Translate(L10n.Npc.Friendship), friendship, this.Text);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Npc.TalkedToday), this.Stringify(Game1.player.friendshipData[child.Name].TalkedToToday));
}
}
// villagers
else
{
// birthday
if (npc.Birthday_Season != null)
{
SDate birthday = new SDate(npc.Birthday_Day, npc.Birthday_Season);
yield return new GenericField(this.GameHelper, this.Text.Get(L10n.Npc.Birthday), this.Text.Stringify(birthday));
}
// friendship
if (Game1.player.friendshipData.ContainsKey(npc.Name))
{
FriendshipModel friendship = this.GameHelper.GetFriendshipForVillager(Game1.player, npc, Game1.player.friendshipData[npc.Name], metadata);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Npc.CanRomance), friendship.IsSpouse ? this.Translate(L10n.Npc.CanRomanceMarried) : this.Stringify(friendship.CanDate));
yield return new CharacterFriendshipField(this.GameHelper, this.Translate(L10n.Npc.Friendship), friendship, this.Text);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Npc.TalkedToday), this.Stringify(friendship.TalkedToday));
yield return new GenericField(this.GameHelper, this.Translate(L10n.Npc.GiftedToday), this.Stringify(friendship.GiftsToday > 0));
if (!friendship.IsSpouse)
yield return new GenericField(this.GameHelper, this.Translate(L10n.Npc.GiftedThisWeek), this.Translate(L10n.Generic.Ratio, new { value = friendship.GiftsThisWeek, max = NPC.maxGiftsPerWeek }));
}
else
yield return new GenericField(this.GameHelper, this.Translate(L10n.Npc.Friendship), this.Translate(L10n.Npc.FriendshipNotMet));
// gift tastes
var giftTastes = this.GetGiftTastes(npc, metadata);
yield return new CharacterGiftTastesField(this.GameHelper, this.Translate(L10n.Npc.LovesGifts), giftTastes, GiftTaste.Love);
yield return new CharacterGiftTastesField(this.GameHelper, this.Translate(L10n.Npc.LikesGifts), giftTastes, GiftTaste.Like);
yield return new CharacterGiftTastesField(this.GameHelper, this.Translate(L10n.Npc.NeutralGifts), giftTastes, GiftTaste.Neutral);
}
break;
case TargetType.Pet:
Pet pet = (Pet)npc;
yield return new CharacterFriendshipField(this.GameHelper, this.Translate(L10n.Pet.Love), this.GameHelper.GetFriendshipForPet(Game1.player, pet), this.Text);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Pet.PettedToday), this.Stringify(this.Reflection.GetField<bool>(pet, "wasPetToday").GetValue()));
break;
case TargetType.Monster:
// basic info
Monster monster = (Monster)npc;
yield return new GenericField(this.GameHelper, this.Translate(L10n.Monster.Invincible), this.Translate(L10n.Generic.Seconds, new { count = this.Reflection.GetField<int>(monster, "invincibleCountdown").GetValue() }), hasValue: monster.isInvincible());
yield return new PercentageBarField(this.GameHelper, this.Translate(L10n.Monster.Health), monster.Health, monster.MaxHealth, Color.Green, Color.Gray, this.Translate(L10n.Generic.PercentRatio, new { percent = Math.Round((monster.Health / (monster.MaxHealth * 1f) * 100)), value = monster.Health, max = monster.MaxHealth }));
yield return new ItemDropListField(this.GameHelper, this.Translate(L10n.Monster.Drops), this.GetMonsterDrops(monster), this.Text, defaultText: this.Translate(L10n.Monster.DropsNothing));
yield return new GenericField(this.GameHelper, this.Translate(L10n.Monster.Experience), this.Stringify(monster.ExperienceGained));
yield return new GenericField(this.GameHelper, this.Translate(L10n.Monster.Defence), this.Stringify(monster.resilience.Value));
yield return new GenericField(this.GameHelper, this.Translate(L10n.Monster.Attack), this.Stringify(monster.DamageToFarmer));
// Adventure Guild quest
AdventureGuildQuestData adventureGuildQuest = metadata.GetAdventurerGuildQuest(monster.Name);
if (adventureGuildQuest != null)
{
int kills = adventureGuildQuest.Targets.Select(p => Game1.stats.getMonstersKilled(p)).Sum();
yield return new GenericField(this.GameHelper, this.Translate(L10n.Monster.AdventureGuild), $"{this.Translate(kills >= adventureGuildQuest.RequiredKills ? L10n.Monster.AdventureGuildComplete : L10n.Monster.AdventureGuildIncomplete)} ({this.Translate(L10n.Monster.AdventureGuildProgress, new { count = kills, requiredCount = adventureGuildQuest.RequiredKills })})");
}
break;
}
}
/// <summary>Get raw debug data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<IDebugField> GetDebugFields(Metadata metadata)
{
NPC target = this.Target;
Pet pet = target as Pet;
// pinned fields
yield return new GenericDebugField("facing direction", this.Stringify((FacingDirection)target.FacingDirection), pinned: true);
yield return new GenericDebugField("walking towards player", this.Stringify(target.IsWalkingTowardPlayer), pinned: true);
if (Game1.player.friendshipData.ContainsKey(target.Name))
{
FriendshipModel friendship = this.GameHelper.GetFriendshipForVillager(Game1.player, target, Game1.player.friendshipData[target.Name], metadata);
yield return new GenericDebugField("friendship", $"{friendship.Points} (max {friendship.MaxPoints})", pinned: true);
}
if (pet != null)
yield return new GenericDebugField("friendship", $"{pet.friendshipTowardFarmer} of {Pet.maxFriendship})", pinned: true);
// raw fields
foreach (IDebugField field in this.GetDebugFieldsFrom(target))
yield return field;
}
/// <summary>Get a monster's possible drops.</summary>
/// <param name="monster">The monster whose drops to get.</param>
private IEnumerable<ItemDropData> GetMonsterDrops(Monster monster)
{
int[] drops = monster.objectsToDrop.ToArray();
ItemDropData[] possibleDrops = this.GameHelper.GetMonsterData().First(p => p.Name == monster.Name).Drops;
return (
from possibleDrop in possibleDrops
let isGuaranteed = drops.Contains(possibleDrop.ItemID)
select new ItemDropData(possibleDrop.ItemID, possibleDrop.MaxDrop, isGuaranteed ? 1 : possibleDrop.Probability)
);
}
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
public override bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size)
{
NPC npc = this.Target;
// use character portrait (most NPCs)
if (npc.Portrait != null)
{
spriteBatch.DrawSprite(npc.Portrait, new Rectangle(0, 0, NPC.portrait_width, NPC.portrait_height), position.X, position.Y, Color.White, size.X / NPC.portrait_width);
return true;
}
// else draw sprite (e.g. for pets)
npc.Sprite.draw(spriteBatch, position, 1, 0, 0, Color.White, scale: size.X / npc.Sprite.getWidth());
return true;
}
/*********
** Private methods
*********/
/// <summary>Get how much an NPC likes receiving each item as a gift.</summary>
/// <param name="npc">The NPC.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
private IDictionary<GiftTaste, Item[]> GetGiftTastes(NPC npc, Metadata metadata)
{
return this.GameHelper.GetGiftTastes(npc, metadata)
.GroupBy(entry => entry.Value) // gift taste
.ToDictionary(
tasteGroup => tasteGroup.Key, // gift taste
tasteGroup => tasteGroup.Select(entry => (Item)entry.Key).ToArray() // items
);
}
/// <summary>Get the number of days until a child grows to the next stage.</summary>
/// <param name="stage">The child's current growth stage.</param>
/// <param name="daysOld">The child's current age in days.</param>
/// <returns>Returns a number of days, or <c>-1</c> if the child won't grow any further.</returns>
/// <remarks>Derived from <see cref="Child.dayUpdate"/>.</remarks>
private int GetDaysToNextChildGrowth(ChildAge stage, int daysOld)
{
switch (stage)
{
case ChildAge.Newborn:
return 13 - daysOld;
case ChildAge.Baby:
return 27 - daysOld;
case ChildAge.Crawler:
return 55 - daysOld;
default:
return -1;
}
}
}
}

View File

@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using StardewModdingAPI;
using StardewModdingAPI.Utilities;
using StardewValley;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>Describes a farm animal.</summary>
internal class FarmAnimalSubject : BaseSubject
{
/*********
** Fields
*********/
/// <summary>The lookup target.</summary>
private readonly FarmAnimal Target;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="animal">The lookup target.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
/// <remarks>Reverse engineered from <see cref="FarmAnimal"/>.</remarks>
public FarmAnimalSubject(GameHelper gameHelper, FarmAnimal animal, ITranslationHelper translations)
: base(gameHelper, animal.displayName, null, animal.type.Value, translations)
{
this.Target = animal;
}
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<ICustomField> GetData(Metadata metadata)
{
FarmAnimal animal = this.Target;
// calculate maturity
bool isFullyGrown = animal.age.Value >= animal.ageWhenMature.Value;
int daysUntilGrown = 0;
SDate dayOfMaturity = null;
if (!isFullyGrown)
{
daysUntilGrown = animal.ageWhenMature.Value - animal.age.Value;
dayOfMaturity = SDate.Now().AddDays(daysUntilGrown);
}
// yield fields
yield return new CharacterFriendshipField(this.GameHelper, this.Translate(L10n.Animal.Love), this.GameHelper.GetFriendshipForAnimal(Game1.player, animal, metadata), this.Text);
yield return new PercentageBarField(this.GameHelper, this.Translate(L10n.Animal.Happiness), animal.happiness.Value, byte.MaxValue, Color.Green, Color.Gray, this.Translate(L10n.Generic.Percent, new { percent = Math.Round(animal.happiness.Value / (metadata.Constants.AnimalMaxHappiness * 1f) * 100) }));
yield return new GenericField(this.GameHelper, this.Translate(L10n.Animal.Mood), animal.getMoodMessage());
yield return new GenericField(this.GameHelper, this.Translate(L10n.Animal.Complaints), this.GetMoodReason(animal));
yield return new ItemIconField(this.GameHelper, this.Translate(L10n.Animal.ProduceReady), animal.currentProduce.Value > 0 ? this.GameHelper.GetObjectBySpriteIndex(animal.currentProduce.Value) : null);
if (!isFullyGrown)
yield return new GenericField(this.GameHelper, this.Translate(L10n.Animal.Growth), $"{this.Translate(L10n.Generic.Days, new { count = daysUntilGrown })} ({this.Stringify(dayOfMaturity)})");
yield return new GenericField(this.GameHelper, this.Translate(L10n.Animal.SellsFor), GenericField.GetSaleValueString(animal.getSellPrice(), 1, this.Text));
}
/// <summary>Get raw debug data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<IDebugField> GetDebugFields(Metadata metadata)
{
FarmAnimal target = this.Target;
// pinned fields
yield return new GenericDebugField("age", $"{target.age} days", pinned: true);
yield return new GenericDebugField("friendship", $"{target.friendshipTowardFarmer} (max {metadata.Constants.AnimalMaxHappiness})", pinned: true);
yield return new GenericDebugField("fullness", this.Stringify(target.fullness.Value), pinned: true);
yield return new GenericDebugField("happiness", this.Stringify(target.happiness.Value), pinned: true);
// raw fields
foreach (IDebugField field in this.GetDebugFieldsFrom(target))
yield return field;
}
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
public override bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size)
{
FarmAnimal animal = this.Target;
animal.Sprite.draw(spriteBatch, position, 1, 0, 0, Color.White, scale: size.X / animal.Sprite.getWidth());
return true;
}
/*********
** Private methods
*********/
/// <summary>Get a short explanation for the animal's current mod.</summary>
/// <param name="animal">The farm animal.</param>
private string GetMoodReason(FarmAnimal animal)
{
List<string> factors = new List<string>();
// winter without heat
if (Game1.IsWinter && Game1.currentLocation.numberOfObjectsWithName(Constant.ItemNames.Heater) <= 0)
factors.Add(this.Translate(L10n.Animal.ComplaintsNoHeater));
// mood
switch (animal.moodMessage.Value)
{
case FarmAnimal.newHome:
factors.Add(this.Translate(L10n.Animal.ComplaintsNewHome));
break;
case FarmAnimal.hungry:
factors.Add(this.Translate(L10n.Animal.ComplaintsHungry));
break;
case FarmAnimal.disturbedByDog:
factors.Add(this.Translate(L10n.Animal.ComplaintsWildAnimalAttack));
break;
case FarmAnimal.leftOutAtNight:
factors.Add(this.Translate(L10n.Animal.ComplaintsLeftOut));
break;
}
// not pet
if (!animal.wasPet.Value)
factors.Add(this.Translate(L10n.Animal.ComplaintsNotPetted));
// return factors
return string.Join(", ", factors);
}
}
}

View File

@ -0,0 +1,202 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
//using System.Xml.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using StardewModdingAPI;
using StardewValley;
using StardewValley.Objects;
using SFarmer = StardewValley.Farmer;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>Describes a farmer (i.e. player).</summary>
internal class FarmerSubject : BaseSubject
{
/*********
** Fields
*********/
/// <summary>Simplifies access to private game code.</summary>
private readonly IReflectionHelper Reflection;
/// <summary>The lookup target.</summary>
private readonly SFarmer Target;
/// <summary>Whether this is being displayed on the load menu, before the save data is fully initialised.</summary>
private readonly bool IsLoadMenu;
///// <summary>The raw save data for this player, if <see cref="IsLoadMenu"/> is true.</summary>
//private readonly Lazy<XElement> RawSaveData;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="farmer">The lookup target.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
/// <param name="reflectionHelper">Simplifies access to private game code.</param>
/// <param name="isLoadMenu">Whether this is being displayed on the load menu, before the save data is fully initialised.</param>
public FarmerSubject(GameHelper gameHelper, SFarmer farmer, ITranslationHelper translations, IReflectionHelper reflectionHelper, bool isLoadMenu = false)
: base(gameHelper, farmer.Name, null, translations.Get(L10n.Types.Player), translations)
{
this.Reflection = reflectionHelper;
this.Target = farmer;
this.IsLoadMenu = isLoadMenu;
//this.RawSaveData = isLoadMenu
// ? new Lazy<XElement>(() => this.ReadSaveFile(farmer.slotName))
// : null;
}
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<ICustomField> GetData(Metadata metadata)
{
SFarmer target = this.Target;
int maxSkillPoints = metadata.Constants.PlayerMaxSkillPoints;
int[] skillPointsPerLevel = metadata.Constants.PlayerSkillPointsPerLevel;
string luckSummary = this.Translate(L10n.Player.LuckSummary, new { percent = (Game1.dailyLuck >= 0 ? "+" : "") + Math.Round(Game1.dailyLuck * 100, 2) });
yield return new GenericField(this.GameHelper, this.Translate(L10n.Player.Gender), this.Translate(target.IsMale ? L10n.Player.GenderMale : L10n.Player.GenderFemale));
yield return new GenericField(this.GameHelper, this.Translate(L10n.Player.FarmName), target.farmName.Value);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Player.FarmMap), this.GetFarmType());
yield return new GenericField(this.GameHelper, this.Translate(L10n.Player.FavoriteThing), target.favoriteThing.Value);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Player.Spouse), this.GetSpouseName());
yield return new SkillBarField(this.GameHelper, this.Translate(L10n.Player.FarmingSkill), target.experiencePoints[SFarmer.farmingSkill], maxSkillPoints, skillPointsPerLevel, this.Text);
yield return new SkillBarField(this.GameHelper, this.Translate(L10n.Player.MiningSkill), target.experiencePoints[SFarmer.miningSkill], maxSkillPoints, skillPointsPerLevel, this.Text);
yield return new SkillBarField(this.GameHelper, this.Translate(L10n.Player.ForagingSkill), target.experiencePoints[SFarmer.foragingSkill], maxSkillPoints, skillPointsPerLevel, this.Text);
yield return new SkillBarField(this.GameHelper, this.Translate(L10n.Player.FishingSkill), target.experiencePoints[SFarmer.fishingSkill], maxSkillPoints, skillPointsPerLevel, this.Text);
yield return new SkillBarField(this.GameHelper, this.Translate(L10n.Player.CombatSkill), target.experiencePoints[SFarmer.combatSkill], maxSkillPoints, skillPointsPerLevel, this.Text);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Player.Luck), $"{this.GetSpiritLuckMessage()}{Environment.NewLine}({luckSummary})");
}
/// <summary>Get raw debug data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<IDebugField> GetDebugFields(Metadata metadata)
{
SFarmer target = this.Target;
// pinned fields
yield return new GenericDebugField("immunity", target.immunity, pinned: true);
yield return new GenericDebugField("resilience", target.resilience, pinned: true);
yield return new GenericDebugField("magnetic radius", target.MagneticRadius, pinned: true);
// raw fields
foreach (IDebugField field in this.GetDebugFieldsFrom(target))
yield return field;
}
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
public override bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size)
{
SFarmer target = this.Target;
if (this.IsLoadMenu)
target.FarmerRenderer.draw(spriteBatch, new FarmerSprite.AnimationFrame(0, 0, false, false), 0, new Rectangle(0, 0, 16, 32), position, Vector2.Zero, 0.8f, 2, Color.White, 0.0f, 1f, target);
else
{
FarmerSprite sprite = target.FarmerSprite;
target.FarmerRenderer.draw(spriteBatch, sprite.CurrentAnimationFrame, sprite.CurrentFrame, sprite.SourceRect, position, Vector2.Zero, 0.8f, Color.White, 0, 1f, target);
}
return true;
}
/*********
** Private methods
*********/
/// <summary>Get a summary of the player's luck today.</summary>
/// <remarks>Derived from <see cref="GameLocation.answerDialogueAction"/>.</remarks>
private string GetSpiritLuckMessage()
{
// inject daily luck if not loaded yet
//if (this.IsLoadMenu)
//{
// string rawDailyLuck = this.RawSaveData.Value?.Element("dailyLuck")?.Value;
// if (rawDailyLuck == null)
// return null;
// Game1.dailyLuck = double.Parse(rawDailyLuck);
//}
// get daily luck message
TV tv = new TV();
return this.Reflection.GetMethod(tv, "getFortuneForecast").Invoke<string>();
}
/// <summary>Get the human-readable farm type selected by the player.</summary>
private string GetFarmType()
{
// get farm type
int farmType = Game1.whichFarm;
//if (this.IsLoadMenu)
//{
// string rawType = this.RawSaveData.Value?.Element("whichFarm")?.Value;
// farmType = rawType != null ? int.Parse(rawType) : -1;
//}
// get type name
switch (farmType)
{
case -1:
return null;
case Farm.combat_layout:
return Game1.content.LoadString("Strings\\UI:Character_FarmCombat").Split('_').FirstOrDefault();
case Farm.default_layout:
return Game1.content.LoadString("Strings\\UI:Character_FarmStandard").Split('_').FirstOrDefault();
case Farm.forest_layout:
return Game1.content.LoadString("Strings\\UI:Character_FarmForaging").Split('_').FirstOrDefault();
case Farm.mountains_layout:
return Game1.content.LoadString("Strings\\UI:Character_FarmMining").Split('_').FirstOrDefault();
case Farm.riverlands_layout:
return Game1.content.LoadString("Strings\\UI:Character_FarmFishing").Split('_').FirstOrDefault();
default:
return this.Translate(L10n.Player.FarmMapCustom);
}
}
/// <summary>Get the player's spouse name, if they're married.</summary>
/// <returns>Returns the spouse name, or <c>null</c> if they're not married.</returns>
private string GetSpouseName()
{
if (this.IsLoadMenu)
return this.Target.spouse;
long? spousePlayerID = this.Target.team.GetSpouse(this.Target.UniqueMultiplayerID);
SFarmer spousePlayer = spousePlayerID.HasValue ? Game1.getFarmerMaybeOffline(spousePlayerID.Value) : null;
return spousePlayer?.displayName ?? Game1.player.getSpouse()?.displayName;
}
/// <summary>Load the raw save file as an XML document.</summary>
/// <param name="slotName">The slot file to read.</param>
//private XElement ReadSaveFile(string slotName)
//{
// if (slotName == null)
// return null; // not available (e.g. farmhand)
// // get file
// FileInfo file = new FileInfo(Path.Combine(StardewModdingAPI.Constants.SavesPath, slotName, slotName));
// if (!file.Exists)
// return null;
// // read contents
// string text = File.ReadAllText(file.FullName);
// return XElement.Parse(text);
//}
}
}

View File

@ -0,0 +1,218 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using StardewModdingAPI;
using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.TerrainFeatures;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>Describes a non-fruit tree.</summary>
internal class FruitTreeSubject : BaseSubject
{
/*********
** Fields
*********/
/// <summary>The underlying target.</summary>
private readonly FruitTree Target;
/// <summary>The tree's tile position.</summary>
private readonly Vector2 Tile;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="tree">The lookup target.</param>
/// <param name="tile">The tree's tile position.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
public FruitTreeSubject(GameHelper gameHelper, FruitTree tree, Vector2 tile, ITranslationHelper translations)
: base(gameHelper, translations.Get(L10n.FruitTree.Name, new { fruitName = gameHelper.GetObjectBySpriteIndex(tree.indexOfFruit.Value).DisplayName }), null, translations.Get(L10n.Types.FruitTree), translations)
{
this.Target = tree;
this.Tile = tile;
}
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
/// <remarks>Tree growth algorithm reverse engineered from <see cref="FruitTree.dayUpdate"/>.</remarks>
public override IEnumerable<ICustomField> GetData(Metadata metadata)
{
FruitTree tree = this.Target;
// get basic info
bool isMature = tree.daysUntilMature.Value <= 0;
bool isDead = tree.stump.Value;
bool isStruckByLightning = tree.struckByLightningCountdown.Value > 0;
// show next fruit
if (isMature && !isDead)
{
string label = this.Translate(L10n.FruitTree.NextFruit);
if (isStruckByLightning)
yield return new GenericField(this.GameHelper, label, this.Translate(L10n.FruitTree.NextFruitStruckByLightning, new { count = tree.struckByLightningCountdown }));
else if (Game1.currentSeason != tree.fruitSeason.Value && !tree.GreenHouseTree)
yield return new GenericField(this.GameHelper, label, this.Translate(L10n.FruitTree.NextFruitOutOfSeason));
else if (tree.fruitsOnTree.Value == FruitTree.maxFruitsOnTrees)
yield return new GenericField(this.GameHelper, label, this.Translate(L10n.FruitTree.NextFruitMaxFruit));
else
yield return new GenericField(this.GameHelper, label, this.Translate(L10n.Generic.Tomorrow));
}
// show growth data
if (!isMature)
{
SDate dayOfMaturity = SDate.Now().AddDays(tree.daysUntilMature.Value);
string grownOnDateText = this.Translate(L10n.FruitTree.GrowthSummary, new { date = this.Stringify(dayOfMaturity) });
string daysUntilGrownText = this.Text.GetPlural(tree.daysUntilMature.Value, L10n.Generic.Tomorrow, L10n.Generic.InXDays).Tokens(new { count = tree.daysUntilMature });
string growthText = $"{grownOnDateText} ({daysUntilGrownText})";
yield return new GenericField(this.GameHelper, this.Translate(L10n.FruitTree.NextFruit), this.Translate(L10n.FruitTree.NextFruitTooYoung));
yield return new GenericField(this.GameHelper, this.Translate(L10n.FruitTree.Growth), growthText);
if (this.HasAdjacentObjects(this.Tile))
yield return new GenericField(this.GameHelper, this.Translate(L10n.FruitTree.Complaints), this.Translate(L10n.FruitTree.ComplaintsAdjacentObjects));
}
else
{
// get quality schedule
ItemQuality currentQuality = this.GetCurrentQuality(tree, metadata.Constants.FruitTreeQualityGrowthTime);
if (currentQuality == ItemQuality.Iridium)
yield return new GenericField(this.GameHelper, this.Translate(L10n.FruitTree.Quality), this.Translate(L10n.FruitTree.QualityNow, new { quality = this.Translate(L10n.For(currentQuality)) }));
else
{
string[] summary = this
.GetQualitySchedule(tree, currentQuality, metadata.Constants.FruitTreeQualityGrowthTime)
.Select(entry =>
{
// read schedule
ItemQuality quality = entry.Key;
int daysLeft = entry.Value;
SDate date = SDate.Now().AddDays(daysLeft);
int yearOffset = date.Year - Game1.year;
// generate summary line
string qualityName = this.Translate(L10n.For(quality));
if (daysLeft <= 0)
return "-" + this.Translate(L10n.FruitTree.QualityNow, new { quality = qualityName });
string line;
if (yearOffset == 0)
line = $"-{this.Translate(L10n.FruitTree.QualityOnDate, new { quality = qualityName, date = this.Stringify(date) })}";
else if (yearOffset == 1)
line = $"-{this.Translate(L10n.FruitTree.QualityOnDateNextYear, new { quality = qualityName, date = this.Stringify(date) })}";
else
line = $"-{this.Translate(L10n.FruitTree.QualityOnDate, new { quality = qualityName, date = this.Text.Stringify(date, withYear: true), year = date.Year })}";
line += $" ({this.Text.GetPlural(daysLeft, L10n.Generic.Tomorrow, L10n.Generic.InXDays).Tokens(new { count = daysLeft })})";
return line;
})
.ToArray();
yield return new GenericField(this.GameHelper, this.Translate(L10n.FruitTree.Quality), string.Join(Environment.NewLine, summary));
}
}
// show season
yield return new GenericField(this.GameHelper, this.Translate(L10n.FruitTree.Season), this.Translate(L10n.FruitTree.SeasonSummary, new { season = this.Text.GetSeasonName(tree.fruitSeason.Value) }));
}
/// <summary>Get raw debug data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<IDebugField> GetDebugFields(Metadata metadata)
{
FruitTree target = this.Target;
// pinned fields
yield return new GenericDebugField("mature in", $"{target.daysUntilMature} days", pinned: true);
yield return new GenericDebugField("growth stage", target.growthStage.Value, pinned: true);
yield return new GenericDebugField("health", target.health.Value, pinned: true);
// raw fields
foreach (IDebugField field in this.GetDebugFieldsFrom(target))
yield return field;
}
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
public override bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size)
{
this.Target.drawInMenu(spriteBatch, position, Vector2.Zero, 1, 1);
return true;
}
/*********
** Private methods
*********/
/// <summary>Whether there are adjacent objects that prevent growth.</summary>
/// <param name="position">The tree's position in the current location.</param>
private bool HasAdjacentObjects(Vector2 position)
{
GameLocation location = Game1.currentLocation;
return (
from adjacentTile in Utility.getSurroundingTileLocationsArray(position)
let isOccupied = location.isTileOccupied(adjacentTile)
let isEmptyDirt = location.terrainFeatures.ContainsKey(adjacentTile) && location.terrainFeatures[adjacentTile] is HoeDirt && ((HoeDirt)location.terrainFeatures[adjacentTile])?.crop == null
select isOccupied && !isEmptyDirt
).Any(p => p);
}
/// <summary>Get the fruit quality produced by a tree.</summary>
/// <param name="tree">The fruit tree.</param>
/// <param name="daysPerQuality">The number of days before the tree begins producing a higher quality.</param>
private ItemQuality GetCurrentQuality(FruitTree tree, int daysPerQuality)
{
int maturityLevel = Math.Max(0, Math.Min(3, -tree.daysUntilMature.Value / daysPerQuality));
switch (maturityLevel)
{
case 0:
return ItemQuality.Normal;
case 1:
return ItemQuality.Silver;
case 2:
return ItemQuality.Gold;
case 3:
return ItemQuality.Iridium;
default:
throw new NotSupportedException($"Unexpected quality level {maturityLevel}.");
}
}
/// <summary>Get a schedule indicating when a fruit tree will begin producing higher-quality fruit.</summary>
/// <param name="tree">The fruit tree.</param>
/// <param name="currentQuality">The current quality produced by the tree.</param>
/// <param name="daysPerQuality">The number of days before the tree begins producing a higher quality.</param>
private IEnumerable<KeyValuePair<ItemQuality, int>> GetQualitySchedule(FruitTree tree, ItemQuality currentQuality, int daysPerQuality)
{
if (tree.daysUntilMature.Value > 0)
yield break; // not mature yet
// yield current
yield return new KeyValuePair<ItemQuality, int>(currentQuality, 0);
// yield future qualities
int dayOffset = daysPerQuality - Math.Abs(tree.daysUntilMature.Value % daysPerQuality);
foreach (ItemQuality futureQuality in new[] { ItemQuality.Silver, ItemQuality.Gold, ItemQuality.Iridium })
{
if (currentQuality >= futureQuality)
continue;
yield return new KeyValuePair<ItemQuality, int>(futureQuality, dayOffset);
dayOffset += daysPerQuality;
}
}
}
}

View File

@ -0,0 +1,43 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>Provides metadata about something in the game.</summary>
internal interface ISubject
{
/*********
** Accessors
*********/
/// <summary>The display name.</summary>
string Name { get; }
/// <summary>The item description (if applicable).</summary>
string Description { get; }
/// <summary>The item type (if applicable).</summary>
string Type { get; }
/*********
** Public methods
*********/
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
IEnumerable<ICustomField> GetData(Metadata metadata);
/// <summary>Get raw debug data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
IEnumerable<IDebugField> GetDebugFields(Metadata metadata);
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size);
}
}

View File

@ -0,0 +1,698 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common.DataParsers;
using Pathoschild.Stardew.LookupAnything.Framework.Constants;
using Pathoschild.Stardew.LookupAnything.Framework.Data;
using Pathoschild.Stardew.LookupAnything.Framework.DebugFields;
using Pathoschild.Stardew.LookupAnything.Framework.Fields;
using Pathoschild.Stardew.LookupAnything.Framework.Models;
using StardewModdingAPI;
using StardewModdingAPI.Utilities;
using StardewValley;
using StardewValley.Locations;
using StardewValley.Objects;
using SObject = StardewValley.Object;
namespace Pathoschild.Stardew.LookupAnything.Framework.Subjects
{
/// <summary>Describes a Stardew Valley item.</summary>
internal class ItemSubject : BaseSubject
{
/*********
** Fields
*********/
/// <summary>The lookup target.</summary>
private readonly Item Target;
/// <summary>The menu item to render, which may be different from the item that was looked up (e.g. for fences).</summary>
private readonly Item DisplayItem;
/// <summary>The crop which will drop the item (if applicable).</summary>
private readonly Crop FromCrop;
/// <summary>The crop grown by this seed item (if applicable).</summary>
private readonly Crop SeedForCrop;
/// <summary>The context of the object being looked up.</summary>
private readonly ObjectContext Context;
/// <summary>Whether the item quality is known. This is <c>true</c> for an inventory item, <c>false</c> for a map object.</summary>
private readonly bool KnownQuality;
/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="gameHelper">Provides utility methods for interacting with the game code.</param>
/// <param name="translations">Provides translations stored in the mod folder.</param>
/// <param name="item">The underlying target.</param>
/// <param name="context">The context of the object being looked up.</param>
/// <param name="knownQuality">Whether the item quality is known. This is <c>true</c> for an inventory item, <c>false</c> for a map object.</param>
/// <param name="fromCrop">The crop associated with the item (if applicable).</param>
public ItemSubject(GameHelper gameHelper, ITranslationHelper translations, Item item, ObjectContext context, bool knownQuality, Crop fromCrop = null)
: base(gameHelper, translations)
{
this.Target = item;
this.DisplayItem = this.GetMenuItem(item);
this.FromCrop = fromCrop;
if ((item as SObject)?.Type == "Seeds")
this.SeedForCrop = new Crop(item.ParentSheetIndex, 0, 0);
this.Context = context;
this.KnownQuality = knownQuality;
this.Initialise(this.DisplayItem.DisplayName, this.GetDescription(this.DisplayItem), this.GetTypeValue(this.DisplayItem));
}
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<ICustomField> GetData(Metadata metadata)
{
// get data
Item item = this.Target;
SObject obj = item as SObject;
bool isCrop = this.FromCrop != null;
bool isSeed = this.SeedForCrop != null;
bool isDeadCrop = this.FromCrop?.dead.Value == true;
bool canSell = obj?.canBeShipped() == true || metadata.Shops.Any(shop => shop.BuysCategories.Contains(item.Category));
// get overrides
bool showInventoryFields = true;
{
ObjectData objData = metadata.GetObject(item, this.Context);
if (objData != null)
{
this.Name = objData.NameKey != null ? this.Translate(objData.NameKey) : this.Name;
this.Description = objData.DescriptionKey != null ? this.Translate(objData.DescriptionKey) : this.Description;
this.Type = objData.TypeKey != null ? this.Translate(objData.TypeKey) : this.Type;
showInventoryFields = objData.ShowInventoryFields ?? true;
}
}
// don't show data for dead crop
if (isDeadCrop)
{
yield return new GenericField(this.GameHelper, this.Translate(L10n.Crop.Summary), this.Translate(L10n.Crop.SummaryDead));
yield break;
}
// crop fields
foreach (ICustomField field in this.GetCropFields(this.FromCrop ?? this.SeedForCrop, isSeed, metadata))
yield return field;
// indoor pot crop
if (obj is IndoorPot pot)
{
Crop potCrop = pot.hoeDirt.Value.crop;
if (potCrop != null)
{
Item drop = this.GameHelper.GetObjectBySpriteIndex(potCrop.indexOfHarvest.Value);
yield return new LinkField(this.GameHelper, this.Translate(L10n.Item.Contents), drop.DisplayName, () => new ItemSubject(this.GameHelper, this.Text, this.GameHelper.GetObjectBySpriteIndex(potCrop.indexOfHarvest.Value), ObjectContext.World, knownQuality: false, fromCrop: potCrop));
}
}
// machine output
foreach (ICustomField field in this.GetMachineOutputFields(obj, metadata))
yield return field;
// item
if (showInventoryFields)
{
// needed for
foreach (ICustomField field in this.GetNeededForFields(obj, metadata))
yield return field;
// sale data
if (canSell && !isCrop)
{
// sale price
string saleValueSummary = GenericField.GetSaleValueString(this.GetSaleValue(item, this.KnownQuality, metadata), item.Stack, this.Text);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.SellsFor), saleValueSummary);
// sell to
List<string> buyers = new List<string>();
if (obj?.canBeShipped() == true)
buyers.Add(this.Translate(L10n.Item.SellsToShippingBox));
buyers.AddRange(
from shop in metadata.Shops
where shop.BuysCategories.Contains(item.Category)
let name = this.Translate(shop.DisplayKey).ToString()
orderby name
select name
);
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.SellsTo), string.Join(", ", buyers));
}
// gift tastes
var giftTastes = this.GetGiftTastes(item, metadata);
yield return new ItemGiftTastesField(this.GameHelper, this.Translate(L10n.Item.LovesThis), giftTastes, GiftTaste.Love);
yield return new ItemGiftTastesField(this.GameHelper, this.Translate(L10n.Item.LikesThis), giftTastes, GiftTaste.Like);
}
// fence
if (item is Fence fence)
{
string healthLabel = this.Translate(L10n.Item.FenceHealth);
// health
if (Game1.getFarm().isBuildingConstructed(Constant.BuildingNames.GoldClock))
yield return new GenericField(this.GameHelper, healthLabel, this.Translate(L10n.Item.FenceHealthGoldClock));
else
{
float maxHealth = fence.isGate.Value ? fence.maxHealth.Value * 2 : fence.maxHealth.Value;
float health = fence.health.Value / maxHealth;
double daysLeft = Math.Round(fence.health.Value * metadata.Constants.FenceDecayRate / 60 / 24);
double percent = Math.Round(health * 100);
yield return new PercentageBarField(this.GameHelper, healthLabel, (int)fence.health.Value, (int)maxHealth, Color.Green, Color.Red, this.Translate(L10n.Item.FenceHealthSummary, new { percent = percent, count = daysLeft }));
}
}
// recipes
if (item.GetSpriteType() == ItemSpriteType.Object)
{
RecipeModel[] recipes = this.GameHelper.GetRecipesForIngredient(this.DisplayItem).ToArray();
if (recipes.Any())
yield return new RecipesForIngredientField(this.GameHelper, this.Translate(L10n.Item.Recipes), item, recipes, this.Text);
}
// owned and times cooked/crafted
if (showInventoryFields && !isCrop && !(item is Tool))
{
// owned
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.Owned), this.Translate(L10n.Item.OwnedSummary, new { count = this.GameHelper.CountOwnedItems(item) }));
// times crafted
RecipeModel[] recipes = this.GameHelper
.GetRecipes()
.Where(recipe => recipe.OutputItemIndex == this.Target.ParentSheetIndex)
.ToArray();
if (recipes.Any())
{
string label = this.Translate(recipes.First().Type == RecipeType.Cooking ? L10n.Item.Cooked : L10n.Item.Crafted);
int timesCrafted = recipes.Sum(recipe => recipe.GetTimesCrafted(Game1.player));
yield return new GenericField(this.GameHelper, label, this.Translate(L10n.Item.CraftedSummary, new { count = timesCrafted }));
}
}
// see also crop
bool seeAlsoCrop =
isSeed
&& item.ParentSheetIndex != this.SeedForCrop.indexOfHarvest.Value // skip seeds which produce themselves (e.g. coffee beans)
&& !(item.ParentSheetIndex >= 495 && item.ParentSheetIndex <= 497) // skip random seasonal seeds
&& item.ParentSheetIndex != 770; // skip mixed seeds
if (seeAlsoCrop)
{
Item drop = this.GameHelper.GetObjectBySpriteIndex(this.SeedForCrop.indexOfHarvest.Value);
yield return new LinkField(this.GameHelper, this.Translate(L10n.Item.SeeAlso), drop.DisplayName, () => new ItemSubject(this.GameHelper, this.Text, drop, ObjectContext.Inventory, false, this.SeedForCrop));
}
}
/// <summary>Get the data to display for this subject.</summary>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public override IEnumerable<IDebugField> GetDebugFields(Metadata metadata)
{
Item target = this.Target;
SObject obj = target as SObject;
Crop crop = this.FromCrop ?? this.SeedForCrop;
// pinned fields
yield return new GenericDebugField("item ID", target.ParentSheetIndex, pinned: true);
yield return new GenericDebugField("category", $"{target.Category} ({target.getCategoryName()})", pinned: true);
if (obj != null)
{
yield return new GenericDebugField("edibility", obj.Edibility, pinned: true);
yield return new GenericDebugField("item type", obj.Type, pinned: true);
}
if (crop != null)
{
yield return new GenericDebugField("crop fully grown", this.Stringify(crop.fullyGrown.Value), pinned: true);
yield return new GenericDebugField("crop phase", $"{crop.currentPhase} (day {crop.dayOfCurrentPhase} in phase)", pinned: true);
}
// raw fields
foreach (IDebugField field in this.GetDebugFieldsFrom(target))
yield return field;
if (crop != null)
{
foreach (IDebugField field in this.GetDebugFieldsFrom(crop))
yield return new GenericDebugField($"crop::{field.Label}", field.Value, field.HasValue, field.IsPinned);
}
}
/// <summary>Draw the subject portrait (if available).</summary>
/// <param name="spriteBatch">The sprite batch being drawn.</param>
/// <param name="position">The position at which to draw.</param>
/// <param name="size">The size of the portrait to draw.</param>
/// <returns>Returns <c>true</c> if a portrait was drawn, else <c>false</c>.</returns>
public override bool DrawPortrait(SpriteBatch spriteBatch, Vector2 position, Vector2 size)
{
Item item = this.DisplayItem;
// draw stackable object
if ((item as SObject)?.Stack > 1)
{
// remove stack number (doesn't play well with clipped content)
SObject obj = (SObject)item;
obj = new SObject(obj.ParentSheetIndex, 1, obj.IsRecipe, obj.Price, obj.Quality);
obj.bigCraftable.Value = obj.bigCraftable.Value;
obj.drawInMenu(spriteBatch, position, 1);
return true;
}
// draw generic item
item.drawInMenu(spriteBatch, position, 1);
return true;
}
/*********
** Private methods
*********/
/// <summary>Get the equivalent menu item for the specified target. (For example, the inventory item matching a fence object.)</summary>
/// <param name="item">The target item.</param>
private Item GetMenuItem(Item item)
{
// fence
if (item is Fence fence)
{
// get equivalent object's sprite ID
FenceType fenceType = (FenceType)fence.whichType.Value;
int? spriteID = null;
if (fence.isGate.Value)
spriteID = 325;
else if (fenceType == FenceType.Wood)
spriteID = 322;
else if (fenceType == FenceType.Stone)
spriteID = 323;
else if (fenceType == FenceType.Iron)
spriteID = 324;
else if (fenceType == FenceType.Hardwood)
spriteID = 298;
// get object
if (spriteID.HasValue)
return new SObject(spriteID.Value, 1);
}
return item;
}
/// <summary>Get the item description.</summary>
/// <param name="item">The item.</param>
[SuppressMessage("ReSharper", "AssignmentIsFullyDiscarded", Justification = "Discarding the value is deliberate. We need to call the property to trigger the data load, but we don't actually need the result.")]
private string GetDescription(Item item)
{
try
{
_ = item.DisplayName; // force display name to load, which is needed to get the description outside the inventory for some reason
return item.getDescription();
}
catch (KeyNotFoundException)
{
return null; // e.g. incubator
}
}
/// <summary>Get the item type.</summary>
/// <param name="item">The item.</param>
private string GetTypeValue(Item item)
{
string categoryName = item.getCategoryName();
return !string.IsNullOrWhiteSpace(categoryName)
? categoryName
: this.Translate(L10n.Types.Other);
}
/// <summary>Get the custom fields for a crop.</summary>
/// <param name="crop">The crop to represent.</param>
/// <param name="isSeed">Whether the crop being displayed is for an unplanted seed.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
private IEnumerable<ICustomField> GetCropFields(Crop crop, bool isSeed, Metadata metadata)
{
if (crop == null)
yield break;
var data = new CropDataParser(crop);
// add next-harvest field
if (!isSeed)
{
// get next harvest
SDate nextHarvest = data.GetNextHarvest();
int daysToNextHarvest = nextHarvest.DaysSinceStart - SDate.Now().DaysSinceStart;
// generate field
string summary;
if (data.CanHarvestNow)
summary = this.Translate(L10n.Crop.HarvestNow);
else if (!Game1.currentLocation.IsGreenhouse && !data.Seasons.Contains(nextHarvest.Season))
summary = this.Translate(L10n.Crop.HarvestTooLate, new { date = this.Stringify(nextHarvest) });
else
summary = $"{this.Stringify(nextHarvest)} ({this.Text.GetPlural(daysToNextHarvest, L10n.Generic.Tomorrow, L10n.Generic.InXDays).Tokens(new { count = daysToNextHarvest })})";
yield return new GenericField(this.GameHelper, this.Translate(L10n.Crop.Harvest), summary);
}
// crop summary
{
List<string> summary = new List<string>();
// harvest
summary.Add(data.HasMultipleHarvests
? this.Translate(L10n.Crop.SummaryHarvestOnce, new { daysToFirstHarvest = data.DaysToFirstHarvest })
: this.Translate(L10n.Crop.SummaryHarvestMulti, new { daysToFirstHarvest = data.DaysToFirstHarvest, daysToNextHarvests = data.DaysToSubsequentHarvest })
);
// seasons
summary.Add(this.Translate(L10n.Crop.SummarySeasons, new { seasons = string.Join(", ", this.Text.GetSeasonNames(data.Seasons)) }));
// drops
if (crop.minHarvest != crop.maxHarvest && crop.chanceForExtraCrops.Value > 0)
summary.Add(this.Translate(L10n.Crop.SummaryDropsXToY, new { min = crop.minHarvest, max = crop.maxHarvest, percent = Math.Round(crop.chanceForExtraCrops.Value * 100, 2) }));
else if (crop.minHarvest.Value > 1)
summary.Add(this.Translate(L10n.Crop.SummaryDropsX, new { count = crop.minHarvest }));
// crop sale price
Item drop = data.GetSampleDrop();
summary.Add(this.Translate(L10n.Crop.SummarySellsFor, new { price = GenericField.GetSaleValueString(this.GetSaleValue(drop, false, metadata), 1, this.Text) }));
// generate field
yield return new GenericField(this.GameHelper, this.Translate(L10n.Crop.Summary), "-" + string.Join($"{Environment.NewLine}-", summary));
}
}
/// <summary>Get the custom fields for machine output.</summary>
/// <param name="machine">The machine whose output to represent.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
private IEnumerable<ICustomField> GetMachineOutputFields(SObject machine, Metadata metadata)
{
if (machine == null)
yield break;
SObject heldObj = machine.heldObject.Value;
int minutesLeft = machine.MinutesUntilReady;
// cask
if (machine is Cask cask)
{
// output item
if (heldObj != null)
{
ItemQuality curQuality = (ItemQuality)heldObj.Quality;
string curQualityName = this.Translate(L10n.For(curQuality));
// calculate aging schedule
float effectiveAge = metadata.Constants.CaskAgeSchedule.Values.Max() - cask.daysToMature.Value;
var schedule =
(
from entry in metadata.Constants.CaskAgeSchedule
let quality = entry.Key
let baseDays = entry.Value
where baseDays > effectiveAge
orderby baseDays ascending
let daysLeft = (int)Math.Ceiling((baseDays - effectiveAge) / cask.agingRate.Value)
select new
{
Quality = quality,
DaysLeft = daysLeft,
HarvestDate = SDate.Now().AddDays(daysLeft)
}
)
.ToArray();
// display fields
yield return new ItemIconField(this.GameHelper, this.Translate(L10n.Item.Contents), heldObj);
if (minutesLeft <= 0 || !schedule.Any())
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.CaskSchedule), this.Translate(L10n.Item.CaskScheduleNow, new { quality = curQualityName }));
else
{
string scheduleStr = string.Join(Environment.NewLine, (
from entry in schedule
let tokens = new { quality = this.Translate(L10n.For(entry.Quality)), count = entry.DaysLeft, date = entry.HarvestDate }
let str = this.Text.GetPlural(entry.DaysLeft, L10n.Item.CaskScheduleTomorrow, L10n.Item.CaskScheduleInXDays).Tokens(tokens)
select $"-{str}"
));
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.CaskSchedule), this.Translate(L10n.Item.CaskSchedulePartial, new { quality = curQualityName }) + Environment.NewLine + scheduleStr);
}
}
}
// crab pot
else if (machine is CrabPot pot)
{
// bait
if (heldObj == null)
{
if (pot.bait.Value != null)
yield return new ItemIconField(this.GameHelper, this.Translate(L10n.Item.CrabpotBait), pot.bait.Value);
else if (Game1.player.professions.Contains(11)) // no bait needed if luremaster
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.CrabpotBait), this.Translate(L10n.Item.CrabpotBaitNotNeeded));
else
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.CrabpotBait), this.Translate(L10n.Item.CrabpotBaitNeeded));
}
// output item
if (heldObj != null)
{
string summary = this.Translate(L10n.Item.ContentsReady, new { name = heldObj.DisplayName });
yield return new ItemIconField(this.GameHelper, this.Translate(L10n.Item.Contents), heldObj, summary);
}
}
// furniture
else if (machine is Furniture)
{
// displayed item
if (heldObj != null)
{
string summary = this.Translate(L10n.Item.ContentsPlaced, new { name = heldObj.DisplayName });
yield return new ItemIconField(this.GameHelper, this.Translate(L10n.Item.Contents), heldObj, summary);
}
}
// auto-grabber
else if (machine.ParentSheetIndex == Constant.ObjectIndexes.AutoGrabber)
{
string readyText = this.Text.Stringify(heldObj is Chest output && output.items.Any());
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.Contents), readyText);
}
// generic machine
else
{
// output item
if (heldObj != null)
{
string summary = minutesLeft <= 0
? this.Translate(L10n.Item.ContentsReady, new { name = heldObj.DisplayName })
: this.Translate(L10n.Item.ContentsPartial, new { name = heldObj.DisplayName, time = this.Stringify(TimeSpan.FromMinutes(minutesLeft)) });
yield return new ItemIconField(this.GameHelper, this.Translate(L10n.Item.Contents), heldObj, summary);
}
}
}
/// <summary>Get the custom fields indicating what an item is needed for.</summary>
/// <param name="obj">The machine whose output to represent.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
private IEnumerable<ICustomField> GetNeededForFields(SObject obj, Metadata metadata)
{
if (obj == null)
yield break;
List<string> neededFor = new List<string>();
// fetch info
var recipes =
(
from recipe in this.GameHelper.GetRecipesForIngredient(this.DisplayItem)
let item = recipe.CreateItem(this.DisplayItem)
orderby item.DisplayName
select new { recipe.Type, item.DisplayName, TimesCrafted = recipe.GetTimesCrafted(Game1.player) }
)
.ToArray();
// bundles
{
string[] missingBundles =
(
from bundle in this.GetUnfinishedBundles(obj)
orderby bundle.Area, bundle.DisplayName
let countNeeded = this.GetIngredientCountNeeded(bundle, obj)
select countNeeded > 1
? $"{this.GetTranslatedBundleArea(bundle)}: {bundle.DisplayName} x {countNeeded}"
: $"{this.GetTranslatedBundleArea(bundle)}: {bundle.DisplayName}"
)
.ToArray();
if (missingBundles.Any())
neededFor.Add(this.Translate(L10n.Item.NeededForCommunityCenter, new { bundles = string.Join(", ", missingBundles) }));
}
// polyculture achievement (ship 15 crops)
if (metadata.Constants.PolycultureCrops.Contains(obj.ParentSheetIndex))
{
int needed = metadata.Constants.PolycultureCount - this.GameHelper.GetShipped(obj.ParentSheetIndex);
if (needed > 0)
neededFor.Add(this.Translate(L10n.Item.NeededForPolyculture, new { count = needed }));
}
// full shipment achievement (ship every item)
if (this.GameHelper.GetFullShipmentAchievementItems().Any(p => p.Key == obj.ParentSheetIndex && !p.Value))
neededFor.Add(this.Translate(L10n.Item.NeededForFullShipment));
// full collection achievement (donate every artifact)
LibraryMuseum museum = Game1.locations.OfType<LibraryMuseum>().FirstOrDefault();
if (museum != null && museum.isItemSuitableForDonation(obj))
neededFor.Add(this.Translate(L10n.Item.NeededForFullCollection));
// gourmet chef achievement (cook every recipe)
{
string[] uncookedNames = (from recipe in recipes where recipe.Type == RecipeType.Cooking && recipe.TimesCrafted <= 0 select recipe.DisplayName).ToArray();
if (uncookedNames.Any())
neededFor.Add(this.Translate(L10n.Item.NeededForGourmetChef, new { recipes = string.Join(", ", uncookedNames) }));
}
// craft master achievement (craft every item)
{
string[] uncraftedNames = (from recipe in recipes where recipe.Type == RecipeType.Crafting && recipe.TimesCrafted <= 0 select recipe.DisplayName).ToArray();
if (uncraftedNames.Any())
neededFor.Add(this.Translate(L10n.Item.NeededForCraftMaster, new { recipes = string.Join(", ", uncraftedNames) }));
}
// yield
if (neededFor.Any())
yield return new GenericField(this.GameHelper, this.Translate(L10n.Item.NeededFor), string.Join(", ", neededFor));
}
/// <summary>Get unfinished bundles which require this item.</summary>
/// <param name="item">The item for which to find bundles.</param>
private IEnumerable<BundleModel> GetUnfinishedBundles(SObject item)
{
// no bundles for Joja members
if (Game1.player.hasOrWillReceiveMail(Constant.MailLetters.JojaMember))
yield break;
// get community center
CommunityCenter communityCenter = Game1.locations.OfType<CommunityCenter>().First();
if (communityCenter.areAllAreasComplete())
yield break;
// get bundles
if (item.GetType() == typeof(SObject) && !item.bigCraftable.Value) // avoid false positives with hats, furniture, etc
{
foreach (BundleModel bundle in this.GameHelper.GetBundleData())
{
// ignore completed bundle
if (communityCenter.isBundleComplete(bundle.ID))
continue;
bool isMissing = this.GetIngredientsFromBundle(bundle, item).Any(p => this.IsIngredientNeeded(bundle, p));
if (isMissing)
yield return bundle;
}
}
}
/// <summary>Get the translated name for a bundle's area.</summary>
/// <param name="bundle">The bundle.</param>
private string GetTranslatedBundleArea(BundleModel bundle)
{
switch (bundle.Area)
{
case "Pantry":
return this.Translate(L10n.BundleAreas.Pantry);
case "Crafts Room":
return this.Translate(L10n.BundleAreas.CraftsRoom);
case "Fish Tank":
return this.Translate(L10n.BundleAreas.FishTank);
case "Boiler Room":
return this.Translate(L10n.BundleAreas.BoilerRoom);
case "Vault":
return this.Translate(L10n.BundleAreas.Vault);
case "Bulletin Board":
return this.Translate(L10n.BundleAreas.BulletinBoard);
default:
return bundle.Area;
}
}
/// <summary>Get the possible sale values for an item.</summary>
/// <param name="item">The item.</param>
/// <param name="qualityIsKnown">Whether the item quality is known. This is <c>true</c> for an inventory item, <c>false</c> for a map object.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
private IDictionary<ItemQuality, int> GetSaleValue(Item item, bool qualityIsKnown, Metadata metadata)
{
// get sale price
// derived from ShopMenu::receiveLeftClick
int GetPrice(Item i)
{
int price = (i as SObject)?.sellToStorePrice() ?? (i.salePrice() / 2);
return price > 0 ? price : 0;
}
// single quality
if (!this.GameHelper.CanHaveQuality(item) || qualityIsKnown)
{
ItemQuality quality = qualityIsKnown && item is SObject obj
? (ItemQuality)obj.Quality
: ItemQuality.Normal;
return new Dictionary<ItemQuality, int> { [quality] = GetPrice(item) };
}
// multiple qualities
int[] iridiumItems = metadata.Constants.ItemsWithIridiumQuality;
var prices = new Dictionary<ItemQuality, int>
{
[ItemQuality.Normal] = GetPrice(new SObject(item.ParentSheetIndex, 1)),
[ItemQuality.Silver] = GetPrice(new SObject(item.ParentSheetIndex, 1, quality: (int)ItemQuality.Silver)),
[ItemQuality.Gold] = GetPrice(new SObject(item.ParentSheetIndex, 1, quality: (int)ItemQuality.Gold))
};
if (item.GetSpriteType() == ItemSpriteType.Object && (iridiumItems.Contains(item.Category) || iridiumItems.Contains(item.ParentSheetIndex)))
prices[ItemQuality.Iridium] = GetPrice(new SObject(item.ParentSheetIndex, 1, quality: (int)ItemQuality.Iridium));
return prices;
}
/// <summary>Get how much each NPC likes receiving an item as a gift.</summary>
/// <param name="item">The potential gift item.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
private IDictionary<GiftTaste, string[]> GetGiftTastes(Item item, Metadata metadata)
{
return this.GameHelper.GetGiftTastes(item, metadata)
.GroupBy(p => p.Value, p => p.Key.getName())
.ToDictionary(p => p.Key, p => p.Distinct().ToArray());
}
/// <summary>Get bundle ingredients matching the given item.</summary>
/// <param name="bundle">The bundle to search.</param>
/// <param name="item">The item to match.</param>
private IEnumerable<BundleIngredientModel> GetIngredientsFromBundle(BundleModel bundle, SObject item)
{
return bundle.Ingredients
.Where(p => p.ItemID == item.ParentSheetIndex && p.Quality <= (ItemQuality)item.Quality); // get ingredients
}
/// <summary>Get whether an ingredient is still needed for a bundle.</summary>
/// <param name="bundle">The bundle to check.</param>
/// <param name="ingredient">The ingredient to check.</param>
private bool IsIngredientNeeded(BundleModel bundle, BundleIngredientModel ingredient)
{
CommunityCenter communityCenter = Game1.locations.OfType<CommunityCenter>().First();
return !communityCenter.bundles[bundle.ID][ingredient.Index];
}
/// <summary>Get the number of an ingredient needed for a bundle.</summary>
/// <param name="bundle">The bundle to check.</param>
/// <param name="item">The ingredient to check.</param>
private int GetIngredientCountNeeded(BundleModel bundle, SObject item)
{
return this.GetIngredientsFromBundle(bundle, item)
.Where(p => this.IsIngredientNeeded(bundle, p))
.Sum(p => p.Stack);
}
}
}

Some files were not shown because too many files have changed in this diff Show More