Merge pull request #96 from conundrum9999/main

Text descriptions for character creation; speed optimizations for object search; load NVDA's dll in place.
master
Mohammad Shoaib Khan 2023-04-07 12:00:47 +05:30 committed by GitHub
commit 0e1f9d0cab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2643 additions and 1518 deletions

6
.gitattributes vendored
View File

@ -61,3 +61,9 @@
#*.PDF diff=astextplain #*.PDF diff=astextplain
#*.rtf diff=astextplain #*.rtf diff=astextplain
#*.RTF diff=astextplain #*.RTF diff=astextplain
# Keep line endings consistent for files.
*.cs eol=crlf
*.json eol=crlf
LICENSE* eol=crlf

View File

@ -55,7 +55,7 @@ namespace stardew_access.ScreenReader
/// <returns>Name of the object as the first item (name) and category as the second item (category). Returns null if no object found.</returns> /// <returns>Name of the object as the first item (name) and category as the second item (category). Returns null if no object found.</returns>
public (string? name, string? category) GetNameWithCategoryNameAtTile(Vector2 tile) public (string? name, string? category) GetNameWithCategoryNameAtTile(Vector2 tile)
{ {
return TileInfo.getNameWithCategoryNameAtTile(tile); return TileInfo.getNameWithCategoryNameAtTile(tile, null);
} }
/// <summary> /// <summary>
@ -65,7 +65,7 @@ namespace stardew_access.ScreenReader
/// <returns>Name of the object. Returns null if no object found.</returns> /// <returns>Name of the object. Returns null if no object found.</returns>
public string? GetNameAtTile(Vector2 tile) public string? GetNameAtTile(Vector2 tile)
{ {
return TileInfo.getNameAtTile(tile); return TileInfo.GetNameAtTile(tile, null);
} }
/// <summary>Speaks the text via the loaded screen reader (if any).</summary> /// <summary>Speaks the text via the loaded screen reader (if any).</summary>

View File

@ -18,7 +18,7 @@ namespace stardew_access
return; return;
#region Read Tile #region Read Tile
helper.ConsoleCommands.Add("readtile", "Toggle read tile feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("readtile", "Toggle read tile feature.", (string command, string[] args) =>
{ {
MainClass.Config.ReadTile = !MainClass.Config.ReadTile; MainClass.Config.ReadTile = !MainClass.Config.ReadTile;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -26,7 +26,7 @@ namespace stardew_access
MainClass.InfoLog("Read Tile is " + (MainClass.Config.ReadTile ? "on" : "off")); MainClass.InfoLog("Read Tile is " + (MainClass.Config.ReadTile ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("flooring", "Toggle flooring in read tile.", (string commmand, string[] args) => helper.ConsoleCommands.Add("flooring", "Toggle flooring in read tile.", (string command, string[] args) =>
{ {
MainClass.Config.ReadFlooring = !MainClass.Config.ReadFlooring; MainClass.Config.ReadFlooring = !MainClass.Config.ReadFlooring;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -34,7 +34,7 @@ namespace stardew_access
MainClass.InfoLog("Flooring is " + (MainClass.Config.ReadFlooring ? "on" : "off")); MainClass.InfoLog("Flooring is " + (MainClass.Config.ReadFlooring ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("watered", "Toggle speaking watered or unwatered for crops.", (string commmand, string[] args) => helper.ConsoleCommands.Add("watered", "Toggle speaking watered or unwatered for crops.", (string command, string[] args) =>
{ {
MainClass.Config.WateredToggle = !MainClass.Config.WateredToggle; MainClass.Config.WateredToggle = !MainClass.Config.WateredToggle;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -44,7 +44,7 @@ namespace stardew_access
#endregion #endregion
#region Radar Feature #region Radar Feature
helper.ConsoleCommands.Add("radar", "Toggle radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("radar", "Toggle radar feature.", (string command, string[] args) =>
{ {
MainClass.Config.Radar = !MainClass.Config.Radar; MainClass.Config.Radar = !MainClass.Config.Radar;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -52,14 +52,14 @@ namespace stardew_access
MainClass.InfoLog("Radar " + (MainClass.Config.Radar ? "on" : "off")); MainClass.InfoLog("Radar " + (MainClass.Config.Radar ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("rdebug", "Toggle debugging in radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rdebug", "Toggle debugging in radar feature.", (string command, string[] args) =>
{ {
MainClass.radarDebug = !MainClass.radarDebug; MainClass.radarDebug = !MainClass.radarDebug;
MainClass.InfoLog("Radar debugging " + (MainClass.radarDebug ? "on" : "off")); MainClass.InfoLog("Radar debugging " + (MainClass.radarDebug ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("rstereo", "Toggle stereo sound in radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rstereo", "Toggle stereo sound in radar feature.", (string command, string[] args) =>
{ {
MainClass.Config.RadarStereoSound = !MainClass.Config.RadarStereoSound; MainClass.Config.RadarStereoSound = !MainClass.Config.RadarStereoSound;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -67,14 +67,14 @@ namespace stardew_access
MainClass.InfoLog("Stereo sound is " + (MainClass.Config.RadarStereoSound ? "on" : "off")); MainClass.InfoLog("Stereo sound is " + (MainClass.Config.RadarStereoSound ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("rfocus", "Toggle focus mode in radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfocus", "Toggle focus mode in radar feature.", (string command, string[] args) =>
{ {
bool focus = MainClass.RadarFeature.ToggleFocus(); bool focus = MainClass.RadarFeature.ToggleFocus();
MainClass.InfoLog("Focus mode is " + (focus ? "on" : "off")); MainClass.InfoLog("Focus mode is " + (focus ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("rdelay", "Set the delay of radar feature in milliseconds.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rdelay", "Set the delay of radar feature in milliseconds.", (string command, string[] args) =>
{ {
string? delayInString = null; string? delayInString = null;
@ -107,7 +107,7 @@ namespace stardew_access
}); });
helper.ConsoleCommands.Add("rrange", "Set the range of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rrange", "Set the range of radar feature.", (string command, string[] args) =>
{ {
string? rangeInString = null; string? rangeInString = null;
@ -142,7 +142,7 @@ namespace stardew_access
#region Exclusions #region Exclusions
helper.ConsoleCommands.Add("readd", "Add an object key to the exclusions list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("readd", "Add an object key to the exclusions list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@ -167,7 +167,7 @@ namespace stardew_access
} }
}); });
helper.ConsoleCommands.Add("reremove", "Remove an object key from the exclusions list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("reremove", "Remove an object key from the exclusions list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@ -192,7 +192,7 @@ namespace stardew_access
} }
}); });
helper.ConsoleCommands.Add("relist", "List all the exclusions in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("relist", "List all the exclusions in the radar feature.", (string command, string[] args) =>
{ {
if (MainClass.RadarFeature.exclusions.Count > 0) if (MainClass.RadarFeature.exclusions.Count > 0)
{ {
@ -209,20 +209,20 @@ namespace stardew_access
} }
}); });
helper.ConsoleCommands.Add("reclear", "Clear the focus exclusions in the radar featrure.", (string commmand, string[] args) => helper.ConsoleCommands.Add("reclear", "Clear the focus exclusions in the radar featrure.", (string command, string[] args) =>
{ {
MainClass.RadarFeature.exclusions.Clear(); MainClass.RadarFeature.exclusions.Clear();
MainClass.InfoLog($"Cleared the focus list in the exclusions feature."); MainClass.InfoLog($"Cleared the focus list in the exclusions feature.");
}); });
helper.ConsoleCommands.Add("recount", "Number of exclusions in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("recount", "Number of exclusions in the radar feature.", (string command, string[] args) =>
{ {
MainClass.InfoLog($"There are {MainClass.RadarFeature.exclusions.Count} exclusiond in the radar feature."); MainClass.InfoLog($"There are {MainClass.RadarFeature.exclusions.Count} exclusiond in the radar feature.");
}); });
#endregion #endregion
#region Focus #region Focus
helper.ConsoleCommands.Add("rfadd", "Add an object key to the focus list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfadd", "Add an object key to the focus list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@ -247,7 +247,7 @@ namespace stardew_access
} }
}); });
helper.ConsoleCommands.Add("rfremove", "Remove an object key from the focus list of radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfremove", "Remove an object key from the focus list of radar feature.", (string command, string[] args) =>
{ {
string? keyToAdd = null; string? keyToAdd = null;
@ -272,7 +272,7 @@ namespace stardew_access
} }
}); });
helper.ConsoleCommands.Add("rflist", "List all the exclusions in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rflist", "List all the exclusions in the radar feature.", (string command, string[] args) =>
{ {
if (MainClass.RadarFeature.focus.Count > 0) if (MainClass.RadarFeature.focus.Count > 0)
{ {
@ -289,13 +289,13 @@ namespace stardew_access
} }
}); });
helper.ConsoleCommands.Add("rfclear", "Clear the focus list in the radar featrure.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfclear", "Clear the focus list in the radar featrure.", (string command, string[] args) =>
{ {
MainClass.RadarFeature.focus.Clear(); MainClass.RadarFeature.focus.Clear();
MainClass.InfoLog($"Cleared the focus list in the radar feature."); MainClass.InfoLog($"Cleared the focus list in the radar feature.");
}); });
helper.ConsoleCommands.Add("rfcount", "Number of list in the radar feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("rfcount", "Number of list in the radar feature.", (string command, string[] args) =>
{ {
MainClass.InfoLog($"There are {MainClass.RadarFeature.focus.Count} objects in the focus list in the radar feature."); MainClass.InfoLog($"There are {MainClass.RadarFeature.focus.Count} objects in the focus list in the radar feature.");
}); });
@ -304,7 +304,7 @@ namespace stardew_access
#endregion #endregion
#region Tile marking #region Tile marking
helper.ConsoleCommands.Add("mark", "Marks the player's position for use in building construction in Carpenter Menu.", (string commmand, string[] args) => helper.ConsoleCommands.Add("mark", "Marks the player's position for use in building construction in Carpenter Menu.", (string command, string[] args) =>
{ {
if (Game1.currentLocation is not Farm) if (Game1.currentLocation is not Farm)
{ {
@ -332,7 +332,7 @@ namespace stardew_access
MainClass.InfoLog($"Location {(int)Game1.player.getTileX()}x {(int)Game1.player.getTileY()}y added at {index} index."); MainClass.InfoLog($"Location {(int)Game1.player.getTileX()}x {(int)Game1.player.getTileY()}y added at {index} index.");
}); });
helper.ConsoleCommands.Add("marklist", "List all marked positions.", (string commmand, string[] args) => helper.ConsoleCommands.Add("marklist", "List all marked positions.", (string command, string[] args) =>
{ {
string toPrint = ""; string toPrint = "";
for (int i = 0; i < BuildingOperations.marked.Length; i++) for (int i = 0; i < BuildingOperations.marked.Length; i++)
@ -349,12 +349,12 @@ namespace stardew_access
MainClass.InfoLog($"Marked positions:{toPrint}\nOpen command menu and use pageup and pagedown to check the list"); MainClass.InfoLog($"Marked positions:{toPrint}\nOpen command menu and use pageup and pagedown to check the list");
}); });
helper.ConsoleCommands.Add("buildlist", "List all buildings for selection for upgrading/demolishing/painting", (string commmand, string[] args) => helper.ConsoleCommands.Add("buildlist", "List all buildings for selection for upgrading/demolishing/painting", (string command, string[] args) =>
{ {
onBuildListCalled(); onBuildListCalled();
}); });
helper.ConsoleCommands.Add("buildsel", "Select the building index which you want to upgrade/demolish/paint", (string commmand, string[] args) => helper.ConsoleCommands.Add("buildsel", "Select the building index which you want to upgrade/demolish/paint", (string command, string[] args) =>
{ {
if ((Game1.activeClickableMenu is not CarpenterMenu && Game1.activeClickableMenu is not PurchaseAnimalsMenu && Game1.activeClickableMenu is not AnimalQueryMenu) || !CarpenterMenuPatch.isOnFarm) if ((Game1.activeClickableMenu is not CarpenterMenu && Game1.activeClickableMenu is not PurchaseAnimalsMenu && Game1.activeClickableMenu is not AnimalQueryMenu) || !CarpenterMenuPatch.isOnFarm)
{ {
@ -451,28 +451,29 @@ namespace stardew_access
#endregion #endregion
#region Other #region Other
helper.ConsoleCommands.Add("refsr", "Refresh screen reader", (string commmand, string[] args) => helper.ConsoleCommands.Add("refsr", "Refresh screen reader", (string command, string[] args) =>
{ {
MainClass.ScreenReader.InitializeScreenReader(); MainClass.ScreenReader.InitializeScreenReader();
MainClass.InfoLog("Screen Reader refreshed!"); MainClass.InfoLog("Screen Reader refreshed!");
}); });
helper.ConsoleCommands.Add("refmc", "Refresh mod config", (string commmand, string[] args) => helper.ConsoleCommands.Add("refmc", "Refresh mod config", (string command, string[] args) =>
{ {
MainClass.Config = helper.ReadConfig<ModConfig>(); MainClass.Config = helper.ReadConfig<ModConfig>();
MainClass.InfoLog("Mod Config refreshed!"); MainClass.InfoLog("Mod Config refreshed!");
}); });
helper.ConsoleCommands.Add("refst", "Refresh static tiles", (string commmand, string[] args) => helper.ConsoleCommands.Add("refst", "Refresh static tiles", (string command, string[] args) =>
{ {
MainClass.STiles = new Features.StaticTiles(); StaticTiles.LoadTilesFiles();
StaticTiles.SetupTilesDicts();
MainClass.InfoLog("Static tiles refreshed!"); MainClass.InfoLog("Static tiles refreshed!");
}); });
helper.ConsoleCommands.Add("hnspercent", "Toggle between speaking in percentage or full health and stamina.", (string commmand, string[] args) => helper.ConsoleCommands.Add("hnspercent", "Toggle between speaking in percentage or full health and stamina.", (string command, string[] args) =>
{ {
MainClass.Config.HealthNStaminaInPercentage = !MainClass.Config.HealthNStaminaInPercentage; MainClass.Config.HealthNStaminaInPercentage = !MainClass.Config.HealthNStaminaInPercentage;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -480,7 +481,7 @@ namespace stardew_access
MainClass.InfoLog("Speaking in percentage is " + (MainClass.Config.HealthNStaminaInPercentage ? "on" : "off")); MainClass.InfoLog("Speaking in percentage is " + (MainClass.Config.HealthNStaminaInPercentage ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("snapmouse", "Toggle snap mouse feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("snapmouse", "Toggle snap mouse feature.", (string command, string[] args) =>
{ {
MainClass.Config.SnapMouse = !MainClass.Config.SnapMouse; MainClass.Config.SnapMouse = !MainClass.Config.SnapMouse;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -488,7 +489,7 @@ namespace stardew_access
MainClass.InfoLog("Snap Mouse is " + (MainClass.Config.SnapMouse ? "on" : "off")); MainClass.InfoLog("Snap Mouse is " + (MainClass.Config.SnapMouse ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("warning", "Toggle warnings feature.", (string commmand, string[] args) => helper.ConsoleCommands.Add("warning", "Toggle warnings feature.", (string command, string[] args) =>
{ {
MainClass.Config.Warning = !MainClass.Config.Warning; MainClass.Config.Warning = !MainClass.Config.Warning;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);
@ -496,7 +497,7 @@ namespace stardew_access
MainClass.InfoLog("Warnings is " + (MainClass.Config.Warning ? "on" : "off")); MainClass.InfoLog("Warnings is " + (MainClass.Config.Warning ? "on" : "off"));
}); });
helper.ConsoleCommands.Add("tts", "Toggles the screen reader/tts", (string commmand, string[] args) => helper.ConsoleCommands.Add("tts", "Toggles the screen reader/tts", (string command, string[] args) =>
{ {
MainClass.Config.TTS = !MainClass.Config.TTS; MainClass.Config.TTS = !MainClass.Config.TTS;
helper.WriteConfig(MainClass.Config); helper.WriteConfig(MainClass.Config);

View File

@ -0,0 +1,812 @@
using Microsoft.Xna.Framework;
using Netcode;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;
using StardewValley.Objects;
using StardewValley.TerrainFeatures;
using static stardew_access.Features.Utils;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
namespace stardew_access.Features
{
/// <summary>
/// Provides methods to locate tiles of interest in various game locations that are conditional or unpredictable (I.E. not static).
/// </summary>
/// <remarks>
/// The DynamicTiles class currently supports the following location types:
/// - Beach
/// - BoatTunnel
/// - CommunityCenter
/// - Farm
/// - FarmHouse
/// - Forest
/// - IslandFarmHouse
/// - IslandLocation
/// - LibraryMuseum
/// - Town
///
/// And the following Island LocationTypes:
/// - IslandNorth
/// - IslandWest
/// - VolcanoDungeon
///
/// The class also supports the following named locations:
/// - Barn (and its upgraded versions)
/// - Coop (and its upgraded versions)
///
/// The class does not yet support the following location types, but consider adding support in future updates:
/// - AbandonedJojaMart
/// - AdventureGuild
/// - BathHousePool
/// - BeachNightMarket
/// - BugLand
/// - BusStop
/// - Caldera
/// - Cellar
/// - Club
/// - Desert
/// - FarmCave
/// - FishShop
/// - JojaMart
/// - ManorHouse
/// - MermaidHouse
/// - Mine
/// - Mountain
/// - MovieTheater
/// - Railroad
/// - SeedShop
/// - Sewer
/// - Submarine
/// - Summit
/// - WizardHouse
/// - Woods
///
/// The class does not yet support the following named locations, but consider adding support in future updates:
/// - "AnimalShop"
/// - "Backwoods"
/// - "BathHouse_Entry"
/// - "BathHouse_MensLocker"
/// - "BathHouse_WomensLocker"
/// - "Blacksmith"
/// - "ElliottHouse"
/// - "FarmGreenHouse"
/// - "Greenhouse"
/// - "HaleyHouse"
/// - "HarveyRoom"
/// - "Hospital"
/// - "JoshHouse"
/// - "LeahHouse"
/// - "LeoTreeHouse"
/// - "Saloon"
/// - "SamHouse"
/// - "SandyHouse"
/// - "ScienceHouse"
/// - "SebastianRoom"
/// - "SkullCave"
/// - "Sunroom"
/// - "Tent"
/// - "Trailer"
/// - "Trailer_Big"
/// - "Tunnel"
/// - "WitchHut"
/// - "WitchSwamp"
/// - "WitchWarpCave"
/// - "WizardHouseBasement"
///
/// The class does not yet support the following IslandLocation location types, but consider adding support in future updates:
/// - IslandEast
/// - IslandFarmCave
/// - IslandFieldOffice
/// - IslandHut
/// - IslandShrine
/// - IslandSouth
/// - IslandSouthEast
/// - IslandSouthEastCave
/// - IslandWestCave1
///
/// The class does not yet support the following IslandLocation named locations, but consider adding support in future updates:
/// - "CaptainRoom"
/// - "IslandNorthCave1"
/// - "QiNutRoom"
/// </remarks>
public class DynamicTiles
{
// Static instance for the singleton pattern
private static DynamicTiles? _instance;
/// <summary>
/// The singleton instance of the <see cref="DynamicTiles"/> class.
/// </summary>
public static DynamicTiles Instance
{
get
{
_instance ??= new DynamicTiles();
return _instance;
}
}
// HashSet for storing which unimplemented locations have been previously logged
private static readonly HashSet<object> loggedLocations = new();
// Dictionary of coordinates for feeding benches in barns and coops
private static readonly Dictionary<string, (int minX, int maxX, int y)> FeedingBenchBounds = new()
{
{ "Barn", (8, 11, 3) },
{ "Barn2", (8, 15, 3) },
{ "Big Barn", (8, 15, 3) },
{ "Barn3", (8, 19, 3) },
{ "Deluxe Barn", (8, 19, 3) },
{ "Coop", (6, 9, 3) },
{ "Coop2", (6, 13, 3) },
{ "Big Coop", (6, 13, 3) },
{ "Coop3", (6, 17, 3) },
{ "Deluxe Coop", (6, 17, 3) }
};
// Dictionary to hold event info
private static readonly Dictionary<string, Dictionary<(int X, int Y), string>> EventInteractables;
/// <summary>
/// Initializes a new instance of the <see cref="DynamicTiles"/> class.
/// Loads the event file.
/// </summary>
static DynamicTiles()
{
EventInteractables = LoadEventTiles();
}
/// <summary>
/// Loads event tiles from the "event-tiles.json" file and returns a dictionary representation of the data.
/// </summary>
/// <returns>
/// A dictionary with event names as keys and nested dictionaries as values, where nested dictionaries have
/// coordinate tuples (x, y) as keys and tile names as values.
/// </returns>
private static Dictionary<string, Dictionary<(int x, int y), string>> LoadEventTiles()
{
JsonElement json = LoadJsonFile("event-tiles.json");
if (json.ValueKind == JsonValueKind.Undefined)
{
// If the JSON couldn't be loaded or parsed, return an empty dictionary
return new Dictionary<string, Dictionary<(int x, int y), string>>();
}
var eventTiles = new Dictionary<string, Dictionary<(int x, int y), string>>();
// Iterate over the JSON properties to create a dictionary representation of the data
foreach (JsonProperty eventProperty in json.EnumerateObject())
{
string eventName = eventProperty.Name;
var coordinates = new Dictionary<(int x, int y), string>();
// Iterate over the coordinate properties to create a nested dictionary with coordinate tuples as keys
foreach (JsonProperty coordinateProperty in eventProperty.Value.EnumerateObject())
{
string[] xy = coordinateProperty.Name.Split(',');
int x = int.Parse(xy[0]);
int y = int.Parse(xy[1]);
coordinates.Add((x, y), value: coordinateProperty.Value.GetString() ?? string.Empty);
}
eventTiles.Add(eventName, coordinates);
}
return eventTiles;
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in a Beach.
/// </summary>
/// <param name="beach">The Beach to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetBeachInfo(Beach beach, int x, int y, bool lessInfo = false)
{
if (MainClass.ModHelper == null)
{
return (null, null);
}
if (MainClass.ModHelper.Reflection.GetField<NPC>(beach, "oldMariner").GetValue() is NPC mariner && mariner.getTileLocation() == new Vector2(x, y))
{
return ("Old Mariner", CATEGORY.NPCs);
}
else if (x == 58 && y == 13)
{
if (!beach.bridgeFixed.Value)
{
return ("Repair Bridge", CATEGORY.Interactables);
}
else
{
return ("Bridge", CATEGORY.Bridges);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a BoatTunnel.
/// </summary>
/// <param name="boatTunnel">The BoatTunnel to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetBoatTunnelInfo(BoatTunnel boatTunnel, int x, int y, bool lessInfo = false)
{
// Check if the player has received the specified mail or not
bool HasMail(string mail) => Game1.MasterPlayer.hasOrWillReceiveMail(mail);
// If the position matches one of the interactable elements in the boat tunnel
if ((x, y) == (4, 9) || (x, y) == (6, 8) || (x, y) == (8, 9))
{
string mail = (x, y) switch
{
(4, 9) => "willyBoatFixed",
(6, 8) => "willyBoatHull",
(8, 9) => "willyBoatAnchor",
_ => throw new InvalidOperationException("Unexpected (x, y) values"),
};
string itemName = (x, y) switch
{
(4, 9) => "Ticket Machine",
(6, 8) => "Boat Hull",
(8, 9) => "Boat Anchor",
_ => throw new InvalidOperationException("Unexpected (x, y) values"),
};
CATEGORY category = (x, y) == (4, 9) ? CATEGORY.Interactables : (!HasMail(mail) ? CATEGORY.Interactables : CATEGORY.Decor);
return ((!HasMail(mail) ? "Repair " : "") + itemName, category);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a CommunityCenter.
/// </summary>
/// <param name="communityCenter">The CommunityCenter to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetCommunityCenterInfo(CommunityCenter communityCenter, int x, int y, bool lessInfo = false)
{
if (communityCenter.missedRewardsChestVisible.Value && x == 22 && y == 10)
{
return ("Missed Rewards Chest", CATEGORY.Containers);
}
return (null, null);
}
/// <summary>
/// Gets the building information for a given position on a farm.
/// </summary>
/// <param name="building">The Building instance.</param>
/// <param name="x">The x-coordinate of the position.</param>
/// <param name="y">The y-coordinate of the position.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the door or building found, or (null, null) if no door or building is found.</returns>
private static (string? name, CATEGORY? category) GetBuildingInfo(Building building, int x, int y, bool lessInfo = false)
{
string name = building.buildingType.Value;
int buildingTileX = building.tileX.Value;
int buildingTileY = building.tileY.Value;
// If the building is a FishPond, prepend the fish name
if (building is FishPond fishPond && fishPond.fishType.Value >= 0)
{
name = $"{Game1.objectInformation[fishPond.fishType.Value].Split('/')[4]} {name}";
}
// Calculate differences in x and y coordinates
int offsetX = x - buildingTileX;
int offsetY = y - buildingTileY;
// Check if the position matches the human door
if (building.humanDoor.Value.X == offsetX && building.humanDoor.Value.Y == offsetY)
{
return (name + " Door", CATEGORY.Doors);
}
// Check if the position matches the animal door
else if (building.animalDoor.Value.X == offsetX && building.animalDoor.Value.Y == offsetY)
{
return (name + " Animal Door " + ((building.animalDoorOpen.Value) ? "Opened" : "Closed"), CATEGORY.Doors);
}
// Check if the position matches the building's top-left corner
else if (offsetX == 0 && offsetY == 0)
{
return (name, CATEGORY.Buildings);
}
// Special handling for Mill buildings
else if (building is Mill)
{
// Check if the position matches the input
if (offsetX == 1 && offsetY == 1)
{
return (name + " input", CATEGORY.Buildings);
}
// Check if the position matches the output
else if (offsetX == 3 && offsetY == 1)
{
return (name + " output", CATEGORY.Buildings);
}
}
// Return the building name for any other position within the building's area
return (name, CATEGORY.Buildings);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a Farm.
/// </summary>
/// <param name="farm">The Farm to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetFarmInfo(Farm farm, int x, int y, bool lessInfo = false)
{
var mainMailboxPos = farm.GetMainMailboxPosition();
Building building = farm.getBuildingAt(new Vector2(x, y));
if (mainMailboxPos.X == x && mainMailboxPos.Y == y)
{
return ("Mail box", CATEGORY.Interactables);
}
else if (building is not null) // Check if there is a building at the current position
{
return GetBuildingInfo(building, x, y, lessInfo);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a FarmHouse.
/// </summary>
/// <param name="farmHouse">The FarmHouse to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetFarmHouseInfo(FarmHouse farmHouse, int x, int y, bool lessInfo = false)
{
if (farmHouse.upgradeLevel >= 1)
{
int kitchenX = farmHouse.getKitchenStandingSpot().X;
int kitchenY = farmHouse.getKitchenStandingSpot().Y - 1;
if (kitchenX == x && kitchenY == y)
{
return ("Stove", CATEGORY.Interactables);
}
else if (kitchenX + 1 == x && kitchenY == y)
{
return ("Sink", CATEGORY.Others);
}
else if (farmHouse.fridgePosition.X == x && farmHouse.fridgePosition.Y == y)
{
return ("Fridge", CATEGORY.Interactables);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a Forest.
/// </summary>
/// <param name="forest">The Forest to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetForestInfo(Forest forest, int x, int y, bool lessInfo = false)
{
if (forest.travelingMerchantDay && x == 27 && y == 11)
{
return ("Travelling Cart", CATEGORY.Interactables);
}
else if (forest.log != null && x == 2 && y == 7)
{
return ("Log", CATEGORY.Interactables);
}
else if (forest.log == null && x == 0 && y == 7)
{
return ("Secret Woods Entrance", CATEGORY.Doors);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandFarmHouse.
/// </summary>
/// <param name="islandFarmHouse">The IslandFarmHouse to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandFarmHouseInfo(IslandFarmHouse islandFarmHouse, int x, int y, bool lessInfo = false)
{
int fridgeX = islandFarmHouse.fridgePosition.X;
int fridgeY = islandFarmHouse.fridgePosition.Y;
if (fridgeX - 2 == x && fridgeY == y)
{
return ("Stove", CATEGORY.Interactables);
}
else if (fridgeX - 1 == x && fridgeY == y)
{
return ("Sink", CATEGORY.Others);
}
else if (fridgeX == x && fridgeY == y)
{
return ("Fridge", CATEGORY.Interactables);
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandNorth.
/// </summary>
/// <param name="islandNorth">The IslandNorth to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandNorthInfo(IslandNorth islandNorth, int x, int y, bool lessInfo = false)
{
// Check if the trader is activated and the coordinates match the trader's location
if (islandNorth.traderActivated.Value && x == 36 && y == 71)
{
return ("Island Trader", CATEGORY.Interactables);
}
// Return (null, null) if no relevant object is found
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandWest.
/// </summary>
/// <param name="islandWest">The IslandWest to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandWestInfo(IslandWest islandWest, int x, int y, bool lessInfo = false)
{
// Check if the coordinates match the shipping bin's location
if ((islandWest.shippingBinPosition.X == x || (islandWest.shippingBinPosition.X + 1) == x) && islandWest.shippingBinPosition.Y == y)
{
return ("Shipping Bin", CATEGORY.Interactables);
}
// Return (null, null) if no relevant object is found
return (null, null);
}
/// <summary>
/// Retrieves information about tiles at a given coordinate in a VolcanoDungeon.
/// </summary>
/// <param name="dungeon">The VolcanoDungeon to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name of the tile and the CATEGORY, or (null, null) if no relevant tile is found.</returns>
private static (string? name, CATEGORY? category) GetVolcanoDungeonInfo(VolcanoDungeon dungeon, int x, int y, bool lessInfo = false)
{
if (!lessInfo)
{
if (dungeon.IsCooledLava(x, y))
{
return ("Cooled lava", CATEGORY.WaterTiles);
}
else if (StardewValley.Monsters.LavaLurk.IsLavaTile(dungeon, x, y))
{
return ("Lava", CATEGORY.WaterTiles);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in a named IslandLocation.
/// </summary>
/// <param name="islandLocation">The named IslandLocation to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetNamedIslandLocationInfo(IslandLocation islandLocation, int x, int y, bool lessInfo = false)
{
object locationType = islandLocation is not null and IslandLocation ? islandLocation.Name ?? "Undefined Island Location" : islandLocation!.GetType();
// Implement specific logic for named IslandLocations here, if necessary
// Unimplemented locations are logged.
// Check if the location has already been logged
if (!loggedLocations.Contains(locationType))
{
// Log the message
MainClass.DebugLog($"Called GetNamedIslandLocationInfo with unimplemented IslandLocation of type {islandLocation.GetType()} and name {islandLocation.Name}");
// Add the location to the HashSet to prevent logging it again
loggedLocations.Add(locationType);
}
return (null, null);
}
/// <summary>
/// Retrieves the name of the IslandGemBird based on its item index value.
/// </summary>
/// <param name="bird">The IslandGemBird instance.</param>
/// <returns>A string representing the name of the IslandGemBird.</returns>
private static String GetGemBirdName(IslandGemBird bird)
{
// Use a switch expression to return the appropriate bird name based on the item index value
return bird.itemIndex.Value switch
{
60 => "Emerald Gem Bird",
62 => "Aquamarine Gem Bird",
64 => "Ruby Gem Bird",
66 => "Amethyst Gem Bird",
68 => "Topaz Gem Bird",
_ => "Gem Bird", // Default case for when the item index does not match any of the specified values
};
}
/// <summary>
/// Gets the parrot perch information at the specified tile coordinates in the given island location.
/// </summary>
/// <param name="x">The x-coordinate of the tile to check.</param>
/// <param name="y">The y-coordinate of the tile to check.</param>
/// <param name="islandLocation">The IslandLocation where the parrot perch might be found.</param>
/// <returns>A string containing the parrot perch information if a parrot perch is found at the specified tile; null if no parrot perch is found.</returns>
private static string? GetParrotPerchAtTile(IslandLocation islandLocation, int x, int y)
{
// Use LINQ to find the first parrot perch at the specified tile (x, y) coordinates
var foundPerch = islandLocation.parrotUpgradePerches.FirstOrDefault(perch => perch.tilePosition.Value.Equals(new Point(x, y)));
// If a parrot perch was found at the specified tile coordinates
if (foundPerch != null)
{
string toSpeak = $"Parrot required nuts {foundPerch.requiredNuts.Value}";
// Return appropriate string based on the current state of the parrot perch
return foundPerch.currentState.Value switch
{
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Idle => foundPerch.IsAvailable() ? toSpeak : "Empty parrot perch",
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.StartBuilding => "Parrots started building request",
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Building => "Parrots building request",
StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Complete => "Request Completed",
_ => toSpeak,
};
}
// If no parrot perch was found, return null
return null;
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandLocation.
/// </summary>
/// <param name="islandLocation">The IslandLocation to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetIslandLocationInfo(IslandLocation islandLocation, int x, int y, bool lessInfo = false)
{
var nutTracker = Game1.player.team.collectedNutTracker;
string? parrot = GetParrotPerchAtTile(islandLocation, x, y);
if (islandLocation.IsBuriedNutLocation(new Point(x, y)) && !nutTracker.ContainsKey($"Buried_{islandLocation.Name}_{x}_{y}"))
{
return ("Diggable spot", CATEGORY.Interactables);
}
else if (islandLocation.locationGemBird.Value is IslandGemBird bird && ((int)bird.position.X / Game1.tileSize) == x && ((int)bird.position.Y / Game1.tileSize) == y)
{
return (GetGemBirdName(bird), CATEGORY.NPCs);
}
else if (parrot != null)
{
return (parrot, CATEGORY.Buildings);
}
return islandLocation switch
{
IslandNorth islandNorth => GetIslandNorthInfo(islandNorth, x, y, lessInfo),
IslandWest islandWest => GetIslandWestInfo(islandWest, x, y, lessInfo),
VolcanoDungeon dungeon => GetVolcanoDungeonInfo(dungeon, x, y, lessInfo),
_ => GetNamedIslandLocationInfo(islandLocation, x, y, lessInfo)
};
}
/// <summary>
/// Retrieves the value of the "Action" property from the Buildings layer tile at the given coordinates.
/// </summary>
/// <param name="libraryMuseum">The LibraryMuseum containing the tile.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>The value of the "Action" property as a string, or null if the property is not found.</returns>
private static string? GetTileActionPropertyValue(LibraryMuseum libraryMuseum, int x, int y, bool lessInfo = false)
{
xTile.Tiles.Tile tile = libraryMuseum.map.GetLayer("Buildings").PickTile(new xTile.Dimensions.Location(x * 64, y * 64), Game1.viewport.Size);
return tile.Properties.TryGetValue("Action", out xTile.ObjectModel.PropertyValue? value) ? value.ToString() : null;
}
/// <summary>
/// Retrieves information about interactables, NPCs, or other features at a given coordinate in a LibraryMuseum.
/// </summary>
/// <param name="libraryMuseum">The LibraryMuseum to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetLibraryMuseumInfo(LibraryMuseum libraryMuseum, int x, int y, bool lessInfo = false)
{
if (libraryMuseum.museumPieces.TryGetValue(new Vector2(x, y), out int museumPiece))
{
string displayName = Game1.objectInformation[museumPiece].Split('/')[0];
return ($"{displayName} showcase", CATEGORY.Interactables);
}
int booksFound = Game1.netWorldState.Value.LostBooksFound.Value;
string? action = libraryMuseum.doesTileHaveProperty(x, y, "Action", "Buildings");
if (action != null && action.Contains("Notes"))
{
string? actionPropertyValue = GetTileActionPropertyValue(libraryMuseum, x, y, lessInfo);
if (actionPropertyValue != null)
{
int which = Convert.ToInt32(actionPropertyValue.Split(' ')[1]);
if (booksFound >= which)
{
string message = Game1.content.LoadString("Strings\\Notes:" + which);
return ($"{message.Split('\n')[0]} Book", CATEGORY.Interactables);
}
return ($"Lost Book", CATEGORY.Others);
}
}
return (null, null);
}
/// <summary>
/// Retrieves information about interactables or other features at a given coordinate in a Town.
/// </summary>
/// <param name="town">The Town to search.</param>
/// <param name="x">The x-coordinate to search.</param>
/// <param name="y">The y-coordinate to search.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found.</returns>
private static (string? name, CATEGORY? category) GetTownInfo(Town town, int x, int y, bool lessInfo = false)
{
if (SpecialOrder.IsSpecialOrdersBoardUnlocked() && x == 62 && y == 93)
{
return ("Special quest board", CATEGORY.Interactables);
}
return (null, null);
}
/// <summary>
/// Gets the feeding bench information for barns and coops.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x coordinate of the tile.</param>
/// <param name="y">The y coordinate of the tile.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple of (string? name, CATEGORY? category) for the feeding bench, or null if not applicable.</returns>
private static (string? name, CATEGORY? category)? GetFeedingBenchInfo(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
string locationName = currentLocation.Name;
if (FeedingBenchBounds.TryGetValue(locationName, out var bounds) && x >= bounds.minX && x <= bounds.maxX && y == bounds.y)
{
(string? name, CATEGORY category) = TileInfo.getObjectAtTile(currentLocation, x, y, true);
return (name?.Contains("hay", StringComparison.OrdinalIgnoreCase) == true ? "Feeding Bench" : "Empty Feeding Bench", category);
}
return null;
}
/// <summary>
/// Gets information about the current location by its name.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x coordinate of the tile.</param>
/// <param name="y">The y coordinate of the tile.</param>
/// <param name="lessInfo">Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false.</param>
/// <returns>A tuple of (string? name, CATEGORY? category) for the object in the location, or null if not applicable.</returns>
private static (string? name, CATEGORY? category) GetLocationByNameInfo(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
object locationType = currentLocation is not null and GameLocation ? currentLocation.Name ?? "Undefined GameLocation" : currentLocation!.GetType(); string locationName = currentLocation.Name ?? "";
if (locationName.Contains("coop", StringComparison.OrdinalIgnoreCase) || locationName.Contains("barn", StringComparison.OrdinalIgnoreCase))
{
var feedingBenchInfo = GetFeedingBenchInfo(currentLocation, x, y);
if (feedingBenchInfo.HasValue)
{
return feedingBenchInfo.Value;
} // else if something other than feeding benches in barns and coops...
} //else if something other than barns and coops...
// Unimplemented locations are logged.
// Check if the location has already been logged
if (!loggedLocations.Contains(locationType))
{
// Log the message
MainClass.DebugLog($"Called GetLocationByNameInfo with unimplemented GameLocation of type {currentLocation.GetType()} and name {currentLocation.Name}");
// Add the location to the HashSet to prevent logging it again
loggedLocations.Add(locationType);
}
return (null, null);
}
/// <summary>
/// Retrieves the dynamic tile information for the given coordinates in the specified location.
/// </summary>
/// <param name="currentLocation">The current GameLocation instance.</param>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="lessInfo">An optional boolean to return less detailed information. Defaults to false.</param>
/// <returns>A tuple containing the name and CATEGORY of the dynamic tile, or null values if not found.</returns>
public static (string? name, CATEGORY? category) GetDynamicTileAt(GameLocation currentLocation, int x, int y, bool lessInfo = false)
{
// Check for panning spots
if (currentLocation.orePanPoint.Value != Point.Zero && currentLocation.orePanPoint.Value == new Point(x, y))
{
return ("panning spot", CATEGORY.Interactables);
}
// Check if the current location has an event
else if (currentLocation.currentEvent is not null)
{
string eventName = currentLocation.currentEvent.FestivalName;
// Attempt to retrieve the nested dictionary for the event name from the EventInteractables dictionary
if (EventInteractables.TryGetValue(eventName, out var coordinateDictionary))
{
// Attempt to retrieve the interactable value from the nested dictionary using the coordinates (x, y) as the key
if (coordinateDictionary.TryGetValue((x, y), value: out var interactable))
{
// If the interactable value is found, return the corresponding category and interactable name
return (interactable, CATEGORY.Interactables);
}
}
}
// Retrieve dynamic tile information based on the current location type
return currentLocation switch
{
Beach beach => GetBeachInfo(beach, x, y, lessInfo),
BoatTunnel boatTunnel => GetBoatTunnelInfo(boatTunnel, x, y, lessInfo),
CommunityCenter communityCenter => GetCommunityCenterInfo(communityCenter, x, y, lessInfo),
Farm farm => GetFarmInfo(farm, x, y, lessInfo),
FarmHouse farmHouse => GetFarmHouseInfo(farmHouse, x, y, lessInfo),
Forest forest => GetForestInfo(forest, x, y, lessInfo),
IslandFarmHouse islandFarmHouse => GetIslandFarmHouseInfo(islandFarmHouse, x, y, lessInfo),
IslandLocation islandLocation => GetIslandLocationInfo(islandLocation, x, y, lessInfo),
LibraryMuseum libraryMuseum => GetLibraryMuseumInfo(libraryMuseum, x, y, lessInfo),
Town town => GetTownInfo(town, x, y, lessInfo),
_ => GetLocationByNameInfo(currentLocation, x, y, lessInfo)
};
}
}
}

View File

@ -10,21 +10,23 @@ namespace stardew_access.Features
internal static int prevSlotIndex = -999; internal static int prevSlotIndex = -999;
internal static bool narrateHoveredSlot(InventoryMenu inventoryMenu, List<ClickableComponent> inventory, IList<Item> actualInventory, int x, int y, internal static bool narrateHoveredSlot(InventoryMenu inventoryMenu, List<ClickableComponent> inventory, IList<Item> actualInventory, int x, int y,
bool giveExtraDetails = false, int hoverPrice = -1, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, bool? giveExtraDetails = null, int hoverPrice = -1, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1,
bool handleHighlightedItem = false, String highlightedItemPrefix = "", String highlightedItemSuffix = "") bool handleHighlightedItem = false, String highlightedItemPrefix = "", String highlightedItemSuffix = "")
{ {
if (narrateHoveredSlotAndReturnIndex(inventoryMenu, inventory, actualInventory, x, y, if (narrateHoveredSlotAndReturnIndex(inventoryMenu, inventory, actualInventory, x, y,
giveExtraDetails = false, hoverPrice = -1, extraItemToShowIndex = -1, extraItemToShowAmount = -1, giveExtraDetails, hoverPrice, extraItemToShowIndex, extraItemToShowAmount,
handleHighlightedItem = false, highlightedItemPrefix = "", highlightedItemSuffix = "") == -999) handleHighlightedItem, highlightedItemPrefix, highlightedItemSuffix) == -999)
return false; return false;
return true; return true;
} }
internal static int narrateHoveredSlotAndReturnIndex(InventoryMenu inventoryMenu, List<ClickableComponent> inventory, IList<Item> actualInventory, int x, int y, internal static int narrateHoveredSlotAndReturnIndex(InventoryMenu inventoryMenu, List<ClickableComponent> inventory, IList<Item> actualInventory, int x, int y,
bool giveExtraDetails = false, int hoverPrice = -1, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1, bool? giveExtraDetails = null, int hoverPrice = -1, int extraItemToShowIndex = -1, int extraItemToShowAmount = -1,
bool handleHighlightedItem = false, String highlightedItemPrefix = "", String highlightedItemSuffix = "") bool handleHighlightedItem = false, String highlightedItemPrefix = "", String highlightedItemSuffix = "")
{ {
if (giveExtraDetails is null)
giveExtraDetails = !MainClass.Config.DisableInventoryVerbosity;
for (int i = 0; i < inventory.Count; i++) for (int i = 0; i < inventory.Count; i++)
{ {
if (!inventory[i].containsPoint(x, y)) continue; if (!inventory[i].containsPoint(x, y)) continue;
@ -45,27 +47,31 @@ namespace stardew_access.Features
string name = $"{namePrefix}{actualInventory[i].DisplayName}{nameSuffix}"; string name = $"{namePrefix}{actualInventory[i].DisplayName}{nameSuffix}";
int stack = actualInventory[i].Stack; int stack = actualInventory[i].Stack;
string quality = getQualityFromItem(actualInventory[i]); string quality = getQualityFromItem(actualInventory[i]);
string healthNStamine = getHealthNStaminaFromItem(actualInventory[i]); string healthNStamina = getHealthNStaminaFromItem(actualInventory[i]);
string buffs = getBuffsFromItem(actualInventory[i]); string buffs = getBuffsFromItem(actualInventory[i]);
string description = actualInventory[i].getDescription(); string description = actualInventory[i].getDescription();
string price = getPrice(hoverPrice); string price = getPrice(hoverPrice);
string requirements = getExtraItemInfo(extraItemToShowIndex, extraItemToShowAmount); string requirements = getExtraItemInfo(extraItemToShowIndex, extraItemToShowAmount);
if (giveExtraDetails) string details;
if (giveExtraDetails == true)
{ {
if (stack > 1) if (stack > 1)
toSpeak = $"{stack} {name} {quality}, \n{requirements}, \n{price}, \n{description}, \n{healthNStamine}, \n{buffs}"; toSpeak = $"{stack} {name}"; // {quality}, \n{requirements}, \n{price}, \n{description}, \n{healthNStamina}, \n{buffs}";
else else
toSpeak = $"{name} {quality}, \n{requirements}, \n{price}, \n{description}, \n{healthNStamine}, \n{buffs}"; toSpeak = $"{name}"; //{quality}, \n{requirements}, \n{price}, \n{description}, \n{healthNStamina}, \n{buffs}";
details = string.Join(",\n", new string[] { quality, requirements, price, description, healthNStamina, buffs }.Where(c => !string.IsNullOrEmpty(c)));
} }
else else
{ {
if (stack > 1) if (stack > 1)
toSpeak = $"{stack} {name} {quality}, \n{requirements}, \n{price}"; toSpeak = $"{stack} {name}"; //{quality}, \n{requirements}, \n{price}";
else else
toSpeak = $"{name} {quality}, \n{requirements}, \n{price}"; toSpeak = $"{name}"; //{quality}, \n{requirements}, \n{price}";
details = string.Join(",\n", new string[] { quality, requirements, price }.Where(c => !string.IsNullOrEmpty(c)));
} }
if (!string.IsNullOrEmpty(details))
toSpeak = $"{toSpeak}, {details}";
checkAndSpeak(toSpeak, i); checkAndSpeak(toSpeak, i);
prevSlotIndex = i; prevSlotIndex = i;

View File

@ -1,3 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using StardewValley; using StardewValley;
using StardewValley.Objects; using StardewValley.Objects;
@ -6,9 +8,9 @@ namespace stardew_access.Features
{ {
public class Radar public class Radar
{ {
private List<Vector2> closed; private readonly List<Vector2> closed;
private List<Furniture> furnitures; private readonly List<Furniture> furnitures;
private List<NPC> npcs; private readonly List<NPC> npcs;
public List<string> exclusions; public List<string> exclusions;
private List<string> temp_exclusions; private List<string> temp_exclusions;
public List<string> focus; public List<string> focus;
@ -91,10 +93,11 @@ namespace stardew_access.Features
/// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns> /// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns>
public Dictionary<Vector2, (string, string)> SearchNearbyTiles(Vector2 center, int limit, bool playSound = true) public Dictionary<Vector2, (string, string)> SearchNearbyTiles(Vector2 center, int limit, bool playSound = true)
{ {
Dictionary<Vector2, (string, string)> detectedTiles = new Dictionary<Vector2, (string, string)>(); var currentLocation = Game1.currentLocation;
Dictionary<Vector2, (string, string)> detectedTiles = new();
Queue<Vector2> toSearch = new Queue<Vector2>(); Queue<Vector2> toSearch = new();
List<Vector2> searched = new List<Vector2>(); HashSet<Vector2> searched = new();
int[] dirX = { -1, 0, 1, 0 }; int[] dirX = { -1, 0, 1, 0 };
int[] dirY = { 0, 1, 0, -1 }; int[] dirY = { 0, 1, 0, -1 };
@ -105,10 +108,10 @@ namespace stardew_access.Features
{ {
Vector2 item = toSearch.Dequeue(); Vector2 item = toSearch.Dequeue();
if (playSound) if (playSound)
CheckTileAndPlaySound(item); CheckTileAndPlaySound(item, currentLocation);
else else
{ {
(bool, string?, string) tileInfo = CheckTile(item); (bool, string?, string) tileInfo = CheckTile(item, currentLocation);
if (tileInfo.Item1 && tileInfo.Item2 != null) if (tileInfo.Item1 && tileInfo.Item2 != null)
{ {
// Add detected tile to the dictionary // Add detected tile to the dictionary
@ -118,7 +121,7 @@ namespace stardew_access.Features
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
Vector2 dir = new Vector2(item.X + dirX[i], item.Y + dirY[i]); Vector2 dir = new(item.X + dirX[i], item.Y + dirY[i]);
if (isValid(dir, center, searched, limit)) if (isValid(dir, center, searched, limit))
{ {
@ -128,6 +131,7 @@ namespace stardew_access.Features
} }
} }
searched.Clear();
return detectedTiles; return detectedTiles;
} }
@ -137,12 +141,15 @@ namespace stardew_access.Features
/// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns> /// <returns>A dictionary with all the detected tiles along with the name of the object on it and it's category.</returns>
public Dictionary<Vector2, (string, string)> SearchLocation() public Dictionary<Vector2, (string, string)> SearchLocation()
{ {
Dictionary<Vector2, (string, string)> detectedTiles = new Dictionary<Vector2, (string, string)>(); //var watch = new Stopwatch();
//watch.Start();
var currentLocation = Game1.currentLocation;
Dictionary<Vector2, (string, string)> detectedTiles = new();
Vector2 position = Vector2.Zero; Vector2 position = Vector2.Zero;
(bool, string? name, string category) tileInfo; (bool, string? name, string category) tileInfo;
Queue<Vector2> toSearch = new Queue<Vector2>(); Queue<Vector2> toSearch = new();
List<Vector2> searched = new List<Vector2>(); HashSet<Vector2> searched = new();
int[] dirX = { -1, 0, 1, 0 }; int[] dirX = { -1, 0, 1, 0 };
int[] dirY = { 0, 1, 0, -1 }; int[] dirY = { 0, 1, 0, -1 };
int count = 0; int count = 0;
@ -150,10 +157,15 @@ namespace stardew_access.Features
toSearch.Enqueue(Game1.player.getTileLocation()); toSearch.Enqueue(Game1.player.getTileLocation());
searched.Add(Game1.player.getTileLocation()); searched.Add(Game1.player.getTileLocation());
//watch.Stop();
//var elapsedMs = watch.ElapsedMilliseconds;
//MainClass.DebugLog($"Search init duration: {elapsedMs}");
//watch.Reset();
//watch.Start();
while (toSearch.Count > 0) while (toSearch.Count > 0)
{ {
Vector2 item = toSearch.Dequeue(); Vector2 item = toSearch.Dequeue();
tileInfo = CheckTile(item, true); tileInfo = CheckTile(item, currentLocation, true);
if (tileInfo.Item1 && tileInfo.name != null) if (tileInfo.Item1 && tileInfo.name != null)
{ {
// Add detected tile to the dictionary // Add detected tile to the dictionary
@ -164,16 +176,19 @@ namespace stardew_access.Features
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
{ {
Vector2 dir = new Vector2(item.X + dirX[i], item.Y + dirY[i]); Vector2 dir = new(item.X + dirX[i], item.Y + dirY[i]);
if (!searched.Contains(dir) && (TileInfo.isWarpPointAtTile((int)dir.X, (int)dir.Y) || Game1.currentLocation.isTileOnMap(dir))) if (!searched.Contains(dir) && (TileInfo.isWarpPointAtTile(currentLocation, (int)dir.X, (int)dir.Y) || currentLocation.isTileOnMap(dir)))
{ {
toSearch.Enqueue(dir); toSearch.Enqueue(dir);
searched.Add(dir); searched.Add(dir);
} }
} }
} }
//watch.Stop();
//elapsedMs = watch.ElapsedMilliseconds;
//MainClass.DebugLog($"Search loop duration: {elapsedMs}; {count} iterations.");
searched.Clear();
return detectedTiles; return detectedTiles;
} }
@ -185,7 +200,7 @@ namespace stardew_access.Features
/// <param name="searched">The list of searched items.</param> /// <param name="searched">The list of searched items.</param>
/// <param name="limit">The radius of search</param> /// <param name="limit">The radius of search</param>
/// <returns>Returns true if the tile is valid for search.</returns> /// <returns>Returns true if the tile is valid for search.</returns>
public bool isValid(Vector2 item, Vector2 center, List<Vector2> searched, int limit) public bool isValid(Vector2 item, Vector2 center, HashSet<Vector2> searched, int limit)
{ {
if (Math.Abs(item.X - center.X) > limit) if (Math.Abs(item.X - center.X) > limit)
return false; return false;
@ -198,29 +213,28 @@ namespace stardew_access.Features
return true; return true;
} }
public (bool, string? name, string category) CheckTile(Vector2 position, bool lessInfo = false) public (bool, string? name, string category) CheckTile(Vector2 position, GameLocation currentLocation, bool lessInfo = false)
{ {
(string? name, CATEGORY? category) tileDetail = TileInfo.getNameWithCategoryAtTile(position, lessInfo); (string? name, CATEGORY? category) = TileInfo.getNameWithCategoryAtTile(position, currentLocation, lessInfo);
if (tileDetail.name == null) if (name == null)
return (false, null, CATEGORY.Others.ToString()); return (false, null, CATEGORY.Others.ToString());
if (tileDetail.category == null) category ??= CATEGORY.Others;
tileDetail.category = CATEGORY.Others;
return (true, tileDetail.name, tileDetail.category.ToString()); return (true, name, category.ToString());
} }
public void CheckTileAndPlaySound(Vector2 position) public void CheckTileAndPlaySound(Vector2 position, GameLocation currentLocation)
{ {
try try
{ {
if (Game1.currentLocation.isObjectAtTile((int)position.X, (int)position.Y)) if (currentLocation.isObjectAtTile((int)position.X, (int)position.Y))
{ {
(string? name, CATEGORY category) objDetails = TileInfo.getObjectAtTile((int)position.X, (int)position.Y); (string? name, CATEGORY category) objDetails = TileInfo.getObjectAtTile(currentLocation, (int)position.X, (int)position.Y);
string? objectName = objDetails.name; string? objectName = objDetails.name;
CATEGORY category = objDetails.category; CATEGORY category = objDetails.category;
StardewValley.Object obj = Game1.currentLocation.getObjectAtTile((int)position.X, (int)position.Y); StardewValley.Object obj = currentLocation.getObjectAtTile((int)position.X, (int)position.Y);
if (objectName != null) if (objectName != null)
{ {
@ -231,23 +245,22 @@ namespace stardew_access.Features
if (!furnitures.Contains((Furniture)obj)) if (!furnitures.Contains((Furniture)obj))
{ {
furnitures.Add((Furniture)obj); furnitures.Add((Furniture)obj);
PlaySoundAt(position, objectName, category); PlaySoundAt(position, objectName, category, currentLocation);
} }
} }
else else
PlaySoundAt(position, objectName, category); PlaySoundAt(position, objectName, category, currentLocation);
} }
} }
else else
{ {
(string? name, CATEGORY? category) tileDetail = TileInfo.getNameWithCategoryAtTile(position); (string? name, CATEGORY? category) = TileInfo.getNameWithCategoryAtTile(position, currentLocation);
if (tileDetail.name != null) if (name != null)
{ {
if (tileDetail.category == null) category ??= CATEGORY.Others;
tileDetail.category = CATEGORY.Others;
PlaySoundAt(position, tileDetail.name, tileDetail.category); PlaySoundAt(position, name, category, currentLocation);
} }
} }
} }
@ -257,7 +270,7 @@ namespace stardew_access.Features
} }
} }
public void PlaySoundAt(Vector2 position, string searchQuery, CATEGORY category) public void PlaySoundAt(Vector2 position, string searchQuery, CATEGORY category, GameLocation currentLocation)
{ {
#region Check whether to skip the object or not #region Check whether to skip the object or not
@ -318,19 +331,19 @@ namespace stardew_access.Features
if (dy < 0 && (Math.Abs(dy) >= Math.Abs(dx))) // Object is at top if (dy < 0 && (Math.Abs(dy) >= Math.Abs(dx))) // Object is at top
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "top"), position); currentLocation.localSoundAt(GetSoundName(category, "top"), position);
} }
else if (dx > 0 && (Math.Abs(dx) >= Math.Abs(dy))) // Object is at right else if (dx > 0 && (Math.Abs(dx) >= Math.Abs(dy))) // Object is at right
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "right"), position); currentLocation.localSoundAt(GetSoundName(category, "right"), position);
} }
else if (dx < 0 && (Math.Abs(dx) > Math.Abs(dy))) // Object is at left else if (dx < 0 && (Math.Abs(dx) > Math.Abs(dy))) // Object is at left
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "left"), position); currentLocation.localSoundAt(GetSoundName(category, "left"), position);
} }
else if (dy > 0 && (Math.Abs(dy) > Math.Abs(dx))) // Object is at bottom else if (dy > 0 && (Math.Abs(dy) > Math.Abs(dx))) // Object is at bottom
{ {
Game1.currentLocation.localSoundAt(GetSoundName(category, "bottom"), position); currentLocation.localSoundAt(GetSoundName(category, "bottom"), position);
} }
} }

View File

@ -89,9 +89,10 @@ namespace stardew_access.Features
MainClass.ScreenReader.PrevTextTile = " "; MainClass.ScreenReader.PrevTextTile = " ";
} }
bool isColliding = TileInfo.isCollidingAtTile(x, y); var currentLocation = Game1.currentLocation;
bool isColliding = TileInfo.IsCollidingAtTile(currentLocation, x, y);
(string? name, string? category) info = TileInfo.getNameWithCategoryNameAtTile(tile); (string? name, string? category) info = TileInfo.getNameWithCategoryNameAtTile(tile, currentLocation);
#region Narrate toSpeak #region Narrate toSpeak
if (info.name != null) if (info.name != null)

View File

@ -1,206 +1,515 @@
using Newtonsoft.Json.Linq; using System.IO;
using System.Text.Json;
using System.Linq;
using System.Collections.Generic;
using StardewValley; using StardewValley;
using static stardew_access.Features.Utils;
namespace stardew_access.Features namespace stardew_access.Features
{ {
public class StaticTiles public class StaticTiles
{ {
private JObject? staticTilesData = null; // Static instance for the singleton pattern
private JObject? customTilesData = null; private static StaticTiles? _instance;
public StaticTiles() /// <summary>
/// The singleton instance of the <see cref="StaticTiles"/> class.
/// </summary>
public static StaticTiles Instance
{ {
if (MainClass.ModHelper == null) get
return;
try
{ {
using (StreamReader file = new StreamReader(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "static-tiles.json"))) _instance ??= new StaticTiles();
{ return _instance;
string json = file.ReadToEnd();
staticTilesData = JObject.Parse(json);
}
MainClass.InfoLog($"Loaded static-tile.json");
}
catch (System.Exception)
{
MainClass.ErrorLog($"static-tiles.json file not found or an error occured while initializing static-tiles.json\nThe path of the file should be:\n\t{Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "static-tiles.json")}");
}
try
{
using (StreamReader file = new StreamReader(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "custom-tiles.json")))
{
string json = file.ReadToEnd();
customTilesData = JObject.Parse(json);
}
MainClass.InfoLog($"Loaded custom-tile.json");
}
catch (System.Exception)
{
MainClass.InfoLog($"custom-tiles.json file not found or an error occured while initializing custom-tiles.json\nThe path of the file should be:\n\t{Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "custom-tiles.json")}");
} }
} }
public bool isAvailable(string locationName) /// <summary>
/// A nullable JsonElement containing static tile data.
/// </summary>
private static JsonElement? staticTilesData;
/// <summary>
/// A nullable JsonElement containing custom tile data.
/// </summary>
private static JsonElement? customTilesData;
/// <summary>
/// A dictionary that maps location names to tile data dictionaries for static tiles.
/// Each tile data dictionary maps tile coordinates (x, y) to a tuple containing the object name and category.
/// </summary>
private static Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> staticTilesDataDict = new();
/// <summary>
/// A dictionary that maps location names to tile data dictionaries for custom tiles.
/// Each tile data dictionary maps tile coordinates (x, y) to a tuple containing the object name and category.
/// </summary>
private static Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> customTilesDataDict = new();
/// <summary>
/// The file name of the JSON file containing static tile data.
/// </summary>
private const string StaticTilesFileName = "static-tiles.json";
/// <summary>
/// The file name of the JSON file containing custom tile data.
/// </summary>
private const string CustomTilesFileName = "custom-tiles.json";
/// <summary>
/// A dictionary that contains conditional lambda functions for checking specific game conditions.
/// Each lambda function takes two arguments: a conditionType (string) and a uniqueModId (string) and returns a boolean value.
/// </summary>
/// <remarks>
/// The following lambda functions are currently supported:
/// <list type="bullet">
/// <item>
/// <description>"Farm": Checks if the current in-game farm type matches the given farm type (conditionType).</description>
/// </item>
/// <item>
/// <description>"JojaMember": Checks if the player has the "JojaMember" mail. The input arguments are ignored.</description>
/// </item>
/// </list>
/// Additional lambda functions can be added as needed.
/// </remarks>
private static readonly Dictionary<string, Func<string, string, bool>> conditionals = new()
{ {
List<JObject> allData = new List<JObject>(); ["Farm"] = (conditionType, uniqueModId) =>
if (customTilesData != null) allData.Add(customTilesData);
if (staticTilesData != null) allData.Add(staticTilesData);
foreach (JObject data in allData)
{ {
foreach (KeyValuePair<string, JToken?> location in data) if (string.IsNullOrEmpty(uniqueModId))
{ {
if (location.Key.Contains("||") && MainClass.ModHelper != null) // Branch for vanilla locations
// Calculate farmTypeIndex using the switch expression
int farmTypeIndex = conditionType.ToLower() switch
{ {
string uniqueModID = location.Key.Substring(location.Key.LastIndexOf("||") + 2); "default" => 0,
string locationNameInJson = location.Key.Remove(location.Key.LastIndexOf("||")); "riverlands" => 1,
bool isLoaded = MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModID); "forest" => 2,
"mountains" => 3,
"combat" => 4,
"fourcorners" => 5,
"beach" => 6,
_ => 7,
};
if (!isLoaded) continue; // Skip if the specified mod is not loaded // Return true if the farmTypeIndex matches the current in-game farm type, otherwise false
if (locationName.ToLower().Equals(locationNameInJson.ToLower())) return true; return farmTypeIndex == Game1.whichFarm;
}
else
{
// Branch for mod locations
// Log an error message and return false, as mod locations are not yet supported for the Farm conditional
MainClass.ErrorLog("Mod locations are not yet supported for the Farm conditional.");
return false;
}
},
["JojaMember"] = (conditionType, uniqueModId) =>
{
// Return true if the player has the "JojaMember" mail, otherwise false
return Game1.MasterPlayer.mailReceived.Contains("JojaMember");
}
};
/// <summary>
/// Initializes a new instance of the <see cref="StaticTiles"/> class.
/// Loads the tile files and sets up the tile dictionaries.
/// </summary>
private StaticTiles()
{
LoadTilesFiles();
SetupTilesDicts();
}
/// <summary>
/// Loads the static and custom tile files.
/// </summary>
public static void LoadTilesFiles()
{
if (MainClass.ModHelper is null) return;
staticTilesData = LoadJsonFile(StaticTilesFileName);
customTilesData = LoadJsonFile(CustomTilesFileName);
}
/// <summary>
/// Adds a conditional lambda function to the conditionals dictionary at runtime.
/// </summary>
/// <param name="conditionName">The name of the condition to be added.</param>
/// <param name="conditionLambda">The lambda function to be added. It should accept two strings (conditionName and uniqueModID) and return a bool.</param>
/// <returns>Returns true if the lambda was added successfully, and false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown if the conditionName or conditionLambda is null or empty.</exception>
public static bool AddConditionalLambda(string conditionName, Func<string, string, bool> conditionLambda)
{
// Check if the conditionName is not null or empty
if (string.IsNullOrEmpty(conditionName))
{
throw new ArgumentException("Condition name cannot be null or empty.", nameof(conditionName));
}
// Check if the conditionLambda is not null
if (conditionLambda == null)
{
throw new ArgumentException("Condition lambda cannot be null.", nameof(conditionLambda));
}
// Check if the conditionName already exists in the dictionary
if (conditionals.ContainsKey(conditionName))
{
MainClass.ErrorLog($"A conditional with the name '{conditionName}' already exists.");
return false;
}
// Add the lambda to the dictionary
conditionals.Add(conditionName, conditionLambda);
return true;
}
/// <summary>
/// Creates a location tile dictionary based on the given JSON dictionary.
/// </summary>
/// <param name="jsonDict">The JSON dictionary containing location tile data.</param>
/// <returns>A dictionary mapping tile coordinates to tile names and categories.</returns>
public static Dictionary<(short x, short y), (string name, CATEGORY category)> CreateLocationTileDict(JsonElement locationJson)
{
var jsonDict = locationJson.EnumerateObject().ToDictionary(p => p.Name, p => p.Value);
var locationData = new Dictionary<(short x, short y), (string name, CATEGORY category)>(jsonDict.Count);
// Iterate over the JSON dictionary
foreach (var item in jsonDict)
{
var name = item.Key;
// Error handling: Check if "x" and "y" properties exist in the JSON object
if (!item.Value.TryGetProperty("x", out var xElement) || !item.Value.TryGetProperty("y", out var yElement))
{
MainClass.ErrorLog($"Missing x or y property for {name}");
continue;
}
var xValues = xElement.EnumerateArray().Select(x => x.GetInt16()).ToArray();
var yValues = yElement.EnumerateArray().Select(y => y.GetInt16()).ToArray();
// Error handling: Ensure that x and y arrays are not empty
if (xValues.Length == 0 || yValues.Length == 0)
{
MainClass.ErrorLog($"Empty x or y array for {name}");
continue;
}
// Get the "type" property if it exists, otherwise use the default value "Others"
var type = item.Value.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : "Others";
// Obtain the category instance
var category = CATEGORY.FromString(type!);
// Iterate over y and x values, adding entries to the locationData dictionary
for (int j = 0; j < yValues.Length; j++)
{
var y = yValues[j];
for (int i = 0; i < xValues.Length; i++)
{
var x = xValues[i];
locationData.TryAdd((x, y), (name, category));
} }
else if (locationName.ToLower().Equals(location.Key.ToLower()))
return true;
} }
} }
return false; return locationData;
} }
public string? getStaticTileInfoAt(int x, int y) /// <summary>
/// Represents the different categories of locations.
/// </summary>
public enum LocationCategory
{ {
return getStaticTileInfoAtWithCategory(x, y).name; /// <summary>
/// Represents mod locations with conditional requirements.
/// </summary>
ModConditional,
/// <summary>
/// Represents mod locations without conditional requirements.
/// </summary>
Mod,
/// <summary>
/// Represents vanilla locations with conditional requirements.
/// </summary>
VanillaConditional,
/// <summary>
/// Represents vanilla locations without conditional requirements.
/// </summary>
Vanilla
} }
public (string? name, CATEGORY category) getStaticTileInfoAtWithCategory(int x, int y) { /// <summary>
List<JObject> allData = new List<JObject>(); /// Determines the location category based on the given location name.
/// </summary>
if (customTilesData != null) allData.Add(customTilesData); /// <param name="name">The location name.</param>
if (staticTilesData != null) allData.Add(staticTilesData); /// <returns>The location category.</returns>
public static LocationCategory GetLocationCategory(string name)
foreach (JObject data in allData) {
foreach (KeyValuePair<string, JToken?> location in data)
{
string locationName = location.Key;
if (locationName.Contains("||") && MainClass.ModHelper != null)
{
// Mod Specific Tiles
// We can add tiles that only get detected when the specified mod is loaded.
// Syntax: <location name>||<Mod's unique id, look into the mod's manifest.json for unique id>
// Example: THe following tile will only be detected if Stardew Valley Expanded mod is installed
// {
// .
// .
// .
// "Town||FlashShifter.StardewValleyExpandedCP":{
// "<Tile Name>":{
// "x": [<x location(s)>],
// "y": [<y location(s)>],
// "type": "<Category name>"
// }
// },
// .
// .
// .
// }
string uniqueModID = locationName.Substring(locationName.LastIndexOf("||") + 2);
locationName = locationName.Remove(locationName.LastIndexOf("||"));
bool isLoaded = MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModID);
if (!isLoaded) continue; // Skip if the specified mod is not loaded
}
if (locationName.Contains("_") && locationName.ToLower().StartsWith("farm_"))
{
string farmType = locationName.Substring(locationName.LastIndexOf("_") + 1);
int farmTypeIndex = getFarmTypeIndex(farmType);
locationName = locationName.Remove(locationName.LastIndexOf("_"));
if (farmTypeIndex != Game1.whichFarm) continue; // Skip if current farm type does not matches
// if (Game1.whichModFarm != null) MainClass.DebugLog($"{farmType} {Game1.whichModFarm.MapName}");
if (farmTypeIndex != 7 || Game1.whichModFarm == null || !farmType.ToLower().Equals(Game1.whichModFarm.MapName.ToLower())) continue; // Not tested but should work
}
if (locationName.ToLower().Equals("town_joja") && Game1.MasterPlayer.mailReceived.Contains("JojaMember"))
{
locationName = "town";
}
if (!Game1.currentLocation.Name.ToLower().Equals(locationName.ToLower())) continue;
if (location.Value == null) continue;
foreach (var tile in ((JObject)location.Value))
{
if (tile.Value == null) continue;
JToken? tileXArray = tile.Value["x"];
JToken? tileYArray = tile.Value["y"];
JToken? tileType = tile.Value["type"];
if (tileXArray == null || tileYArray == null || tileType == null)
continue;
bool isXPresent = false;
bool isYPresent = false;
foreach (var item in tileXArray)
{
if (short.Parse(item.ToString()) != x)
continue;
isXPresent = true;
break;
}
foreach (var item in tileYArray)
{
if (short.Parse(item.ToString()) != y)
continue;
isYPresent = true;
break;
}
if (isXPresent && isYPresent)
{
string key = tile.Key;
if (key.Contains('[') && key.Contains(']'))
{
int i1 = key.IndexOf('[');
int i2 = key.LastIndexOf(']');
if (i1 < i2)
{
key = key.Remove(i1, ++i2 - i1);
}
}
return (key.Trim(), CATEGORY.FromString(tileType.ToString().ToLower()));
}
}
}
}
return (null, CATEGORY.Others);
}
private int getFarmTypeIndex(string farmType)
{ {
return farmType.ToLower() switch bool hasDoubleUnderscore = name.Contains("__");
bool hasDoubleVerticalBar = name.Contains("||");
if (hasDoubleUnderscore && hasDoubleVerticalBar)
return LocationCategory.ModConditional;
if (hasDoubleVerticalBar)
return LocationCategory.Mod;
if (hasDoubleUnderscore)
return LocationCategory.VanillaConditional;
return LocationCategory.Vanilla;
}
/// <summary>
/// Sorts location data from a JsonElement into four dictionaries based on their type (mod conditional, mod, vanilla conditional, or vanilla).
/// </summary>
/// <param name="json">A JsonElement containing location data.</param>
/// <returns>
/// A tuple containing four dictionaries:
/// - modConditionalLocations: A dictionary of mod locations with conditionals.
/// - modLocations: A dictionary of mod locations without conditionals.
/// - vanillaConditionalLocations: A dictionary of vanilla locations with conditionals.
/// - vanillaLocations: A dictionary of vanilla locations without conditionals.
/// Each dictionary maps a location name to another dictionary, which maps tile coordinates (x, y) to a tuple containing the object name and category.
/// </returns>
/// <remarks>
/// This function iterates over the properties of the input JsonElement and categorizes each location based on the naming conventions.
/// If a location has a conditional, the function checks if the condition is met before adding it to the respective dictionary.
/// If a mod location is specified, the function checks if the mod is loaded before adding it to the respective dictionary.
/// </remarks>
public static (
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> modConditionalLocations,
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> modLocations,
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> vanillaConditionalLocations,
Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> vanillaLocations
) SortLocationsByType(JsonElement json)
{
var modConditionalLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var modLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var vanillaConditionalLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var vanillaLocations = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
var categoryDicts = new Dictionary<LocationCategory, Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>>
{ {
"default" => 0, { LocationCategory.ModConditional, modConditionalLocations },
"riverlands" => 1, { LocationCategory.Mod, modLocations },
"forest" => 2, { LocationCategory.VanillaConditional, vanillaConditionalLocations },
"mountains" => 3, { LocationCategory.Vanilla, vanillaLocations }
"combat" => 4,
"fourcorners" => 5,
"beach" => 6,
_ => 7,
}; };
foreach (var property in json.EnumerateObject())
{
if (property.Value.ValueKind != JsonValueKind.Object)
{
MainClass.ErrorLog($"Invalid value type for {property.Name}");
continue;
}
string propertyName = property.Name;
string uniqueModId = "";
var splitModId = propertyName.Split("||", StringSplitOptions.RemoveEmptyEntries);
if (splitModId.Length == 2)
{
propertyName = splitModId[0];
uniqueModId = splitModId[1];
if (MainClass.ModHelper == null || !MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModId))
{
continue;
}
}
var category = GetLocationCategory(propertyName);
if (category == LocationCategory.VanillaConditional || category == LocationCategory.ModConditional)
{
var splitPropertyName = propertyName.Split("__", StringSplitOptions.RemoveEmptyEntries);
if (splitPropertyName.Length == 2)
{
propertyName = splitPropertyName[0];
string conditionalName = splitPropertyName[1];
if (conditionals.TryGetValue(conditionalName, out var conditionalFunc))
{
if (!conditionalFunc(conditionalName, uniqueModId))
{
continue;
}
}
else
{
MainClass.ErrorLog($"Unknown conditional name: {conditionalName}");
continue;
}
}
}
var locationDict = CreateLocationTileDict(property.Value);
if (categoryDicts.TryGetValue(category, out var targetDict))
{
targetDict.Add(propertyName, locationDict);
}
else
{
MainClass.ErrorLog($"Unknown location category for {propertyName}");
}
}
return (modConditionalLocations, modLocations, vanillaConditionalLocations, vanillaLocations);
}
/// <summary>
/// Merges the contents of the source dictionary into the destination dictionary.
/// If a key exists in both dictionaries and the associated values are dictionaries, the function merges them recursively.
/// If the values are not dictionaries, the value from the source dictionary overwrites the value in the destination dictionary.
/// </summary>
/// <typeparam name="TKey">The type of keys in the dictionaries.</typeparam>
/// <typeparam name="TValue">The type of values in the dictionaries.</typeparam>
/// <param name="destinationDictionary">The destination dictionary to merge the source dictionary into.</param>
/// <param name="sourceDictionary">The source dictionary containing the data to merge into the destination dictionary.</param>
private static void MergeDicts<TKey, TValue>(
Dictionary<TKey, TValue> destinationDictionary,
Dictionary<TKey, TValue> sourceDictionary) where TKey : notnull
{
if (destinationDictionary == null || sourceDictionary == null)
{
// Log a warning or throw an exception if either dictionary is null
return;
}
foreach (var sourceEntry in sourceDictionary)
{
// Try to get the existing value from the destination dictionary
if (destinationDictionary.TryGetValue(sourceEntry.Key, out var existingValue))
{
// If both existing value and the source value are dictionaries,
// merge them recursively
if (existingValue is Dictionary<TKey, TValue> existingDictionary
&& sourceEntry.Value is Dictionary<TKey, TValue> sourceSubDictionary)
{
MergeDicts(existingDictionary, sourceSubDictionary);
}
else
{
// Overwrite the existing value if it's not a dictionary
destinationDictionary[sourceEntry.Key] = sourceEntry.Value;
}
}
else
{
// Add a new entry if the key doesn't exist in the destination dictionary
destinationDictionary[sourceEntry.Key] = sourceEntry.Value;
}
}
}
/// <summary>
/// Builds a dictionary containing location data and tile information based on the provided JsonElement.
/// </summary>
/// <param name="json">A JsonElement containing the location and tile data.</param>
/// <returns>A dictionary containing location data and tile information.</returns>
public static Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>> BuildTilesDict(JsonElement json)
{
// Sort the locations by their types (modConditional, mod, vanillaConditional, vanilla)
var (modConditionalLocations, modLocations, vanillaConditionalLocations, vanillaLocations) = SortLocationsByType(json);
// Create a merged dictionary to store all the location dictionaries
var mergedDict = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>(StringComparer.OrdinalIgnoreCase);
// Merge each category-specific dictionary into the merged dictionary. Prioritize conditional locations whose conditions are true and mod locations where the corresponding mod is loaded. Overwrite their default and vanilla versions, respectively.
MergeDicts(mergedDict, modConditionalLocations);
MergeDicts(mergedDict, modLocations);
MergeDicts(mergedDict, vanillaConditionalLocations);
MergeDicts(mergedDict, vanillaLocations);
return mergedDict;
}
/// <summary>
/// Sets up the tile dictionaries (staticTilesDataDict and customTilesDataDict) using the data from the loaded JsonElements.
/// </summary>
public static void SetupTilesDicts()
{
if (staticTilesData.HasValue && staticTilesData.Value.ValueKind != JsonValueKind.Undefined)
{
staticTilesDataDict = BuildTilesDict(staticTilesData.Value);
}
else
{
staticTilesDataDict = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
}
if (customTilesData.HasValue && customTilesData.Value.ValueKind != JsonValueKind.Undefined)
{
customTilesDataDict = BuildTilesDict(customTilesData.Value);
}
else
{
customTilesDataDict = new Dictionary<string, Dictionary<(short x, short y), (string name, CATEGORY category)>>();
}
}
/// <summary>
/// Retrieves the tile information (name and optionally category) from the dictionaries based on the specified location and coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="currentLocationName">The name of the current location. Defaults to Game1.currentLocation.Name.</param>
/// <param name="includeCategory">Specifies whether to include the tile's category in the returned tuple.</param>
/// <returns>A tuple containing the tile's name and optionally its category. If the tile is not found, the name will be null and the category will be CATEGORY.Others if requested.</returns>
private static (string? name, CATEGORY? category) GetTileInfoAt(int x, int y, string? currentLocationName = null, bool includeCategory = false)
{
currentLocationName ??= Game1.currentLocation.Name;
if (customTilesDataDict != null && customTilesDataDict.TryGetValue(currentLocationName, out var customLocationDict))
{
if (customLocationDict != null && customLocationDict.TryGetValue(((short)x, (short)y), out var customTile))
{
return (customTile.name, includeCategory ? customTile.category : (CATEGORY?)null);
}
}
if (staticTilesDataDict != null && staticTilesDataDict.TryGetValue(currentLocationName, out var staticLocationDict))
{
if (staticLocationDict != null && staticLocationDict.TryGetValue(((short)x, (short)y), out var staticTile))
{
return (staticTile.name, includeCategory ? staticTile.category : (CATEGORY?)null);
}
}
return (null, includeCategory ? CATEGORY.Others : (CATEGORY?)null);
}
/// <summary>
/// Retrieves the tile name from the dictionaries based on the specified location and coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="currentLocationName">The name of the current location. Defaults to Game1.currentLocation.Name.</param>
/// <returns>The name of the tile if found, or null if not found.</returns>
public static string GetStaticTileNameAt(int x, int y, string? currentLocationName = null)
{
var (name, _) = GetTileInfoAt(x, y, currentLocationName, includeCategory: false);
return name ?? "";
}
/// <summary>
/// Retrieves the tile information (name and category) from the dictionaries based on the specified location and coordinates.
/// </summary>
/// <param name="x">The x-coordinate of the tile.</param>
/// <param name="y">The y-coordinate of the tile.</param>
/// <param name="currentLocationName">The name of the current location. Defaults to Game1.currentLocation.Name.</param>
/// <returns>A tuple containing the tile's name and category. If the tile is not found, the name will be null and the category will be CATEGORY.Others.</returns>
public static (string? name, CATEGORY category) GetStaticTileInfoAtWithCategory(int x, int y, string? currentLocationName = null)
{
var (name, category) = GetTileInfoAt(x, y, currentLocationName, includeCategory: true);
return (name, category ?? CATEGORY.Others);
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -173,7 +173,7 @@ namespace stardew_access.Features
if (!tryMoveTileView(delta)) return; if (!tryMoveTileView(delta)) return;
Vector2 position = this.GetTileCursorPosition(); Vector2 position = this.GetTileCursorPosition();
Vector2 tile = this.GetViewingTile(); Vector2 tile = this.GetViewingTile();
String? name = TileInfo.getNameAtTile(tile); String? name = TileInfo.GetNameAtTile(tile);
// Prepend the player's name if the viewing tile is occupied by the player itself // Prepend the player's name if the viewing tile is occupied by the player itself
if (CurrentPlayer.PositionX == tile.X && CurrentPlayer.PositionY == tile.Y) if (CurrentPlayer.PositionX == tile.X && CurrentPlayer.PositionY == tile.Y)
@ -184,7 +184,7 @@ namespace stardew_access.Features
if (name == null) if (name == null)
{ {
// Report if a tile is empty or blocked if there is nothing on it // Report if a tile is empty or blocked if there is nothing on it
if (TileInfo.isCollidingAtTile((int)tile.X, (int)tile.Y)) if (TileInfo.IsCollidingAtTile(Game1.currentLocation, (int)tile.X, (int)tile.Y))
{ {
name = "blocked"; name = "blocked";
} }
@ -278,11 +278,12 @@ namespace stardew_access.Features
private static bool isPositionOnMap(Vector2 position) private static bool isPositionOnMap(Vector2 position)
{ {
var currentLocation = Game1.currentLocation;
// Check whether the position is a warp point, if so then return true, sometimes warp points are 1 tile off the map for example in coops and barns // Check whether the position is a warp point, if so then return true, sometimes warp points are 1 tile off the map for example in coops and barns
if (TileInfo.isWarpPointAtTile((int)(position.X / Game1.tileSize), (int)(position.Y / Game1.tileSize))) return true; if (TileInfo.isWarpPointAtTile(currentLocation, (int)(position.X / Game1.tileSize), (int)(position.Y / Game1.tileSize))) return true;
//position does not take viewport into account since the entire map needs to be checked. //position does not take viewport into account since the entire map needs to be checked.
Map map = Game1.currentLocation.map; Map map = currentLocation.map;
if (position.X < 0 || position.X > map.Layers[0].DisplayWidth) return false; if (position.X < 0 || position.X > map.Layers[0].DisplayWidth) return false;
if (position.Y < 0 || position.Y > map.Layers[0].DisplayHeight) return false; if (position.Y < 0 || position.Y > map.Layers[0].DisplayHeight) return false;
return true; return true;

View File

@ -1,11 +1,26 @@
using System.Text.Json;
namespace stardew_access.Features namespace stardew_access.Features
{ {
/// <summary> /// <summary>
/// This is a custom enum class and contains the name of groups the objects are divided into for the feature /// Represents categories that objects can belong to. This class provides predefined categories
/// accessible as static properties and supports adding new categories at runtime. Predefined categories
/// can be accessed like enum values, while both static and dynamic categories can be accessed via the
/// `Categories` property or the `FromString` method.
/// </summary> /// </summary>
public class CATEGORY /// <remarks>
/// The CATEGORY.Others is used as a default value by the FromString method.
/// Use the FromString method to obtain an existing category.
///
/// Examples:
/// - Access a predefined category like an enum: CATEGORY.Farmers
/// - Obtain a category using the FromString method: CATEGORY.FromString("farmer")
/// - Add a new category: CATEGORY.AddNewCategory("custom_category")
/// - Retrieve a category using the public dictionary: CATEGORY.Categories["custom_category"]
/// - Obtain the string representation of a category: CATEGORY.Farmers.ToString()
/// </remarks>
public sealed class CATEGORY
{ {
private string _typeKeyWord; private readonly string _typeKeyWord;
private CATEGORY(string typeKeyWord) private CATEGORY(string typeKeyWord)
{ {
@ -17,83 +32,135 @@ namespace stardew_access.Features
return _typeKeyWord; return _typeKeyWord;
} }
public static IReadOnlyDictionary<string, CATEGORY> Categories => _categories;
private static readonly Dictionary<string, CATEGORY> _categories = new(StringComparer.OrdinalIgnoreCase)
{
{"farmer", new CATEGORY("farmer")},
{"animal", new CATEGORY("animal")},
{"npc", new CATEGORY("npc")},
{"furniture", new CATEGORY("furniture")},
{"flooring", new CATEGORY("flooring")},
{"debris", new CATEGORY("debris")},
{"crop", new CATEGORY("crop")},
{"tree", new CATEGORY("tree")},
{"bush", new CATEGORY("bush")},
{"building", new CATEGORY("building")},
{"mine item", new CATEGORY("mine item")},
{"resource clump", new CATEGORY("resource clump")},
{"container", new CATEGORY("container")},
{"bundle", new CATEGORY("bundle")},
{"door", new CATEGORY("door")},
{"water", new CATEGORY("water")},
{"interactable", new CATEGORY("interactable")},
{"decoration", new CATEGORY("decoration")},
{"machine", new CATEGORY("machine")},
{"bridge", new CATEGORY("bridge")},
{"dropped item", new CATEGORY("dropped item")},
{"other", new CATEGORY("other")}
};
/// <summary>
/// Retrieves a CATEGORY instance by its string name.
/// Names are case-insensitive. If the name is not found, returns the 'Others' category.
/// </summary>
/// <param name="name">The string name of the category to retrieve.</param>
/// <returns>The CATEGORY instance corresponding to the given name or the 'Others' category if not found.</returns>
/// <exception cref="ArgumentNullException">Thrown when the provided name is null.</exception>
public static CATEGORY FromString(string name) public static CATEGORY FromString(string name)
{ {
if (name == "farmer") if (string.IsNullOrEmpty(name))
return CATEGORY.Farmers; {
else if (name == "animal") throw new ArgumentException("Category name cannot be null or empty.", nameof(name));
return CATEGORY.FarmAnimals; }
else if (name == "npc")
return CATEGORY.NPCs;
else if (name == "furniture")
return CATEGORY.Furnitures;
else if (name == "flooring")
return CATEGORY.Flooring;
else if (name == "debris")
return CATEGORY.Debris;
else if (name == "crop")
return CATEGORY.Crops;
else if (name == "tree")
return CATEGORY.Trees;
else if (name == "bush")
return CATEGORY.Bush;
else if (name == "building")
return CATEGORY.Buildings;
else if (name == "mine item")
return CATEGORY.MineItems;
else if (name == "resource clump")
return CATEGORY.ResourceClumps;
else if (name == "container")
return CATEGORY.Containers;
else if (name == "bundle")
return CATEGORY.JunimoBundle;
else if (name == "door")
return CATEGORY.Doors;
else if (name == "water")
return CATEGORY.WaterTiles;
else if (name == "interactable")
return CATEGORY.Interactables;
else if (name == "decoration")
return CATEGORY.Decor;
else if (name == "machine")
return CATEGORY.Machines;
else if (name == "bridge")
return CATEGORY.Bridges;
else if (name == "dropped item")
return CATEGORY.DroppedItems;
else if (name == "other")
return CATEGORY.Others;
return Others; return Categories.TryGetValue(name, out CATEGORY? category) ? category ?? CATEGORY.Others : CATEGORY.Others;
} }
public static CATEGORY Farmers = new CATEGORY("farmer"); /// <summary>
public static CATEGORY FarmAnimals = new CATEGORY("animal"); /// Adds a new CATEGORY with the specified name.
public static CATEGORY NPCs = new CATEGORY("npc"); /// Names are case-insensitive.
public static CATEGORY Furnitures = new CATEGORY("furniture"); /// </summary>
public static CATEGORY Flooring = new CATEGORY("flooring"); /// <param name="name">The name of the new category to be added.</param>
public static CATEGORY Debris = new CATEGORY("debris"); /// <returns>
public static CATEGORY Crops = new CATEGORY("crop"); /// True if a new category was added; false if the category already exists.
public static CATEGORY Trees = new CATEGORY("tree"); /// </returns>
public static CATEGORY Bush = new CATEGORY("bush"); /// <exception cref="ArgumentNullException">Thrown if the provided name is null or empty.</exception>
public static CATEGORY Buildings = new CATEGORY("building"); public static bool AddNewCategory(string name)
public static CATEGORY MineItems = new CATEGORY("mine item"); {
public static CATEGORY ResourceClumps = new CATEGORY("resource clump"); if (string.IsNullOrEmpty(name))
public static CATEGORY Containers = new CATEGORY("container"); {
public static CATEGORY JunimoBundle = new CATEGORY("bundle"); throw new ArgumentException("Name cannot be null or empty.", nameof(name));
public static CATEGORY Doors = new CATEGORY("door"); // Also includes ladders and elevators }
public static CATEGORY WaterTiles = new CATEGORY("water");
public static CATEGORY Interactables = new CATEGORY("interactable");
public static CATEGORY Decor = new CATEGORY("decoration");
public static CATEGORY Machines = new CATEGORY("machine");
public static CATEGORY Bridges = new CATEGORY("bridge");
public static CATEGORY DroppedItems = new CATEGORY("dropped item");
public static CATEGORY Others = new CATEGORY("other");
if (!Categories.ContainsKey(name))
{
_categories[name] = new CATEGORY(name);
return true;
}
return false;
}
public static CATEGORY Farmers => FromString("farmer");
public static CATEGORY FarmAnimals => FromString("animal");
public static CATEGORY NPCs => FromString("npc");
public static CATEGORY Furnitures => FromString("furniture");
public static CATEGORY Flooring => FromString("flooring");
public static CATEGORY Debris => FromString("debris");
public static CATEGORY Crops => FromString("crop");
public static CATEGORY Trees => FromString("tree");
public static CATEGORY Bush => FromString("bush");
public static CATEGORY Buildings => FromString("building");
public static CATEGORY MineItems => FromString("mine item");
public static CATEGORY ResourceClumps => FromString("resource clump");
public static CATEGORY Containers => FromString("container");
public static CATEGORY JunimoBundle => FromString("bundle");
public static CATEGORY Doors => FromString("door");
public static CATEGORY WaterTiles => FromString("water");
public static CATEGORY Interactables => FromString("interactable");
public static CATEGORY Decor => FromString("decoration");
public static CATEGORY Machines => FromString("machine");
public static CATEGORY Bridges => FromString("bridge");
public static CATEGORY DroppedItems => FromString("dropped item");
public static CATEGORY Others => FromString("other");
} }
public enum MachineState public enum MachineState
{ {
Ready, Busy, Waiting Ready, Busy, Waiting
} }
public static class Utils
{
/// <summary>
/// Loads a JSON file from the specified file name in the assets folder.
/// </summary>
/// <param name="fileName">The name of the JSON file to load.</param>
/// <returns>A <see cref="JsonElement"/> containing the deserialized JSON data, or default if an error occurs.</returns>
public static JsonElement LoadJsonFile(string fileName)
{
string filePath = Path.Combine(MainClass.ModHelper!.DirectoryPath, "assets", fileName);
try
{
string json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize<JsonElement>(json);
}
catch (FileNotFoundException ex)
{
MainClass.ErrorLog($"{fileName} file not found: {ex.Message}");
}
catch (JsonException ex)
{
MainClass.ErrorLog($"Error parsing {fileName}: {ex.Message}");
}
catch (Exception ex)
{
MainClass.ErrorLog($"An error occurred while initializing {fileName}: {ex.Message}");
}
return default;
}
}
} }

View File

@ -7,6 +7,7 @@ using stardew_access.Patches;
using stardew_access.ScreenReader; using stardew_access.ScreenReader;
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework;
using StardewValley.Menus; using StardewValley.Menus;
using Microsoft.Xna.Framework.Input;
namespace stardew_access namespace stardew_access
{ {
@ -20,7 +21,6 @@ namespace stardew_access
private Harmony? harmony; private Harmony? harmony;
private static IMonitor? monitor; private static IMonitor? monitor;
private static Radar? radarFeature; private static Radar? radarFeature;
private static StaticTiles? sTiles;
private static IScreenReader? screenReader; private static IScreenReader? screenReader;
private static IModHelper? modHelper; private static IModHelper? modHelper;
private static TileViewer? tileViewer; private static TileViewer? tileViewer;
@ -30,17 +30,6 @@ namespace stardew_access
internal static ModConfig Config { get => config; set => config = value; } internal static ModConfig Config { get => config; set => config = value; }
public static IModHelper? ModHelper { get => modHelper; } public static IModHelper? ModHelper { get => modHelper; }
public static StaticTiles STiles
{
get
{
if (sTiles == null)
sTiles = new StaticTiles();
return sTiles;
}
set => sTiles = value;
}
public static Radar RadarFeature public static Radar RadarFeature
{ {
get get
@ -141,10 +130,17 @@ namespace stardew_access
helper.Events.Input.ButtonPressed += this.OnButtonPressed; helper.Events.Input.ButtonPressed += this.OnButtonPressed;
helper.Events.GameLoop.UpdateTicked += this.onUpdateTicked; helper.Events.GameLoop.UpdateTicked += this.onUpdateTicked;
helper.Events.GameLoop.DayStarted += this.onDayStarted;
AppDomain.CurrentDomain.DomainUnload += OnExit; AppDomain.CurrentDomain.DomainUnload += OnExit;
AppDomain.CurrentDomain.ProcessExit += OnExit; AppDomain.CurrentDomain.ProcessExit += OnExit;
} }
/// <summary>Returns the Screen Reader class for other mods to use.</summary>
public override object GetApi()
{
return new API();
}
public void OnExit(object? sender, EventArgs? e) public void OnExit(object? sender, EventArgs? e)
{ {
// This closes the connection with the screen reader, important for linux // This closes the connection with the screen reader, important for linux
@ -153,10 +149,10 @@ namespace stardew_access
ScreenReader.CloseScreenReader(); ScreenReader.CloseScreenReader();
} }
/// <summary>Returns the Screen Reader class for other mods to use.</summary> private void onDayStarted(object? sender, DayStartedEventArgs? e)
public override object GetApi()
{ {
return new API(); StaticTiles.LoadTilesFiles();
StaticTiles.SetupTilesDicts();
} }
private void onUpdateTicked(object? sender, UpdateTickedEventArgs? e) private void onUpdateTicked(object? sender, UpdateTickedEventArgs? e)
@ -166,10 +162,8 @@ namespace stardew_access
// Narrates currently selected inventory slot // Narrates currently selected inventory slot
Other.narrateCurrentSlot(); Other.narrateCurrentSlot();
// Narrate current location's name // Narrate current location's name
Other.narrateCurrentLocation(); Other.narrateCurrentLocation();
//handle TileCursor update logic //handle TileCursor update logic
TileViewerFeature.update(); TileViewerFeature.update();
@ -179,27 +173,44 @@ namespace stardew_access
if (Config.ReadTile) if (Config.ReadTile)
ReadTileFeature.update(); ReadTileFeature.update();
if (!RadarFeature.isRunning && Config.Radar) RunRadarFeatureIfEnabled();
{
RadarFeature.isRunning = true;
RadarFeature.Run();
Task.Delay(RadarFeature.delay).ContinueWith(_ => { RadarFeature.isRunning = false; });
}
if (!isNarratingHudMessage) RunHudMessageNarration();
{
isNarratingHudMessage = true;
Other.narrateHudMessages();
Task.Delay(300).ContinueWith(_ => { isNarratingHudMessage = false; });
}
if (Game1.player != null) RefreshBuildListIfRequired();
async void RunRadarFeatureIfEnabled()
{ {
if (Game1.timeOfDay >= 600 && prevDate != CurrentPlayer.Date) if (!RadarFeature.isRunning && Config.Radar)
{ {
prevDate = CurrentPlayer.Date; RadarFeature.isRunning = true;
DebugLog("Refreshing buildlist..."); RadarFeature.Run();
CustomCommands.onBuildListCalled(); await Task.Delay(RadarFeature.delay);
RadarFeature.isRunning = false;
}
}
async void RunHudMessageNarration()
{
if (!isNarratingHudMessage)
{
isNarratingHudMessage = true;
Other.narrateHudMessages();
await Task.Delay(300);
isNarratingHudMessage = false;
}
}
void RefreshBuildListIfRequired()
{
if (Game1.player != null)
{
if (Game1.timeOfDay >= 600 && prevDate != CurrentPlayer.Date)
{
prevDate = CurrentPlayer.Date;
DebugLog("Refreshing buildlist...");
CustomCommands.onBuildListCalled();
}
} }
} }
} }
@ -209,49 +220,56 @@ namespace stardew_access
if (e == null) if (e == null)
return; return;
#region Simulate left and right clicks void SimulateMouseClicks(Action<int, int> leftClickHandler, Action<int, int> rightClickHandler)
if (Game1.activeClickableMenu != null && !TextBoxPatch.isAnyTextBoxActive)
{ {
bool isCustomizingCharacter = Game1.activeClickableMenu is CharacterCustomization || (TitleMenu.subMenu != null && TitleMenu.subMenu is CharacterCustomization); int mouseX = Game1.getMouseX(true);
int mouseY = Game1.getMouseY(true);
#region Mouse Click Simulation
if (Config.LeftClickMainKey.JustPressed() || Config.LeftClickAlternateKey.JustPressed()) if (Config.LeftClickMainKey.JustPressed() || Config.LeftClickAlternateKey.JustPressed())
{ {
Game1.activeClickableMenu.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true)); leftClickHandler(mouseX, mouseY);
} }
else if (Config.RightClickMainKey.JustPressed() || Config.RightClickAlternateKey.JustPressed())
if (Config.RightClickMainKey.JustPressed() || Config.RightClickAlternateKey.JustPressed())
{ {
Game1.activeClickableMenu.receiveRightClick(Game1.getMouseX(true), Game1.getMouseY(true)); rightClickHandler(mouseX, mouseY);
} }
#endregion
} }
if (Game1.currentMinigame != null && !TextBoxPatch.isAnyTextBoxActive) #region Simulate left and right clicks
if (!TextBoxPatch.isAnyTextBoxActive)
{ {
#region Mouse Click Simulation if (Game1.activeClickableMenu != null)
if (Config.LeftClickMainKey.JustPressed() || Config.LeftClickAlternateKey.JustPressed())
{ {
Game1.currentMinigame.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true)); SimulateMouseClicks(
(x, y) => Game1.activeClickableMenu.receiveLeftClick(x, y),
(x, y) => Game1.activeClickableMenu.receiveRightClick(x, y)
);
} }
else if (Game1.currentMinigame != null)
if (Config.RightClickMainKey.JustPressed() || Config.RightClickAlternateKey.JustPressed())
{ {
Game1.currentMinigame.receiveRightClick(Game1.getMouseX(true), Game1.getMouseY(true)); SimulateMouseClicks(
(x, y) => Game1.currentMinigame.receiveLeftClick(x, y),
(x, y) => Game1.currentMinigame.receiveRightClick(x, y)
);
} }
#endregion
} }
#endregion #endregion
if (!Context.IsPlayerFree) if (!Context.IsPlayerFree)
return; return;
// Stops the auto walk controller if any movement key(WASD) is pressed void Narrate(string message) => MainClass.ScreenReader.Say(message, true);
if (TileViewerFeature.isAutoWalking &&
(e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveUpButton[0])) bool IsMovementKey(SButton button)
|| e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveDownButton[0])) {
|| e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveLeftButton[0])) return button.Equals(SButtonExtensions.ToSButton(Game1.options.moveUpButton[0]))
|| e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveRightButton[0])))) || button.Equals(SButtonExtensions.ToSButton(Game1.options.moveDownButton[0]))
|| button.Equals(SButtonExtensions.ToSButton(Game1.options.moveLeftButton[0]))
|| button.Equals(SButtonExtensions.ToSButton(Game1.options.moveRightButton[0]));
}
// Stops the auto walk controller if any movement key(WASD) is pressed
if (TileViewerFeature.isAutoWalking && IsMovementKey(e.Button))
{ {
TileViewerFeature.stopAutoWalking(wasForced: true); TileViewerFeature.stopAutoWalking(wasForced: true);
} }
@ -259,25 +277,17 @@ namespace stardew_access
// Narrate Current Location // Narrate Current Location
if (Config.LocationKey.JustPressed()) if (Config.LocationKey.JustPressed())
{ {
string toSpeak = $"{Game1.currentLocation.Name}"; Narrate(Game1.currentLocation.Name);
MainClass.ScreenReader.Say(toSpeak, true);
return; return;
} }
// Narrate Position // Narrate Position
if (Config.PositionKey.JustPressed()) if (Config.PositionKey.JustPressed())
{ {
string toSpeak; string toSpeak = Config.VerboseCoordinates
if (Config.VerboseCoordinates) ? $"X: {CurrentPlayer.PositionX}, Y: {CurrentPlayer.PositionY}"
{ : $"{CurrentPlayer.PositionX}, {CurrentPlayer.PositionY}";
toSpeak = $"X: {CurrentPlayer.PositionX}, Y: {CurrentPlayer.PositionY}"; Narrate(toSpeak);
}
else
{
toSpeak = $"{CurrentPlayer.PositionX}, {CurrentPlayer.PositionY}";
}
MainClass.ScreenReader.Say(toSpeak, true);
return; return;
} }
@ -287,29 +297,25 @@ namespace stardew_access
if (ModHelper == null) if (ModHelper == null)
return; return;
string toSpeak; string toSpeak = Config.HealthNStaminaInPercentage
if (Config.HealthNStaminaInPercentage) ? ModHelper.Translation.Get("manuallytriggered.healthnstamina.percent", new { health = CurrentPlayer.PercentHealth, stamina = CurrentPlayer.PercentStamina })
toSpeak = ModHelper.Translation.Get("manuallytriggered.healthnstamina.percent", new { health = CurrentPlayer.PercentHealth, stamina = CurrentPlayer.PercentStamina }); : ModHelper.Translation.Get("manuallytriggered.healthnstamina.normal", new { health = CurrentPlayer.CurrentHealth, stamina = CurrentPlayer.CurrentStamina });
else
toSpeak = ModHelper.Translation.Get("manuallytriggered.healthnstamina.normal", new { health = CurrentPlayer.CurrentHealth, stamina = CurrentPlayer.CurrentStamina });
MainClass.ScreenReader.Say(toSpeak, true); Narrate(toSpeak);
return; return;
} }
// Narrate money at hand // Narrate money at hand
if (Config.MoneyKey.JustPressed()) if (Config.MoneyKey.JustPressed())
{ {
string toSpeak = $"You have {CurrentPlayer.Money}g"; Narrate($"You have {CurrentPlayer.Money}g");
MainClass.ScreenReader.Say(toSpeak, true);
return; return;
} }
// Narrate time and season // Narrate time and season
if (Config.TimeNSeasonKey.JustPressed()) if (Config.TimeNSeasonKey.JustPressed())
{ {
string toSpeak = $"Time is {CurrentPlayer.TimeOfDay} and it is {CurrentPlayer.Day} {CurrentPlayer.Date} of {CurrentPlayer.Season}"; Narrate($"Time is {CurrentPlayer.TimeOfDay} and it is {CurrentPlayer.Day} {CurrentPlayer.Date} of {CurrentPlayer.Season}");
MainClass.ScreenReader.Say(toSpeak, true);
return; return;
} }
@ -331,28 +337,27 @@ namespace stardew_access
TileViewerFeature.HandleInput(); TileViewerFeature.HandleInput();
} }
public static void ErrorLog(string message) private static void LogMessage(string message, LogLevel logLevel)
{ {
if (monitor == null) if (monitor == null)
return; return;
monitor.Log(message, LogLevel.Error); monitor.Log(message, logLevel);
}
public static void ErrorLog(string message)
{
LogMessage(message, LogLevel.Error);
} }
public static void InfoLog(string message) public static void InfoLog(string message)
{ {
if (monitor == null) LogMessage(message, LogLevel.Info);
return;
monitor.Log(message, LogLevel.Info);
} }
public static void DebugLog(string message) public static void DebugLog(string message)
{ {
if (monitor == null) LogMessage(message, LogLevel.Debug);
return;
monitor.Log(message, LogLevel.Debug);
} }
} }
} }

View File

@ -34,7 +34,7 @@ namespace stardew_access.Patches
if (cueName == "grassyStep" || cueName == "sandyStep" || cueName == "snowyStep" || cueName == "stoneStep" || cueName == "thudStep" || cueName == "woodyStep") if (cueName == "grassyStep" || cueName == "sandyStep" || cueName == "snowyStep" || cueName == "stoneStep" || cueName == "thudStep" || cueName == "woodyStep")
{ {
Vector2 nextTile = CurrentPlayer.FacingTile; Vector2 nextTile = CurrentPlayer.FacingTile;
if (TileInfo.isCollidingAtTile((int)nextTile.X, (int)nextTile.Y)) if (TileInfo.IsCollidingAtTile(Game1.currentLocation, (int)nextTile.X, (int)nextTile.Y))
{ {
if (prevTile != nextTile) if (prevTile != nextTile)
{ {

View File

@ -1,5 +1,7 @@
using System.Text.Json;
using StardewValley; using StardewValley;
using StardewValley.Menus; using StardewValley.Menus;
using static stardew_access.Features.Utils;
namespace stardew_access.Patches namespace stardew_access.Patches
{ {
@ -7,28 +9,69 @@ namespace stardew_access.Patches
{ {
private static bool isRunning = false; private static bool isRunning = false;
private static int saveGameIndex = -1; private static int saveGameIndex = -1;
public static string characterCreationMenuQueryKey = " "; private static string characterCreationMenuQueryKey = " ";
public static string prevPants = " "; private static string prevPants = " ";
public static string prevShirt = " "; private static string prevShirt = " ";
public static string prevHair = " "; private static string prevHair = " ";
public static string prevAccessory = " "; private static string prevAccessory = " ";
public static string prevSkin = " "; private static string prevSkin = " ";
public static string prevEyeColor = " "; private static string prevEyeColor = " ";
public static string prevEyeColorHue = " "; private static string prevEyeColorHue = " ";
public static string prevEyeColorSaturation = " "; private static string prevEyeColorSaturation = " ";
public static string prevEyeColorValue = " "; private static string prevEyeColorValue = " ";
public static string prevHairColor = " "; private static string prevHairColor = " ";
public static string prevHairColorHue = " "; private static string prevHairColorHue = " ";
public static string prevHairColorSaturation = " "; private static string prevHairColorSaturation = " ";
public static string prevHairColorValue = " "; private static string prevHairColorValue = " ";
public static string prevPantsColor = " "; private static string prevPantsColor = " ";
public static string prevPantsColorHue = " "; private static string prevPantsColorHue = " ";
public static string prevPantsColorSaturation = " "; private static string prevPantsColorSaturation = " ";
public static string prevPantsColorValue = " "; private static string prevPantsColorValue = " ";
public static string prevPetName = " "; private static string prevPet = " ";
public static bool characterDesignToggle = false; private static bool characterDesignToggle = false;
public static bool characterDesignToggleShouldSpeak = true; private static bool characterDesignToggleShouldSpeak = true;
public static ClickableComponent? currentComponent = null; private static ClickableComponent? currentComponent = null;
private static Dictionary<string, Dictionary<int, string>> descriptions
{
get
{
if (_descriptions == null)
{
_descriptions = LoadDescriptionJson();
}
return _descriptions;
}
}
private static Dictionary<string, Dictionary<int, string>>? _descriptions;
private static Dictionary<string, Dictionary<int, string>> LoadDescriptionJson()
{
MainClass.DebugLog("Attempting to load json");
JsonElement jsonElement = LoadJsonFile("new-character-appearance-descriptions.json");
if (jsonElement.ValueKind == JsonValueKind.Undefined)
{
return new Dictionary<string, Dictionary<int, string>>();
}
Dictionary<string, Dictionary<int, string>> result = new Dictionary<string, Dictionary<int, string>>();
foreach (JsonProperty category in jsonElement.EnumerateObject())
{
Dictionary<int, string> innerDictionary = new Dictionary<int, string>();
foreach (JsonProperty item in category.Value.EnumerateObject())
{
int index = int.Parse(item.Name);
innerDictionary[index] = item.Value.GetString() ?? "";
}
result[category.Name] = innerDictionary;
MainClass.InfoLog($"Loaded key '{category.Name}' with {innerDictionary.Count} entries in the sub dictionary.");
}
return result;
}
internal static void DrawPatch(CharacterCustomization __instance, bool ___skipIntro, internal static void DrawPatch(CharacterCustomization __instance, bool ___skipIntro,
ClickableComponent ___startingCabinsLabel, ClickableComponent ___difficultyModifierLabel, TextBox ___nameBox, ClickableComponent ___startingCabinsLabel, ClickableComponent ___difficultyModifierLabel, TextBox ___nameBox,
@ -126,24 +169,24 @@ namespace stardew_access.Patches
private static string getChangesToSpeak(CharacterCustomization __instance) private static string getChangesToSpeak(CharacterCustomization __instance)
{ {
string toSpeak = ""; string toSpeak = "";
string currentPetName = getCurrentPetName(); string currentPet = GetCurrentPet();
string currentSkin = getCurrentSkin(); string currentSkin = GetCurrentSkin();
string currentHair = getCurrentHair(); string currentHair = GetCurrentHair();
string currentShirt = getCurrentShirt(); string currentShirt = GetCurrentShirt();
string currentPants = getCurrentPants(); string currentPants = GetCurrentPants();
string currentAccessory = getCurrentAccessory(); string currentAccessory = GetCurrentAccessory();
string currentEyeColor = getCurrentEyeColor(); string currentEyeColor = GetCurrentEyeColor();
string currentEyeColorHue = getCurrentEyeColorHue(__instance); string currentEyeColorHue = GetCurrentEyeColorHue(__instance);
string currentEyeColorSaturation = getCurrentEyeColorSaturation(__instance); string currentEyeColorSaturation = GetCurrentEyeColorSaturation(__instance);
string currentEyeColorValue = getCurrentEyeColorValue(__instance); string currentEyeColorValue = GetCurrentEyeColorValue(__instance);
string currentHairColor = getCurrentHairColor(); string currentHairColor = GetCurrentHairColor();
string currentHairColorHue = getCurrentHairColorHue(__instance); string currentHairColorHue = GetCurrentHairColorHue(__instance);
string currentHairColorSaturation = getCurrentHairColorSaturation(__instance); string currentHairColorSaturation = GetCurrentHairColorSaturation(__instance);
string currentHairColorValue = getCurrentHairColorValue(__instance); string currentHairColorValue = GetCurrentHairColorValue(__instance);
string currentPantsColor = getCurrentPantsColor(); string currentPantsColor = GetCurrentPantsColor();
string currentPantsColorHue = getCurrentPantsColorHue(__instance); string currentPantsColorHue = GetCurrentPantsColorHue(__instance);
string currentPantsColorSaturation = getCurrentPantsColorSaturation(__instance); string currentPantsColorSaturation = GetCurrentPantsColorSaturation(__instance);
string currentPantsColorValue = getCurrentPantsColorValue(__instance); string currentPantsColorValue = GetCurrentPantsColorValue(__instance);
if (characterDesignToggle) if (characterDesignToggle)
{ {
@ -339,11 +382,11 @@ namespace stardew_access.Patches
} }
} }
if (prevPetName != currentPetName) if (prevPet != currentPet)
{ {
prevPetName = currentPetName; prevPet = currentPet;
if (currentPetName != "") if (currentPet != "")
toSpeak = $"{toSpeak} \n Current Pet: {currentPetName}"; toSpeak = $"{toSpeak} \n Current Pet: {currentPet}";
} }
return toSpeak.Trim(); return toSpeak.Trim();
} }
@ -693,145 +736,99 @@ namespace stardew_access.Patches
} }
// Most values (exception noted below) are 0 indexed internally but visually start from 1. Thus we increment before returning. // Most values (exception noted below) are 0 indexed internally but visually start from 1. Thus we increment before returning.
private static string getCurrentSkin() private static string GetCurrentPet(bool lessInfo = false)
{
if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == "Skin"))
return $"Skin tone: {Game1.player.skin.Value + 1}";
return "";
}
private static string getCurrentHair()
{
if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == "Hair"))
return $"hair style: {Game1.player.hair.Value + 1}";
return "";
}
private static string getCurrentShirt()
{
if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == "Shirt"))
return $"Shirt: {Game1.player.shirt.Value + 1}";
return "";
}
private static string getCurrentPants()
{
if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == "Pants Style"))
return $"Pants: {Game1.player.pants.Value + 1}";
return "";
}
private static string getCurrentAccessory()
{
// Internally accessory starts from -1 while displaying +1 on screen.
if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == "Acc"))
return $"accessory: {Game1.player.accessory.Value + 2}";
return "";
}
private static string getCurrentEyeColor()
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 522 && currentComponent.myID <= 524)))
return $"Eye color: {Game1.player.newEyeColor.R}, {Game1.player.newEyeColor.G}, {Game1.player.newEyeColor.B}";
return "";
}
private static string getCurrentEyeColorHue(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(522, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 522 && currentComponent.myID <= 524)))
return sb.value!.ToString();
return "";
}
private static string getCurrentEyeColorSaturation(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(523, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 522 && currentComponent.myID <= 524)))
return sb.value!.ToString();
return "";
}
private static string getCurrentEyeColorValue(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(524, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 522 && currentComponent.myID <= 524)))
return sb.value!.ToString();
return "";
}
private static string getCurrentHairColor()
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 525 && currentComponent.myID <= 527)))
return $"Hair color: {Game1.player.hairstyleColor.R}, {Game1.player.hairstyleColor.G}, {Game1.player.hairstyleColor.B}";
return "";
}
private static string getCurrentHairColorHue(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(525, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 525 && currentComponent.myID <= 527)))
return sb.value!.ToString();
return "";
}
private static string getCurrentHairColorSaturation(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(526, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 525 && currentComponent.myID <= 527)))
return sb.value!.ToString();
return "";
}
private static string getCurrentHairColorValue(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(527, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 525 && currentComponent.myID <= 527)))
return sb.value!.ToString();
return "";
}
private static string getCurrentPantsColor()
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 528 && currentComponent.myID <= 530)))
return $"Pants color: {Game1.player.pantsColor.R}, {Game1.player.pantsColor.G}, {Game1.player.pantsColor.B}";
return "";
}
private static string getCurrentPantsColorHue(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(528, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 528 && currentComponent.myID <= 530)))
return sb.value!.ToString();
return "";
}
private static string getCurrentPantsColorSaturation(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(529, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 528 && currentComponent.myID <= 530)))
return sb.value!.ToString();
return "";
}
private static string getCurrentPantsColorValue(CharacterCustomization __instance)
{
SliderBar sb = getCurrentSliderBar(530, __instance)!;
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= 528 && currentComponent.myID <= 530)))
return sb.value!.ToString();
return "";
}
private static string getCurrentPetName()
{ {
if (currentComponent != null && currentComponent.name == "Pet") if (currentComponent != null && currentComponent.name == "Pet")
{ {
return ((Game1.player.catPerson) ? "Cat" : "Dog") + " Breed: " + Game1.player.whichPetBreed; int whichPetBreed = Game1.player.whichPetBreed + 1;
}
else if (!lessInfo)
{ {
return ""; string petType = Game1.player.catPerson ? "Cat" : "Dog";
if (descriptions.TryGetValue(petType, out var innerDict) && innerDict.TryGetValue(whichPetBreed, out var description))
{
return description;
}
else
{
MainClass.ErrorLog($"Warning: Description for {petType} with index {whichPetBreed} not found in the dictionary.");
}
}
return $"{(Game1.player.catPerson ? "Cat" : "Dog")} #{whichPetBreed + 1}";
} }
return "";
} }
private static string GetCurrentAttributeValue(string componentName, Func<int> getValue, bool lessInfo = false)
{
if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == componentName))
{
int index = getValue();
if (!lessInfo)
{
if (descriptions.TryGetValue(componentName, out var innerDict))
{
if (innerDict.TryGetValue(index, out var description))
{
return description;
}
else
{
MainClass.ErrorLog($"Warning: Description for {componentName} with index {index} not found in the inner dictionary.");
}
}
else
{
MainClass.ErrorLog($"Warning: Description for {componentName} not found in the outer dictionary.");
}
}
return $"{componentName}: {index}";
}
return "";
}
private static string GetCurrentSkin(bool lessInfo = false) => GetCurrentAttributeValue("Skin", () => Game1.player.skin.Value + 1, lessInfo);
private static string GetCurrentHair(bool lessInfo = false) => GetCurrentAttributeValue("Hair", () => Game1.player.hair.Value + 1, lessInfo);
private static string GetCurrentShirt(bool lessInfo = false) => GetCurrentAttributeValue("Shirt", () => Game1.player.shirt.Value + 1, lessInfo);
private static string GetCurrentPants(bool lessInfo = false) => GetCurrentAttributeValue("Pants Style", () => Game1.player.pants.Value + 1, lessInfo);
private static string GetCurrentAccessory(bool lessInfo = false) => GetCurrentAttributeValue("Accessory", () => Game1.player.accessory.Value + 2, lessInfo);
private static string GetCurrentColorAttributeValue(string componentName, int minID, int maxID, Func<string> getValue)
{
if (currentComponent != null && (currentComponent.myID == 507 || (currentComponent.myID >= minID && currentComponent.myID <= maxID)))
{
return $"{componentName}: {getValue()}";
}
return "";
}
private static string GetCurrentEyeColor() => GetCurrentColorAttributeValue("Eye color", 522, 524, () => $"{Game1.player.newEyeColor.R}, {Game1.player.newEyeColor.G}, {Game1.player.newEyeColor.B}");
private static string GetCurrentEyeColorHue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Eye color hue", 522, 524, () => (getCurrentSliderBar(522, __instance)!.value!.ToString()));
private static string GetCurrentEyeColorSaturation(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Eye color saturation", 522, 524, () => (getCurrentSliderBar(523, __instance)!.value!.ToString()));
private static string GetCurrentEyeColorValue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Eye color value", 522, 524, () => (getCurrentSliderBar(524, __instance)!.value!.ToString()));
private static string GetCurrentHairColor() => GetCurrentColorAttributeValue("Hair color", 525, 527, () => $"{Game1.player.hairstyleColor.R}, {Game1.player.hairstyleColor.G}, {Game1.player.hairstyleColor.B}");
private static string GetCurrentHairColorHue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Hair color hue", 525, 527, () => (getCurrentSliderBar(525, __instance)!.value!.ToString()));
private static string GetCurrentHairColorSaturation(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Hair color saturation", 525, 527, () => (getCurrentSliderBar(526, __instance)!.value!.ToString()));
private static string GetCurrentHairColorValue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Hair color value", 525, 527, () => (getCurrentSliderBar(527, __instance)!.value!.ToString()));
private static string GetCurrentPantsColor() => GetCurrentColorAttributeValue("Pants color", 528, 530, () => $"{Game1.player.pantsColor.R}, {Game1.player.pantsColor.G}, {Game1.player.pantsColor.B}");
private static string GetCurrentPantsColorHue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Pants color hue", 528, 530, () => (getCurrentSliderBar(528, __instance)!.value!.ToString()));
private static string GetCurrentPantsColorSaturation(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Pants color saturation", 528, 530, () => (getCurrentSliderBar(529, __instance)!.value!.ToString()));
private static string GetCurrentPantsColorValue(CharacterCustomization __instance) => GetCurrentColorAttributeValue("Pants color value", 528, 530, () => (getCurrentSliderBar(530, __instance)!.value!.ToString()));
} }
} }

View File

@ -0,0 +1,8 @@
using System;
using System.Runtime.InteropServices;
public static class NativeMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetDllDirectory(string lpPathName);
}

View File

@ -1,4 +1,5 @@
using DavyKager; using DavyKager;
using System.IO;
namespace stardew_access.ScreenReader namespace stardew_access.ScreenReader
{ {
@ -15,6 +16,13 @@ namespace stardew_access.ScreenReader
public void InitializeScreenReader() public void InitializeScreenReader()
{ {
if (MainClass.ModHelper is not null)
{
// Set the path to load Tolk.dll from
string dllDirectory = Path.Combine(MainClass.ModHelper.DirectoryPath, "libraries", "windows");;
// Call SetDllDirectory to change the DLL search path
NativeMethods.SetDllDirectory(dllDirectory);
}
MainClass.InfoLog("Initializing Tolk..."); MainClass.InfoLog("Initializing Tolk...");
Tolk.TrySAPI(true); Tolk.TrySAPI(true);
Tolk.Load(); Tolk.Load();

View File

@ -0,0 +1,31 @@
{
"Egg Festival": {
"21,55": "Egg Festival Shop"
},
"Flower Dance": {
"28,37": "Flower Dance Shop"
},
"Luau": {
"35,13": "Soup Pot"
},
"Spirit's Eve": {
"25,49": "Spirit's Eve Shop"
},
"Stardew Valley Fair": {
"16,52": "Stardew Valley Fair Shop",
"23,62": "Slingshot Game",
"34,65": "Purchase Star Tokens",
"33,70": "The Wheel",
"23,70": "Fishing Challenge",
"47,87": "Fortune Teller",
"38,59": "Grange Display",
"30,56": "Strength Game",
"26,33": "Free Burgers"
},
"Festival of Ice": {
"55,31": "Travelling Cart"
},
"Feast of the Winter Star": {
"18,61": "Feast of the Winter Star Shop"
}
}

View File

@ -0,0 +1,256 @@
{
"Cat": {
"1": "Orange cat",
"2": "Gray Tabby cat with white belly",
"3": "Yellow cat with purple collar"
},
"Dog": {
"1": "Golden-brown Hound with blue collar",
"2": "Brown Shepherd",
"3": "Tan body, brown eared, long furred terrier"
},
"Skin": {
"1": "Pale, beige undertone",
"2": "Tan, pink undertone",
"3": "Pale, pink undertone",
"4": "Pale",
"5": "Brown, red undertone",
"6": "Brown, pink undertone",
"7": "Brown, warm tone",
"8": "Beige, orange undertone",
"9": "Light Brown",
"10": "Pale, pink overtone",
"11": "Pale, grey undertone",
"12": "Tan, warm tone",
"13": "Pale Green",
"14": "Pale Fuchsia",
"15": "Brown, full tone",
"16": "Tan, red overtone",
"17": "Pale Blue",
"18": "Green",
"19": "Pale Red",
"20": "Pale Purple",
"21": "Yellow, full tone",
"22": "Gray",
"23": "Pale, yellow undertone",
"24": "Pale, ivory undertone"
},
"Hair": {
"1": "Right-side part, short and unkempt",
"2": "Middle-part, six inches length",
"3": "Left-side part, swept bangs, combed back",
"4": "Afro",
"5": "Right-side part, unkempt with bangs",
"6": "Shaved back and sides",
"7": "Right-part open pompadour, chin length",
"8": "Right-part, short and combed back",
"9": "Right-part with bangs, large wayward spikes",
"10": "Right-part, side bob",
"11": "Pompadour, short and combed back",
"12": "Short with faded sides, combed back",
"13": "Middle-part, low ponytail",
"14": "Wayward dreads, six inches length, undershave",
"15": "Left-part with long bang, combed back",
"16": "Middle-part, 4 inches length, undercut",
"17": "Right-part, high ponytail, swept bangs",
"18": "Right-side part, shoulder length, low pigtails",
"19": "Right-side part, short with long swept bangs",
"20": "Updo, three tight buns on top of head",
"21": "Short and combed",
"22": "Right-side part, short, high pigtails",
"23": "Right-side part with bangs, tight high bun",
"24": "Right-side part with bangs, unkempt, six inches",
"25": "Right-side part, swept bangs, mid-back length",
"26": "Fifties style, teased, curly ended bob",
"27": "Middle-part, thigh-length",
"28": "Right-side part, swept bangs, chin length",
"29": "Middle-part, waist length, low ponytail",
"30": "Waist length with bangs, straight, tapered ends",
"31": "Right-side part with bangs, low pigtails",
"32": "Dual twisted side-buns, Princess Leia style",
"33": "Right-side part, swept bangs, short",
"34": "Right-side part, hip-length, pigtail braids",
"35": "Right-side part, mid-back length, pigtail braids",
"36": "High ponytail, mini bangs",
"37": "Middle-part, swept over right shoulder",
"38": "Right side part with bangs, high pigtails",
"39": "Black hairband, chin length",
"40": "Black hairband with bangs, shoulder length",
"41": "Left-side part, loose curls, shoulder length",
"42": "Shoulder length with mini bangs, curly",
"43": "Long on top with highlights, combed back",
"44": "Right-side part, swept bangs, short",
"45": "Middle-part, fade with 4 inches on top",
"46": "Cornrows, chin length",
"47": "Left-side part, short and combed",
"48": "Middle-part, swept bangs, chin length",
"49": "Middle-part, unkempt, partial ponytail",
"50": "Liberty spike style, shaved sides",
"51": "Donut cut, shoulder length",
"52": "Donut cut, short",
"53": "Bald or shaved",
"54": "Shaved, half-inch length, widow's peak",
"55": "Shaved, half-inch length, unkempt",
"56": "Shaved, half-inch length, straight hairline",
"101": "Left-side part with bangs, wavy, waist length",
"102": "Right-side part, hip-length, curly",
"103": "Right-side part, waist length, straight",
"104": "Middle-part, waist length, low ponytail",
"105": "Middle-part, waist length, high braid",
"106": "Right-side part with bangs, swept to shoulder",
"107": "Right-side part, unkempt, swept to shoulder",
"108": "Bob with bangs",
"109": "Left-side part, short, combed",
"110": "Wavy with bangs, 8 inches",
"111": "Wavy with bangs, shoulder length",
"112": "Dreads, neat, 4 inches length",
"113": "Short and unkempt",
"114": "Middle-part, six inches length",
"115": "Right-side part, shoulder length, unkempt",
"116": "Middle-part, teased, shoulder length",
"117": "Middle-part with bangs, short",
"118": "Left-side part with bangs, unkempt, short"
},
"Shirt": {
"1": "Red, denim overalls",
"2": "Brown button up",
"3": "Light Green, brown belt",
"4": "Black, gray splat design",
"5": "Black skull design",
"6": "Blue Gray, cloud design",
"7": "Cream, light blue horizontal stripe",
"8": "Green, denim overalls",
"9": "Yellow, brown horizontal zig zag",
"10": "Blue Green, cloud design",
"11": "Black, white letter A",
"12": "Green, collar cinches",
"13": "Lime Green, green stripes",
"14": "Red, white horizontal stripes",
"15": "Black, white ribcage design",
"16": "Brown, Tan, Light Brown stripes",
"17": "Blue, yellow dots",
"18": "Green, brown suspenders",
"19": "Brown jacket, Gray tee",
"20": "White, blue kerchief",
"21": "Green tank, Gray tee",
"22": "Ochre, green horizontal stripe",
"23": "Red button up",
"24": "Green button up",
"25": "Light Blue button up",
"26": "Blue button up",
"27": "Sea Green, horizontal white stripe",
"28": "Purple, light equal sign design",
"29": "Black, purple heart design",
"30": "White vertical gradient",
"31": "Brown jacket, Black shirt",
"32": "Brown Gray, angled button up",
"33": "Red, brown belt",
"34": "Green, strung collar",
"35": "Green bodice, gold belt, brown sleeves",
"36": "Red, white collar, buttoned",
"37": "Light Purple, zippered",
"38": "Gray to Black vertical gradient",
"39": "White, wide collar",
"40": "Sea Green and Brown stripes",
"41": "Purple vertical gradient",
"42": "White, horizontal cream stripe",
"43": "Green vertical gradient, belt",
"44": "Blue vertical gradient",
"45": "Blue, strung collar, white spot",
"46": "Brown vertical gradient",
"47": "Purple Vertical Gradient",
"48": "Brown, silver belt",
"49": "Black, gray bat design",
"50": "Light Purple, purple stripe",
"51": "Light Pink tank, purple shirt",
"52": "Pink tank, light purple tee",
"53": "Purple, vertical rainbow column",
"54": "Black, green belt",
"55": "Sea Green, white shoulder stripe",
"56": "Red, horizontal yellow stripe",
"57": "Lime Green, wide collar",
"58": "White and Gray stripes, red vest",
"59": "Blue, light blue shoulder stripe",
"60": "Ochre, yellow shoulder stripe",
"61": "Blue, wide collar",
"62": "Tan, stripes and dots",
"63": "Blue, white collar and stripe",
"64": "Red, silver collar",
"65": "Patchwork Blue",
"66": "Green, white undershirt",
"67": "Gray, mouse face design",
"68": "Yellow, low overalls",
"69": "Light Green, upper frog face",
"70": "Green, brown belt",
"71": "Fuchsia, light purple stripe",
"72": "White, denim overalls, brown belt",
"73": "Cream crop hoodie, blue tank",
"74": "Dark Blue and Purple horizontal split",
"75": "Blue, red overalls, brown belt",
"76": "Black, green mushroom cloud design",
"77": "Light Purple, brown belt",
"78": "White, tongue out frowny face",
"79": "Purple, white kerchief",
"80": "Black, blue overalls",
"81": "Gray, white shoulder stripe",
"82": "Green, light green waist stripe",
"83": "Dark Blue",
"84": "Black, wide collar",
"85": "Black",
"86": "Red, button up, open neck",
"87": "Teal, brown suspenders",
"88": "White button up, red kerchief",
"89": "Yellow, green vest",
"90": "Purple Bowling Style",
"91": "Black Hoodie",
"92": "Green, collared, white kerchief",
"93": "Pink, light pink shoulder stripe",
"94": "White, black spots",
"95": "Brown, red and yellow tie",
"96": "Yellow, black eyes with blush",
"97": "Green, dark green spots",
"98": "Gray, button up, dark vertical stripe",
"99": "Black peacoat, white shirt collar",
"100": "Purple, black overalls",
"101": "Light Blue, horizontal dark stripe",
"102": "Black, white front",
"103": "Canvas, blond leather belt",
"104": "Gray stripes, black overalls",
"105": "Green and Teal stripes",
"106": "Blue, white letter J",
"107": "Green and Black Horizontal split",
"108": "Fuchsia, white shoulder stripe",
"109": "Brown Orange",
"110": "Purple button up, dark vertical stripe",
"111": "Brown button up, dark vertical stripe",
"112": "Olive green, dark vertical stripe"
},
"Pants Style": {
"1": "Long",
"2": "Shorts",
"3": "Long Skirt",
"4": "Skirt"
},
"Accessory": {
"1": "Blank",
"2": "Full beard and mustache",
"3": "Full mustache",
"4": "Full mustache wrinkles",
"5": "Goatee",
"6": "Mutton chops",
"7": "Full beard and mustache, untrimmed",
"8": "Gold earrings",
"9": "Turquoise earrings",
"10": "Black full-frame glasses",
"11": "Lipstick",
"12": "Top-frame glasses",
"13": "Bushy eyebrows",
"14": "Robo-visor",
"15": "Circular black frame glasses",
"16": "Red necklace",
"17": "Black sunglasses",
"18": "Blue necklace",
"19": "Gray sunglasses",
"20": "Orange beak"
}
}

View File

@ -674,6 +674,7 @@
"type": "decoration" "type": "decoration"
} }
}, },
"farmhouse": null,
"fishshop": { "fishshop": {
"Shop Counter": { "Shop Counter": {
"x": [4, 5, 6], "x": [4, 5, 6],

View File

@ -1,7 +1,7 @@
{ {
"Name": "Stardew Access", "Name": "Stardew Access",
"Author": "Mohammad Shoaib", "Author": "Mohammad Shoaib",
"Version": "1.3.4", "Version": "1.3.5-beta2",
"Description": "An accessibility mod with screen reader support!", "Description": "An accessibility mod with screen reader support!",
"UniqueID": "shoaib.stardewaccess", "UniqueID": "shoaib.stardewaccess",
"EntryDll": "stardew-access.dll", "EntryDll": "stardew-access.dll",

View File

@ -13,7 +13,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Lib.Harmony" Version="2.2.2" /> <PackageReference Include="Lib.Harmony" Version="2.2.2" />
<PackageReference Include="newtonsoft.json" Version="13.0.2" />
<PackageReference Include="Pathoschild.Stardew.ModBuildConfig" Version="4.1.0" /> <PackageReference Include="Pathoschild.Stardew.ModBuildConfig" Version="4.1.0" />
<Reference Include="./TolkDotNet.dll" /> <Reference Include="./TolkDotNet.dll" />
</ItemGroup> </ItemGroup>