Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Lookup Anything] Look up a body of water to see fishing data #1033

Open
wants to merge 20 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9e417ef
Add CheckboxList class
b3nk3lly Sep 12, 2024
062100d
Refactor checkbox fields to new CheckboxList class
b3nk3lly Sep 12, 2024
158b251
Add fishing area lookup
b3nk3lly Sep 13, 2024
e4797d4
Update fishing tile lookup to include location metadata
b3nk3lly Sep 16, 2024
3ae94c9
Clean up method overloading
b3nk3lly Sep 16, 2024
f3f278f
Remove fish spawns from results if the fish cannot spawn on the given…
b3nk3lly Sep 16, 2024
27df309
Add localised strings for fishing area subject type
b3nk3lly Sep 16, 2024
2684eb3
Fix whitespace at the end of checkbox lists
b3nk3lly Sep 16, 2024
946b9d9
Loosen requirements for using FishingAreaSubject so that the Mountain…
b3nk3lly Sep 16, 2024
e4238d3
Add progression mode option for hiding fish spawn conditions
b3nk3lly Sep 18, 2024
e1084ca
Fix IndexOutOfRangeException
b3nk3lly Sep 18, 2024
2602177
Clean up spawn condition code
b3nk3lly Sep 19, 2024
fe539a9
Fix NullReferenceException
b3nk3lly Sep 19, 2024
1c36ca2
Exclude furniture from spawn rules
b3nk3lly Oct 4, 2024
7cdf48d
Update CheckboxList constructor to accept an array
b3nk3lly Nov 9, 2024
6643762
Add IsHidden field to CheckboxList and simplify drawing logic when ch…
b3nk3lly Nov 9, 2024
d2d2b1f
Fix metadata fish appearing twice in spawn rules
b3nk3lly Nov 9, 2024
569db58
Fix label for FishingAreaSubject not showing proper display name in t…
b3nk3lly Nov 9, 2024
43ad01f
Add special case for fish caught on any mine level when checking spaw…
b3nk3lly Nov 9, 2024
68cd795
Migrate CheckboxList to file-scoped namespace
b3nk3lly Nov 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 96 additions & 32 deletions LookupAnything/DataParser.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.Xna.Framework;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.Common.Integrations.ExtraMachineConfig;
Expand All @@ -22,6 +23,7 @@
using StardewValley.Internal;
using StardewValley.ItemTypeDefinitions;
using StardewValley.TokenizableStrings;
using StardewValley.Tools;
using SFarmer = StardewValley.Farmer;
using SObject = StardewValley.Object;

Expand All @@ -36,6 +38,9 @@ internal class DataParser
/// <summary>The placeholder item ID for a recipe which can't be parsed due to its complexity.</summary>
public const string ComplexRecipeId = "__COMPLEX_RECIPE__";

/// <summary>A regex pattern matching the UndergroundMine location with an optional mine level.</summary>
private static readonly Regex MineLevelPattern = new(@"UndergroundMine(\d*)", RegexOptions.Compiled | RegexOptions.IgnoreCase);


/*********
** Public methods
Expand Down Expand Up @@ -159,6 +164,59 @@ public IEnumerable<FishPondDropData> GetFishPondDrops(FishPondData data)
}
}

/// <summary>Read parsed data about the spawn rules for fish in a specific location.</summary>
/// <param name="location">The location for which to get the spawn rules.</param>
/// <param name="tile">The tile for which to get the spawn rules.</param>
/// <param name="fishAreaId">The internal ID of the fishing area for which to get the spawn rules.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
public IEnumerable<FishSpawnData> GetFishSpawnRules(GameLocation location, Vector2 tile, string fishAreaId, Metadata metadata)
{
HashSet<string> seenFishIDs = [];

// parse game data
foreach (SpawnFishData fishData in location.GetData().Fish)
{
if (fishData.ItemId == null)
continue;

seenFishIDs.Add(fishData.ItemId);

// check if fish can spawn in this body of water
if (fishData.FishAreaId != null && fishData.FishAreaId != fishAreaId)
continue;

// check if bobber is in proper position
if (fishData.BobberPosition.HasValue && !fishData.BobberPosition.GetValueOrDefault().Contains((int)tile.X, (int)tile.Y))
continue;

// check if player is in proper position
if (fishData.PlayerPosition.HasValue && !fishData.PlayerPosition.GetValueOrDefault().Contains(Game1.player.TilePoint.X, Game1.player.TilePoint.Y))
continue;

// check if data is for a fish or jelly (i.e., not furniture)
ParsedItemData fish = ItemRegistry.GetDataOrErrorItem(fishData.ItemId);
if (fish.ObjectType != "Fish")
continue;

yield return this.GetFishSpawnRules(fish, metadata);
}

// parse metadata
foreach((string fishID, FishSpawnData spawnData) in metadata.CustomFishSpawnRules)
{
// skip if we already checked this fish, even if we rejected it (e.g., due to spawning only in a certain fishing area in a location)
if (seenFishIDs.Contains(fishID))
continue;

// skip if spawn location doesn't match
if (spawnData.Locations == null || !spawnData.Locations.Any(loc => loc.MatchesLocation(location.Name)))
continue;

ParsedItemData fish = ItemRegistry.GetDataOrErrorItem(fishID);
yield return this.GetFishSpawnRules(fish, metadata);
}
}

/// <summary>Read parsed data about the spawn rules for a specific fish.</summary>
/// <param name="fish">The fish item data.</param>
/// <param name="metadata">Provides metadata that's not available from the game data directly.</param>
Expand Down Expand Up @@ -324,6 +382,44 @@ public string GetLocationDisplayName(FishSpawnLocationData fishSpawnData)
return this.GetLocationDisplayName(fishSpawnData.LocationId, locationData, fishSpawnData.Area);
}

/// <summary>Get the translated display name for a location and optional fish area.</summary>
/// <param name="id">The location's internal name.</param>
/// <param name="data">The location data, if available.</param>
/// <param name="fishAreaId">The fish area ID within the location, if applicable.</param>
public string GetLocationDisplayName(string id, LocationData? data, string? fishAreaId)
{
// special cases
{
// special case: mine level
Match mineLevel = MineLevelPattern.Match(id);
if (mineLevel.Success)
{
// sometimes the mine level is provided as the fish area id; other times it's included in the location id
string level = fishAreaId ?? mineLevel.Groups[1].Value;

return string.IsNullOrWhiteSpace(level) ? this.GetLocationDisplayName(id, data) : I18n.Location_UndergroundMine_Level(level);
}

// skip: no area set
if (string.IsNullOrWhiteSpace(fishAreaId))
return this.GetLocationDisplayName(id, data);
}

// get base data
string locationName = this.GetLocationDisplayName(id, data);
string areaName = TokenParser.ParseText(data?.FishAreas?.GetValueOrDefault(fishAreaId)?.DisplayName);

// build translation
string displayName = I18n.GetByKey($"location.{id}.{fishAreaId}", new { locationName }).UsePlaceholder(false); // predefined translation
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = !string.IsNullOrWhiteSpace(areaName)
? I18n.Location_FishArea(locationName: locationName, areaName: areaName)
: I18n.Location_UnknownFishArea(locationName: locationName, id: fishAreaId);
}
return displayName;
}

/// <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()
Expand Down Expand Up @@ -652,38 +748,6 @@ from result in itemQueryResults
/*********
** Private methods
*********/
/// <summary>Get the translated display name for a location and optional fish area.</summary>
/// <param name="id">The location's internal name.</param>
/// <param name="data">The location data, if available.</param>
/// <param name="fishAreaId">The fish area ID within the location, if applicable.</param>
private string GetLocationDisplayName(string id, LocationData? data, string? fishAreaId)
{
// special cases
{
// skip: no area set
if (string.IsNullOrWhiteSpace(fishAreaId))
return this.GetLocationDisplayName(id, data);

// special case: mine level
if (string.Equals(id, "UndergroundMine", StringComparison.OrdinalIgnoreCase))
return I18n.Location_UndergroundMine_Level(level: fishAreaId);
}

// get base data
string locationName = this.GetLocationDisplayName(id, data);
string areaName = TokenParser.ParseText(data?.FishAreas?.GetValueOrDefault(fishAreaId)?.DisplayName);

// build translation
string displayName = I18n.GetByKey($"location.{id}.{fishAreaId}", new { locationName }).UsePlaceholder(false); // predefined translation
if (string.IsNullOrWhiteSpace(displayName))
{
displayName = !string.IsNullOrWhiteSpace(areaName)
? I18n.Location_FishArea(locationName: locationName, areaName: areaName)
: I18n.Location_UnknownFishArea(locationName: locationName, id: fishAreaId);
}
return displayName;
}

/// <summary>Get the translated display name for a location.</summary>
/// <param name="id">The location's internal name.</param>
/// <param name="data">The location data, if available.</param>
Expand Down
124 changes: 54 additions & 70 deletions LookupAnything/Framework/Fields/CheckboxListField.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Pathoschild.Stardew.Common.UI;
using Pathoschild.Stardew.LookupAnything.Framework.Fields.Models;
using StardewValley;

namespace Pathoschild.Stardew.LookupAnything.Framework.Fields;
Expand All @@ -15,111 +15,95 @@ internal class CheckboxListField : GenericField
** Fields
*********/
/// <summary>The checkbox values to display.</summary>
protected KeyValuePair<IFormattedText[], bool>[] Checkboxes;
protected CheckboxList[] CheckboxLists;

/// <summary>The intro text to show before the checkboxes.</summary>
protected IFormattedText[]? Intro;
/// <summary>The size of each checkbox to draw.</summary>
protected float CheckboxSize;

/// <summary>The height of one line of the checkbox list.</summary>
protected float LineHeight;


/*********
** Public methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="label">A short field label.</param>
/// <param name="checkboxes">The checkbox labels and values to display.</param>
public CheckboxListField(string label, IEnumerable<KeyValuePair<IFormattedText[], bool>> checkboxes)
/// <param name="checkboxLists">A list of checkbox labels and values to display.</param>
public CheckboxListField(string label, params CheckboxList[] checkboxLists)
: this(label)
{
this.Checkboxes = checkboxes.ToArray();
this.CheckboxLists = checkboxLists;
}

/// <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;

foreach (CheckboxList checkboxList in this.CheckboxLists)
{
topOffset += this.DrawCheckboxList(checkboxList, spriteBatch, font, new Vector2(position.X, position.Y + topOffset), wrapWidth).Y;
}

return new Vector2(wrapWidth, topOffset - this.LineHeight);
}


/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="label">A short field label.</param>
/// <param name="checkboxes">The checkbox labels and values to display.</param>
public CheckboxListField(string label, params KeyValuePair<IFormattedText[], bool>[] checkboxes)
: this(label)
protected CheckboxListField(string label)
: base(label, hasValue: true)
{
this.Checkboxes = checkboxes;
this.CheckboxLists = [];
this.CheckboxSize = CommonSprites.Icons.FilledCheckbox.Width * (Game1.pixelZoom / 2);
this.LineHeight = Math.Max(this.CheckboxSize, Game1.smallFont.MeasureString("ABC").Y);
}

/// <inheritdoc />
public override Vector2? DrawValue(SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
/// <summary>Draw a list of checkboxes.</summary>
/// <param name="checkboxList">The list of checkboxes to draw.</param>
/// <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>
protected Vector2 DrawCheckboxList(CheckboxList checkboxList, SpriteBatch spriteBatch, SpriteFont font, Vector2 position, float wrapWidth)
{
float topOffset = 0;
float checkboxSize = CommonSprites.Icons.FilledCheckbox.Width * (Game1.pixelZoom / 2);
float lineHeight = Math.Max(checkboxSize, Game1.smallFont.MeasureString("ABC").Y);
float checkboxOffset = (lineHeight - checkboxSize) / 2;
float checkboxOffset = (this.LineHeight - this.CheckboxSize) / 2;

if (this.Intro != null)
topOffset += spriteBatch.DrawTextBlock(font, this.Intro, position, wrapWidth).Y;
if (checkboxList.IntroData != null)
topOffset += this.DrawIconText(spriteBatch, font, new Vector2(position.X, position.Y + topOffset), wrapWidth, checkboxList.IntroData.Text, Color.Black, checkboxList.IntroData.Icon, new Vector2(this.LineHeight)).Y;

foreach ((IFormattedText[] label, bool isChecked) in this.Checkboxes)
foreach (CheckboxList.Checkbox checkbox in checkboxList.Checkboxes)
{
// draw icon
spriteBatch.Draw(
texture: CommonSprites.Icons.Sheet,
position: new Vector2(position.X, position.Y + topOffset + checkboxOffset),
sourceRectangle: isChecked ? CommonSprites.Icons.FilledCheckbox : CommonSprites.Icons.EmptyCheckbox,
sourceRectangle: checkbox.IsChecked ? CommonSprites.Icons.FilledCheckbox : CommonSprites.Icons.EmptyCheckbox,
color: Color.White,
rotation: 0,
origin: Vector2.Zero,
scale: checkboxSize / CommonSprites.Icons.FilledCheckbox.Width,
scale: this.CheckboxSize / CommonSprites.Icons.FilledCheckbox.Width,
effects: SpriteEffects.None,
layerDepth: 1f
);

// draw text
Vector2 textSize = spriteBatch.DrawTextBlock(Game1.smallFont, label, new Vector2(position.X + checkboxSize + 7, position.Y + topOffset), wrapWidth - checkboxSize - 7);
Vector2 textSize = spriteBatch.DrawTextBlock(Game1.smallFont, checkbox.Text, new Vector2(position.X + this.CheckboxSize + 7, position.Y + topOffset), wrapWidth - this.CheckboxSize - 7);

// update offset
topOffset += Math.Max(checkboxSize, textSize.Y);
// update offset for next checkbox
topOffset += Math.Max(this.CheckboxSize, textSize.Y);
}

return new Vector2(wrapWidth, topOffset);
}

/// <summary>Add intro text before the checkboxes.</summary>
/// <param name="text">The text to show before the checkboxes.</param>
public CheckboxListField AddIntro(params IFormattedText[] text)
{
this.Intro = text;
return this;
}

/// <summary>Add intro text before the checkboxes.</summary>
/// <param name="text">The text to show before the checkboxes.</param>
public CheckboxListField AddIntro(params string[] text)
{
return this.AddIntro(
text.Select(p => (IFormattedText)new FormattedText(p)).ToArray()
);
}

/// <summary>Build a checkbox entry.</summary>
/// <param name="value">Whether the value is enabled.</param>
/// <param name="text">The checkbox text to display.</param>
public static KeyValuePair<IFormattedText[], bool> Checkbox(bool value, params IFormattedText[] text)
{
return new KeyValuePair<IFormattedText[], bool>(text, value);
}

/// <summary>Build a checkbox entry.</summary>
/// <param name="value">Whether the value is enabled.</param>
/// <param name="text">The checkbox text to display.</param>
public static KeyValuePair<IFormattedText[], bool> Checkbox(bool value, string text)
{
return CheckboxListField.Checkbox(value, new FormattedText(text));
}


/*********
** Protected methods
*********/
/// <summary>Construct an instance.</summary>
/// <param name="label">A short field label.</param>
protected CheckboxListField(string label)
: base(label, hasValue: true)
{
this.Checkboxes = [];
return new Vector2(position.X, topOffset);
}
}
Loading