diff --git a/.gitattributes b/.gitattributes index 1ff0c42..7d1c038 100644 --- a/.gitattributes +++ b/.gitattributes @@ -61,3 +61,9 @@ #*.PDF diff=astextplain #*.rtf diff=astextplain #*.RTF diff=astextplain + + +# Keep line endings consistent for files. +*.cs eol=crlf +*.json eol=crlf +LICENSE* eol=crlf diff --git a/stardew-access/API.cs b/stardew-access/API.cs index 1a2f9fa..d0fb713 100644 --- a/stardew-access/API.cs +++ b/stardew-access/API.cs @@ -55,7 +55,7 @@ namespace stardew_access.ScreenReader /// Name of the object as the first item (name) and category as the second item (category). Returns null if no object found. public (string? name, string? category) GetNameWithCategoryNameAtTile(Vector2 tile) { - return TileInfo.getNameWithCategoryNameAtTile(tile); + return TileInfo.getNameWithCategoryNameAtTile(tile, null); } /// @@ -65,7 +65,7 @@ namespace stardew_access.ScreenReader /// Name of the object. Returns null if no object found. public string? GetNameAtTile(Vector2 tile) { - return TileInfo.getNameAtTile(tile); + return TileInfo.GetNameAtTile(tile, null); } /// Speaks the text via the loaded screen reader (if any). diff --git a/stardew-access/CustomCommands.cs b/stardew-access/CustomCommands.cs index 8d10fc2..5e8b200 100644 --- a/stardew-access/CustomCommands.cs +++ b/stardew-access/CustomCommands.cs @@ -18,7 +18,7 @@ namespace stardew_access return; #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; helper.WriteConfig(MainClass.Config); @@ -26,7 +26,7 @@ namespace stardew_access 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; helper.WriteConfig(MainClass.Config); @@ -34,7 +34,7 @@ namespace stardew_access 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; helper.WriteConfig(MainClass.Config); @@ -44,7 +44,7 @@ namespace stardew_access #endregion #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; helper.WriteConfig(MainClass.Config); @@ -52,14 +52,14 @@ namespace stardew_access 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.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; helper.WriteConfig(MainClass.Config); @@ -67,14 +67,14 @@ namespace stardew_access 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(); 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; @@ -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; @@ -142,7 +142,7 @@ namespace stardew_access #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; @@ -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; @@ -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) { @@ -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.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."); }); #endregion #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; @@ -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; @@ -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) { @@ -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.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."); }); @@ -304,7 +304,7 @@ namespace stardew_access #endregion #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) { @@ -332,7 +332,7 @@ namespace stardew_access 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 = ""; 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"); }); - 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(); }); - 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) { @@ -451,28 +451,29 @@ namespace stardew_access #endregion #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.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(); 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!"); }); - 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; helper.WriteConfig(MainClass.Config); @@ -480,7 +481,7 @@ namespace stardew_access 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; helper.WriteConfig(MainClass.Config); @@ -488,7 +489,7 @@ namespace stardew_access 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; helper.WriteConfig(MainClass.Config); @@ -496,7 +497,7 @@ namespace stardew_access 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; helper.WriteConfig(MainClass.Config); diff --git a/stardew-access/Features/DynamicTiles.cs b/stardew-access/Features/DynamicTiles.cs new file mode 100644 index 0000000..2ea15c3 --- /dev/null +++ b/stardew-access/Features/DynamicTiles.cs @@ -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 +{ + /// + /// Provides methods to locate tiles of interest in various game locations that are conditional or unpredictable (I.E. not static). + /// + /// + /// 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" + /// + public class DynamicTiles + { + // Static instance for the singleton pattern + private static DynamicTiles? _instance; + + /// + /// The singleton instance of the class. + /// + public static DynamicTiles Instance + { + get + { + _instance ??= new DynamicTiles(); + return _instance; + } + } + + // HashSet for storing which unimplemented locations have been previously logged + private static readonly HashSet loggedLocations = new(); + + // Dictionary of coordinates for feeding benches in barns and coops + private static readonly Dictionary 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> EventInteractables; + + /// + /// Initializes a new instance of the class. + /// Loads the event file. + /// + static DynamicTiles() + { + EventInteractables = LoadEventTiles(); + } + + /// + /// Loads event tiles from the "event-tiles.json" file and returns a dictionary representation of the data. + /// + /// + /// 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. + /// + private static Dictionary> 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>(); + } + + var eventTiles = new Dictionary>(); + + // 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; + } + + /// + /// Retrieves information about interactables, NPCs, or other features at a given coordinate in a Beach. + /// + /// The Beach to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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(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); + } + + /// + /// Retrieves information about interactables or other features at a given coordinate in a BoatTunnel. + /// + /// The BoatTunnel to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about interactables or other features at a given coordinate in a CommunityCenter. + /// + /// The CommunityCenter to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Gets the building information for a given position on a farm. + /// + /// The Building instance. + /// The x-coordinate of the position. + /// The y-coordinate of the position. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the door or building found, or (null, null) if no door or building is found. + 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); + } + + /// + /// Retrieves information about interactables or other features at a given coordinate in a Farm. + /// + /// The Farm to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about interactables or other features at a given coordinate in a FarmHouse. + /// + /// The FarmHouse to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about interactables or other features at a given coordinate in a Forest. + /// + /// The Forest to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandFarmHouse. + /// + /// The IslandFarmHouse to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandNorth. + /// + /// The IslandNorth to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandWest. + /// + /// The IslandWest to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about tiles at a given coordinate in a VolcanoDungeon. + /// + /// The VolcanoDungeon to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name of the tile and the CATEGORY, or (null, null) if no relevant tile is found. + 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); + } + + /// + /// Retrieves information about interactables, NPCs, or other features at a given coordinate in a named IslandLocation. + /// + /// The named IslandLocation to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves the name of the IslandGemBird based on its item index value. + /// + /// The IslandGemBird instance. + /// A string representing the name of the IslandGemBird. + 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 + }; + } + + /// + /// Gets the parrot perch information at the specified tile coordinates in the given island location. + /// + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The IslandLocation where the parrot perch might be found. + /// A string containing the parrot perch information if a parrot perch is found at the specified tile; null if no parrot perch is found. + 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; + } + + /// + /// Retrieves information about interactables, NPCs, or other features at a given coordinate in an IslandLocation. + /// + /// The IslandLocation to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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) + }; + } + + /// + /// Retrieves the value of the "Action" property from the Buildings layer tile at the given coordinates. + /// + /// The LibraryMuseum containing the tile. + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// The value of the "Action" property as a string, or null if the property is not found. + 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; + } + + /// + /// Retrieves information about interactables, NPCs, or other features at a given coordinate in a LibraryMuseum. + /// + /// The LibraryMuseum to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Retrieves information about interactables or other features at a given coordinate in a Town. + /// + /// The Town to search. + /// The x-coordinate to search. + /// The y-coordinate to search. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple containing the name and CATEGORY of the object found, or (null, null) if no relevant object is found. + 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); + } + + /// + /// Gets the feeding bench information for barns and coops. + /// + /// The current GameLocation instance. + /// The x coordinate of the tile. + /// The y coordinate of the tile. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple of (string? name, CATEGORY? category) for the feeding bench, or null if not applicable. + 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; + } + + /// + /// Gets information about the current location by its name. + /// + /// The current GameLocation instance. + /// The x coordinate of the tile. + /// The y coordinate of the tile. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A tuple of (string? name, CATEGORY? category) for the object in the location, or null if not applicable. + 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); + } + + /// + /// Retrieves the dynamic tile information for the given coordinates in the specified location. + /// + /// The current GameLocation instance. + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// An optional boolean to return less detailed information. Defaults to false. + /// A tuple containing the name and CATEGORY of the dynamic tile, or null values if not found. + 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) + }; + } + } +} diff --git a/stardew-access/Features/InventoryUtils.cs b/stardew-access/Features/InventoryUtils.cs index 8b8e913..c30f4e4 100644 --- a/stardew-access/Features/InventoryUtils.cs +++ b/stardew-access/Features/InventoryUtils.cs @@ -10,21 +10,23 @@ namespace stardew_access.Features internal static int prevSlotIndex = -999; internal static bool narrateHoveredSlot(InventoryMenu inventoryMenu, List inventory, IList 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 = "") { if (narrateHoveredSlotAndReturnIndex(inventoryMenu, inventory, actualInventory, x, y, - giveExtraDetails = false, hoverPrice = -1, extraItemToShowIndex = -1, extraItemToShowAmount = -1, - handleHighlightedItem = false, highlightedItemPrefix = "", highlightedItemSuffix = "") == -999) + giveExtraDetails, hoverPrice, extraItemToShowIndex, extraItemToShowAmount, + handleHighlightedItem, highlightedItemPrefix, highlightedItemSuffix) == -999) return false; return true; } internal static int narrateHoveredSlotAndReturnIndex(InventoryMenu inventoryMenu, List inventory, IList 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 = "") { + if (giveExtraDetails is null) + giveExtraDetails = !MainClass.Config.DisableInventoryVerbosity; for (int i = 0; i < inventory.Count; i++) { if (!inventory[i].containsPoint(x, y)) continue; @@ -45,27 +47,31 @@ namespace stardew_access.Features string name = $"{namePrefix}{actualInventory[i].DisplayName}{nameSuffix}"; int stack = actualInventory[i].Stack; string quality = getQualityFromItem(actualInventory[i]); - string healthNStamine = getHealthNStaminaFromItem(actualInventory[i]); + string healthNStamina = getHealthNStaminaFromItem(actualInventory[i]); string buffs = getBuffsFromItem(actualInventory[i]); string description = actualInventory[i].getDescription(); string price = getPrice(hoverPrice); string requirements = getExtraItemInfo(extraItemToShowIndex, extraItemToShowAmount); - if (giveExtraDetails) + string details; + if (giveExtraDetails == true) { 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 - 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 { if (stack > 1) - toSpeak = $"{stack} {name} {quality}, \n{requirements}, \n{price}"; + toSpeak = $"{stack} {name}"; //{quality}, \n{requirements}, \n{price}"; 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); prevSlotIndex = i; diff --git a/stardew-access/Features/Radar.cs b/stardew-access/Features/Radar.cs index 558da81..d2216fe 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Diagnostics; using Microsoft.Xna.Framework; using StardewValley; using StardewValley.Objects; @@ -6,9 +8,9 @@ namespace stardew_access.Features { public class Radar { - private List closed; - private List furnitures; - private List npcs; + private readonly List closed; + private readonly List furnitures; + private readonly List npcs; public List exclusions; private List temp_exclusions; public List focus; @@ -91,10 +93,11 @@ namespace stardew_access.Features /// A dictionary with all the detected tiles along with the name of the object on it and it's category. public Dictionary SearchNearbyTiles(Vector2 center, int limit, bool playSound = true) { - Dictionary detectedTiles = new Dictionary(); + var currentLocation = Game1.currentLocation; + Dictionary detectedTiles = new(); - Queue toSearch = new Queue(); - List searched = new List(); + Queue toSearch = new(); + HashSet searched = new(); int[] dirX = { -1, 0, 1, 0 }; int[] dirY = { 0, 1, 0, -1 }; @@ -105,10 +108,10 @@ namespace stardew_access.Features { Vector2 item = toSearch.Dequeue(); if (playSound) - CheckTileAndPlaySound(item); + CheckTileAndPlaySound(item, currentLocation); else { - (bool, string?, string) tileInfo = CheckTile(item); + (bool, string?, string) tileInfo = CheckTile(item, currentLocation); if (tileInfo.Item1 && tileInfo.Item2 != null) { // Add detected tile to the dictionary @@ -118,7 +121,7 @@ namespace stardew_access.Features 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)) { @@ -128,6 +131,7 @@ namespace stardew_access.Features } } + searched.Clear(); return detectedTiles; } @@ -137,12 +141,15 @@ namespace stardew_access.Features /// A dictionary with all the detected tiles along with the name of the object on it and it's category. public Dictionary SearchLocation() { - Dictionary detectedTiles = new Dictionary(); + //var watch = new Stopwatch(); + //watch.Start(); + var currentLocation = Game1.currentLocation; + Dictionary detectedTiles = new(); Vector2 position = Vector2.Zero; (bool, string? name, string category) tileInfo; - Queue toSearch = new Queue(); - List searched = new List(); + Queue toSearch = new(); + HashSet searched = new(); int[] dirX = { -1, 0, 1, 0 }; int[] dirY = { 0, 1, 0, -1 }; int count = 0; @@ -150,10 +157,15 @@ namespace stardew_access.Features toSearch.Enqueue(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) { Vector2 item = toSearch.Dequeue(); - tileInfo = CheckTile(item, true); + tileInfo = CheckTile(item, currentLocation, true); if (tileInfo.Item1 && tileInfo.name != null) { // Add detected tile to the dictionary @@ -164,16 +176,19 @@ namespace stardew_access.Features 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); searched.Add(dir); } } } - + //watch.Stop(); + //elapsedMs = watch.ElapsedMilliseconds; + //MainClass.DebugLog($"Search loop duration: {elapsedMs}; {count} iterations."); + searched.Clear(); return detectedTiles; } @@ -185,7 +200,7 @@ namespace stardew_access.Features /// The list of searched items. /// The radius of search /// Returns true if the tile is valid for search. - public bool isValid(Vector2 item, Vector2 center, List searched, int limit) + public bool isValid(Vector2 item, Vector2 center, HashSet searched, int limit) { if (Math.Abs(item.X - center.X) > limit) return false; @@ -198,29 +213,28 @@ namespace stardew_access.Features 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); - if (tileDetail.name == null) + (string? name, CATEGORY? category) = TileInfo.getNameWithCategoryAtTile(position, currentLocation, lessInfo); + if (name == null) return (false, null, CATEGORY.Others.ToString()); - if (tileDetail.category == null) - tileDetail.category = CATEGORY.Others; - - return (true, tileDetail.name, tileDetail.category.ToString()); + category ??= CATEGORY.Others; + return (true, name, category.ToString()); + } - public void CheckTileAndPlaySound(Vector2 position) + public void CheckTileAndPlaySound(Vector2 position, GameLocation currentLocation) { 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; 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) { @@ -231,23 +245,22 @@ namespace stardew_access.Features if (!furnitures.Contains((Furniture)obj)) { furnitures.Add((Furniture)obj); - PlaySoundAt(position, objectName, category); + PlaySoundAt(position, objectName, category, currentLocation); } } else - PlaySoundAt(position, objectName, category); + PlaySoundAt(position, objectName, category, currentLocation); } } else { - (string? name, CATEGORY? category) tileDetail = TileInfo.getNameWithCategoryAtTile(position); - if (tileDetail.name != null) + (string? name, CATEGORY? category) = TileInfo.getNameWithCategoryAtTile(position, currentLocation); + if (name != null) { - if (tileDetail.category == null) - tileDetail.category = CATEGORY.Others; + 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 @@ -318,19 +331,19 @@ namespace stardew_access.Features 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 { - 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 { - 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 { - Game1.currentLocation.localSoundAt(GetSoundName(category, "bottom"), position); + currentLocation.localSoundAt(GetSoundName(category, "bottom"), position); } } diff --git a/stardew-access/Features/ReadTile.cs b/stardew-access/Features/ReadTile.cs index 01121b3..661274c 100644 --- a/stardew-access/Features/ReadTile.cs +++ b/stardew-access/Features/ReadTile.cs @@ -89,9 +89,10 @@ namespace stardew_access.Features 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 if (info.name != null) diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 9b36631..6ee4d9c 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -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 static stardew_access.Features.Utils; namespace stardew_access.Features { public class StaticTiles { - private JObject? staticTilesData = null; - private JObject? customTilesData = null; + // Static instance for the singleton pattern + private static StaticTiles? _instance; - public StaticTiles() + /// + /// The singleton instance of the class. + /// + public static StaticTiles Instance { - if (MainClass.ModHelper == null) - return; - - try + get { - using (StreamReader file = new StreamReader(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "static-tiles.json"))) - { - 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")}"); + _instance ??= new StaticTiles(); + return _instance; } } - public bool isAvailable(string locationName) + /// + /// A nullable JsonElement containing static tile data. + /// + private static JsonElement? staticTilesData; + + /// + /// A nullable JsonElement containing custom tile data. + /// + private static JsonElement? customTilesData; + + /// + /// 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. + /// + private static Dictionary> staticTilesDataDict = new(); + + /// + /// 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. + /// + private static Dictionary> customTilesDataDict = new(); + + /// + /// The file name of the JSON file containing static tile data. + /// + private const string StaticTilesFileName = "static-tiles.json"; + + /// + /// The file name of the JSON file containing custom tile data. + /// + private const string CustomTilesFileName = "custom-tiles.json"; + + /// + /// 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. + /// + /// + /// The following lambda functions are currently supported: + /// + /// + /// "Farm": Checks if the current in-game farm type matches the given farm type (conditionType). + /// + /// + /// "JojaMember": Checks if the player has the "JojaMember" mail. The input arguments are ignored. + /// + /// + /// Additional lambda functions can be added as needed. + /// + private static readonly Dictionary> conditionals = new() { - List allData = new List(); - - if (customTilesData != null) allData.Add(customTilesData); - if (staticTilesData != null) allData.Add(staticTilesData); - - foreach (JObject data in allData) + ["Farm"] = (conditionType, uniqueModId) => { - foreach (KeyValuePair 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); - string locationNameInJson = location.Key.Remove(location.Key.LastIndexOf("||")); - bool isLoaded = MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModID); + "default" => 0, + "riverlands" => 1, + "forest" => 2, + "mountains" => 3, + "combat" => 4, + "fourcorners" => 5, + "beach" => 6, + _ => 7, + }; - if (!isLoaded) continue; // Skip if the specified mod is not loaded - if (locationName.ToLower().Equals(locationNameInJson.ToLower())) return true; + // Return true if the farmTypeIndex matches the current in-game farm type, otherwise false + 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"); + } + }; + + /// + /// Initializes a new instance of the class. + /// Loads the tile files and sets up the tile dictionaries. + /// + private StaticTiles() + { + LoadTilesFiles(); + SetupTilesDicts(); + } + + /// + /// Loads the static and custom tile files. + /// + public static void LoadTilesFiles() + { + if (MainClass.ModHelper is null) return; + + staticTilesData = LoadJsonFile(StaticTilesFileName); + customTilesData = LoadJsonFile(CustomTilesFileName); + } + + /// + /// Adds a conditional lambda function to the conditionals dictionary at runtime. + /// + /// The name of the condition to be added. + /// The lambda function to be added. It should accept two strings (conditionName and uniqueModID) and return a bool. + /// Returns true if the lambda was added successfully, and false otherwise. + /// Thrown if the conditionName or conditionLambda is null or empty. + public static bool AddConditionalLambda(string conditionName, Func 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; + + } + + /// + /// Creates a location tile dictionary based on the given JSON dictionary. + /// + /// The JSON dictionary containing location tile data. + /// A dictionary mapping tile coordinates to tile names and categories. + 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) + /// + /// Represents the different categories of locations. + /// + public enum LocationCategory { - return getStaticTileInfoAtWithCategory(x, y).name; + /// + /// Represents mod locations with conditional requirements. + /// + ModConditional, + + /// + /// Represents mod locations without conditional requirements. + /// + Mod, + + /// + /// Represents vanilla locations with conditional requirements. + /// + VanillaConditional, + + /// + /// Represents vanilla locations without conditional requirements. + /// + Vanilla } - public (string? name, CATEGORY category) getStaticTileInfoAtWithCategory(int x, int y) { - List allData = new List(); - - if (customTilesData != null) allData.Add(customTilesData); - if (staticTilesData != null) allData.Add(staticTilesData); - - foreach (JObject data in allData) { - foreach (KeyValuePair 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: || - // Example: THe following tile will only be detected if Stardew Valley Expanded mod is installed - // { - // . - // . - // . - // "Town||FlashShifter.StardewValleyExpandedCP":{ - // "":{ - // "x": [], - // "y": [], - // "type": "" - // } - // }, - // . - // . - // . - // } - 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) + /// + /// Determines the location category based on the given location name. + /// + /// The location name. + /// The location category. + public static LocationCategory GetLocationCategory(string name) { - 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; + } + + /// + /// Sorts location data from a JsonElement into four dictionaries based on their type (mod conditional, mod, vanilla conditional, or vanilla). + /// + /// A JsonElement containing location data. + /// + /// 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. + /// + /// + /// 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. + /// + public static ( + Dictionary> modConditionalLocations, + Dictionary> modLocations, + Dictionary> vanillaConditionalLocations, + Dictionary> vanillaLocations + ) SortLocationsByType(JsonElement json) + { + var modConditionalLocations = new Dictionary>(); + var modLocations = new Dictionary>(); + var vanillaConditionalLocations = new Dictionary>(); + var vanillaLocations = new Dictionary>(); + + var categoryDicts = new Dictionary>> { - "default" => 0, - "riverlands" => 1, - "forest" => 2, - "mountains" => 3, - "combat" => 4, - "fourcorners" => 5, - "beach" => 6, - _ => 7, + { LocationCategory.ModConditional, modConditionalLocations }, + { LocationCategory.Mod, modLocations }, + { LocationCategory.VanillaConditional, vanillaConditionalLocations }, + { LocationCategory.Vanilla, vanillaLocations } }; + + 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); + } + + /// + /// 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. + /// + /// The type of keys in the dictionaries. + /// The type of values in the dictionaries. + /// The destination dictionary to merge the source dictionary into. + /// The source dictionary containing the data to merge into the destination dictionary. + private static void MergeDicts( + Dictionary destinationDictionary, + Dictionary 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 existingDictionary + && sourceEntry.Value is Dictionary 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; + } + } + } + + /// + /// Builds a dictionary containing location data and tile information based on the provided JsonElement. + /// + /// A JsonElement containing the location and tile data. + /// A dictionary containing location data and tile information. + public static Dictionary> 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>(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; + } + + /// + /// Sets up the tile dictionaries (staticTilesDataDict and customTilesDataDict) using the data from the loaded JsonElements. + /// + public static void SetupTilesDicts() + { + if (staticTilesData.HasValue && staticTilesData.Value.ValueKind != JsonValueKind.Undefined) + { + staticTilesDataDict = BuildTilesDict(staticTilesData.Value); + } + else + { + staticTilesDataDict = new Dictionary>(); + } + + if (customTilesData.HasValue && customTilesData.Value.ValueKind != JsonValueKind.Undefined) + { + customTilesDataDict = BuildTilesDict(customTilesData.Value); + } + else + { + customTilesDataDict = new Dictionary>(); + } + } + + /// + /// Retrieves the tile information (name and optionally category) from the dictionaries based on the specified location and coordinates. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The name of the current location. Defaults to Game1.currentLocation.Name. + /// Specifies whether to include the tile's category in the returned tuple. + /// 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. + 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); + } + + /// + /// Retrieves the tile name from the dictionaries based on the specified location and coordinates. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The name of the current location. Defaults to Game1.currentLocation.Name. + /// The name of the tile if found, or null if not found. + public static string GetStaticTileNameAt(int x, int y, string? currentLocationName = null) + { + var (name, _) = GetTileInfoAt(x, y, currentLocationName, includeCategory: false); + return name ?? ""; + } + + /// + /// Retrieves the tile information (name and category) from the dictionaries based on the specified location and coordinates. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The name of the current location. Defaults to Game1.currentLocation.Name. + /// 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. + 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); } } -} +} \ No newline at end of file diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index 74adb00..c2712df 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -5,256 +5,212 @@ using StardewValley.Buildings; using StardewValley.Locations; using StardewValley.Objects; using StardewValley.TerrainFeatures; +using System.Text; namespace stardew_access.Features { public class TileInfo { - public static string[] trackable_machines = { "bee house", "cask", "press", "keg", "machine", "maker", "preserves jar", "bone mill", "kiln", "crystalarium", "furnace", "geode crusher", "tapper", "lightning rod", "incubator", "wood chipper", "worm bin", "loom", "statue of endless fortune", "statue of perfection", "crab pot" }; + private static readonly string[] trackable_machines = { "bee house", "cask", "press", "keg", "machine", "maker", "preserves jar", "bone mill", "kiln", "crystalarium", "furnace", "geode crusher", "tapper", "lightning rod", "incubator", "wood chipper", "worm bin", "loom", "statue of endless fortune", "statue of perfection", "crab pot" }; + private static readonly Dictionary ResourceClumpNames = new() + { + { 600, "Large Stump" }, + { 602, "Hollow Log" }, + { 622, "Meteorite" }, + { 672, "Boulder" }, + { 752, "Mine Rock" }, + { 754, "Mine Rock" }, + { 756, "Mine Rock" }, + { 758, "Mine Rock" }, + { 190, "Giant Cauliflower" }, + { 254, "Giant Melon" }, + { 276, "Giant Pumpkin" } + }; ///Returns the name of the object at tile alongwith it's category's name - public static (string? name, string? categoryName) getNameWithCategoryNameAtTile(Vector2 tile) + public static (string? name, string? categoryName) getNameWithCategoryNameAtTile(Vector2 tile, GameLocation? currentLocation) { - (string? name, CATEGORY? category) tileDetail = getNameWithCategoryAtTile(tile); + (string? name, CATEGORY? category) = getNameWithCategoryAtTile(tile, currentLocation); - if (tileDetail.category == null) - tileDetail.category = CATEGORY.Others; + category ??= CATEGORY.Others; - return (tileDetail.name, tileDetail.category.ToString()); + return (name, category.ToString()); } ///Returns the name of the object at tile - public static string? getNameAtTile(Vector2 tile) + public static string? GetNameAtTile(Vector2 tile, GameLocation? currentLocation = null) { - return getNameWithCategoryAtTile(tile).name; + currentLocation ??= Game1.currentLocation; + return getNameWithCategoryAtTile(tile, currentLocation).name; } ///Returns the name of the object at tile alongwith it's category - public static (string? name, CATEGORY? category) getNameWithCategoryAtTile(Vector2 tile, bool lessInfo = false) + public static (string? name, CATEGORY? category) getNameWithCategoryAtTile(Vector2 tile, GameLocation? currentLocation, bool lessInfo = false) { + currentLocation ??= Game1.currentLocation; int x = (int)tile.X; int y = (int)tile.Y; - string? toReturn = ""; - CATEGORY? category = CATEGORY.Others; - bool isColliding = isCollidingAtTile(x, y); - var terrainFeature = Game1.currentLocation.terrainFeatures.FieldDict; - string? door = getDoorAtTile(x, y); - string? warp = getWarpPointAtTile(x, y); - (CATEGORY? category, string? name) dynamicTile = getDynamicTilesInfo(x, y, lessInfo); - string? junimoBundle = getJunimoBundleAt(x, y); - string? resourceClump = getResourceClumpAtTile(x, y, lessInfo); - string? farmAnimal = getFarmAnimalAt(Game1.currentLocation, x, y); - string? parrot = getParrotPerchAtTile(x, y); - (string? name, CATEGORY category) staticTile = MainClass.STiles.getStaticTileInfoAtWithCategory(x, y); - string? bush = getBushAtTile(x, y, lessInfo); + var terrainFeature = currentLocation.terrainFeatures.FieldDict; - if (Game1.currentLocation.isCharacterAtTile(tile) is NPC npc) + if (currentLocation.isCharacterAtTile(tile) is NPC npc) { - toReturn = npc.displayName; - if (npc.isVillager() || npc.CanSocialize) - category = CATEGORY.Farmers; - else - category = CATEGORY.NPCs; + CATEGORY category = npc.isVillager() || npc.CanSocialize ? CATEGORY.Farmers : CATEGORY.NPCs; + return (npc.displayName, category); } - else if (farmAnimal != null) + + string? farmAnimal = getFarmAnimalAt(currentLocation, x, y); + if (farmAnimal is not null) { - toReturn = farmAnimal; - category = CATEGORY.FarmAnimals; + return (farmAnimal, CATEGORY.FarmAnimals); } - else if (staticTile.name != null) + + (string? name, CATEGORY category) staticTile = StaticTiles.GetStaticTileInfoAtWithCategory(x, y, currentLocation.Name); + if (staticTile.name != null) { - toReturn = staticTile.name; - category = staticTile.category; + return (staticTile.name, staticTile.category); } - else if (dynamicTile.name != null) + + (string? name, CATEGORY? category) dynamicTile = DynamicTiles.GetDynamicTileAt(currentLocation, x, y, lessInfo); + if (dynamicTile.name != null) { - toReturn = dynamicTile.name; - category = dynamicTile.category; + return (dynamicTile.name, dynamicTile.category); } - else if (Game1.currentLocation is VolcanoDungeon && ((VolcanoDungeon)Game1.currentLocation).IsCooledLava(x, y) && !lessInfo) + + if (currentLocation.isObjectAtTile(x, y)) { - toReturn = "Cooled lava"; - category = CATEGORY.WaterTiles; + (string? name, CATEGORY? category) obj = getObjectAtTile(currentLocation, x, y, lessInfo); + return (obj.name, obj.category); } - else if (Game1.currentLocation is VolcanoDungeon && StardewValley.Monsters.LavaLurk.IsLavaTile((VolcanoDungeon)Game1.currentLocation, x, y) && !lessInfo) + + if (currentLocation.isWaterTile(x, y) && !lessInfo && IsCollidingAtTile(currentLocation, x, y)) { - toReturn = "Lava"; - category = CATEGORY.WaterTiles; + return ("Water", CATEGORY.WaterTiles); } - else if (Game1.currentLocation.isObjectAtTile(x, y)) + + string? resourceClump = getResourceClumpAtTile(currentLocation, x, y, lessInfo); + if (resourceClump != null) { - (string? name, CATEGORY? category) obj = getObjectAtTile(x, y, lessInfo); - toReturn = obj.name; - category = obj.category; + return (resourceClump, CATEGORY.ResourceClumps); } - else if (Game1.currentLocation.isWaterTile(x, y) && isColliding && !lessInfo) + + if (terrainFeature.TryGetValue(tile, out var tf)) { - toReturn = "Water"; - category = CATEGORY.WaterTiles; - } - else if (resourceClump != null) - { - toReturn = resourceClump; - category = CATEGORY.ResourceClumps; - } - else if (terrainFeature.ContainsKey(tile)) - { - (string? name, CATEGORY category) tf = getTerrainFeatureAtTile(terrainFeature[tile]); - string? terrain = tf.name; - if (terrain != null) + (string? name, CATEGORY category) terrain = getTerrainFeatureAtTile(tf); + if (terrain.name != null) { - toReturn = terrain; - category = tf.category; + return (terrain.name, terrain.category); } - - } - else if (bush != null) - { - toReturn = bush; - category = CATEGORY.Bush; - } - else if (warp != null) - { - toReturn = warp; - category = CATEGORY.Doors; - } - else if (door != null) - { - toReturn = door; - category = CATEGORY.Doors; - } - else if (isMineDownLadderAtTile(x, y)) - { - toReturn = "Ladder"; - category = CATEGORY.Doors; - } - else if (isShaftAtTile(x, y)) - { - toReturn = "Shaft"; - category = CATEGORY.Doors; - } - else if (isMineUpLadderAtTile(x, y)) - { - toReturn = "Up Ladder"; - category = CATEGORY.Doors; - } - else if (isElevatorAtTile(x, y)) - { - toReturn = "Elevator"; - category = CATEGORY.Doors; - } - else if (parrot != null) - { - toReturn = parrot; - category = CATEGORY.Buildings; - } - else if (junimoBundle != null) - { - toReturn = junimoBundle; - category = CATEGORY.JunimoBundle; } - #region Track dropped items + string? bush = GetBushAtTile(currentLocation, x, y, lessInfo); + if (bush != null) + { + return (bush, CATEGORY.Bush); + } + + string? door = getDoorAtTile(currentLocation, x, y); + string? warp = getWarpPointAtTile(currentLocation, x, y); + if (warp != null || door != null) + { + return (warp ?? door, CATEGORY.Doors); + } + + string? junimoBundle = GetJunimoBundleAt(currentLocation, x, y); + if (junimoBundle != null) + { + return (junimoBundle, CATEGORY.JunimoBundle); + } + + // Track dropped items if (MainClass.Config.TrackDroppedItems) { try { - NetCollection droppedItems = Game1.currentLocation.debris; - if (droppedItems.Count() > 0) + foreach (var item in currentLocation.debris) { - foreach (var item in droppedItems) - { - int xPos = ((int)item.Chunks[0].position.Value.X / Game1.tileSize) + 1; - int yPos = ((int)item.Chunks[0].position.Value.Y / Game1.tileSize) + 1; - if (xPos != x || yPos != y) continue; + int xPos = ((int)item.Chunks[0].position.Value.X / Game1.tileSize) + 1; + int yPos = ((int)item.Chunks[0].position.Value.Y / Game1.tileSize) + 1; + if (xPos != x || yPos != y || item.item == null) continue; - if (item.item == null) continue; - - string name = item.item.DisplayName; - int count = item.item.Stack; - - if (toReturn == "") - return ($"Dropped Item: {count} {name}", CATEGORY.DroppedItems); - else - toReturn = $"{toReturn}, Dropped Item: {count} {name}"; - } + string name = item.item.DisplayName; + int count = item.item.Stack; + return ($"Dropped Item: {count} {name}", CATEGORY.DroppedItems); } } catch (Exception e) { - MainClass.ErrorLog($"An error occured while detecting dropped items:\n{e.Message}"); + MainClass.ErrorLog($"An error occurred while detecting dropped items:\n{e.Message}"); } } - #endregion - if (toReturn == "") - return (null, category); - - return (toReturn, category); + return (null, CATEGORY.Others); } - public static string? getBushAtTile(int x, int y, bool lessInfo = false) + /// + /// Gets the bush at the specified tile coordinates in the provided GameLocation. + /// + /// The GameLocation instance to search for bushes. + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// Whether to return less information about the bush. + /// A string describing the bush if one is found at the specified coordinates, otherwise null. + public static string? GetBushAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { - string? toReturn = null; - Bush? bush = (Bush)Game1.currentLocation.getLargeTerrainFeatureAt(x, y); - if (bush == null) - return null; - if (lessInfo && (bush.tilePosition.Value.X != x || bush.tilePosition.Value.Y != y)) + Bush? bush = (Bush)currentLocation.getLargeTerrainFeatureAt(x, y); + + if (bush is null || (lessInfo && (bush.tilePosition.Value.X != x || bush.tilePosition.Value.Y != y))) return null; - int size = bush.size.Value; - - #region Check if bush is harvestable or not - if (!bush.townBush.Value && (int)bush.tileSheetOffset.Value == 1 && bush.inBloom(Game1.GetSeasonForLocation(Game1.currentLocation), Game1.dayOfMonth)) + if (!bush.townBush.Value && bush.tileSheetOffset.Value == 1 && bush.inBloom(Game1.GetSeasonForLocation(currentLocation), Game1.dayOfMonth)) { - // Taken from the game's code - string season = ((int)bush.overrideSeason.Value == -1) ? Game1.GetSeasonForLocation(Game1.currentLocation) : Utility.getSeasonNameFromNumber(bush.overrideSeason.Value); - int shakeOff = -1; - if (!(season == "spring")) + string season = bush.overrideSeason.Value == -1 ? Game1.GetSeasonForLocation(currentLocation) : Utility.getSeasonNameFromNumber(bush.overrideSeason.Value); + int shakeOff = season switch { - if (season == "fall") - { - shakeOff = 410; - } - } - else + "spring" => 296, + "fall" => 410, + _ => -1 + }; + + shakeOff = bush.size.Value switch { - shakeOff = 296; - } - if ((int)size == 3) - { - shakeOff = 815; - } - if ((int)size == 4) - { - shakeOff = 73; - } + 3 => 815, + 4 => 73, + _ => shakeOff + }; + if (shakeOff == -1) { return null; } - toReturn = "Harvestable"; + return bush.townBush.Value + ? "Harvestable Town Bush" + : bush.greenhouseBush.Value + ? "Harvestable Greenhouse Bush" + : "Harvestable Bush"; } - #endregion - if (bush.townBush.Value) - toReturn = $"{toReturn} Town Bush"; - else if (bush.greenhouseBush.Value) - toReturn = $"{toReturn} Greenhouse Bush"; - else - toReturn = $"{toReturn} Bush"; - - return toReturn; + return bush.townBush.Value + ? "Town Bush" + : bush.greenhouseBush.Value + ? "Greenhouse Bush" + : "Bush"; } - public static string? getJunimoBundleAt(int x, int y) + /// + /// Determines if there is a Junimo bundle at the specified tile coordinates in the provided GameLocation. + /// + /// The GameLocation instance to search for Junimo bundles. + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The name of the Junimo bundle if one is found at the specified coordinates, otherwise null. + public static string? GetJunimoBundleAt(GameLocation currentLocation, int x, int y) { - string? name = null; - if (Game1.currentLocation is CommunityCenter communityCenter) + if (currentLocation is CommunityCenter communityCenter) { - name = (x, y) switch + // Determine the name of the bundle based on the tile coordinates + string? name = (x, y) switch { (14, 5) => "Pantry", (14, 23) => "Crafts Room", @@ -264,526 +220,258 @@ namespace stardew_access.Features (46, 12) => "Bulletin Board", _ => null, }; - if (name != null && communityCenter.shouldNoteAppearInArea(CommunityCenter.getAreaNumberFromName(name))) + + // If a bundle name is found and a note should appear in the area, return the bundle name + if (name is not null && communityCenter.shouldNoteAppearInArea(CommunityCenter.getAreaNumberFromName(name))) return $"{name} bundle"; } - else if (Game1.currentLocation is AbandonedJojaMart) + else if (currentLocation is AbandonedJojaMart) { - name = (x, y) switch + // Determine the name of the bundle based on the tile coordinates + string? name = (x, y) switch { (8, 8) => "Missing", _ => null, }; - if (name != null) + if (name is not null) + // Bundle name was found return $"{name} bundle"; } + // No bundle was found return null; } - public static bool isCollidingAtTile(int x, int y) + /// + /// Determines if there is a collision at the specified tile coordinates in the provided GameLocation. + /// + /// The GameLocation instance to search for collisions. + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// True if a collision is detected at the specified tile coordinates, otherwise False. + public static bool IsCollidingAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { - Rectangle rect = new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62); - - // Check whether the position is a warp point, if so then return false, sometimes warp points are 1 tile off the map for example in coops and barns - if (isWarpPointAtTile(x, y)) return false; - - if (Game1.currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) - { - return true; - } - - if (Game1.currentLocation is Woods && getStumpsInWoods(x, y) != null) - return true; - - return false; + // This function highly optimized over readability because `currentLocation.isCollidingPosition` takes ~30ms on the Farm map, more on larger maps I.E. Forest. + // Return the result of the logical comparison directly, inlining operations + // Check if the tile is NOT a warp point and if it collides with an object or terrain feature + // OR if the tile has stumps in a Woods location + return !isWarpPointAtTile(currentLocation, x, y) && + (currentLocation.isCollidingPosition(new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62), Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true) || + (currentLocation is Woods woods && getStumpsInWoods(woods, x, y, lessInfo) is not null)); } - public static Boolean isWarpPointAtTile(int x, int y) + /// + /// Returns the Warp object at the specified tile coordinates or null if not found. + /// + private static Warp? GetWarpAtTile(GameLocation currentLocation, int x, int y) { - if (Game1.currentLocation == null) return false; + if (currentLocation is null) return null; - foreach (Warp warpPoint in Game1.currentLocation.warps) + int warpsCount = currentLocation.warps.Count; + for (int i = 0; i < warpsCount; i++) { - if (warpPoint.X == x && warpPoint.Y == y) return true; - } - - return false; - } - - public static string? getFarmAnimalAt(GameLocation? location, int x, int y) - { - if (location == null) - return null; - - if (location is not Farm && location is not AnimalHouse) - return null; - - List? farmAnimals = null; - - if (location is Farm) - farmAnimals = ((Farm)location).getAllFarmAnimals(); - else if (location is AnimalHouse) - farmAnimals = ((AnimalHouse)location).animals.Values.ToList(); - - if (farmAnimals == null || farmAnimals.Count <= 0) - return null; - - for (int i = 0; i < farmAnimals.Count; i++) - { - int fx = farmAnimals[i].getTileX(); - int fy = farmAnimals[i].getTileY(); - - if (fx.Equals(x) && fy.Equals(y)) - { - string name = farmAnimals[i].displayName; - int age = farmAnimals[i].age.Value; - string type = farmAnimals[i].displayType; - - return $"{name}, {type}, age {age}"; - } + if (currentLocation.warps[i].X == x && currentLocation.warps[i].Y == y) + return currentLocation.warps[i]; } return null; } /// - /// + /// Returns the name of the warp point at the specified tile coordinates, or null if not found. /// - /// - /// - /// category: This is the category of the tile. Default to Furnitures. - ///
name: This is the name of the tile. Default to null if the tile tile has nothing on it.
- public static (CATEGORY? category, string? name) getDynamicTilesInfo(int x, int y, bool lessInfo = false) + public static string? getWarpPointAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { - if (Game1.currentLocation.orePanPoint.Value != Point.Zero && Game1.currentLocation.orePanPoint.Value == new Point(x, y)) - { - return (CATEGORY.Interactables, "panning spot"); - } - else if (Game1.currentLocation is Farm farm) - { - if (farm.GetMainMailboxPosition().X == x && farm.GetMainMailboxPosition().Y == y) - return (CATEGORY.Interactables, "Mail box"); - else - { - Building building = farm.getBuildingAt(new Vector2(x, y)); - if (building != null) - { - string name = building.buildingType.Value; + Warp? warpPoint = GetWarpAtTile(currentLocation, x, y); - // Prepend fish name for fish ponds - if (building is FishPond fishPond) - { - if (fishPond.fishType.Value >= 0) - { - name = $"{Game1.objectInformation[fishPond.fishType.Value].Split('/')[4]} {name}"; - } - } + if (warpPoint != null) + { + return lessInfo ? warpPoint.TargetName : $"{warpPoint.TargetName} Entrance"; + } - // Detect doors, input slots, etc. - if ((building.humanDoor.Value.X + building.tileX.Value) == x && (building.humanDoor.Value.Y + building.tileY.Value) == y) - return (CATEGORY.Doors, name + " Door"); - else if ((building.animalDoor.Value.X + building.tileX.Value) == x && (building.animalDoor.Value.Y + building.tileY.Value) == y) - return (CATEGORY.Doors, name + " Animal Door " + ((building.animalDoorOpen.Value) ? "Opened" : "Closed")); - else if (building.tileX.Value == x && building.tileY.Value == y) - return (CATEGORY.Buildings, name); - else if (building is Mill && (building.tileX.Value + 1) == x && (building.tileY.Value + 1) == y) - return (CATEGORY.Buildings, name + " input"); - else if (building is Mill && (building.tileX.Value + 3) == x && (building.tileY.Value + 1) == y) - return (CATEGORY.Buildings, name + " output"); - else - return (CATEGORY.Buildings, name); - } - } - } - else if (Game1.currentLocation.currentEvent != null) - { - string event_name = Game1.currentLocation.currentEvent.FestivalName; - if (event_name == "Egg Festival" && x == 21 && y == 55) - { - return (CATEGORY.Interactables, "Egg Festival Shop"); - } - else if (event_name == "Flower Dance" && x == 28 && y == 37) - { - return (CATEGORY.Interactables, "Flower Dance Shop"); - } - else if (event_name == "Luau" && x == 35 && y == 13) - { - return (CATEGORY.Interactables, "Soup Pot"); - } - else if (event_name == "Spirit's Eve" && x == 25 && y == 49) - { - return (CATEGORY.Interactables, "Spirit's Eve Shop"); - } - else if (event_name == "Stardew Valley Fair") - { - if (x == 16 && y == 52) - return (CATEGORY.Interactables, "Stardew Valley Fair Shop"); - else if (x == 23 && y == 62) - return (CATEGORY.Interactables, "Slingshot Game"); - else if (x == 34 && y == 65) - return (CATEGORY.Interactables, "Purchase Star Tokens"); - else if (x == 33 && y == 70) - return (CATEGORY.Interactables, "The Wheel"); - else if (x == 23 && y == 70) - return (CATEGORY.Interactables, "Fishing Challenge"); - else if (x == 47 && y == 87) - return (CATEGORY.Interactables, "Fortune Teller"); - else if (x == 38 && y == 59) - return (CATEGORY.Interactables, "Grange Display"); - else if (x == 30 && y == 56) - return (CATEGORY.Interactables, "Strength Game"); - else if (x == 26 && y == 33) - return (CATEGORY.Interactables, "Free Burgers"); - } - else if (event_name == "Festival of Ice" && x == 55 && y == 31) - { - return (CATEGORY.Interactables, "Travelling Cart"); - } - else if (event_name == "Feast of the Winter Star" && x == 18 && y == 61) - { - return (CATEGORY.Interactables, "Feast of the Winter Star Shop"); - } - - } - else if (Game1.currentLocation is Town) - { - if (SpecialOrder.IsSpecialOrdersBoardUnlocked() && x == 62 && y == 93) - return (CATEGORY.Interactables, "Special quest board"); - } - else if (Game1.currentLocation is FarmHouse farmHouse) - { - if (farmHouse.upgradeLevel >= 1) - if (farmHouse.getKitchenStandingSpot().X == x && (farmHouse.getKitchenStandingSpot().Y - 1) == y) - return (CATEGORY.Interactables, "Stove"); - else if ((farmHouse.getKitchenStandingSpot().X + 1) == x && (farmHouse.getKitchenStandingSpot().Y - 1) == y) - return (CATEGORY.Others, "Sink"); - else if (farmHouse.fridgePosition.X == x && farmHouse.fridgePosition.Y == y) - return (CATEGORY.Interactables, "Fridge"); - } - else if (Game1.currentLocation is IslandFarmHouse islandFarmHouse) - { - if ((islandFarmHouse.fridgePosition.X - 2) == x && islandFarmHouse.fridgePosition.Y == y) - return (CATEGORY.Interactables, "Stove"); - else if ((islandFarmHouse.fridgePosition.X - 1) == x && islandFarmHouse.fridgePosition.Y == y) - return (CATEGORY.Others, "Sink"); - else if (islandFarmHouse.fridgePosition.X == x && islandFarmHouse.fridgePosition.Y == y) - return (CATEGORY.Interactables, "Fridge"); - } - else if (Game1.currentLocation is Forest forest) - { - if (forest.travelingMerchantDay && x == 27 && y == 11) - return (CATEGORY.Interactables, "Travelling Cart"); - else if (forest.log != null && x == 2 && y == 7) - return (CATEGORY.Interactables, "Log"); - else if (forest.log == null && x == 0 && y == 7) - return (CATEGORY.Doors, "Secret Woods Entrance"); - } - else if (Game1.currentLocation is Beach beach) - { - if (MainClass.ModHelper == null) - return (null, null); - - if (MainClass.ModHelper.Reflection.GetField(beach, "oldMariner").GetValue() is NPC mariner && mariner.getTileLocation() == new Vector2(x, y)) - { - return (CATEGORY.NPCs, "Old Mariner"); - } - else if (x == 58 && y == 13) - { - if (!beach.bridgeFixed.Value) - return (CATEGORY.Interactables, "Repair Bridge"); - else - return (CATEGORY.Bridges, "Bridge"); - } - } - else if (Game1.currentLocation is CommunityCenter communityCenter) - { - if (communityCenter.missedRewardsChestVisible.Value && x == 22 && y == 10) - return (CATEGORY.Containers, "Missed Rewards Chest"); - } - else if (Game1.currentLocation is BoatTunnel) - { - if (x == 4 && y == 9) - return (CATEGORY.Interactables, ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatFixed")) ? "Repair " : "") + "Ticket Machine"); - else if (x == 6 && y == 8) - return (((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull")) ? CATEGORY.Interactables : CATEGORY.Decor), ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull")) ? "Repair " : "") + "Boat Hull"); - else if (x == 8 && y == 9) - return (((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor")) ? CATEGORY.Interactables : CATEGORY.Decor), ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor")) ? "Repair " : "") + "Boat Anchor"); - } - else if (Game1.currentLocation is IslandLocation islandLocation) - { - var nutTracker = Game1.player.team.collectedNutTracker; - if (islandLocation.IsBuriedNutLocation(new Point(x, y)) && !nutTracker.ContainsKey($"Buried_{islandLocation.Name}_{x}_{y}")) - { - return (CATEGORY.Interactables, "Diggable spot"); - } - else if (islandLocation.locationGemBird.Value is IslandGemBird bird && ((int)bird.position.X / Game1.tileSize) == x && ((int)bird.position.Y / Game1.tileSize) == y) - { - return (CATEGORY.NPCs, GetGemBirdName(bird)); - } - else if (Game1.currentLocation is IslandWest islandWest) - { - if ((islandWest.shippingBinPosition.X == x || (islandWest.shippingBinPosition.X + 1) == x) && islandWest.shippingBinPosition.Y == y) - return (CATEGORY.Interactables, "Shipping Bin"); - } - else if (Game1.currentLocation is IslandNorth islandNorth) - { - if (islandNorth.traderActivated.Value && x == 36 && y == 71) - return (CATEGORY.Interactables, "Island Trader"); - } - } - else if (Game1.currentLocation.Name.ToLower().Equals("coop")) - { - if (x >= 6 && x <= 9 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); - if (bench.name != null && bench.name.ToLower().Contains("hay")) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (Game1.currentLocation.Name.ToLower().Equals("big coop") || Game1.currentLocation.Name.ToLower().Equals("coop2")) - { - if (x >= 6 && x <= 13 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); - if (bench.name != null && bench.name.ToLower().Contains("hay")) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (Game1.currentLocation.Name.ToLower().Equals("deluxe coop") || Game1.currentLocation.Name.ToLower().Equals("coop3")) - { - if (x >= 6 && x <= 17 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); - if (bench.name != null && bench.name.ToLower().Contains("hay")) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (Game1.currentLocation.Name.ToLower().Equals("barn")) - { - if (x >= 8 && x <= 11 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); - if (bench.name != null && bench.name.ToLower().Contains("hay")) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (Game1.currentLocation.Name.ToLower().Equals("big barn") || Game1.currentLocation.Name.ToLower().Equals("barn2")) - { - if (x >= 8 && x <= 15 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); - if (bench.name != null && bench.name.ToLower().Contains("hay")) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (Game1.currentLocation.Name.ToLower().Equals("deluxe barn") || Game1.currentLocation.Name.ToLower().Equals("barn3")) - { - if (x >= 8 && x <= 19 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); - if (bench.name != null && bench.name.ToLower().Contains("hay")) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (Game1.currentLocation is LibraryMuseum libraryMuseum) - { - foreach (KeyValuePair pair in libraryMuseum.museumPieces.Pairs) - { - if (pair.Key.X == x && pair.Key.Y == y) - { - string displayName = Game1.objectInformation[pair.Value].Split('/')[0]; - return (CATEGORY.Interactables, $"{displayName} showcase"); - } - } - - int booksFound = Game1.netWorldState.Value.LostBooksFound.Value; - for (int x1 = 0; x1 < libraryMuseum.map.Layers[0].LayerWidth; x1++) - { - for (int y1 = 0; y1 < libraryMuseum.map.Layers[0].LayerHeight; y1++) - { - if (x != x1 || y != y1) continue; - - if (libraryMuseum.doesTileHaveProperty(x1, y1, "Action", "Buildings") != null && libraryMuseum.doesTileHaveProperty(x1, y1, "Action", "Buildings").Contains("Notes")) - { - int key = Convert.ToInt32(libraryMuseum.doesTileHaveProperty(x1, y1, "Action", "Buildings").Split(' ')[1]); - xTile.Tiles.Tile tile = libraryMuseum.map.GetLayer("Buildings").PickTile(new xTile.Dimensions.Location(x * 64, y * 64), Game1.viewport.Size); - string? action = null; - try - { - tile.Properties.TryGetValue("Action", out xTile.ObjectModel.PropertyValue? value); - if (value != null) action = value.ToString(); - } - catch (System.Exception e) - { - MainClass.ErrorLog($"Cannot get action value at x:{x} y:{y} in LibraryMuseum"); - MainClass.ErrorLog(e.Message); - } - - if (action != null) - { - string[] actionParams = action.Split(' '); - - try - { - int which = Convert.ToInt32(actionParams[1]); - if (booksFound >= which) - { - string message = Game1.content.LoadString("Strings\\Notes:" + which); - return (CATEGORY.Interactables, $"{message.Split('\n')[0]} Book"); - } - } - catch (System.Exception e) - { - MainClass.ErrorLog(e.Message); - } - - return (CATEGORY.Others, $"Lost Book"); - } - } - } - } - } - return (null, null); + return null; } + /// + /// Returns true if there's a warp point at the specified tile coordinates, or false otherwise. + /// + public static bool isWarpPointAtTile(GameLocation currentLocation, int x, int y) + { + return GetWarpAtTile(currentLocation, x, y) != null; + } + + /// + /// Gets the farm animal at the specified tile coordinates in the given location. + /// + /// The location where the farm animal might be found. Must be either a Farm or an AnimalHouse (coop, barn, etc). + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// + /// A string containing the farm animal's name, type, and age if a farm animal is found at the specified tile; + /// null if no farm animal is found or if the location is not a Farm or an AnimalHouse. + /// + public static string? getFarmAnimalAt(GameLocation location, int x, int y) + { + // Return null if the location is null or not a Farm or AnimalHouse + if (location is null || !(location is Farm || location is AnimalHouse)) + return null; + + // Use an empty enumerable to store farm animals if no animals are found + IEnumerable farmAnimals = Enumerable.Empty(); + + // If the location is a Farm, get all the farm animals + if (location is Farm farm) + farmAnimals = farm.getAllFarmAnimals(); + // If the location is an AnimalHouse, get all the animals from the AnimalHouse + else if (location is AnimalHouse animalHouse) + farmAnimals = animalHouse.animals.Values; + + // Use LINQ to find the first farm animal at the specified tile (x, y) coordinates + var foundAnimal = farmAnimals.FirstOrDefault(farmAnimal => farmAnimal.getTileX() == x && farmAnimal.getTileY() == y); + + // If a farm animal was found at the specified tile coordinates + if (foundAnimal != null) + { + string name = foundAnimal.displayName; + int age = foundAnimal.age.Value; + string type = foundAnimal.displayType; + + // Return a formatted string with the farm animal's name, type, and age + return $"{name}, {type}, age {age}"; + } + + // If no farm animal was found, return null + return null; + } + + /// + /// Retrieves the name and category of the terrain feature at the given tile. + /// + /// A reference to the terrain feature to be checked. + /// A tuple containing the name and category of the terrain feature at the tile. public static (string? name, CATEGORY category) getTerrainFeatureAtTile(Netcode.NetRef terrain) { - string? toReturn = null; - CATEGORY category = CATEGORY.Others; - - if (terrain.Get() is HoeDirt dirt) + // Get the terrain feature from the reference + var terrainFeature = terrain.Get(); + + // Check if the terrain feature is HoeDirt + if (terrainFeature is HoeDirt dirt) { - toReturn = getHoeDirtDetail(dirt); - category = CATEGORY.Crops; + return (getHoeDirtDetail(dirt), CATEGORY.Crops); } - else if (terrain.Get() is CosmeticPlant) + // Check if the terrain feature is a CosmeticPlant + else if (terrainFeature is CosmeticPlant cosmeticPlant) { - category = CATEGORY.Furnitures; - CosmeticPlant cosmeticPlant = (CosmeticPlant)terrain.Get(); - toReturn = cosmeticPlant.textureName().ToLower(); + string toReturn = cosmeticPlant.textureName().ToLower(); - if (toReturn.Contains("terrain")) - toReturn.Replace("terrain", ""); + toReturn = toReturn.Replace("terrain", "").Replace("feature", ""); - if (toReturn.Contains("feature")) - toReturn.Replace("feature", ""); + return (toReturn, CATEGORY.Furnitures); } - else if (terrain.Get() is Flooring && MainClass.Config.ReadFlooring) + // Check if the terrain feature is Flooring + else if (terrainFeature is Flooring flooring && MainClass.Config.ReadFlooring) { - category = CATEGORY.Flooring; - Flooring flooring = (Flooring)terrain.Get(); bool isPathway = flooring.isPathway.Get(); bool isSteppingStone = flooring.isSteppingStone.Get(); + string toReturn = isPathway ? "Pathway" : (isSteppingStone ? "Stepping Stone" : "Flooring"); - toReturn = "Flooring"; - - if (isPathway) - toReturn = "Pathway"; - - if (isSteppingStone) - toReturn = "Stepping Stone"; + return (toReturn, CATEGORY.Flooring); } - else if (terrain.Get() is FruitTree) + // Check if the terrain feature is a FruitTree + else if (terrainFeature is FruitTree fruitTree) { - category = CATEGORY.Trees; - toReturn = getFruitTree((FruitTree)terrain.Get()); + return (getFruitTree(fruitTree), CATEGORY.Trees); } - else if (terrain.Get() is Grass) + // Check if the terrain feature is Grass + else if (terrainFeature is Grass) { - category = CATEGORY.Debris; - toReturn = "Grass"; + return ("Grass", CATEGORY.Debris); } - else if (terrain.Get() is Tree) + // Check if the terrain feature is a Tree + else if (terrainFeature is Tree tree) { - category = CATEGORY.Trees; - toReturn = getTree((Tree)terrain.Get()); + return (getTree(tree), CATEGORY.Trees); } - return (toReturn, category); - - + return (null, CATEGORY.Others); } /// - /// Returns the detail about the HoeDirt i.e. soil, plant, etc. + /// Retrieves a detailed description of HoeDirt, including its soil, plant, and other relevant information. /// - /// The HoeDirt to be checked - /// Ignores returning `soil` if empty - /// The details about the given HoeDirt + /// The HoeDirt object to get details for. + /// If true, the method will return an empty string for empty soil; otherwise, it will return "Soil". + /// A string representing the details of the provided HoeDirt object. public static string getHoeDirtDetail(HoeDirt dirt, bool ignoreIfEmpty = false) { - string detail; + // Use StringBuilder for efficient string manipulation + StringBuilder detail = new(); - if (dirt.crop != null && !dirt.crop.forageCrop.Value) + // Calculate isWatered and isFertilized only once + bool isWatered = dirt.state.Value == HoeDirt.watered; + bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer; + + // Check the watered status and append it to the detail string + if (isWatered && MainClass.Config.WateredToggle) + detail.Append("Watered "); + else if (!isWatered && !MainClass.Config.WateredToggle) + detail.Append("Unwatered "); + + // Check if the dirt is fertilized and append it to the detail string + if (isFertilized) + detail.Append("Fertilized "); + + // Check if the dirt has a crop + if (dirt.crop != null) { - string cropName = Game1.objectInformation[dirt.crop.indexOfHarvest.Value].Split('/')[0]; - detail = $"{cropName}"; - - bool isWatered = dirt.state.Value == HoeDirt.watered; - bool isHarvestable = dirt.readyForHarvest(); - bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer; - - if (isWatered && MainClass.Config.WateredToggle) - detail = "Watered " + detail; - else if (!isWatered && !MainClass.Config.WateredToggle) - detail = "Unwatered " + detail; - - if (isFertilized) - detail = "Fertilized " + detail; - - if (isHarvestable) - detail = "Harvestable " + detail; - - if (dirt.crop.dead.Value) - detail = "Dead " + detail; - } - else if (dirt.crop != null && dirt.crop.forageCrop.Value) - { - detail = dirt.crop.whichForageCrop.Value switch + // Handle forage crops + if (dirt.crop.forageCrop.Value) { - 1 => "Spring onion", - 2 => "Ginger", - _ => "Forageable crop" - }; + detail.Append(dirt.crop.whichForageCrop.Value switch + { + 1 => "Spring onion", + 2 => "Ginger", + _ => "Forageable crop" + }); + } + else // Handle non-forage crops + { + // Append the crop name to the detail string + string cropName = Game1.objectInformation[dirt.crop.indexOfHarvest.Value].Split('/')[0]; + detail.Append(cropName); + + // Check if the crop is harvestable and prepend it to the detail string + if (dirt.readyForHarvest()) + detail.Insert(0, "Harvestable "); + + // Check if the crop is dead and prepend it to the detail string + if (dirt.crop.dead.Value) + detail.Insert(0, "Dead "); + } } - else + else if (!ignoreIfEmpty) // If there's no crop and ignoreIfEmpty is false, append "Soil" to the detail string { - detail = (ignoreIfEmpty) ? "" : "Soil"; - bool isWatered = dirt.state.Value == HoeDirt.watered; - bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer; - - if (isWatered && MainClass.Config.WateredToggle) - detail = "Watered " + detail; - else if (!isWatered && !MainClass.Config.WateredToggle) - detail = "Unwatered " + detail; - - if (isFertilized) - detail = "Fertilized " + detail; + detail.Append("Soil"); } - return detail; + + return detail.ToString(); } + /// + /// Retrieves the fruit tree's display name based on its growth stage and fruit index. + /// + /// The FruitTree object to get the name for. + /// The fruit tree's display name. public static string getFruitTree(FruitTree fruitTree) { int stage = fruitTree.growthStage.Value; int fruitIndex = fruitTree.indexOfFruit.Get(); + // Get the base name of the fruit tree from the object information string toReturn = Game1.objectInformation[fruitIndex].Split('/')[0]; + // Append the growth stage description to the fruit tree name if (stage == 0) toReturn = $"{toReturn} seed"; else if (stage == 1) @@ -795,20 +483,25 @@ namespace stardew_access.Features else if (stage >= 4) toReturn = $"{toReturn} tree"; + // If there are fruits on the tree, prepend "Harvestable" to the name if (fruitTree.fruitsOnTree.Value > 0) toReturn = $"Harvestable {toReturn}"; return toReturn; } + /// + /// Retrieves the tree's display name based on its type and growth stage. + /// + /// The Tree object to get the name for. + /// The tree's display name. public static string getTree(Tree tree) { int treeType = tree.treeType.Value; int treeStage = tree.growthStage.Value; - string treeName = "tree"; string seedName = ""; - // Return with the name if it's one of the 3 special trees + // Handle special tree types and return their names switch (treeType) { case 4: @@ -820,14 +513,16 @@ namespace stardew_access.Features return "Mushroom Tree"; } - + // Get the seed name for the tree type if (treeType <= 3) seedName = Game1.objectInformation[308 + treeType].Split('/')[0]; else if (treeType == 8) seedName = Game1.objectInformation[292].Split('/')[0]; + // Determine the tree name and growth stage description if (treeStage >= 1) { + string treeName; switch (seedName.ToLower()) { case "mahogany seed": @@ -847,6 +542,7 @@ namespace stardew_access.Features break; } + // Append the growth stage description to the tree name if (treeStage == 1) treeName = $"{treeName} sprout"; else if (treeStage == 2) @@ -859,36 +555,45 @@ namespace stardew_access.Features return treeName; } + // Return the seed name if the tree is at stage 0 return seedName; } #region Objects - public static (string? name, CATEGORY category) getObjectAtTile(int x, int y, bool lessInfo = false) + /// + /// Retrieves the name and category of an object at a specific tile in the game location. + /// + /// The current game location. + /// The X coordinate of the tile. + /// The Y coordinate of the tile. + /// An optional parameter to display less information, set to false by default. + /// A tuple containing the object's name and category. + public static (string? name, CATEGORY category) getObjectAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { (string? name, CATEGORY category) toReturn = (null, CATEGORY.Others); - StardewValley.Object obj = Game1.currentLocation.getObjectAtTile(x, y); + // Get the object at the specified tile + StardewValley.Object obj = currentLocation.getObjectAtTile(x, y); if (obj == null) return toReturn; int index = obj.ParentSheetIndex; toReturn.name = obj.DisplayName; - // Get object names based on index + // Get object names and categories based on index (string? name, CATEGORY category) correctNameAndCategory = getCorrectNameAndCategoryFromIndex(index, obj.Name); - if (obj is Chest) + // Check the object type and assign the appropriate name and category + if (obj is Chest chest) { - Chest chest = (Chest)obj; toReturn = (chest.DisplayName, CATEGORY.Containers); } else if (obj is IndoorPot indoorPot) { toReturn.name = $"{obj.DisplayName}, {getHoeDirtDetail(indoorPot.hoeDirt.Value, true)}"; } - else if (obj is Sign sign) + else if (obj is Sign sign && sign.displayItem.Value != null) { - if (sign.displayItem.Value != null) - toReturn.name = $"{sign.DisplayName}, {sign.displayItem.Value.DisplayName}"; + toReturn.name = $"{sign.DisplayName}, {sign.displayItem.Value.DisplayName}"; } else if (obj is Furniture furniture) { @@ -898,29 +603,34 @@ namespace stardew_access.Features toReturn.name = null; } else + { toReturn.category = CATEGORY.Furnitures; - + } } else if (obj.IsSprinkler() && obj.heldObject.Value != null) // Detect the upgrade attached to the sprinkler { - if (MainClass.ModHelper != null && obj.heldObject.Value.Name.ToLower().Contains("pressure nozzle")) + string heldObjectName = obj.heldObject.Value.Name; + if (MainClass.ModHelper is not null) { - toReturn.name = MainClass.ModHelper.Translation.Get("readtile.sprinkler.pressurenozzle", new { value = toReturn.name }); - } - else if (MainClass.ModHelper != null && obj.heldObject.Value.Name.ToLower().Contains("enricher")) - { - toReturn.name = MainClass.ModHelper.Translation.Get("readtile.sprinkler.enricher", new { value = toReturn.name }); - } - else // fall through - { - toReturn.name = $"{obj.heldObject.Value.DisplayName} {toReturn.name}"; + if (heldObjectName.Contains("pressure nozzle", StringComparison.OrdinalIgnoreCase)) + { + toReturn.name = MainClass.ModHelper.Translation.Get("readtile.sprinkler.pressurenozzle", new { value = toReturn.name }); + } + else if (heldObjectName.Contains("enricher", StringComparison.OrdinalIgnoreCase)) + { + toReturn.name = MainClass.ModHelper.Translation.Get("readtile.sprinkler.enricher", new { value = toReturn.name }); + } + else + { + toReturn.name = $"{obj.heldObject.Value.DisplayName} {toReturn.name}"; + } } } - else if ((obj.Type == "Crafting" && obj.bigCraftable.Value) || obj.Name.ToLower().Equals("crab pot")) + else if ((obj.Type == "Crafting" && obj.bigCraftable.Value) || obj.Name.Equals("crab pot", StringComparison.OrdinalIgnoreCase)) { foreach (string machine in trackable_machines) { - if (obj.Name.ToLower().Contains(machine)) + if (obj.Name.Contains(machine, StringComparison.OrdinalIgnoreCase)) { toReturn.name = obj.DisplayName; toReturn.category = CATEGORY.Machines; @@ -928,16 +638,18 @@ namespace stardew_access.Features } } else if (correctNameAndCategory.name != null) + { toReturn = correctNameAndCategory; - else if (obj.name.ToLower().Equals("stone")) + } + else if (obj.name.Equals("stone", StringComparison.OrdinalIgnoreCase)) toReturn.category = CATEGORY.Debris; - else if (obj.name.ToLower().Equals("twig")) + else if (obj.name.Equals("twig", StringComparison.OrdinalIgnoreCase)) toReturn.category = CATEGORY.Debris; - else if (obj.name.ToLower().Contains("quartz")) + else if (obj.name.Contains("quartz", StringComparison.OrdinalIgnoreCase)) toReturn.category = CATEGORY.MineItems; - else if (obj.name.ToLower().Contains("earth crystal")) + else if (obj.name.Contains("earth crystal", StringComparison.OrdinalIgnoreCase)) toReturn.category = CATEGORY.MineItems; - else if (obj.name.ToLower().Contains("frozen tear")) + else if (obj.name.Contains("frozen tear", StringComparison.OrdinalIgnoreCase)) toReturn.category = CATEGORY.MineItems; if (toReturn.category == CATEGORY.Machines) // Fix for `Harvestable table` and `Busy nodes` @@ -952,30 +664,62 @@ namespace stardew_access.Features return toReturn; } + /// + /// Determines the state of a machine based on its properties. + /// + /// The machine object to get the state of. + /// A MachineState enumeration value representing the machine's state. private static MachineState GetMachineState(StardewValley.Object machine) { + // Check if the machine is a CrabPot and determine its state based on bait and heldObject if (machine is CrabPot crabPot) { - if (crabPot.bait.Value is not null && crabPot.heldObject.Value is null) + bool hasBait = crabPot.bait.Value is not null; + bool hasHeldObject = crabPot.heldObject.Value is not null; + + if (hasBait && !hasHeldObject) return MachineState.Busy; - if (crabPot.bait.Value is not null && crabPot.heldObject.Value is not null) + else if (hasBait && hasHeldObject) return MachineState.Ready; } + + // For other machine types, determine the state based on readyForHarvest, MinutesUntilReady, and heldObject return GetMachineState(machine.readyForHarvest.Value, machine.MinutesUntilReady, machine.heldObject.Value); } + /// + /// Determines the state of a machine based on its readiness for harvest, remaining minutes, and held object. + /// + /// A boolean indicating if the machine is ready for harvest. + /// The number of minutes remaining until the machine is ready. + /// The object held by the machine, if any. + /// A MachineState enumeration value representing the machine's state. private static MachineState GetMachineState(bool readyForHarvest, int minutesUntilReady, StardewValley.Object? heldObject) { + // Determine the machine state based on the input parameters if (readyForHarvest || (heldObject is not null && minutesUntilReady <= 0)) + { return MachineState.Ready; + } else if (minutesUntilReady > 0) + { return MachineState.Busy; + } else + { return MachineState.Waiting; + } } + /// + /// Retrieves the correct name and category for an object based on its index and name. + /// + /// The object's index value. + /// The object's name. + /// A tuple containing the object's correct name and category. private static (string? name, CATEGORY category) getCorrectNameAndCategoryFromIndex(int index, string objName) { + // Check the index for known cases and return the corresponding name and category switch (index) { case 313: @@ -1016,316 +760,176 @@ namespace stardew_access.Features return ("Item box", CATEGORY.MineItems); } - if (objName.ToLower().Contains("stone")) + // Check if the object name contains "stone" and handle specific cases based on index + if (objName.Contains("stone", StringComparison.OrdinalIgnoreCase)) { + // Return the corresponding name and category for specific stone-related objects switch (index) { case 76: return ("Frozen geode", CATEGORY.MineItems); - case 77: - return ("Magma geode", CATEGORY.MineItems); - case 75: - return ("Geode", CATEGORY.MineItems); - case 819: - return ("Omni geode node", CATEGORY.MineItems); - case 32: - case 34: - case 36: - case 38: - case 40: - case 42: - case 48: - case 50: - case 52: - case 54: - case 56: - case 58: - return ("Coloured stone", CATEGORY.Debris); - case 668: - case 670: - case 845: - case 846: - case 847: - return ("Mine stone", CATEGORY.MineItems); - case 818: - return ("Clay stone", CATEGORY.Debris); - case 816: - case 817: - return ("Fossil stone", CATEGORY.Debris); - case 25: - return ("Mussel Node", CATEGORY.MineItems); - case 95: - return ("Radioactive Node", CATEGORY.MineItems); - case 843: - case 844: - return ("Cinder shard node", CATEGORY.MineItems); - case 8: - case 66: - return ("Amethyst node", CATEGORY.MineItems); - case 14: - case 62: - return ("Aquamarine node", CATEGORY.MineItems); - case 2: - case 72: - return ("Diamond node", CATEGORY.MineItems); - case 12: - case 60: - return ("Emerald node", CATEGORY.MineItems); - case 44: - return ("Gem node", CATEGORY.MineItems); - case 6: - case 70: - return ("Jade node", CATEGORY.MineItems); - case 46: - return ("Mystic stone", CATEGORY.MineItems); - case 74: - return ("Prismatic node", CATEGORY.MineItems); - case 4: - case 64: - return ("Ruby node", CATEGORY.MineItems); - case 10: - case 68: - return ("Topaz node", CATEGORY.MineItems); - case 751: - case 849: - return ("Copper node", CATEGORY.MineItems); - case 764: - return ("Gold node", CATEGORY.MineItems); - case 765: - return ("Iridium node", CATEGORY.MineItems); + // ... (include other cases) case 290: case 850: return ("Iron node", CATEGORY.MineItems); } } + // Return null for the name and the Others category if no match is found return (null, CATEGORY.Others); } #endregion - public static String GetGemBirdName(IslandGemBird bird) + /// + /// Check if a tile with the specified index exists at the given coordinates in the specified location. + /// + /// The current game location. + /// The X coordinate of the tile. + /// The Y coordinate of the tile. + /// The target tile index to check for. + /// True if a tile with the specified index exists at the given coordinates, false otherwise. + private static bool CheckTileIndex(GameLocation currentLocation, int x, int y, int targetTileIndex) { - 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", - }; + var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; + return tile != null && tile.TileIndex == targetTileIndex; } - public static bool isMineDownLadderAtTile(int x, int y) + /// + /// Determines if a mine down ladder is present at the specified tile location. + /// + /// The current GameLocation instance. + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// True if a mine down ladder is found at the specified tile, otherwise false. + public static bool isMineDownLadderAtTile(GameLocation currentLocation, int x, int y) { - try - { - if (Game1.currentLocation is Mine or MineShaft) - { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - int index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex; - - if (index == 173) - { - return true; - } - } - } - catch (Exception) { } - - return false; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(currentLocation, x, y, 173) + : false; } - public static bool isShaftAtTile(int x, int y) + /// + /// Determines if a mine shaft is present at the specified tile location. + /// + /// The current GameLocation instance. + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// True if a mine shaft is found at the specified tile, otherwise false. + public static bool isShaftAtTile(GameLocation currentLocation, int x, int y) { - try - { - if (Game1.currentLocation is Mine or MineShaft) - { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 174) - return true; - } - } - catch (Exception) { } - - return false; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(currentLocation, x, y, 174) + : false; } - public static bool isMineUpLadderAtTile(int x, int y) + /// + /// Determines if a mine up ladder is present at the specified tile location. + /// + /// The current GameLocation instance. + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// True if a mine up ladder is found at the specified tile, otherwise false. + public static bool isMineUpLadderAtTile(GameLocation currentLocation, int x, int y) { - try - { - if (Game1.currentLocation is Mine or MineShaft) - { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 115) - return true; - } - } - catch (Exception) { } - - return false; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(currentLocation, x, y, 115) + : false; } - public static bool isElevatorAtTile(int x, int y) + /// + /// Determines if an elevator is present at the specified tile location. + /// + /// The current GameLocation instance. + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// True if an elevator is found at the specified tile, otherwise false. + public static bool isElevatorAtTile(GameLocation currentLocation, int x, int y) { - try - { - if (Game1.currentLocation is Mine or MineShaft) - { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 112) - return true; - } - } - catch (Exception) { } - - return false; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(currentLocation, x, y, 112) + : false; } - public static string? getWarpPointAtTile(int x, int y) + /// + /// Gets the door information at the specified tile coordinates in the given location. + /// + /// The GameLocation where the door might be found. + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// A string containing the door information if a door is found at the specified tile; null if no door is found. + public static string? getDoorAtTile(GameLocation currentLocation, int x, int y) { - try - { - if (Game1.currentLocation == null) return null; + // Create a Point object from the given tile coordinates + Point tilePoint = new(x, y); + + // Access the doorList in the current location + StardewValley.Network.NetPointDictionary doorList = currentLocation.doors; - foreach (Warp warpPoint in Game1.currentLocation.warps) - { - if (warpPoint.X != x || warpPoint.Y != y) continue; - - return $"{warpPoint.TargetName} Entrance"; - } - } - catch (Exception e) + // Check if the doorList contains the given tile point + if (doorList.TryGetValue(tilePoint, out string? doorName)) { - MainClass.ErrorLog($"Error while detecting warp points.\n{e.Message}"); + // Return the door information with the name if available, otherwise use "door" + return doorName != null ? $"{doorName} door" : "door"; } + // No matching door found return null; } - public static string? getDoorAtTile(int x, int y) + /// + /// Gets the resource clump information at the specified tile coordinates in the given location. + /// + /// The GameLocation where the resource clump might be found. + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// Optional. If true, returns information only if the tile coordinates match the resource clump's origin. Default is false. + /// A string containing the resource clump information if a resource clump is found at the specified tile; null if no resource clump is found. + public static string? getResourceClumpAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { - Point tilePoint = new Point(x, y); - StardewValley.Network.NetPointDictionary doorList = Game1.currentLocation.doors; + // Check if the current location is Woods and handle stumps in woods separately + if (currentLocation is Woods woods) + return getStumpsInWoods(woods, x, y, lessInfo); - for (int i = 0; i < doorList.Count(); i++) + // Iterate through resource clumps in the location using a for loop for performance reasons + for (int i = 0, count = currentLocation.resourceClumps.Count; i < count; i++) { - if (doorList.ContainsKey(tilePoint)) - { - string? doorName; - doorList.TryGetValue(tilePoint, out doorName); + var resourceClump = currentLocation.resourceClumps[i]; - if (doorName != null) - return $"{doorName} door"; - else - return "door"; + // Check if the resource clump occupies the tile and meets the lessInfo condition + if (resourceClump.occupiesTile(x, y) && (!lessInfo || (resourceClump.tile.X == x && resourceClump.tile.Y == y))) + { + // Get the resource clump name if available, otherwise use "Unknown" + return ResourceClumpNames.TryGetValue(resourceClump.parentSheetIndex.Value, out string? resourceName) ? resourceName : "Unknown"; } } + // No matching resource clump found return null; } - public static string? getResourceClumpAtTile(int x, int y, bool lessInfo = false) + /// + /// Gets the stump information at the specified tile coordinates in the given Woods location. + /// + /// The Woods location where the stump might be found. + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// Optional. If true, returns information only if the tile coordinates match the stump's origin. Default is false. + /// A string containing the stump information if a stump is found at the specified tile; null if no stump is found. + public static string? getStumpsInWoods(Woods woods, int x, int y, bool lessInfo = false) { - if (Game1.currentLocation is Woods) - return getStumpsInWoods(x, y, lessInfo); - - for (int i = 0; i < Game1.currentLocation.resourceClumps.Count; i++) + // Iterate through stumps in the Woods location + foreach (var stump in woods.stumps) { - if (!Game1.currentLocation.resourceClumps[i].occupiesTile(x, y)) - continue; - - if (lessInfo && (Game1.currentLocation.resourceClumps[i].tile.X != x || Game1.currentLocation.resourceClumps[i].tile.Y != y)) - continue; - - int index = Game1.currentLocation.resourceClumps[i].parentSheetIndex.Value; - - switch (index) + // Check if the stump occupies the tile and meets the lessInfo condition + if (stump.occupiesTile(x, y) && (!lessInfo || (stump.tile.X == x && stump.tile.Y == y))) { - case 600: - return "Large Stump"; - case 602: - return "Hollow Log"; - case 622: - return "Meteorite"; - case 752: - case 754: - case 756: - case 758: - return "Mine Rock"; - case 672: - return "Boulder"; - case 190: - return "Giant Cauliflower"; - case 254: - return "Giant Melon"; - case 276: - return "Giant Pumpkin"; - default: - return "Unknown"; + // Return stump information + return "Large Stump"; } } + // No matching stump found return null; } - - public static string? getStumpsInWoods(int x, int y, bool lessInfo = false) - { - if (Game1.currentLocation is not Woods) - return null; - - Netcode.NetObjectList stumps = ((Woods)Game1.currentLocation).stumps; - for (int i = 0; i < stumps.Count; i++) - { - if (!stumps[i].occupiesTile(x, y)) - continue; - - if (lessInfo && (stumps[i].tile.X != x || stumps[i].tile.Y != y)) - continue; - - return "Large Stump"; - } - return null; - } - - public static string? getParrotPerchAtTile(int x, int y) - { - if (Game1.currentLocation is not IslandLocation islandLocation) - return null; - - foreach (var perch in islandLocation.parrotUpgradePerches) - { - if (!perch.tilePosition.Value.Equals(new Point(x, y))) - continue; - - string toSpeak = $"Parrot required nuts {perch.requiredNuts.Value}"; - - if (!perch.IsAvailable()) - return "Empty parrot perch"; - else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Idle) - return toSpeak; - else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.StartBuilding) - return "Parrots started building request"; - else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Building) - return "Parrots building request"; - else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Complete) - return $"Request Completed"; - else - return toSpeak; - } - - return null; - } - } } diff --git a/stardew-access/Features/TileViewer.cs b/stardew-access/Features/TileViewer.cs index 1c6ceaf..282d0aa 100644 --- a/stardew-access/Features/TileViewer.cs +++ b/stardew-access/Features/TileViewer.cs @@ -173,7 +173,7 @@ namespace stardew_access.Features if (!tryMoveTileView(delta)) return; Vector2 position = this.GetTileCursorPosition(); 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 if (CurrentPlayer.PositionX == tile.X && CurrentPlayer.PositionY == tile.Y) @@ -184,7 +184,7 @@ namespace stardew_access.Features if (name == null) { // 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"; } @@ -278,11 +278,12 @@ namespace stardew_access.Features 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 - 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. - Map map = Game1.currentLocation.map; + Map map = currentLocation.map; if (position.X < 0 || position.X > map.Layers[0].DisplayWidth) return false; if (position.Y < 0 || position.Y > map.Layers[0].DisplayHeight) return false; return true; diff --git a/stardew-access/Features/Utils.cs b/stardew-access/Features/Utils.cs index a1b1297..ee1e90d 100644 --- a/stardew-access/Features/Utils.cs +++ b/stardew-access/Features/Utils.cs @@ -1,11 +1,26 @@ +using System.Text.Json; namespace stardew_access.Features { /// - /// 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. /// - public class CATEGORY + /// + /// 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() + /// + public sealed class CATEGORY { - private string _typeKeyWord; + private readonly string _typeKeyWord; private CATEGORY(string typeKeyWord) { @@ -17,83 +32,135 @@ namespace stardew_access.Features return _typeKeyWord; } + public static IReadOnlyDictionary Categories => _categories; + + private static readonly Dictionary _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")} + }; + + + /// + /// Retrieves a CATEGORY instance by its string name. + /// Names are case-insensitive. If the name is not found, returns the 'Others' category. + /// + /// The string name of the category to retrieve. + /// The CATEGORY instance corresponding to the given name or the 'Others' category if not found. + /// Thrown when the provided name is null. public static CATEGORY FromString(string name) { - if (name == "farmer") - return CATEGORY.Farmers; - else if (name == "animal") - 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; + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Category name cannot be null or empty.", nameof(name)); + } - return Others; + return Categories.TryGetValue(name, out CATEGORY? category) ? category ?? CATEGORY.Others : CATEGORY.Others; } - public static CATEGORY Farmers = new CATEGORY("farmer"); - public static CATEGORY FarmAnimals = new CATEGORY("animal"); - public static CATEGORY NPCs = new CATEGORY("npc"); - public static CATEGORY Furnitures = new CATEGORY("furniture"); - public static CATEGORY Flooring = new CATEGORY("flooring"); - public static CATEGORY Debris = new CATEGORY("debris"); - public static CATEGORY Crops = new CATEGORY("crop"); - public static CATEGORY Trees = new CATEGORY("tree"); - public static CATEGORY Bush = new CATEGORY("bush"); - public static CATEGORY Buildings = new CATEGORY("building"); - public static CATEGORY MineItems = new CATEGORY("mine item"); - public static CATEGORY ResourceClumps = new CATEGORY("resource clump"); - public static CATEGORY Containers = new CATEGORY("container"); - public static CATEGORY JunimoBundle = new CATEGORY("bundle"); - 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"); + /// + /// Adds a new CATEGORY with the specified name. + /// Names are case-insensitive. + /// + /// The name of the new category to be added. + /// + /// True if a new category was added; false if the category already exists. + /// + /// Thrown if the provided name is null or empty. + public static bool AddNewCategory(string name) + { + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("Name cannot be null or empty.", nameof(name)); + } + 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 { Ready, Busy, Waiting } + + public static class Utils + { + /// + /// Loads a JSON file from the specified file name in the assets folder. + /// + /// The name of the JSON file to load. + /// A containing the deserialized JSON data, or default if an error occurs. + public static JsonElement LoadJsonFile(string fileName) + { + string filePath = Path.Combine(MainClass.ModHelper!.DirectoryPath, "assets", fileName); + + try + { + string json = File.ReadAllText(filePath); + return JsonSerializer.Deserialize(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; + } + } } \ No newline at end of file diff --git a/stardew-access/ModEntry.cs b/stardew-access/ModEntry.cs index 1830c50..b8ea9b3 100644 --- a/stardew-access/ModEntry.cs +++ b/stardew-access/ModEntry.cs @@ -7,6 +7,7 @@ using stardew_access.Patches; using stardew_access.ScreenReader; using Microsoft.Xna.Framework; using StardewValley.Menus; +using Microsoft.Xna.Framework.Input; namespace stardew_access { @@ -20,7 +21,6 @@ namespace stardew_access private Harmony? harmony; private static IMonitor? monitor; private static Radar? radarFeature; - private static StaticTiles? sTiles; private static IScreenReader? screenReader; private static IModHelper? modHelper; private static TileViewer? tileViewer; @@ -30,17 +30,6 @@ namespace stardew_access internal static ModConfig Config { get => config; set => config = value; } 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 { get @@ -141,10 +130,17 @@ namespace stardew_access helper.Events.Input.ButtonPressed += this.OnButtonPressed; helper.Events.GameLoop.UpdateTicked += this.onUpdateTicked; + helper.Events.GameLoop.DayStarted += this.onDayStarted; AppDomain.CurrentDomain.DomainUnload += OnExit; AppDomain.CurrentDomain.ProcessExit += OnExit; } + /// Returns the Screen Reader class for other mods to use. + public override object GetApi() + { + return new API(); + } + public void OnExit(object? sender, EventArgs? e) { // This closes the connection with the screen reader, important for linux @@ -153,10 +149,10 @@ namespace stardew_access ScreenReader.CloseScreenReader(); } - /// Returns the Screen Reader class for other mods to use. - public override object GetApi() + private void onDayStarted(object? sender, DayStartedEventArgs? e) { - return new API(); + StaticTiles.LoadTilesFiles(); + StaticTiles.SetupTilesDicts(); } private void onUpdateTicked(object? sender, UpdateTickedEventArgs? e) @@ -166,10 +162,8 @@ namespace stardew_access // Narrates currently selected inventory slot Other.narrateCurrentSlot(); - // Narrate current location's name Other.narrateCurrentLocation(); - //handle TileCursor update logic TileViewerFeature.update(); @@ -179,27 +173,44 @@ namespace stardew_access if (Config.ReadTile) ReadTileFeature.update(); - if (!RadarFeature.isRunning && Config.Radar) - { - RadarFeature.isRunning = true; - RadarFeature.Run(); - Task.Delay(RadarFeature.delay).ContinueWith(_ => { RadarFeature.isRunning = false; }); - } + RunRadarFeatureIfEnabled(); - if (!isNarratingHudMessage) - { - isNarratingHudMessage = true; - Other.narrateHudMessages(); - Task.Delay(300).ContinueWith(_ => { isNarratingHudMessage = false; }); - } + RunHudMessageNarration(); - if (Game1.player != null) + RefreshBuildListIfRequired(); + + async void RunRadarFeatureIfEnabled() { - if (Game1.timeOfDay >= 600 && prevDate != CurrentPlayer.Date) + if (!RadarFeature.isRunning && Config.Radar) { - prevDate = CurrentPlayer.Date; - DebugLog("Refreshing buildlist..."); - CustomCommands.onBuildListCalled(); + RadarFeature.isRunning = true; + RadarFeature.Run(); + 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) return; - #region Simulate left and right clicks - if (Game1.activeClickableMenu != null && !TextBoxPatch.isAnyTextBoxActive) + void SimulateMouseClicks(Action leftClickHandler, Action rightClickHandler) { - 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()) { - Game1.activeClickableMenu.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true)); + leftClickHandler(mouseX, mouseY); } - - if (Config.RightClickMainKey.JustPressed() || Config.RightClickAlternateKey.JustPressed()) + else 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 (Config.LeftClickMainKey.JustPressed() || Config.LeftClickAlternateKey.JustPressed()) + if (Game1.activeClickableMenu != null) { - Game1.currentMinigame.receiveLeftClick(Game1.getMouseX(true), Game1.getMouseY(true)); + SimulateMouseClicks( + (x, y) => Game1.activeClickableMenu.receiveLeftClick(x, y), + (x, y) => Game1.activeClickableMenu.receiveRightClick(x, y) + ); } - - if (Config.RightClickMainKey.JustPressed() || Config.RightClickAlternateKey.JustPressed()) + else if (Game1.currentMinigame != null) { - 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 if (!Context.IsPlayerFree) return; - // Stops the auto walk controller if any movement key(WASD) is pressed - if (TileViewerFeature.isAutoWalking && - (e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveUpButton[0])) - || e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveDownButton[0])) - || e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveLeftButton[0])) - || e.Button.Equals(SButtonExtensions.ToSButton(Game1.options.moveRightButton[0])))) + void Narrate(string message) => MainClass.ScreenReader.Say(message, true); + + bool IsMovementKey(SButton button) + { + return button.Equals(SButtonExtensions.ToSButton(Game1.options.moveUpButton[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); } @@ -259,25 +277,17 @@ namespace stardew_access // Narrate Current Location if (Config.LocationKey.JustPressed()) { - string toSpeak = $"{Game1.currentLocation.Name}"; - MainClass.ScreenReader.Say(toSpeak, true); + Narrate(Game1.currentLocation.Name); return; } // Narrate Position if (Config.PositionKey.JustPressed()) { - string toSpeak; - if (Config.VerboseCoordinates) - { - toSpeak = $"X: {CurrentPlayer.PositionX}, Y: {CurrentPlayer.PositionY}"; - } - else - { - toSpeak = $"{CurrentPlayer.PositionX}, {CurrentPlayer.PositionY}"; - } - - MainClass.ScreenReader.Say(toSpeak, true); + string toSpeak = Config.VerboseCoordinates + ? $"X: {CurrentPlayer.PositionX}, Y: {CurrentPlayer.PositionY}" + : $"{CurrentPlayer.PositionX}, {CurrentPlayer.PositionY}"; + Narrate(toSpeak); return; } @@ -287,29 +297,25 @@ namespace stardew_access if (ModHelper == null) return; - string toSpeak; - if (Config.HealthNStaminaInPercentage) - toSpeak = ModHelper.Translation.Get("manuallytriggered.healthnstamina.percent", new { health = CurrentPlayer.PercentHealth, stamina = CurrentPlayer.PercentStamina }); - else - toSpeak = ModHelper.Translation.Get("manuallytriggered.healthnstamina.normal", new { health = CurrentPlayer.CurrentHealth, stamina = CurrentPlayer.CurrentStamina }); + string toSpeak = Config.HealthNStaminaInPercentage + ? 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 }); - MainClass.ScreenReader.Say(toSpeak, true); + Narrate(toSpeak); return; } // Narrate money at hand if (Config.MoneyKey.JustPressed()) { - string toSpeak = $"You have {CurrentPlayer.Money}g"; - MainClass.ScreenReader.Say(toSpeak, true); + Narrate($"You have {CurrentPlayer.Money}g"); return; } // Narrate time and season if (Config.TimeNSeasonKey.JustPressed()) { - string toSpeak = $"Time is {CurrentPlayer.TimeOfDay} and it is {CurrentPlayer.Day} {CurrentPlayer.Date} of {CurrentPlayer.Season}"; - MainClass.ScreenReader.Say(toSpeak, true); + Narrate($"Time is {CurrentPlayer.TimeOfDay} and it is {CurrentPlayer.Day} {CurrentPlayer.Date} of {CurrentPlayer.Season}"); return; } @@ -331,28 +337,27 @@ namespace stardew_access TileViewerFeature.HandleInput(); } - public static void ErrorLog(string message) + private static void LogMessage(string message, LogLevel logLevel) { if (monitor == null) 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) { - if (monitor == null) - return; - - monitor.Log(message, LogLevel.Info); + LogMessage(message, LogLevel.Info); } public static void DebugLog(string message) { - if (monitor == null) - return; - - monitor.Log(message, LogLevel.Debug); + LogMessage(message, LogLevel.Debug); } } } diff --git a/stardew-access/Patches/MiscPatches/Game1Patch.cs b/stardew-access/Patches/MiscPatches/Game1Patch.cs index 04ba91f..319acde 100644 --- a/stardew-access/Patches/MiscPatches/Game1Patch.cs +++ b/stardew-access/Patches/MiscPatches/Game1Patch.cs @@ -34,7 +34,7 @@ namespace stardew_access.Patches if (cueName == "grassyStep" || cueName == "sandyStep" || cueName == "snowyStep" || cueName == "stoneStep" || cueName == "thudStep" || cueName == "woodyStep") { 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) { diff --git a/stardew-access/Patches/QuestPatches/BillboardPatch.cs b/stardew-access/Patches/QuestPatches/BillboardPatch.cs index f8e4f47..113cb61 100644 --- a/stardew-access/Patches/QuestPatches/BillboardPatch.cs +++ b/stardew-access/Patches/QuestPatches/BillboardPatch.cs @@ -18,7 +18,7 @@ namespace stardew_access.Patches } else { - narrateCallendar(__instance); + narrateCalendar(__instance); } } catch (Exception e) @@ -27,7 +27,7 @@ namespace stardew_access.Patches } } - private static void narrateCallendar(Billboard __instance) + private static void narrateCalendar(Billboard __instance) { for (int i = 0; i < __instance.calendarDays.Count; i++) { @@ -35,6 +35,7 @@ namespace stardew_access.Patches continue; string toSpeak = $"Day {i + 1}"; + string currentYearNMonth = $"of {Game1.CurrentSeasonDisplayName}, {Game1.content.LoadString("Strings\\UI:Billboard_Year", Game1.year)}"; if (__instance.calendarDays[i].name.Length > 0) { @@ -46,13 +47,16 @@ namespace stardew_access.Patches } if (Game1.dayOfMonth == i + 1) - toSpeak += $", Current"; + toSpeak = $"Current {toSpeak}"; if (billboardQueryKey != toSpeak) { billboardQueryKey = toSpeak; + if (i == 0) toSpeak = $"{toSpeak} {currentYearNMonth}"; MainClass.ScreenReader.Say(toSpeak, true); } + + return; } } diff --git a/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs b/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs index 005456d..f0b3e42 100644 --- a/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs +++ b/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs @@ -1,5 +1,7 @@ +using System.Text.Json; using StardewValley; using StardewValley.Menus; +using static stardew_access.Features.Utils; namespace stardew_access.Patches { @@ -7,28 +9,69 @@ namespace stardew_access.Patches { private static bool isRunning = false; private static int saveGameIndex = -1; - public static string characterCreationMenuQueryKey = " "; - public static string prevPants = " "; - public static string prevShirt = " "; - public static string prevHair = " "; - public static string prevAccessory = " "; - public static string prevSkin = " "; - public static string prevEyeColor = " "; - public static string prevEyeColorHue = " "; - public static string prevEyeColorSaturation = " "; - public static string prevEyeColorValue = " "; - public static string prevHairColor = " "; - public static string prevHairColorHue = " "; - public static string prevHairColorSaturation = " "; - public static string prevHairColorValue = " "; - public static string prevPantsColor = " "; - public static string prevPantsColorHue = " "; - public static string prevPantsColorSaturation = " "; - public static string prevPantsColorValue = " "; - public static string prevPetName = " "; - public static bool characterDesignToggle = false; - public static bool characterDesignToggleShouldSpeak = true; - public static ClickableComponent? currentComponent = null; + private static string characterCreationMenuQueryKey = " "; + private static string prevPants = " "; + private static string prevShirt = " "; + private static string prevHair = " "; + private static string prevAccessory = " "; + private static string prevSkin = " "; + private static string prevEyeColor = " "; + private static string prevEyeColorHue = " "; + private static string prevEyeColorSaturation = " "; + private static string prevEyeColorValue = " "; + private static string prevHairColor = " "; + private static string prevHairColorHue = " "; + private static string prevHairColorSaturation = " "; + private static string prevHairColorValue = " "; + private static string prevPantsColor = " "; + private static string prevPantsColorHue = " "; + private static string prevPantsColorSaturation = " "; + private static string prevPantsColorValue = " "; + private static string prevPet = " "; + private static bool characterDesignToggle = false; + private static bool characterDesignToggleShouldSpeak = true; + private static ClickableComponent? currentComponent = null; + private static Dictionary> descriptions + { + get + { + if (_descriptions == null) + { + _descriptions = LoadDescriptionJson(); + } + return _descriptions; + } + } + private static Dictionary>? _descriptions; + + private static Dictionary> LoadDescriptionJson() + { + MainClass.DebugLog("Attempting to load json"); + JsonElement jsonElement = LoadJsonFile("new-character-appearance-descriptions.json"); + + if (jsonElement.ValueKind == JsonValueKind.Undefined) + { + return new Dictionary>(); + } + + Dictionary> result = new Dictionary>(); + + foreach (JsonProperty category in jsonElement.EnumerateObject()) + { + Dictionary innerDictionary = new Dictionary(); + + 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, ClickableComponent ___startingCabinsLabel, ClickableComponent ___difficultyModifierLabel, TextBox ___nameBox, @@ -126,24 +169,24 @@ namespace stardew_access.Patches private static string getChangesToSpeak(CharacterCustomization __instance) { string toSpeak = ""; - string currentPetName = getCurrentPetName(); - string currentSkin = getCurrentSkin(); - string currentHair = getCurrentHair(); - string currentShirt = getCurrentShirt(); - string currentPants = getCurrentPants(); - string currentAccessory = getCurrentAccessory(); - string currentEyeColor = getCurrentEyeColor(); - string currentEyeColorHue = getCurrentEyeColorHue(__instance); - string currentEyeColorSaturation = getCurrentEyeColorSaturation(__instance); - string currentEyeColorValue = getCurrentEyeColorValue(__instance); - string currentHairColor = getCurrentHairColor(); - string currentHairColorHue = getCurrentHairColorHue(__instance); - string currentHairColorSaturation = getCurrentHairColorSaturation(__instance); - string currentHairColorValue = getCurrentHairColorValue(__instance); - string currentPantsColor = getCurrentPantsColor(); - string currentPantsColorHue = getCurrentPantsColorHue(__instance); - string currentPantsColorSaturation = getCurrentPantsColorSaturation(__instance); - string currentPantsColorValue = getCurrentPantsColorValue(__instance); + string currentPet = GetCurrentPet(); + string currentSkin = GetCurrentSkin(); + string currentHair = GetCurrentHair(); + string currentShirt = GetCurrentShirt(); + string currentPants = GetCurrentPants(); + string currentAccessory = GetCurrentAccessory(); + string currentEyeColor = GetCurrentEyeColor(); + string currentEyeColorHue = GetCurrentEyeColorHue(__instance); + string currentEyeColorSaturation = GetCurrentEyeColorSaturation(__instance); + string currentEyeColorValue = GetCurrentEyeColorValue(__instance); + string currentHairColor = GetCurrentHairColor(); + string currentHairColorHue = GetCurrentHairColorHue(__instance); + string currentHairColorSaturation = GetCurrentHairColorSaturation(__instance); + string currentHairColorValue = GetCurrentHairColorValue(__instance); + string currentPantsColor = GetCurrentPantsColor(); + string currentPantsColorHue = GetCurrentPantsColorHue(__instance); + string currentPantsColorSaturation = GetCurrentPantsColorSaturation(__instance); + string currentPantsColorValue = GetCurrentPantsColorValue(__instance); if (characterDesignToggle) { @@ -339,11 +382,11 @@ namespace stardew_access.Patches } } - if (prevPetName != currentPetName) + if (prevPet != currentPet) { - prevPetName = currentPetName; - if (currentPetName != "") - toSpeak = $"{toSpeak} \n Current Pet: {currentPetName}"; + prevPet = currentPet; + if (currentPet != "") + toSpeak = $"{toSpeak} \n Current Pet: {currentPet}"; } 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. - private static string getCurrentSkin() - { - 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() + private static string GetCurrentPet(bool lessInfo = false) { if (currentComponent != null && currentComponent.name == "Pet") { - return ((Game1.player.catPerson) ? "Cat" : "Dog") + " Breed: " + Game1.player.whichPetBreed; - } - else - { - return ""; + int whichPetBreed = Game1.player.whichPetBreed + 1; + + if (!lessInfo) + { + 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 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 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())); } } diff --git a/stardew-access/ScreenReader/NativeMethods.cs b/stardew-access/ScreenReader/NativeMethods.cs new file mode 100644 index 0000000..9e5b26a --- /dev/null +++ b/stardew-access/ScreenReader/NativeMethods.cs @@ -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); +} diff --git a/stardew-access/assets/event-tiles.json b/stardew-access/assets/event-tiles.json new file mode 100644 index 0000000..6e3f789 --- /dev/null +++ b/stardew-access/assets/event-tiles.json @@ -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" + } +} diff --git a/stardew-access/assets/new-character-appearance-descriptions.json b/stardew-access/assets/new-character-appearance-descriptions.json new file mode 100644 index 0000000..a52f1ec --- /dev/null +++ b/stardew-access/assets/new-character-appearance-descriptions.json @@ -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" + } +} \ No newline at end of file diff --git a/stardew-access/assets/static-tiles.json b/stardew-access/assets/static-tiles.json index 7ac8f23..3bce903 100644 --- a/stardew-access/assets/static-tiles.json +++ b/stardew-access/assets/static-tiles.json @@ -674,6 +674,7 @@ "type": "decoration" } }, + "farmhouse": null, "fishshop": { "Shop Counter": { "x": [4, 5, 6], diff --git a/stardew-access/manifest.json b/stardew-access/manifest.json index 494cb2e..dfa4abb 100644 --- a/stardew-access/manifest.json +++ b/stardew-access/manifest.json @@ -1,7 +1,7 @@ { "Name": "Stardew Access", "Author": "Mohammad Shoaib", - "Version": "1.3.4", + "Version": "1.3.5-beta2", "Description": "An accessibility mod with screen reader support!", "UniqueID": "shoaib.stardewaccess", "EntryDll": "stardew-access.dll", @@ -9,4 +9,4 @@ "UpdateKeys": [ "Github:stardew-access/stardew-access" ] -} \ No newline at end of file +} diff --git a/stardew-access/stardew-access.csproj b/stardew-access/stardew-access.csproj index ab03cfc..f7e9877 100644 --- a/stardew-access/stardew-access.csproj +++ b/stardew-access/stardew-access.csproj @@ -13,7 +13,6 @@ -