From 1c61580d6bf8b109f23acb63f7aa22eac179dd10 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Fri, 3 Mar 2023 00:12:01 -0800 Subject: [PATCH 01/19] Fix inventory not speaking extra info; not respecting config option. Reduce excess commas. Bump version to 1.3.5-alpha2. --- stardew-access/Features/InventoryUtils.cs | 28 ++++++++++++++--------- stardew-access/manifest.json | 2 +- 2 files changed, 18 insertions(+), 12 deletions(-) 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/manifest.json b/stardew-access/manifest.json index 494cb2e..a95415e 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-alpha2", "Description": "An accessibility mod with screen reader support!", "UniqueID": "shoaib.stardewaccess", "EntryDll": "stardew-access.dll", From 32db14b3290fcfcc06cd0f6c46c148e284066842 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Tue, 4 Apr 2023 22:22:36 -0700 Subject: [PATCH 02/19] add /libraries/windows to the dll search path before loading Tolk. This enables nvdaControllerClient64.dll to be loaded in place. --- stardew-access/ScreenReader/NativeMethods.cs | 8 ++++++++ stardew-access/ScreenReader/ScreenReaderWindows.cs | 8 ++++++++ 2 files changed, 16 insertions(+) create mode 100644 stardew-access/ScreenReader/NativeMethods.cs 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/ScreenReader/ScreenReaderWindows.cs b/stardew-access/ScreenReader/ScreenReaderWindows.cs index d9b2dbc..ee9ed5e 100644 --- a/stardew-access/ScreenReader/ScreenReaderWindows.cs +++ b/stardew-access/ScreenReader/ScreenReaderWindows.cs @@ -1,4 +1,5 @@ using DavyKager; +using System.IO; namespace stardew_access.ScreenReader { @@ -15,6 +16,13 @@ namespace stardew_access.ScreenReader public void InitializeScreenReader() { + if (MainClass.ModHelper is not null) + { + // Set the path to load Tolk.dll from + string dllDirectory = Path.Combine(MainClass.ModHelper.DirectoryPath, "libraries", "windows");; + // Call SetDllDirectory to change the DLL search path + NativeMethods.SetDllDirectory(dllDirectory); + } MainClass.InfoLog("Initializing Tolk..."); Tolk.TrySAPI(true); Tolk.Load(); From e9ba7c96b993855ed4e30e46317ce5d6391bbb5a Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Sat, 25 Feb 2023 21:49:27 -0800 Subject: [PATCH 03/19] Changes `searched` from List to HashSet; saves ~43ms in `SearchLocation --- stardew-access/Features/Radar.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/stardew-access/Features/Radar.cs b/stardew-access/Features/Radar.cs index 558da81..5434f97 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using Microsoft.Xna.Framework; using StardewValley; using StardewValley.Objects; @@ -94,7 +95,7 @@ namespace stardew_access.Features Dictionary detectedTiles = new Dictionary(); Queue toSearch = new Queue(); - List searched = new List(); + HashSet searched = new HashSet(); int[] dirX = { -1, 0, 1, 0 }; int[] dirY = { 0, 1, 0, -1 }; @@ -142,7 +143,7 @@ namespace stardew_access.Features (bool, string? name, string category) tileInfo; Queue toSearch = new Queue(); - List searched = new List(); + HashSet searched = new HashSet(); int[] dirX = { -1, 0, 1, 0 }; int[] dirY = { 0, 1, 0, -1 }; int count = 0; @@ -185,7 +186,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; From a88172c0cf3a5697388d3cf1146e649a99127d71 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Sun, 26 Feb 2023 05:12:05 -0800 Subject: [PATCH 04/19] Replaced some foreach loops with for loops and nullchecks to use `is` instead of `==`. --- stardew-access/Features/Radar.cs | 15 ++++++- stardew-access/Features/TileInfo.cs | 62 +++++++++++++++++++---------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/stardew-access/Features/Radar.cs b/stardew-access/Features/Radar.cs index 5434f97..b7bd95b 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics; using Microsoft.Xna.Framework; using StardewValley; using StardewValley.Objects; @@ -129,6 +130,7 @@ namespace stardew_access.Features } } + searched.Clear(); return detectedTiles; } @@ -138,6 +140,7 @@ 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() { + var watch = System.Diagnostics.Stopwatch.StartNew(); Dictionary detectedTiles = new Dictionary(); Vector2 position = Vector2.Zero; (bool, string? name, string category) tileInfo; @@ -151,6 +154,11 @@ 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}"); + elapsedMs = 0; + watch = System.Diagnostics.Stopwatch.StartNew(); while (toSearch.Count > 0) { Vector2 item = toSearch.Dequeue(); @@ -174,7 +182,10 @@ namespace stardew_access.Features } } } - + watch.Stop(); + elapsedMs = watch.ElapsedMilliseconds; + MainClass.DebugLog($"Search loop duration: {elapsedMs}; {count} iterations."); + searched.Clear(); return detectedTiles; } @@ -209,7 +220,7 @@ namespace stardew_access.Features tileDetail.category = CATEGORY.Others; return (true, tileDetail.name, tileDetail.category.ToString()); - + } public void CheckTileAndPlaySound(Vector2 position) diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index 74adb00..212836c 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -34,7 +34,7 @@ namespace stardew_access.Features { int x = (int)tile.X; int y = (int)tile.Y; - string? toReturn = ""; + string? toReturn = null; CATEGORY? category = CATEGORY.Others; bool isColliding = isCollidingAtTile(x, y); @@ -57,7 +57,7 @@ namespace stardew_access.Features else category = CATEGORY.NPCs; } - else if (farmAnimal != null) + else if (farmAnimal is not null) { toReturn = farmAnimal; category = CATEGORY.FarmAnimals; @@ -161,10 +161,12 @@ namespace stardew_access.Features try { NetCollection droppedItems = Game1.currentLocation.debris; - if (droppedItems.Count() > 0) + int droppedItemsCount = droppedItems.Count(); + if (droppedItemsCount > 0) { - foreach (var item in droppedItems) + for (int i = 0; i < droppedItemsCount; i++) { + var item = droppedItems[i]; 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; @@ -174,10 +176,11 @@ namespace stardew_access.Features string name = item.item.DisplayName; int count = item.item.Stack; - if (toReturn == "") + if (toReturn is null) return ($"Dropped Item: {count} {name}", CATEGORY.DroppedItems); else toReturn = $"{toReturn}, Dropped Item: {count} {name}"; + item = null; } } } @@ -188,9 +191,6 @@ namespace stardew_access.Features } #endregion - if (toReturn == "") - return (null, category); - return (toReturn, category); } @@ -198,7 +198,7 @@ namespace stardew_access.Features { string? toReturn = null; Bush? bush = (Bush)Game1.currentLocation.getLargeTerrainFeatureAt(x, y); - if (bush == null) + if (bush is null) return null; if (lessInfo && (bush.tilePosition.Value.X != x || bush.tilePosition.Value.Y != y)) return null; @@ -264,7 +264,7 @@ namespace stardew_access.Features (46, 12) => "Bulletin Board", _ => null, }; - if (name != null && communityCenter.shouldNoteAppearInArea(CommunityCenter.getAreaNumberFromName(name))) + if (name is not null && communityCenter.shouldNoteAppearInArea(CommunityCenter.getAreaNumberFromName(name))) return $"{name} bundle"; } else if (Game1.currentLocation is AbandonedJojaMart) @@ -275,7 +275,7 @@ namespace stardew_access.Features _ => null, }; - if (name != null) + if (name is not null) return $"{name} bundle"; } @@ -286,6 +286,7 @@ namespace stardew_access.Features { Rectangle rect = new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62); + /* Reference // 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; @@ -294,19 +295,32 @@ namespace stardew_access.Features return true; } - if (Game1.currentLocation is Woods && getStumpsInWoods(x, y) != null) + if (Game1.currentLocation is Woods && getStumpsInWoods(x, y) is not null) return true; return false; + */ + + // Optimized + // Sometimes warp points are 1 tile off the map for example in coops and barns; check that this is not a warp point + if (!isWarpPointAtTile(x, y)) + { + // not a warp point + //directly return the value of the logical comparison rather than wasting time in conditional + return ((Game1.currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) || (Game1.currentLocation is Woods && getStumpsInWoods(x, y) is not null)); + } + // was a warp point; return false + return false; } public static Boolean isWarpPointAtTile(int x, int y) { - if (Game1.currentLocation == null) return false; + if (Game1.currentLocation is null) return false; - foreach (Warp warpPoint in Game1.currentLocation.warps) + int warpsCount = Game1.currentLocation.warps.Count(); + for (int i = 0; i < warpsCount; i++) { - if (warpPoint.X == x && warpPoint.Y == y) return true; + if (Game1.currentLocation.warps[i].X == x && Game1.currentLocation.warps[i].Y == y) return true; } return false; @@ -314,11 +328,11 @@ namespace stardew_access.Features public static string? getFarmAnimalAt(GameLocation? location, int x, int y) { - if (location == null) + if (location is null || (location is not Farm && location is not AnimalHouse)) return null; - if (location is not Farm && location is not AnimalHouse) - return null; + //if (location is not Farm && location is not AnimalHouse) + //return null; List? farmAnimals = null; @@ -368,7 +382,7 @@ namespace stardew_access.Features else { Building building = farm.getBuildingAt(new Vector2(x, y)); - if (building != null) + if (building is not null) { string name = building.buildingType.Value; @@ -397,7 +411,7 @@ namespace stardew_access.Features } } } - else if (Game1.currentLocation.currentEvent != null) + else if (Game1.currentLocation.currentEvent is not null) { string event_name = Game1.currentLocation.currentEvent.FestivalName; if (event_name == "Egg Festival" && x == 21 && y == 55) @@ -1198,8 +1212,10 @@ namespace stardew_access.Features { if (Game1.currentLocation == null) return null; - foreach (Warp warpPoint in Game1.currentLocation.warps) + int warpCount = Game1.currentLocation.warps.Count(); + for (int i = 0; i < warpCount; i++) { + Warp warpPoint = Game1.currentLocation.warps[i]; if (warpPoint.X != x || warpPoint.Y != y) continue; return $"{warpPoint.TargetName} Entrance"; @@ -1303,8 +1319,10 @@ namespace stardew_access.Features if (Game1.currentLocation is not IslandLocation islandLocation) return null; - foreach (var perch in islandLocation.parrotUpgradePerches) + int perchCount = islandLocation.parrotUpgradePerches.Count(); + for (int i = 0; i < perchCount; i++) { + var perch = islandLocation.parrotUpgradePerches[i]; if (!perch.tilePosition.Value.Equals(new Point(x, y))) continue; From 541fd4213381443086fd4235cbc1f24c7b6d75d7 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Wed, 1 Mar 2023 17:20:44 -0800 Subject: [PATCH 05/19] Static tiles uses HashSet to lookup entries. More nullchecks changed to use `is`. --- stardew-access/Features/StaticTiles.cs | 201 +++++++++++++------------ 1 file changed, 106 insertions(+), 95 deletions(-) diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 9b36631..570c24e 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -7,6 +7,8 @@ namespace stardew_access.Features { private JObject? staticTilesData = null; private JObject? customTilesData = null; + HashSet>? staticTilesDataSet = null; + HashSet>? customTilesDataSet = null; public StaticTiles() { @@ -20,6 +22,10 @@ namespace stardew_access.Features string json = file.ReadToEnd(); staticTilesData = JObject.Parse(json); } + if (staticTilesData is not null) + { + staticTilesDataSet = new HashSet>(staticTilesData); + } MainClass.InfoLog($"Loaded static-tile.json"); } @@ -35,6 +41,10 @@ namespace stardew_access.Features string json = file.ReadToEnd(); customTilesData = JObject.Parse(json); } + if (customTilesData is not null) + { + customTilesDataSet = new HashSet>(customTilesData); + } MainClass.InfoLog($"Loaded custom-tile.json"); } @@ -77,114 +87,115 @@ namespace stardew_access.Features return getStaticTileInfoAtWithCategory(x, y).name; } - 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) + public (string? name, CATEGORY category) GetTileFromSet(int x, int y, HashSet> data) { + foreach (KeyValuePair location in data) + { + string locationName = location.Key; + if (locationName.Contains("||") && MainClass.ModHelper != null) { - 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) { - // 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) + if (short.Parse(item.ToString()) != x) continue; - bool isXPresent = false; - bool isYPresent = false; + isXPresent = true; + break; + } - foreach (var item in tileXArray) + 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(']')) { - if (short.Parse(item.ToString()) != x) - continue; + int i1 = key.IndexOf('['); + int i2 = key.LastIndexOf(']'); - 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(']')) + if (i1 < i2) { - int i1 = key.IndexOf('['); - int i2 = key.LastIndexOf(']'); - - if (i1 < i2) - { - key = key.Remove(i1, ++i2 - i1); - } + key = key.Remove(i1, ++i2 - i1); } - - return (key.Trim(), CATEGORY.FromString(tileType.ToString().ToLower())); } + + return (key.Trim(), CATEGORY.FromString(tileType.ToString().ToLower())); } } } + + return (null, CATEGORY.Others); + } + + public (string? name, CATEGORY category) getStaticTileInfoAtWithCategory(int x, int y) { + if (customTilesDataSet != null) return GetTileFromSet(x, y, customTilesDataSet); + if (staticTilesDataSet != null) return GetTileFromSet(x, y, staticTilesDataSet); + return (null, CATEGORY.Others); } From 4b2e31fadcfeda9e84cbff432e5f93f16697d79e Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Wed, 1 Mar 2023 18:07:28 -0800 Subject: [PATCH 06/19] Change comparisons relying on string.ToLower() to use StringComparison.OrdinalIgnoreCase. --- stardew-access/Features/StaticTiles.cs | 45 ++++++++++++++------------ stardew-access/Features/TileInfo.cs | 44 ++++++++++++------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 570c24e..34a36b5 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -12,7 +12,7 @@ namespace stardew_access.Features public StaticTiles() { - if (MainClass.ModHelper == null) + if (MainClass.ModHelper is null) return; try @@ -72,9 +72,9 @@ namespace stardew_access.Features bool isLoaded = MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModID); if (!isLoaded) continue; // Skip if the specified mod is not loaded - if (locationName.ToLower().Equals(locationNameInJson.ToLower())) return true; + if (locationName.Equals(locationNameInJson, StringComparison.OrdinalIgnoreCase)) return true; } - else if (locationName.ToLower().Equals(location.Key.ToLower())) + else if (locationName.Equals(location.Key, StringComparison.OrdinalIgnoreCase)) return true; } } @@ -82,16 +82,12 @@ namespace stardew_access.Features return false; } - public string? getStaticTileInfoAt(int x, int y) + public (string? name, CATEGORY category) GetTileFromSet(int x, int y, GameLocation currentLocation, HashSet> data) { - return getStaticTileInfoAtWithCategory(x, y).name; - } - - public (string? name, CATEGORY category) GetTileFromSet(int x, int y, HashSet> data) { foreach (KeyValuePair location in data) { string locationName = location.Key; - if (locationName.Contains("||") && MainClass.ModHelper != null) + if (locationName.Contains("||") && MainClass.ModHelper is not null) { // Mod Specific Tiles // We can add tiles that only get detected when the specified mod is loaded. @@ -119,34 +115,34 @@ namespace stardew_access.Features if (!isLoaded) continue; // Skip if the specified mod is not loaded } - if (locationName.Contains("_") && locationName.ToLower().StartsWith("farm_")) + if (locationName.StartsWith("farm_", StringComparison.OrdinalIgnoreCase)) { 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 (Game1.whichModFarm is not null) MainClass.DebugLog($"{farmType} {Game1.whichModFarm.MapName}"); + if (farmTypeIndex != 7 || Game1.whichModFarm is null || !farmType.Equals(Game1.whichModFarm.MapName, StringComparison.OrdinalIgnoreCase)) continue; // Not tested but should work } - if (locationName.ToLower().Equals("town_joja") && Game1.MasterPlayer.mailReceived.Contains("JojaMember")) + if (locationName.Equals("town_joja", StringComparison.OrdinalIgnoreCase) && Game1.MasterPlayer.mailReceived.Contains("JojaMember")) { locationName = "town"; } - if (!Game1.currentLocation.Name.ToLower().Equals(locationName.ToLower())) continue; - if (location.Value == null) continue; + if (!currentLocation.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase)) continue; + if (location.Value is null) continue; foreach (var tile in ((JObject)location.Value)) { - if (tile.Value == null) continue; + if (tile.Value is null) continue; JToken? tileXArray = tile.Value["x"]; JToken? tileYArray = tile.Value["y"]; JToken? tileType = tile.Value["type"]; - if (tileXArray == null || tileYArray == null || tileType == null) + if (tileXArray is null || tileYArray is null || tileType is null) continue; bool isXPresent = false; @@ -189,12 +185,19 @@ namespace stardew_access.Features } } - return (null, CATEGORY.Others); + return (null, CATEGORY.Others); } - public (string? name, CATEGORY category) getStaticTileInfoAtWithCategory(int x, int y) { - if (customTilesDataSet != null) return GetTileFromSet(x, y, customTilesDataSet); - if (staticTilesDataSet != null) return GetTileFromSet(x, y, staticTilesDataSet); + public string? getStaticTileInfoAt(int x, int y) + { + return getStaticTileInfoAtWithCategory(x, y).name; + } + + public (string? name, CATEGORY category) getStaticTileInfoAtWithCategory(int x, int y) + { + GameLocation currentLocation = Game1.currentLocation; + if (customTilesDataSet is not null) return GetTileFromSet(x, y, currentLocation, customTilesDataSet); + if (staticTilesDataSet is not null) return GetTileFromSet(x, y, currentLocation, staticTilesDataSet); return (null, CATEGORY.Others); } diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index 212836c..270c960 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -547,67 +547,67 @@ namespace stardew_access.Features return (CATEGORY.Interactables, "Island Trader"); } } - else if (Game1.currentLocation.Name.ToLower().Equals("coop")) + else if (Game1.currentLocation.Name.Equals("coop", StringComparison.OrdinalIgnoreCase)) { 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")) + if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) 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")) + else if (Game1.currentLocation.Name.Equals("coop2", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("big coop", StringComparison.OrdinalIgnoreCase)) { 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")) + if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) 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")) + else if (Game1.currentLocation.Name.Equals("coop3", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("deluxe coop", StringComparison.OrdinalIgnoreCase)) { 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")) + if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); } } - else if (Game1.currentLocation.Name.ToLower().Equals("barn")) + else if (Game1.currentLocation.Name.Equals("barn", StringComparison.OrdinalIgnoreCase)) { 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")) + if (bench.name != null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) 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")) + else if (Game1.currentLocation.Name.Equals("barn2", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("big barn", StringComparison.OrdinalIgnoreCase)) { 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")) + if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) 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")) + else if (Game1.currentLocation.Name.Equals("barn3", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("deluxe barn", StringComparison.OrdinalIgnoreCase)) { 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")) + if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); @@ -917,11 +917,11 @@ namespace stardew_access.Features } 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")) + if (MainClass.ModHelper is not null && obj.heldObject.Value.Name.Contains("pressure nozzle", StringComparison.OrdinalIgnoreCase)) { 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")) + else if (MainClass.ModHelper is not null && obj.heldObject.Value.Name.Contains("enricher", StringComparison.OrdinalIgnoreCase)) { toReturn.name = MainClass.ModHelper.Translation.Get("readtile.sprinkler.enricher", new { value = toReturn.name }); } @@ -930,11 +930,11 @@ namespace stardew_access.Features 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; @@ -943,15 +943,15 @@ 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` @@ -1030,7 +1030,7 @@ namespace stardew_access.Features return ("Item box", CATEGORY.MineItems); } - if (objName.ToLower().Contains("stone")) + if (objName.Contains("stone", StringComparison.OrdinalIgnoreCase)) { switch (index) { From dd812851b4d663978a2acf78f95d6ea46a4e4a08 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Wed, 8 Mar 2023 14:09:13 -0800 Subject: [PATCH 07/19] Add base for improved tile coordinate lookup functionality; typo fixes. * Build tile dictionary at game launch to speed up tile lookups during gameplay. * Add GameLaunched event to setup new dictionary after other mods loaded. * Change `StaticTiles.getStaticTileInfoAtWithCategory` to use new dictionary for lookup. * Various typo fixes and other code cleanup --- stardew-access/CustomCommands.cs | 60 ++--- stardew-access/Features/StaticTiles.cs | 284 +++++++++++++++--------- stardew-access/Features/TileInfo.cs | 2 +- stardew-access/ModEntry.cs | 7 + stardew-access/assets/static-tiles.json | 1 + 5 files changed, 218 insertions(+), 136 deletions(-) diff --git a/stardew-access/CustomCommands.cs b/stardew-access/CustomCommands.cs index 8d10fc2..60c9482 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,28 @@ 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(); 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 +480,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 +488,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 +496,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/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 34a36b5..dbd441d 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -1,15 +1,16 @@ using Newtonsoft.Json.Linq; using StardewValley; +using System.Linq; namespace stardew_access.Features { public class StaticTiles { - private JObject? staticTilesData = null; - private JObject? customTilesData = null; - HashSet>? staticTilesDataSet = null; - HashSet>? customTilesDataSet = null; - + private static JObject? staticTilesData = null; + private static JObject? customTilesData = null; + private static Dictionary?>? staticTilesDataDict = null; + private static Dictionary?>? customTilesDataDict = null; + public StaticTiles() { if (MainClass.ModHelper is null) @@ -17,14 +18,13 @@ namespace stardew_access.Features try { - using (StreamReader file = new StreamReader(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "static-tiles.json"))) + using (StreamReader file = new(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "static-tiles.json"))) { string json = file.ReadToEnd(); staticTilesData = JObject.Parse(json); } if (staticTilesData is not null) { - staticTilesDataSet = new HashSet>(staticTilesData); } MainClass.InfoLog($"Loaded static-tile.json"); @@ -36,14 +36,13 @@ namespace stardew_access.Features try { - using (StreamReader file = new StreamReader(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "custom-tiles.json"))) + using (StreamReader file = new(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "custom-tiles.json"))) { string json = file.ReadToEnd(); customTilesData = JObject.Parse(json); } if (customTilesData is not null) { - customTilesDataSet = new HashSet>(customTilesData); } MainClass.InfoLog($"Loaded custom-tile.json"); @@ -52,11 +51,12 @@ namespace stardew_access.Features { 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")}"); } + this.SetupTilesDicts(); } - public bool isAvailable(string locationName) + public static bool IsAvailable(string locationName) { - List allData = new List(); + List allData = new(); if (customTilesData != null) allData.Add(customTilesData); if (staticTilesData != null) allData.Add(staticTilesData); @@ -67,7 +67,7 @@ namespace stardew_access.Features { if (location.Key.Contains("||") && MainClass.ModHelper != null) { - string uniqueModID = location.Key.Substring(location.Key.LastIndexOf("||") + 2); + string uniqueModID = location.Key[(location.Key.LastIndexOf("||") + 2)..]; string locationNameInJson = location.Key.Remove(location.Key.LastIndexOf("||")); bool isLoaded = MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModID); @@ -82,127 +82,201 @@ namespace stardew_access.Features return false; } - public (string? name, CATEGORY category) GetTileFromSet(int x, int y, GameLocation currentLocation, HashSet> data) + public static (string? name, CATEGORY category) GetTileFromDict(int x, int y) { + if (staticTilesDataDict is not null && staticTilesDataDict.TryGetValue(Game1.currentLocation.Name, out var locationDict)) + { + if (locationDict is not null && locationDict.TryGetValue(((short)x, (short)y), out var tile)) + { + //MainClass.DebugLog($"Tile ({x}, {y}) is in the dict as {tile.name}."); + return tile; + } + } /*else if (locationDict is null) { + //MainClass.DebugLog($"Skipping null entry for location {Game1.currentLocation.Name}."); + } + else { + MainClass.InfoLog($"Location {Game1.currentLocation.Name} not found in static tiles."); + }*/ + return (null, CATEGORY.Others); + } + + private static Dictionary?>? BuildTilesDict(JObject? data) + { + if (data is null) return null; + //MainClass.DebugLog("Loading dict data"); + var comparer = StringComparer.OrdinalIgnoreCase; + Dictionary?> tilesDict = new(comparer); foreach (KeyValuePair location in data) { - string locationName = location.Key; - if (locationName.Contains("||") && MainClass.ModHelper is not null) + try { - // 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); + //MainClass.DebugLog($"Entering loop for location {location}."); + if (location.Value is null) continue; + string locationName = location.Key; + if (locationName.Contains("||") && MainClass.ModHelper is not 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[(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.StartsWith("farm_", StringComparison.OrdinalIgnoreCase)) - { - 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 is not null) MainClass.DebugLog($"{farmType} {Game1.whichModFarm.MapName}"); - if (farmTypeIndex != 7 || Game1.whichModFarm is null || !farmType.Equals(Game1.whichModFarm.MapName, StringComparison.OrdinalIgnoreCase)) continue; // Not tested but should work - } - - if (locationName.Equals("town_joja", StringComparison.OrdinalIgnoreCase) && Game1.MasterPlayer.mailReceived.Contains("JojaMember")) - { - locationName = "town"; - } - - if (!currentLocation.Name.Equals(locationName, StringComparison.OrdinalIgnoreCase)) continue; - if (location.Value is null) continue; - - foreach (var tile in ((JObject)location.Value)) - { - if (tile.Value is null) continue; - - JToken? tileXArray = tile.Value["x"]; - JToken? tileYArray = tile.Value["y"]; - JToken? tileType = tile.Value["type"]; - - if (tileXArray is null || tileYArray is null || tileType is null) + if (!isLoaded) continue; // Skip if the specified mod is not loaded + } + //MainClass.DebugLog($"Loading tiles for {locationName}."); + if (location.Value.Type == JTokenType.Null) + { + tilesDict.Add(location.Key, null); + //MainClass.DebugLog($"Created null entry for location {location.Key}."); + //MainClass.DebugLog("SPAM!!!"); 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) + Dictionary<(short x, short y), (string name, CATEGORY category)>? locationDict = new(); + //MainClass.DebugLog($"Entering tiles loop for {locationName}."); + foreach (var tileInfo in ((JObject)location.Value)) { - if (short.Parse(item.ToString()) != y) - continue; - - isYPresent = true; - break; - } - - if (isXPresent && isYPresent) - { - string key = tile.Key; - if (key.Contains('[') && key.Contains(']')) + if (tileInfo.Value == null) continue; + string key = tileInfo.Key; + var tile = tileInfo.Value; + if (tile.Type == JTokenType.Object ) { - int i1 = key.IndexOf('['); - int i2 = key.LastIndexOf(']'); + JToken? tileXArray = tile["x"]; + JToken? tileYArray = tile["y"]; + JToken? tileType = tile["type"]; - if (i1 < i2) + if (tileXArray is null || tileYArray is null || tileType is null) + continue; + + //MainClass.DebugLog($"Adding tile {key} to location {locationName}."); + if (key.Contains('[') && key.Contains(']')) { - key = key.Remove(i1, ++i2 - i1); + int i1 = key.IndexOf('['); + int i2 = key.LastIndexOf(']'); + + if (i1 < i2) + { + key = key.Remove(i1, ++i2 - i1); + } + } + (string key, CATEGORY category) tileData = (key.Trim(), CATEGORY.FromString(tileType.ToString().ToLower())); + + foreach (var item_x in tileXArray) + { + short x = short.Parse(item_x.ToString()); + foreach (var item_y in tileYArray) + { + short y = short.Parse(item_y.ToString()); + (short x, short y) coords = (x, y); + try + { + locationDict.Add(coords, tileData); + } + catch (System.Exception e) + { + MainClass.ErrorLog($"Failed setting tile {key} for location {locationName}. Reason:\n\t{e}"); + } + } } } - - return (key.Trim(), CATEGORY.FromString(tileType.ToString().ToLower())); + } + //MainClass.DebugLog($"Location Dict has {locationDict.Count} members."); + if (locationDict.Count > 0) + { + //MainClass.DebugLog($"Adding locationDict for {locationName}"); + tilesDict.Add(locationName, locationDict); + //MainClass.DebugLog($"Added locationDict for {locationName}"); + } + } catch (System.Exception e) { + if (location.Value is null || location.Value.Type == JTokenType.Null) + { + tilesDict.Add(location.Key, null); + //MainClass.DebugLog($"Created null entry for location {location.Key}."); + } else { + MainClass.ErrorLog($"Unable to build tiles dict; failed on location {location.Key} with value ({location.Value.GetType()}){location.Value}. Reason:\n\t{e}"); + throw; } } } - return (null, CATEGORY.Others); + if (tilesDict.Count > 0) + { + //MainClass.DebugLog("Dict loaded, returning."); + return tilesDict; + } else { + //MainClass.DebugLog("Dict not loaded, returning null"); + return null; + } } - public string? getStaticTileInfoAt(int x, int y) + public void SetupTilesDicts() { - return getStaticTileInfoAtWithCategory(x, y).name; + //MainClass.DebugLog("Attempting to set dicts"); + try + { + staticTilesDataDict = BuildTilesDict(staticTilesData); + if (staticTilesDataDict is not null) + { + //MainClass.DebugLog($"staticTilesDataDict has {staticTilesDataDict.Count} entries."); + //MainClass.DebugLog($"Keys: {staticTilesDataDict.Keys}"); + } else { + //MainClass.DebugLog("Static tiles not loaded."); + } + } + catch (System.Exception e) + { + MainClass.ErrorLog($"Failed to set static tiles dict. Reason: \n\t{e}"); + } + try + { + customTilesDataDict = BuildTilesDict(customTilesData); + if (customTilesDataDict is not null) + { + //MainClass.DebugLog($"customTilesDataDict has {customTilesDataDict.Count} entries."); + } else { + //MainClass.DebugLog("Custom tiles not loaded."); + } + } + catch (System.Exception e) + { + MainClass.ErrorLog($"Faild to set custom tiles dict. Reason:\n\t{e}"); + } + //MainClass.DebugLog("Successfully created tiles dicts."); } - public (string? name, CATEGORY category) getStaticTileInfoAtWithCategory(int x, int y) + public static string? GetStaticTileInfoAt(int x, int y) { - GameLocation currentLocation = Game1.currentLocation; - if (customTilesDataSet is not null) return GetTileFromSet(x, y, currentLocation, customTilesDataSet); - if (staticTilesDataSet is not null) return GetTileFromSet(x, y, currentLocation, staticTilesDataSet); + return GetStaticTileInfoAtWithCategory(x, y).name; + } + + public static (string? name, CATEGORY category) GetStaticTileInfoAtWithCategory(int x, int y) + { + if (customTilesDataDict is not null) return GetTileFromDict(x, y); + if (staticTilesDataDict is not null) return GetTileFromDict(x, y); return (null, CATEGORY.Others); } - private int getFarmTypeIndex(string farmType) + private static int GetFarmTypeIndex(string farmType) { return farmType.ToLower() switch { diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index 270c960..9fe824b 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -46,7 +46,7 @@ namespace stardew_access.Features 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? name, CATEGORY category) staticTile = StaticTiles.GetStaticTileInfoAtWithCategory(x, y); string? bush = getBushAtTile(x, y, lessInfo); if (Game1.currentLocation.isCharacterAtTile(tile) is NPC npc) diff --git a/stardew-access/ModEntry.cs b/stardew-access/ModEntry.cs index 1830c50..8fb43c2 100644 --- a/stardew-access/ModEntry.cs +++ b/stardew-access/ModEntry.cs @@ -141,6 +141,7 @@ namespace stardew_access helper.Events.Input.ButtonPressed += this.OnButtonPressed; helper.Events.GameLoop.UpdateTicked += this.onUpdateTicked; + helper.Events.GameLoop.GameLaunched += this.onGameLaunched; AppDomain.CurrentDomain.DomainUnload += OnExit; AppDomain.CurrentDomain.ProcessExit += OnExit; } @@ -159,6 +160,12 @@ namespace stardew_access return new API(); } + private void onGameLaunched(object? sender, GameLaunchedEventArgs? e) + { + if (sTiles is not null) + sTiles.SetupTilesDicts(); + } + private void onUpdateTicked(object? sender, UpdateTickedEventArgs? e) { if (!Context.IsPlayerFree) 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], From 5d8f85fa5b71638981d4c200d936aee11b9d4927 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Wed, 8 Mar 2023 20:04:23 -0800 Subject: [PATCH 08/19] Separated tile file loading from StaticTiles constructor --- stardew-access/Features/StaticTiles.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index dbd441d..6a9a50e 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -16,6 +16,12 @@ namespace stardew_access.Features if (MainClass.ModHelper is null) return; + if (staticTilesData is null) LoadTilesFiles(); + this.SetupTilesDicts(); + } + + public static void LoadTilesFiles() + { try { using (StreamReader file = new(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "static-tiles.json"))) @@ -51,9 +57,7 @@ namespace stardew_access.Features { 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")}"); } - this.SetupTilesDicts(); } - public static bool IsAvailable(string locationName) { List allData = new(); From 245959366c71d847082ddeabedd9b126334c8319 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Thu, 30 Mar 2023 03:31:54 -0700 Subject: [PATCH 09/19] Refactor `StaticTiles` and `CATEGORY` classes for enhanced functionality `StaticTiles` now uses `System.Text.Json` and supports conditional and mod-based maps with efficient dictionary lookups. Dictionary can be reloaded as needed, typically once per game day. `CATEGORY` allows runtime addition of new categories, with an indexer function for retrieval, and retains static properties for compiled categories. --- stardew-access/CustomCommands.cs | 3 +- stardew-access/Features/StaticTiles.cs | 742 +++++++++++++++++-------- stardew-access/Features/Utils.cs | 175 +++--- stardew-access/ModEntry.cs | 16 +- 4 files changed, 605 insertions(+), 331 deletions(-) diff --git a/stardew-access/CustomCommands.cs b/stardew-access/CustomCommands.cs index 60c9482..5e8b200 100644 --- a/stardew-access/CustomCommands.cs +++ b/stardew-access/CustomCommands.cs @@ -467,7 +467,8 @@ namespace stardew_access 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!"); }); diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 6a9a50e..731262f 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -1,298 +1,550 @@ -using Newtonsoft.Json.Linq; -using StardewValley; +using System.IO; +using System.Text.Json; using System.Linq; +using System.Collections.Generic; +using StardewValley; namespace stardew_access.Features { public class StaticTiles { - private static JObject? staticTilesData = null; - private static JObject? customTilesData = null; - private static Dictionary?>? staticTilesDataDict = null; - private static Dictionary?>? customTilesDataDict = null; - - public StaticTiles() - { - if (MainClass.ModHelper is null) - return; + // Static instance for the singleton pattern + private static StaticTiles? _instance; - if (staticTilesData is null) LoadTilesFiles(); - this.SetupTilesDicts(); + /// + /// The singleton instance of the class. + /// + public static StaticTiles Instance + { + get + { + if (_instance == null) + { + _instance = new StaticTiles(); + } + return _instance; + } } + /// + /// 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; + + /// + /// 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; + + /// + /// 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 Dictionary> + { + ["Farm"] = (conditionType, uniqueModId) => + { + if (string.IsNullOrEmpty(uniqueModId)) + { + // Branch for vanilla locations + // Calculate farmTypeIndex using the switch expression + int farmTypeIndex = conditionType.ToLower() switch + { + "default" => 0, + "riverlands" => 1, + "forest" => 2, + "mountains" => 3, + "combat" => 4, + "fourcorners" => 5, + "beach" => 6, + _ => 7, + }; + + // 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 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. + private 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; + } + + /// + /// Loads the static and custom tile files. + /// public static void LoadTilesFiles() { - try - { - using (StreamReader file = new(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "static-tiles.json"))) - { - string json = file.ReadToEnd(); - staticTilesData = JObject.Parse(json); - } - if (staticTilesData is not null) - { - } + if (MainClass.ModHelper is null) return; - 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(Path.Combine(MainClass.ModHelper.DirectoryPath, "assets", "custom-tiles.json"))) - { - string json = file.ReadToEnd(); - customTilesData = JObject.Parse(json); - } - if (customTilesData is not null) - { - } - - 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")}"); - } + staticTilesData = LoadJsonFile(StaticTilesFileName); + customTilesData = LoadJsonFile(CustomTilesFileName); } - public static bool IsAvailable(string locationName) + + /// + /// 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) { - List allData = new(); - - if (customTilesData != null) allData.Add(customTilesData); - if (staticTilesData != null) allData.Add(staticTilesData); - - foreach (JObject data in allData) + // Check if the conditionName is not null or empty + if (string.IsNullOrEmpty(conditionName)) { - foreach (KeyValuePair location in data) - { - if (location.Key.Contains("||") && MainClass.ModHelper != null) - { - string uniqueModID = location.Key[(location.Key.LastIndexOf("||") + 2)..]; - string locationNameInJson = location.Key.Remove(location.Key.LastIndexOf("||")); - bool isLoaded = MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModID); + throw new ArgumentException("Condition name cannot be null or empty.", nameof(conditionName)); + } - if (!isLoaded) continue; // Skip if the specified mod is not loaded - if (locationName.Equals(locationNameInJson, StringComparison.OrdinalIgnoreCase)) return true; + // 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.Equals(location.Key, StringComparison.OrdinalIgnoreCase)) - return true; } } - return false; + return locationData; } - public static (string? name, CATEGORY category) GetTileFromDict(int x, int y) + /// + /// Represents the different categories of locations. + /// + public enum LocationCategory { - if (staticTilesDataDict is not null && staticTilesDataDict.TryGetValue(Game1.currentLocation.Name, out var locationDict)) + /// + /// 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 + } + + /// + /// Determines the location category based on the given location name. + /// + /// The location name. + /// The location category. + public static LocationCategory GetLocationCategory(string name) + { + 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>> { - if (locationDict is not null && locationDict.TryGetValue(((short)x, (short)y), out var tile)) + { 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.DebugLog($"Tile ({x}, {y}) is in the dict as {tile.name}."); - return tile; + MainClass.ErrorLog($"Invalid value type for {property.Name}"); + continue; } - } /*else if (locationDict is null) { - //MainClass.DebugLog($"Skipping null entry for location {Game1.currentLocation.Name}."); - } - else { - MainClass.InfoLog($"Location {Game1.currentLocation.Name} not found in static tiles."); - }*/ - return (null, CATEGORY.Others); - } - private static Dictionary?>? BuildTilesDict(JObject? data) - { - if (data is null) return null; - //MainClass.DebugLog("Loading dict data"); - var comparer = StringComparer.OrdinalIgnoreCase; - Dictionary?> tilesDict = new(comparer); - foreach (KeyValuePair location in data) - { - try + string propertyName = property.Name; + string uniqueModId = null; + + var splitModId = propertyName.Split("||", StringSplitOptions.RemoveEmptyEntries); + if (splitModId.Length == 2) { - //MainClass.DebugLog($"Entering loop for location {location}."); - if (location.Value is null) continue; - string locationName = location.Key; - if (locationName.Contains("||") && MainClass.ModHelper is not 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[(locationName.LastIndexOf("||") + 2)..]; - locationName = locationName.Remove(locationName.LastIndexOf("||")); - bool isLoaded = MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModID); + propertyName = splitModId[0]; + uniqueModId = splitModId[1]; - if (!isLoaded) continue; // Skip if the specified mod is not loaded - } - //MainClass.DebugLog($"Loading tiles for {locationName}."); - if (location.Value.Type == JTokenType.Null) + if (MainClass.ModHelper == null || !MainClass.ModHelper.ModRegistry.IsLoaded(uniqueModId)) { - tilesDict.Add(location.Key, null); - //MainClass.DebugLog($"Created null entry for location {location.Key}."); - //MainClass.DebugLog("SPAM!!!"); continue; } - + } - Dictionary<(short x, short y), (string name, CATEGORY category)>? locationDict = new(); - //MainClass.DebugLog($"Entering tiles loop for {locationName}."); - foreach (var tileInfo in ((JObject)location.Value)) + var category = GetLocationCategory(propertyName); + + if (category == LocationCategory.VanillaConditional || category == LocationCategory.ModConditional) + { + var splitPropertyName = propertyName.Split("__", StringSplitOptions.RemoveEmptyEntries); + if (splitPropertyName.Length == 2) { - if (tileInfo.Value == null) continue; - string key = tileInfo.Key; - var tile = tileInfo.Value; - if (tile.Type == JTokenType.Object ) + propertyName = splitPropertyName[0]; + string conditionalName = splitPropertyName[1]; + + if (conditionals.TryGetValue(conditionalName, out var conditionalFunc)) { - JToken? tileXArray = tile["x"]; - JToken? tileYArray = tile["y"]; - JToken? tileType = tile["type"]; - - if (tileXArray is null || tileYArray is null || tileType is null) + if (!conditionalFunc(conditionalName, uniqueModId)) + { continue; - - //MainClass.DebugLog($"Adding tile {key} to location {locationName}."); - if (key.Contains('[') && key.Contains(']')) - { - int i1 = key.IndexOf('['); - int i2 = key.LastIndexOf(']'); - - if (i1 < i2) - { - key = key.Remove(i1, ++i2 - i1); - } - } - (string key, CATEGORY category) tileData = (key.Trim(), CATEGORY.FromString(tileType.ToString().ToLower())); - - foreach (var item_x in tileXArray) - { - short x = short.Parse(item_x.ToString()); - foreach (var item_y in tileYArray) - { - short y = short.Parse(item_y.ToString()); - (short x, short y) coords = (x, y); - try - { - locationDict.Add(coords, tileData); - } - catch (System.Exception e) - { - MainClass.ErrorLog($"Failed setting tile {key} for location {locationName}. Reason:\n\t{e}"); - } - } } } - } - //MainClass.DebugLog($"Location Dict has {locationDict.Count} members."); - if (locationDict.Count > 0) - { - //MainClass.DebugLog($"Adding locationDict for {locationName}"); - tilesDict.Add(locationName, locationDict); - //MainClass.DebugLog($"Added locationDict for {locationName}"); - } - } catch (System.Exception e) { - if (location.Value is null || location.Value.Type == JTokenType.Null) - { - tilesDict.Add(location.Key, null); - //MainClass.DebugLog($"Created null entry for location {location.Key}."); - } else { - MainClass.ErrorLog($"Unable to build tiles dict; failed on location {location.Key} with value ({location.Value.GetType()}){location.Value}. Reason:\n\t{e}"); - throw; + else + { + MainClass.ErrorLog($"Unknown conditional name: {conditionalName}"); + continue; + } } } - } - if (tilesDict.Count > 0) - { - //MainClass.DebugLog("Dict loaded, returning."); - return tilesDict; - } else { - //MainClass.DebugLog("Dict not loaded, returning null"); - return null; - } - } + var locationDict = CreateLocationTileDict(property.Value); - public void SetupTilesDicts() - { - //MainClass.DebugLog("Attempting to set dicts"); - try - { - staticTilesDataDict = BuildTilesDict(staticTilesData); - if (staticTilesDataDict is not null) + if (categoryDicts.TryGetValue(category, out var targetDict)) { - //MainClass.DebugLog($"staticTilesDataDict has {staticTilesDataDict.Count} entries."); - //MainClass.DebugLog($"Keys: {staticTilesDataDict.Keys}"); - } else { - //MainClass.DebugLog("Static tiles not loaded."); + targetDict.Add(propertyName, locationDict); } - } - catch (System.Exception e) - { - MainClass.ErrorLog($"Failed to set static tiles dict. Reason: \n\t{e}"); - } - try - { - customTilesDataDict = BuildTilesDict(customTilesData); - if (customTilesDataDict is not null) + else { - //MainClass.DebugLog($"customTilesDataDict has {customTilesDataDict.Count} entries."); - } else { - //MainClass.DebugLog("Custom tiles not loaded."); + MainClass.ErrorLog($"Unknown location category for {propertyName}"); } } - catch (System.Exception e) + + 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) + { + if (destinationDictionary == null || sourceDictionary == null) { - MainClass.ErrorLog($"Faild to set custom tiles dict. Reason:\n\t{e}"); + // Log a warning or throw an exception if either dictionary is null + return; } - //MainClass.DebugLog("Successfully created tiles dicts."); - } - public static string? GetStaticTileInfoAt(int x, int y) - { - return GetStaticTileInfoAtWithCategory(x, y).name; - } - - public static (string? name, CATEGORY category) GetStaticTileInfoAtWithCategory(int x, int y) - { - if (customTilesDataDict is not null) return GetTileFromDict(x, y); - if (staticTilesDataDict is not null) return GetTileFromDict(x, y); - - return (null, CATEGORY.Others); - } - - private static int GetFarmTypeIndex(string farmType) - { - return farmType.ToLower() switch + foreach (var sourceEntry in sourceDictionary) { - "default" => 0, - "riverlands" => 1, - "forest" => 2, - "mountains" => 3, - "combat" => 4, - "fourcorners" => 5, - "beach" => 6, - _ => 7, - }; + // 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>(); + + // 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) + { + staticTilesDataDict = BuildTilesDict(staticTilesData.Value); + } + else + { + staticTilesDataDict = new Dictionary>(); + } + + if (customTilesData.HasValue) + { + 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) + { + if (currentLocationName == null) + { + 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/Utils.cs b/stardew-access/Features/Utils.cs index a1b1297..87fcdf6 100644 --- a/stardew-access/Features/Utils.cs +++ b/stardew-access/Features/Utils.cs @@ -1,11 +1,25 @@ 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,81 +31,100 @@ namespace stardew_access.Features return _typeKeyWord; } + public static IReadOnlyDictionary Categories => _categories; + + private static readonly Dictionary _categories = new Dictionary(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 : 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 diff --git a/stardew-access/ModEntry.cs b/stardew-access/ModEntry.cs index 8fb43c2..2c40e6a 100644 --- a/stardew-access/ModEntry.cs +++ b/stardew-access/ModEntry.cs @@ -20,7 +20,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 +29,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 @@ -162,8 +150,8 @@ namespace stardew_access private void onGameLaunched(object? sender, GameLaunchedEventArgs? e) { - if (sTiles is not null) - sTiles.SetupTilesDicts(); + StaticTiles.LoadTilesFiles(); + StaticTiles.SetupTilesDicts(); } private void onUpdateTicked(object? sender, UpdateTickedEventArgs? e) From 34c182ddc66a7cec0eb025df46952d9457711f61 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Fri, 31 Mar 2023 14:53:18 -0700 Subject: [PATCH 10/19] Cache the value of `Game1.currentLocation` --- stardew-access/API.cs | 4 +- stardew-access/Features/Radar.cs | 40 ++-- stardew-access/Features/ReadTile.cs | 5 +- stardew-access/Features/StaticTiles.cs | 18 +- stardew-access/Features/TileInfo.cs | 218 +++++++++--------- stardew-access/Features/TileViewer.cs | 7 +- stardew-access/ModEntry.cs | 4 +- .../Patches/MiscPatches/Game1Patch.cs | 2 +- 8 files changed, 152 insertions(+), 146 deletions(-) diff --git a/stardew-access/API.cs b/stardew-access/API.cs index 1a2f9fa..550a6bf 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/Features/Radar.cs b/stardew-access/Features/Radar.cs index b7bd95b..601e3b7 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -93,6 +93,7 @@ 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) { + var currentLocation = Game1.currentLocation; Dictionary detectedTiles = new Dictionary(); Queue toSearch = new Queue(); @@ -107,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 @@ -141,6 +142,7 @@ namespace stardew_access.Features public Dictionary SearchLocation() { var watch = System.Diagnostics.Stopwatch.StartNew(); + var currentLocation = Game1.currentLocation; Dictionary detectedTiles = new Dictionary(); Vector2 position = Vector2.Zero; (bool, string? name, string category) tileInfo; @@ -162,7 +164,7 @@ namespace stardew_access.Features 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 @@ -175,7 +177,7 @@ namespace stardew_access.Features { Vector2 dir = new Vector2(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((int)dir.X, (int)dir.Y, currentLocation) || currentLocation.isTileOnMap(dir))) { toSearch.Enqueue(dir); searched.Add(dir); @@ -210,9 +212,9 @@ 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); + (string? name, CATEGORY? category) tileDetail = TileInfo.getNameWithCategoryAtTile(position, currentLocation, lessInfo); if (tileDetail.name == null) return (false, null, CATEGORY.Others.ToString()); @@ -223,16 +225,16 @@ namespace stardew_access.Features } - 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((int)position.X, (int)position.Y, currentLocation); 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) { @@ -243,23 +245,23 @@ 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); + (string? name, CATEGORY? category) tileDetail = TileInfo.getNameWithCategoryAtTile(position, currentLocation); if (tileDetail.name != null) { if (tileDetail.category == null) tileDetail.category = CATEGORY.Others; - PlaySoundAt(position, tileDetail.name, tileDetail.category); + PlaySoundAt(position, tileDetail.name, tileDetail.category, currentLocation); } } } @@ -269,7 +271,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 @@ -330,19 +332,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..75b37a5 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(x, y, currentLocation); - (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 731262f..78a4c73 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -40,13 +40,13 @@ namespace stardew_access.Features /// 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; + 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; + private static Dictionary> customTilesDataDict = new(); /// /// The file name of the JSON file containing static tile data. @@ -233,7 +233,7 @@ namespace stardew_access.Features var type = item.Value.TryGetProperty("type", out var typeElement) ? typeElement.GetString() : "Others"; // Obtain the category instance - var category = CATEGORY.FromString(type); + 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++) @@ -342,7 +342,7 @@ namespace stardew_access.Features } string propertyName = property.Name; - string uniqueModId = null; + string uniqueModId = ""; var splitModId = propertyName.Split("||", StringSplitOptions.RemoveEmptyEntries); if (splitModId.Length == 2) @@ -407,7 +407,7 @@ namespace stardew_access.Features /// The source dictionary containing the data to merge into the destination dictionary. private static void MergeDicts( Dictionary destinationDictionary, - Dictionary sourceDictionary) + Dictionary sourceDictionary) where TKey : notnull { if (destinationDictionary == null || sourceDictionary == null) { @@ -495,7 +495,7 @@ namespace stardew_access.Features /// 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) + private static (string? name, CATEGORY? category) GetTileInfoAt(int x, int y, string? currentLocationName = null, bool includeCategory = false) { if (currentLocationName == null) { @@ -528,10 +528,10 @@ namespace stardew_access.Features /// 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) + public static string GetStaticTileNameAt(int x, int y, string? currentLocationName = null) { var (name, _) = GetTileInfoAt(x, y, currentLocationName, includeCategory: false); - return name; + return name ?? ""; } /// @@ -541,7 +541,7 @@ namespace stardew_access.Features /// 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) + 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); diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index 9fe824b..485f935 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -13,9 +13,9 @@ namespace stardew_access.Features 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" }; ///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) tileDetail = getNameWithCategoryAtTile(tile, currentLocation); if (tileDetail.category == null) tileDetail.category = CATEGORY.Others; @@ -24,32 +24,34 @@ namespace stardew_access.Features } ///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; + if (currentLocation is null) 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) { + if (currentLocation is null) currentLocation = Game1.currentLocation; int x = (int)tile.X; int y = (int)tile.Y; string? toReturn = null; 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 = StaticTiles.GetStaticTileInfoAtWithCategory(x, y); - string? bush = getBushAtTile(x, y, lessInfo); + bool isColliding = isCollidingAtTile(x, y, currentLocation); + var terrainFeature = currentLocation.terrainFeatures.FieldDict; + string? door = getDoorAtTile(x, y, currentLocation); + string? warp = getWarpPointAtTile(x, y, currentLocation); + (CATEGORY? category, string? name) dynamicTile = getDynamicTilesInfo(x, y, currentLocation, lessInfo); + string? junimoBundle = getJunimoBundleAt(x, y, currentLocation); + string? resourceClump = getResourceClumpAtTile(x, y, currentLocation, lessInfo); + string? farmAnimal = getFarmAnimalAt(currentLocation, x, y); + string? parrot = getParrotPerchAtTile(x, y, currentLocation); + (string? name, CATEGORY category) staticTile = StaticTiles.GetStaticTileInfoAtWithCategory(x, y, currentLocation.Name); + string? bush = getBushAtTile(x, y, currentLocation, lessInfo); - if (Game1.currentLocation.isCharacterAtTile(tile) is NPC npc) + if (currentLocation.isCharacterAtTile(tile) is NPC npc) { toReturn = npc.displayName; if (npc.isVillager() || npc.CanSocialize) @@ -72,23 +74,23 @@ namespace stardew_access.Features toReturn = dynamicTile.name; category = dynamicTile.category; } - else if (Game1.currentLocation is VolcanoDungeon && ((VolcanoDungeon)Game1.currentLocation).IsCooledLava(x, y) && !lessInfo) + else if (currentLocation is VolcanoDungeon && ((VolcanoDungeon)currentLocation).IsCooledLava(x, y) && !lessInfo) { toReturn = "Cooled lava"; category = CATEGORY.WaterTiles; } - else if (Game1.currentLocation is VolcanoDungeon && StardewValley.Monsters.LavaLurk.IsLavaTile((VolcanoDungeon)Game1.currentLocation, x, y) && !lessInfo) + else if (currentLocation is VolcanoDungeon && StardewValley.Monsters.LavaLurk.IsLavaTile((VolcanoDungeon)currentLocation, x, y) && !lessInfo) { toReturn = "Lava"; category = CATEGORY.WaterTiles; } - else if (Game1.currentLocation.isObjectAtTile(x, y)) + else if (currentLocation.isObjectAtTile(x, y)) { - (string? name, CATEGORY? category) obj = getObjectAtTile(x, y, lessInfo); + (string? name, CATEGORY? category) obj = getObjectAtTile(x, y, currentLocation, lessInfo); toReturn = obj.name; category = obj.category; } - else if (Game1.currentLocation.isWaterTile(x, y) && isColliding && !lessInfo) + else if (currentLocation.isWaterTile(x, y) && isColliding && !lessInfo) { toReturn = "Water"; category = CATEGORY.WaterTiles; @@ -124,22 +126,22 @@ namespace stardew_access.Features toReturn = door; category = CATEGORY.Doors; } - else if (isMineDownLadderAtTile(x, y)) + else if (isMineDownLadderAtTile(x, y, currentLocation)) { toReturn = "Ladder"; category = CATEGORY.Doors; } - else if (isShaftAtTile(x, y)) + else if (isShaftAtTile(x, y, currentLocation)) { toReturn = "Shaft"; category = CATEGORY.Doors; } - else if (isMineUpLadderAtTile(x, y)) + else if (isMineUpLadderAtTile(x, y, currentLocation)) { toReturn = "Up Ladder"; category = CATEGORY.Doors; } - else if (isElevatorAtTile(x, y)) + else if (isElevatorAtTile(x, y, currentLocation)) { toReturn = "Elevator"; category = CATEGORY.Doors; @@ -160,7 +162,7 @@ namespace stardew_access.Features { try { - NetCollection droppedItems = Game1.currentLocation.debris; + NetCollection droppedItems = currentLocation.debris; int droppedItemsCount = droppedItems.Count(); if (droppedItemsCount > 0) { @@ -194,10 +196,10 @@ namespace stardew_access.Features return (toReturn, category); } - public static string? getBushAtTile(int x, int y, bool lessInfo = false) + public static string? getBushAtTile(int x, int y, GameLocation currentLocation, bool lessInfo = false) { string? toReturn = null; - Bush? bush = (Bush)Game1.currentLocation.getLargeTerrainFeatureAt(x, y); + Bush? bush = (Bush)currentLocation.getLargeTerrainFeatureAt(x, y); if (bush is null) return null; if (lessInfo && (bush.tilePosition.Value.X != x || bush.tilePosition.Value.Y != y)) @@ -206,10 +208,10 @@ namespace stardew_access.Features 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 && (int)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); + string season = ((int)bush.overrideSeason.Value == -1) ? Game1.GetSeasonForLocation(currentLocation) : Utility.getSeasonNameFromNumber(bush.overrideSeason.Value); int shakeOff = -1; if (!(season == "spring")) { @@ -249,10 +251,10 @@ namespace stardew_access.Features return toReturn; } - public static string? getJunimoBundleAt(int x, int y) + public static string? getJunimoBundleAt(int x, int y, GameLocation currentLocation) { string? name = null; - if (Game1.currentLocation is CommunityCenter communityCenter) + if (currentLocation is CommunityCenter communityCenter) { name = (x, y) switch { @@ -267,7 +269,7 @@ namespace stardew_access.Features 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 { @@ -282,20 +284,20 @@ namespace stardew_access.Features return null; } - public static bool isCollidingAtTile(int x, int y) + public static bool isCollidingAtTile(int x, int y, GameLocation currentLocation) { Rectangle rect = new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62); /* Reference // 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 (isWarpPointAtTile(x, y, currentLocation)) return false; - if (Game1.currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) + if (currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) { return true; } - if (Game1.currentLocation is Woods && getStumpsInWoods(x, y) is not null) + if (currentLocation is Woods && getStumpsInWoods(x, y, currentLocation) is not null) return true; return false; @@ -303,24 +305,24 @@ namespace stardew_access.Features // Optimized // Sometimes warp points are 1 tile off the map for example in coops and barns; check that this is not a warp point - if (!isWarpPointAtTile(x, y)) + if (!isWarpPointAtTile(x, y, currentLocation)) { // not a warp point //directly return the value of the logical comparison rather than wasting time in conditional - return ((Game1.currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) || (Game1.currentLocation is Woods && getStumpsInWoods(x, y) is not null)); + return ((currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) || (currentLocation is Woods && getStumpsInWoods(x, y, currentLocation) is not null)); } // was a warp point; return false return false; } - public static Boolean isWarpPointAtTile(int x, int y) + public static Boolean isWarpPointAtTile(int x, int y, GameLocation currentLocation) { - if (Game1.currentLocation is null) return false; + if (currentLocation is null) return false; - int warpsCount = Game1.currentLocation.warps.Count(); + int warpsCount = currentLocation.warps.Count(); for (int i = 0; i < warpsCount; i++) { - if (Game1.currentLocation.warps[i].X == x && Game1.currentLocation.warps[i].Y == y) return true; + if (currentLocation.warps[i].X == x && currentLocation.warps[i].Y == y) return true; } return false; @@ -369,13 +371,13 @@ namespace stardew_access.Features /// /// 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 (CATEGORY? category, string? name) getDynamicTilesInfo(int x, int y, GameLocation currentLocation, bool lessInfo = false) { - if (Game1.currentLocation.orePanPoint.Value != Point.Zero && Game1.currentLocation.orePanPoint.Value == new Point(x, y)) + if (currentLocation.orePanPoint.Value != Point.Zero && currentLocation.orePanPoint.Value == new Point(x, y)) { return (CATEGORY.Interactables, "panning spot"); } - else if (Game1.currentLocation is Farm farm) + else if (currentLocation is Farm farm) { if (farm.GetMainMailboxPosition().X == x && farm.GetMainMailboxPosition().Y == y) return (CATEGORY.Interactables, "Mail box"); @@ -411,9 +413,9 @@ namespace stardew_access.Features } } } - else if (Game1.currentLocation.currentEvent is not null) + else if (currentLocation.currentEvent is not null) { - string event_name = Game1.currentLocation.currentEvent.FestivalName; + string event_name = currentLocation.currentEvent.FestivalName; if (event_name == "Egg Festival" && x == 21 && y == 55) { return (CATEGORY.Interactables, "Egg Festival Shop"); @@ -461,12 +463,12 @@ namespace stardew_access.Features } } - else if (Game1.currentLocation is Town) + else if (currentLocation is Town) { if (SpecialOrder.IsSpecialOrdersBoardUnlocked() && x == 62 && y == 93) return (CATEGORY.Interactables, "Special quest board"); } - else if (Game1.currentLocation is FarmHouse farmHouse) + else if (currentLocation is FarmHouse farmHouse) { if (farmHouse.upgradeLevel >= 1) if (farmHouse.getKitchenStandingSpot().X == x && (farmHouse.getKitchenStandingSpot().Y - 1) == y) @@ -476,7 +478,7 @@ namespace stardew_access.Features else if (farmHouse.fridgePosition.X == x && farmHouse.fridgePosition.Y == y) return (CATEGORY.Interactables, "Fridge"); } - else if (Game1.currentLocation is IslandFarmHouse islandFarmHouse) + else if (currentLocation is IslandFarmHouse islandFarmHouse) { if ((islandFarmHouse.fridgePosition.X - 2) == x && islandFarmHouse.fridgePosition.Y == y) return (CATEGORY.Interactables, "Stove"); @@ -485,7 +487,7 @@ namespace stardew_access.Features else if (islandFarmHouse.fridgePosition.X == x && islandFarmHouse.fridgePosition.Y == y) return (CATEGORY.Interactables, "Fridge"); } - else if (Game1.currentLocation is Forest forest) + else if (currentLocation is Forest forest) { if (forest.travelingMerchantDay && x == 27 && y == 11) return (CATEGORY.Interactables, "Travelling Cart"); @@ -494,7 +496,7 @@ namespace stardew_access.Features else if (forest.log == null && x == 0 && y == 7) return (CATEGORY.Doors, "Secret Woods Entrance"); } - else if (Game1.currentLocation is Beach beach) + else if (currentLocation is Beach beach) { if (MainClass.ModHelper == null) return (null, null); @@ -511,12 +513,12 @@ namespace stardew_access.Features return (CATEGORY.Bridges, "Bridge"); } } - else if (Game1.currentLocation is CommunityCenter communityCenter) + else if (currentLocation is CommunityCenter communityCenter) { if (communityCenter.missedRewardsChestVisible.Value && x == 22 && y == 10) return (CATEGORY.Containers, "Missed Rewards Chest"); } - else if (Game1.currentLocation is BoatTunnel) + else if (currentLocation is BoatTunnel) { if (x == 4 && y == 9) return (CATEGORY.Interactables, ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatFixed")) ? "Repair " : "") + "Ticket Machine"); @@ -525,7 +527,7 @@ namespace stardew_access.Features 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) + else if (currentLocation is IslandLocation islandLocation) { var nutTracker = Game1.player.team.collectedNutTracker; if (islandLocation.IsBuriedNutLocation(new Point(x, y)) && !nutTracker.ContainsKey($"Buried_{islandLocation.Name}_{x}_{y}")) @@ -536,84 +538,84 @@ namespace stardew_access.Features { return (CATEGORY.NPCs, GetGemBirdName(bird)); } - else if (Game1.currentLocation is IslandWest islandWest) + else if (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) + else if (currentLocation is IslandNorth islandNorth) { if (islandNorth.traderActivated.Value && x == 36 && y == 71) return (CATEGORY.Interactables, "Island Trader"); } } - else if (Game1.currentLocation.Name.Equals("coop", StringComparison.OrdinalIgnoreCase)) + else if (currentLocation.Name.Equals("coop", StringComparison.OrdinalIgnoreCase)) { if (x >= 6 && x <= 9 && y == 3) { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); + (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); } } - else if (Game1.currentLocation.Name.Equals("coop2", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("big coop", StringComparison.OrdinalIgnoreCase)) + else if (currentLocation.Name.Equals("coop2", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("big coop", StringComparison.OrdinalIgnoreCase)) { if (x >= 6 && x <= 13 && y == 3) { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); + (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); } } - else if (Game1.currentLocation.Name.Equals("coop3", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("deluxe coop", StringComparison.OrdinalIgnoreCase)) + else if (currentLocation.Name.Equals("coop3", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("deluxe coop", StringComparison.OrdinalIgnoreCase)) { if (x >= 6 && x <= 17 && y == 3) { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); + (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); } } - else if (Game1.currentLocation.Name.Equals("barn", StringComparison.OrdinalIgnoreCase)) + else if (currentLocation.Name.Equals("barn", StringComparison.OrdinalIgnoreCase)) { if (x >= 8 && x <= 11 && y == 3) { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); + (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); if (bench.name != null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); } } - else if (Game1.currentLocation.Name.Equals("barn2", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("big barn", StringComparison.OrdinalIgnoreCase)) + else if (currentLocation.Name.Equals("barn2", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("big barn", StringComparison.OrdinalIgnoreCase)) { if (x >= 8 && x <= 15 && y == 3) { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); + (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); } } - else if (Game1.currentLocation.Name.Equals("barn3", StringComparison.OrdinalIgnoreCase) || Game1.currentLocation.Name.Equals("deluxe barn", StringComparison.OrdinalIgnoreCase)) + else if (currentLocation.Name.Equals("barn3", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("deluxe barn", StringComparison.OrdinalIgnoreCase)) { if (x >= 8 && x <= 19 && y == 3) { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, true); + (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) return (CATEGORY.Others, "Feeding Bench"); else return (CATEGORY.Others, "Empty Feeding Bench"); } } - else if (Game1.currentLocation is LibraryMuseum libraryMuseum) + else if (currentLocation is LibraryMuseum libraryMuseum) { foreach (KeyValuePair pair in libraryMuseum.museumPieces.Pairs) { @@ -877,11 +879,11 @@ namespace stardew_access.Features } #region Objects - public static (string? name, CATEGORY category) getObjectAtTile(int x, int y, bool lessInfo = false) + public static (string? name, CATEGORY category) getObjectAtTile(int x, int y, GameLocation currentLocation, bool lessInfo = false) { (string? name, CATEGORY category) toReturn = (null, CATEGORY.Others); - StardewValley.Object obj = Game1.currentLocation.getObjectAtTile(x, y); + StardewValley.Object obj = currentLocation.getObjectAtTile(x, y); if (obj == null) return toReturn; int index = obj.ParentSheetIndex; @@ -1130,16 +1132,16 @@ namespace stardew_access.Features }; } - public static bool isMineDownLadderAtTile(int x, int y) + public static bool isMineDownLadderAtTile(int x, int y, GameLocation currentLocation) { try { - if (Game1.currentLocation is Mine or MineShaft) + if (currentLocation is Mine or MineShaft) { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) + if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) return false; - int index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex; + int index = currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex; if (index == 173) { @@ -1152,16 +1154,16 @@ namespace stardew_access.Features return false; } - public static bool isShaftAtTile(int x, int y) + public static bool isShaftAtTile(int x, int y, GameLocation currentLocation) { try { - if (Game1.currentLocation is Mine or MineShaft) + if (currentLocation is Mine or MineShaft) { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) + if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) return false; - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 174) + if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 174) return true; } } @@ -1170,16 +1172,16 @@ namespace stardew_access.Features return false; } - public static bool isMineUpLadderAtTile(int x, int y) + public static bool isMineUpLadderAtTile(int x, int y, GameLocation currentLocation) { try { - if (Game1.currentLocation is Mine or MineShaft) + if (currentLocation is Mine or MineShaft) { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) + if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) return false; - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 115) + if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 115) return true; } } @@ -1188,16 +1190,16 @@ namespace stardew_access.Features return false; } - public static bool isElevatorAtTile(int x, int y) + public static bool isElevatorAtTile(int x, int y, GameLocation currentLocation) { try { - if (Game1.currentLocation is Mine or MineShaft) + if (currentLocation is Mine or MineShaft) { - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) + if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) return false; - if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 112) + if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 112) return true; } } @@ -1206,16 +1208,16 @@ namespace stardew_access.Features return false; } - public static string? getWarpPointAtTile(int x, int y) + public static string? getWarpPointAtTile(int x, int y, GameLocation currentLocation) { try { - if (Game1.currentLocation == null) return null; + if (currentLocation == null) return null; - int warpCount = Game1.currentLocation.warps.Count(); + int warpCount = currentLocation.warps.Count(); for (int i = 0; i < warpCount; i++) { - Warp warpPoint = Game1.currentLocation.warps[i]; + Warp warpPoint = currentLocation.warps[i]; if (warpPoint.X != x || warpPoint.Y != y) continue; return $"{warpPoint.TargetName} Entrance"; @@ -1229,10 +1231,10 @@ namespace stardew_access.Features return null; } - public static string? getDoorAtTile(int x, int y) + public static string? getDoorAtTile(int x, int y, GameLocation currentLocation) { Point tilePoint = new Point(x, y); - StardewValley.Network.NetPointDictionary doorList = Game1.currentLocation.doors; + StardewValley.Network.NetPointDictionary doorList = currentLocation.doors; for (int i = 0; i < doorList.Count(); i++) { @@ -1251,20 +1253,20 @@ namespace stardew_access.Features return null; } - public static string? getResourceClumpAtTile(int x, int y, bool lessInfo = false) + public static string? getResourceClumpAtTile(int x, int y, GameLocation currentLocation, bool lessInfo = false) { - if (Game1.currentLocation is Woods) - return getStumpsInWoods(x, y, lessInfo); + if (currentLocation is Woods) + return getStumpsInWoods(x, y, currentLocation, lessInfo); - for (int i = 0; i < Game1.currentLocation.resourceClumps.Count; i++) + for (int i = 0; i < currentLocation.resourceClumps.Count; i++) { - if (!Game1.currentLocation.resourceClumps[i].occupiesTile(x, y)) + if (!currentLocation.resourceClumps[i].occupiesTile(x, y)) continue; - if (lessInfo && (Game1.currentLocation.resourceClumps[i].tile.X != x || Game1.currentLocation.resourceClumps[i].tile.Y != y)) + if (lessInfo && (currentLocation.resourceClumps[i].tile.X != x || currentLocation.resourceClumps[i].tile.Y != y)) continue; - int index = Game1.currentLocation.resourceClumps[i].parentSheetIndex.Value; + int index = currentLocation.resourceClumps[i].parentSheetIndex.Value; switch (index) { @@ -1295,12 +1297,12 @@ namespace stardew_access.Features return null; } - public static string? getStumpsInWoods(int x, int y, bool lessInfo = false) + public static string? getStumpsInWoods(int x, int y, GameLocation currentLocation, bool lessInfo = false) { - if (Game1.currentLocation is not Woods) + if (currentLocation is not Woods) return null; - Netcode.NetObjectList stumps = ((Woods)Game1.currentLocation).stumps; + Netcode.NetObjectList stumps = ((Woods)currentLocation).stumps; for (int i = 0; i < stumps.Count; i++) { if (!stumps[i].occupiesTile(x, y)) @@ -1314,9 +1316,9 @@ namespace stardew_access.Features return null; } - public static string? getParrotPerchAtTile(int x, int y) + public static string? getParrotPerchAtTile(int x, int y, GameLocation currentLocation) { - if (Game1.currentLocation is not IslandLocation islandLocation) + if (currentLocation is not IslandLocation islandLocation) return null; int perchCount = islandLocation.parrotUpgradePerches.Count(); diff --git a/stardew-access/Features/TileViewer.cs b/stardew-access/Features/TileViewer.cs index 1c6ceaf..cedf825 100644 --- a/stardew-access/Features/TileViewer.cs +++ b/stardew-access/Features/TileViewer.cs @@ -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((int)tile.X, (int)tile.Y, Game1.currentLocation)) { 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((int)(position.X / Game1.tileSize), (int)(position.Y / Game1.tileSize), currentLocation)) 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/ModEntry.cs b/stardew-access/ModEntry.cs index 2c40e6a..e63303d 100644 --- a/stardew-access/ModEntry.cs +++ b/stardew-access/ModEntry.cs @@ -129,7 +129,7 @@ namespace stardew_access helper.Events.Input.ButtonPressed += this.OnButtonPressed; helper.Events.GameLoop.UpdateTicked += this.onUpdateTicked; - helper.Events.GameLoop.GameLaunched += this.onGameLaunched; + helper.Events.GameLoop.DayStarted += this.onDayStarted; AppDomain.CurrentDomain.DomainUnload += OnExit; AppDomain.CurrentDomain.ProcessExit += OnExit; } @@ -148,7 +148,7 @@ namespace stardew_access return new API(); } - private void onGameLaunched(object? sender, GameLaunchedEventArgs? e) + private void onDayStarted(object? sender, DayStartedEventArgs? e) { StaticTiles.LoadTilesFiles(); StaticTiles.SetupTilesDicts(); diff --git a/stardew-access/Patches/MiscPatches/Game1Patch.cs b/stardew-access/Patches/MiscPatches/Game1Patch.cs index 04ba91f..9eb22b8 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((int)nextTile.X, (int)nextTile.Y, Game1.currentLocation)) { if (prevTile != nextTile) { From a7dfeb552643440f156cc799fdbacf1516ef02f0 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Fri, 31 Mar 2023 15:47:53 -0700 Subject: [PATCH 11/19] Optimization and refactoring of TileInfo.cs Reduced number of calls to `isCollidingAtTile` as it calls a StardewValley function that takes ~30 ms to complete. Optimize `getFarmAnimalAt` in `TileInfo`. Refactor `getResourceClumpAtTile` and `getStumpsInWoods`. --- stardew-access/Features/Radar.cs | 6 +- stardew-access/Features/TileInfo.cs | 717 +++++++++++++++------------- stardew-access/Features/Utils.cs | 2 +- 3 files changed, 389 insertions(+), 336 deletions(-) diff --git a/stardew-access/Features/Radar.cs b/stardew-access/Features/Radar.cs index 601e3b7..68f2e61 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -141,7 +141,8 @@ 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() { - var watch = System.Diagnostics.Stopwatch.StartNew(); + var watch = new Stopwatch(); + watch.Start(); var currentLocation = Game1.currentLocation; Dictionary detectedTiles = new Dictionary(); Vector2 position = Vector2.Zero; @@ -160,7 +161,8 @@ namespace stardew_access.Features var elapsedMs = watch.ElapsedMilliseconds; MainClass.DebugLog($"Search init duration: {elapsedMs}"); elapsedMs = 0; - watch = System.Diagnostics.Stopwatch.StartNew(); + watch.Reset(); + watch.Start(); while (toSearch.Count > 0) { Vector2 item = toSearch.Dequeue(); diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index 485f935..a1d08ae 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -11,6 +11,20 @@ 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 Dictionary ResourceClumpNames = new Dictionary + { + { 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, GameLocation? currentLocation) @@ -39,7 +53,9 @@ namespace stardew_access.Features string? toReturn = null; CATEGORY? category = CATEGORY.Others; - bool isColliding = isCollidingAtTile(x, y, currentLocation); + // Commented out; this call takes ~30 ms by itself and is usually not used. + // Called directly only in the if conditional where it is used. + //bool isColliding = isCollidingAtTile(x, y, currentLocation); var terrainFeature = currentLocation.terrainFeatures.FieldDict; string? door = getDoorAtTile(x, y, currentLocation); string? warp = getWarpPointAtTile(x, y, currentLocation); @@ -47,7 +63,7 @@ namespace stardew_access.Features string? junimoBundle = getJunimoBundleAt(x, y, currentLocation); string? resourceClump = getResourceClumpAtTile(x, y, currentLocation, lessInfo); string? farmAnimal = getFarmAnimalAt(currentLocation, x, y); - string? parrot = getParrotPerchAtTile(x, y, currentLocation); + string? parrot = currentLocation is IslandLocation islandLocation ? getParrotPerchAtTile(x, y, islandLocation) : null; (string? name, CATEGORY category) staticTile = StaticTiles.GetStaticTileInfoAtWithCategory(x, y, currentLocation.Name); string? bush = getBushAtTile(x, y, currentLocation, lessInfo); @@ -90,7 +106,7 @@ namespace stardew_access.Features toReturn = obj.name; category = obj.category; } - else if (currentLocation.isWaterTile(x, y) && isColliding && !lessInfo) + else if (currentLocation.isWaterTile(x, y) && !lessInfo && isCollidingAtTile(x, y, currentLocation)) { toReturn = "Water"; category = CATEGORY.WaterTiles; @@ -309,7 +325,7 @@ namespace stardew_access.Features { // not a warp point //directly return the value of the logical comparison rather than wasting time in conditional - return ((currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) || (currentLocation is Woods && getStumpsInWoods(x, y, currentLocation) is not null)); + return currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true) || (currentLocation is Woods woods && getStumpsInWoods(x, y, woods) is not null); } // was a warp point; return false return false; @@ -328,39 +344,47 @@ namespace stardew_access.Features return false; } + /// + /// 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) { - if (location is null || (location is not Farm && location is not AnimalHouse)) + // 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; - //if (location is not Farm && location is not AnimalHouse) - //return null; + // Use an empty enumerable to store farm animals if no animals are found + IEnumerable farmAnimals = Enumerable.Empty(); - List? farmAnimals = null; + // 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; - if (location is Farm) - farmAnimals = ((Farm)location).getAllFarmAnimals(); - else if (location is AnimalHouse) - farmAnimals = ((AnimalHouse)location).animals.Values.ToList(); + // 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 (farmAnimals == null || farmAnimals.Count <= 0) - return null; - - for (int i = 0; i < farmAnimals.Count; i++) + // If a farm animal was found at the specified tile coordinates + if (foundAnimal != null) { - int fx = farmAnimals[i].getTileX(); - int fy = farmAnimals[i].getTileY(); + string name = foundAnimal.displayName; + int age = foundAnimal.age.Value; + string type = foundAnimal.displayType; - 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}"; - } + // 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; } @@ -879,32 +903,40 @@ namespace stardew_access.Features } #region Objects + /// + /// Retrieves the name and category of an object at a specific tile in the game location. + /// + /// The X coordinate of the tile. + /// The Y coordinate of the tile. + /// The current game location. + /// 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(int x, int y, GameLocation currentLocation, bool lessInfo = false) { (string? name, CATEGORY category) toReturn = (null, CATEGORY.Others); + // 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) { @@ -914,22 +946,27 @@ 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 is not null && obj.heldObject.Value.Name.Contains("pressure nozzle", StringComparison.OrdinalIgnoreCase)) + 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 is not null && obj.heldObject.Value.Name.Contains("enricher", StringComparison.OrdinalIgnoreCase)) - { - 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.Equals("crab pot", StringComparison.OrdinalIgnoreCase)) @@ -944,7 +981,9 @@ namespace stardew_access.Features } } else if (correctNameAndCategory.name != null) + { toReturn = correctNameAndCategory; + } else if (obj.name.Equals("stone", StringComparison.OrdinalIgnoreCase)) toReturn.category = CATEGORY.Debris; else if (obj.name.Equals("twig", StringComparison.OrdinalIgnoreCase)) @@ -968,30 +1007,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: @@ -1032,95 +1103,291 @@ namespace stardew_access.Features return ("Item box", CATEGORY.MineItems); } + // 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 + /// + /// Determines if a mine down ladder is present at the specified tile location. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The current GameLocation instance. + /// True if a mine down ladder is found at the specified tile, otherwise false. + public static bool isMineDownLadderAtTile(int x, int y, GameLocation currentLocation) + { + // Check if the current location is a Mine, MineShaft, or has the Name "SkullCave" + if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") + { + // Get the tile from the "Buildings" layer + var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; + + // Check if the tile is not null and its TileIndex is 173, which represents a mine down ladder + return tile != null && tile.TileIndex == 173; + } + + // No mine down ladder found at the specified tile + return false; + } + + /// + /// Determines if a mine shaft is present at the specified tile location. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The current GameLocation instance. + /// True if a mine shaft is found at the specified tile, otherwise false. + public static bool isShaftAtTile(int x, int y, GameLocation currentLocation) + { + // Check if the current location is a Mine, MineShaft, or has the Name "SkullCave" + if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") + { + // Get the tile from the "Buildings" layer + var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; + + // Check if the tile is not null and its TileIndex is 174, which represents a mine shaft + return tile != null && tile.TileIndex == 174; + } + + // No mine shaft found at the specified tile + return false; + } + + /// + /// Determines if a mine up ladder is present at the specified tile location. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The current GameLocation instance. + /// True if a mine up ladder is found at the specified tile, otherwise false. + public static bool isMineUpLadderAtTile(int x, int y, GameLocation currentLocation) + { + // Check if the current location is a Mine, MineShaft, or has the Name "SkullCave" + if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") + { + // Get the tile from the "Buildings" layer + var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; + + // Check if the tile is not null and its TileIndex is 115, which represents a mine up ladder + return tile != null && tile.TileIndex == 115; + } + + // No mine up ladder found at the specified tile + return false; + } + + /// + /// Determines if an elevator is present at the specified tile location. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The current GameLocation instance. + /// True if an elevator is found at the specified tile, otherwise false. + public static bool isElevatorAtTile(int x, int y, GameLocation currentLocation) + { + // Check if the current location is a Mine, MineShaft, or has Name == "SkullCave" + // This accommodates the mod that adds the mine's elevator to the SkullCave. + if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") + { + // Get the tile from the "Buildings" layer + var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; + + // Check if the tile is not null and its TileIndex is 112, which represents an elevator + return tile != null && tile.TileIndex == 112; + } + + // Location doesn't have elevators. + return false; + } + + /// + /// Get the warp point information at the specified tile location. + /// + /// The x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The current GameLocation instance. + /// The warp point information as a string, or null if no warp point is found. + public static string? getWarpPointAtTile(int x, int y, GameLocation currentLocation) + { + // Check if the current location is null + if (currentLocation == null) + { + return null; + } + + // Iterate through the warp points in the current location + int warpCount = currentLocation.warps.Count; + for (int i = 0; i < warpCount; i++) + { + Warp warpPoint = currentLocation.warps[i]; + + // Check if the warp point matches the specified tile coordinates + if (warpPoint.X == x && warpPoint.Y == y) + { + // Return the warp point information + return $"{warpPoint.TargetName} Entrance"; + } + } + + // No warp point found at the specified tile + return null; + } + + /// + /// Gets the door information at the specified tile coordinates in the given location. + /// + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The GameLocation where the door might be found. + /// + /// 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(int x, int y, GameLocation currentLocation) + { + // Create a Point object from the given tile coordinates + Point tilePoint = new Point(x, y); + + // Access the doorList in the current location + StardewValley.Network.NetPointDictionary doorList = currentLocation.doors; + + // Check if the doorList contains the given tile point + if (doorList.TryGetValue(tilePoint, out string? doorName)) + { + // Return the door information with the name if available, otherwise use "door" + return doorName != null ? $"{doorName} door" : "door"; + } + + // No matching door found + return null; + } + + /// + /// Gets the resource clump information at the specified tile coordinates in the given location. + /// + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The GameLocation where the resource clump might be found. + /// 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(int x, int y, GameLocation currentLocation, bool lessInfo = false) + { + // Check if the current location is Woods and handle stumps in woods separately + if (currentLocation is Woods woods) + return getStumpsInWoods(x, y, woods, lessInfo); + + // 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++) + { + var resourceClump = currentLocation.resourceClumps[i]; + + // 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; + } + + /// + /// Gets the stump information at the specified tile coordinates in the given Woods location. + /// + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The Woods location where the stump might be found. + /// 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(int x, int y, Woods woods, bool lessInfo = false) + { + // Iterate through stumps in the Woods location + foreach (var stump in woods.stumps) + { + // 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))) + { + // Return stump information + return "Large Stump"; + } + } + + // No matching stump found + return null; + } + + /// + /// 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. + /// + public static string? getParrotPerchAtTile(int x, int y, IslandLocation islandLocation) + { + // 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 + switch (foundPerch.currentState.Value) + { + case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Idle: + return foundPerch.IsAvailable() ? toSpeak : "Empty parrot perch"; + case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.StartBuilding: + return "Parrots started building request"; + case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Building: + return "Parrots building request"; + case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Complete: + return "Request Completed"; + default: + return toSpeak; + } + } + + // If no parrot perch was found, return null + return null; + } + + /// + /// Retrieves the name of the IslandGemBird based on its item index value. + /// + /// The IslandGemBird instance. + /// A string representing the name of the IslandGemBird. public 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", @@ -1128,224 +1395,8 @@ namespace stardew_access.Features 64 => "Ruby Gem Bird", 66 => "Amethyst Gem Bird", 68 => "Topaz Gem Bird", - _ => "Gem Bird", + _ => "Gem Bird", // Default case for when the item index does not match any of the specified values }; } - - public static bool isMineDownLadderAtTile(int x, int y, GameLocation currentLocation) - { - try - { - if (currentLocation is Mine or MineShaft) - { - if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - int index = currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex; - - if (index == 173) - { - return true; - } - } - } - catch (Exception) { } - - return false; - } - - public static bool isShaftAtTile(int x, int y, GameLocation currentLocation) - { - try - { - if (currentLocation is Mine or MineShaft) - { - if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 174) - return true; - } - } - catch (Exception) { } - - return false; - } - - public static bool isMineUpLadderAtTile(int x, int y, GameLocation currentLocation) - { - try - { - if (currentLocation is Mine or MineShaft) - { - if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 115) - return true; - } - } - catch (Exception) { } - - return false; - } - - public static bool isElevatorAtTile(int x, int y, GameLocation currentLocation) - { - try - { - if (currentLocation is Mine or MineShaft) - { - if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y] == null) - return false; - - if (currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex == 112) - return true; - } - } - catch (Exception) { } - - return false; - } - - public static string? getWarpPointAtTile(int x, int y, GameLocation currentLocation) - { - try - { - if (currentLocation == null) return null; - - int warpCount = currentLocation.warps.Count(); - for (int i = 0; i < warpCount; i++) - { - Warp warpPoint = currentLocation.warps[i]; - if (warpPoint.X != x || warpPoint.Y != y) continue; - - return $"{warpPoint.TargetName} Entrance"; - } - } - catch (Exception e) - { - MainClass.ErrorLog($"Error while detecting warp points.\n{e.Message}"); - } - - return null; - } - - public static string? getDoorAtTile(int x, int y, GameLocation currentLocation) - { - Point tilePoint = new Point(x, y); - StardewValley.Network.NetPointDictionary doorList = currentLocation.doors; - - for (int i = 0; i < doorList.Count(); i++) - { - if (doorList.ContainsKey(tilePoint)) - { - string? doorName; - doorList.TryGetValue(tilePoint, out doorName); - - if (doorName != null) - return $"{doorName} door"; - else - return "door"; - } - } - - return null; - } - - public static string? getResourceClumpAtTile(int x, int y, GameLocation currentLocation, bool lessInfo = false) - { - if (currentLocation is Woods) - return getStumpsInWoods(x, y, currentLocation, lessInfo); - - for (int i = 0; i < currentLocation.resourceClumps.Count; i++) - { - if (!currentLocation.resourceClumps[i].occupiesTile(x, y)) - continue; - - if (lessInfo && (currentLocation.resourceClumps[i].tile.X != x || currentLocation.resourceClumps[i].tile.Y != y)) - continue; - - int index = currentLocation.resourceClumps[i].parentSheetIndex.Value; - - switch (index) - { - 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 null; - } - - public static string? getStumpsInWoods(int x, int y, GameLocation currentLocation, bool lessInfo = false) - { - if (currentLocation is not Woods) - return null; - - Netcode.NetObjectList stumps = ((Woods)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, GameLocation currentLocation) - { - if (currentLocation is not IslandLocation islandLocation) - return null; - - int perchCount = islandLocation.parrotUpgradePerches.Count(); - for (int i = 0; i < perchCount; i++) - { - var perch = islandLocation.parrotUpgradePerches[i]; - 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/Utils.cs b/stardew-access/Features/Utils.cs index 87fcdf6..511e4e2 100644 --- a/stardew-access/Features/Utils.cs +++ b/stardew-access/Features/Utils.cs @@ -74,7 +74,7 @@ namespace stardew_access.Features throw new ArgumentException("Category name cannot be null or empty.", nameof(name)); } - return Categories.TryGetValue(name, out CATEGORY category) ? category : Others; + return Categories.TryGetValue(name, out CATEGORY? category) ? category ?? CATEGORY.Others : CATEGORY.Others; } /// From 35115223d3ed3c5edcbe2d1cbbee7ae57cfa07ac Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Mon, 3 Apr 2023 16:57:57 -0700 Subject: [PATCH 12/19] Update .gitattributes to ensure .cs and .json files keep crlf line endings. --- .gitattributes | 6 ++++++ 1 file changed, 6 insertions(+) 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 From 8509cbfc0bd2260bd14e5d59cf61ba79683ed96f Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Mon, 3 Apr 2023 18:56:55 -0700 Subject: [PATCH 13/19] Refactored getDynamicTilesInfo from TileInfo.cs by moving most of it's functionality to new file DynamicTiles.cs. Refactored handling of dynamic tiles for events; tile coords now loaded from json file. Moved LoadJsonFile function to utils.cs. --- stardew-access/API.cs | 2 +- stardew-access/Features/DynamicTiles.cs | 782 ++++++++++++++++++++++++ stardew-access/Features/Radar.cs | 43 +- stardew-access/Features/StaticTiles.cs | 45 +- stardew-access/Features/TileInfo.cs | 647 +++++--------------- stardew-access/Features/TileViewer.cs | 2 +- stardew-access/Features/Utils.cs | 40 +- stardew-access/assets/event-tiles.json | 31 + 8 files changed, 1013 insertions(+), 579 deletions(-) create mode 100644 stardew-access/Features/DynamicTiles.cs create mode 100644 stardew-access/assets/event-tiles.json diff --git a/stardew-access/API.cs b/stardew-access/API.cs index 550a6bf..d0fb713 100644 --- a/stardew-access/API.cs +++ b/stardew-access/API.cs @@ -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, null); + return TileInfo.GetNameAtTile(tile, null); } /// Speaks the text via the loaded screen reader (if any). diff --git a/stardew-access/Features/DynamicTiles.cs b/stardew-access/Features/DynamicTiles.cs new file mode 100644 index 0000000..cc50483 --- /dev/null +++ b/stardew-access/Features/DynamicTiles.cs @@ -0,0 +1,782 @@ +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. + /// 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) + { + 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. + /// 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) + { + if (boatTunnel is null) + { + throw new ArgumentNullException(nameof(boatTunnel)); + } + + if (x == 4 && y == 9) + { + return ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatFixed") ? "Repair " : "") + "Ticket Machine", CATEGORY.Interactables); + } + else if (x == 6 && y == 8) + { + return ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull") ? "Repair " : "") + "Boat Hull", (!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull") ? CATEGORY.Interactables : CATEGORY.Decor)); + } + else if (x == 8 && y == 9) + { + return ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor") ? "Repair " : "") + "Boat Anchor", (!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor") ? CATEGORY.Interactables : CATEGORY.Decor)); + } + + 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. + /// 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) + { + 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. + /// 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) + { + 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. + /// 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) + { + 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); + } + + 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. + /// 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) + { + 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. + /// 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) + { + 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. + /// 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) + { + 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. + /// 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) + { + // 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. + /// 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) + { + // 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. + /// 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) + { + 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. + /// 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) + { + 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. + /// 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) + { + 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), + IslandWest islandWest => GetIslandWestInfo(islandWest, x, y), + VolcanoDungeon dungeon => GetVolcanoDungeonInfo(dungeon, x, y), + _ => GetNamedIslandLocationInfo(islandLocation, x, y) + }; + } + + /// + /// 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. + /// 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) + { + 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. + /// 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) + { + 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); + + 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. + /// 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) + { + 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. + /// 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) + { + 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(x, y, currentLocation, 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. + /// 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) + { + 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 x-coordinate of the tile. + /// The y-coordinate of the tile. + /// The current GameLocation instance. + /// 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(int x, int y, GameLocation currentLocation, 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), + BoatTunnel boatTunnel => GetBoatTunnelInfo(boatTunnel, x, y), + CommunityCenter communityCenter => GetCommunityCenterInfo(communityCenter, x, y), + Farm farm => GetFarmInfo(farm, x, y), + FarmHouse farmHouse => GetFarmHouseInfo(farmHouse, x, y), + Forest forest => GetForestInfo(forest, x, y), + IslandFarmHouse islandFarmHouse => GetIslandFarmHouseInfo(islandFarmHouse, x, y), + IslandLocation islandLocation => GetIslandLocationInfo(islandLocation, x, y), + LibraryMuseum libraryMuseum => GetLibraryMuseumInfo(libraryMuseum, x, y), + Town town => GetTownInfo(town, x, y), + _ => GetLocationByNameInfo(currentLocation, x, y) + }; + } + } +} diff --git a/stardew-access/Features/Radar.cs b/stardew-access/Features/Radar.cs index 68f2e61..a2752a8 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -8,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; @@ -94,10 +94,10 @@ namespace stardew_access.Features public Dictionary SearchNearbyTiles(Vector2 center, int limit, bool playSound = true) { var currentLocation = Game1.currentLocation; - Dictionary detectedTiles = new Dictionary(); + Dictionary detectedTiles = new(); - Queue toSearch = new Queue(); - HashSet searched = new HashSet(); + Queue toSearch = new(); + HashSet searched = new(); int[] dirX = { -1, 0, 1, 0 }; int[] dirY = { 0, 1, 0, -1 }; @@ -121,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)) { @@ -144,12 +144,12 @@ namespace stardew_access.Features var watch = new Stopwatch(); watch.Start(); var currentLocation = Game1.currentLocation; - Dictionary detectedTiles = new Dictionary(); + Dictionary detectedTiles = new(); Vector2 position = Vector2.Zero; (bool, string? name, string category) tileInfo; - Queue toSearch = new Queue(); - HashSet searched = new HashSet(); + Queue toSearch = new(); + HashSet searched = new(); int[] dirX = { -1, 0, 1, 0 }; int[] dirY = { 0, 1, 0, -1 }; int count = 0; @@ -160,8 +160,7 @@ namespace stardew_access.Features watch.Stop(); var elapsedMs = watch.ElapsedMilliseconds; MainClass.DebugLog($"Search init duration: {elapsedMs}"); - elapsedMs = 0; - watch.Reset(); + watch.Reset(); watch.Start(); while (toSearch.Count > 0) { @@ -177,7 +176,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 (!searched.Contains(dir) && (TileInfo.isWarpPointAtTile((int)dir.X, (int)dir.Y, currentLocation) || currentLocation.isTileOnMap(dir))) { @@ -216,14 +215,13 @@ namespace stardew_access.Features public (bool, string? name, string category) CheckTile(Vector2 position, GameLocation currentLocation, bool lessInfo = false) { - (string? name, CATEGORY? category) tileDetail = TileInfo.getNameWithCategoryAtTile(position, currentLocation, 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; + category ??= CATEGORY.Others; - return (true, tileDetail.name, tileDetail.category.ToString()); + return (true, name, category.ToString()); } @@ -257,13 +255,12 @@ namespace stardew_access.Features } else { - (string? name, CATEGORY? category) tileDetail = TileInfo.getNameWithCategoryAtTile(position, currentLocation); - 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, currentLocation); + PlaySoundAt(position, name, category, currentLocation); } } } diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 78a4c73..204d562 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Linq; using System.Collections.Generic; using StardewValley; +using static stardew_access.Features.Utils; namespace stardew_access.Features { @@ -18,10 +19,7 @@ namespace stardew_access.Features { get { - if (_instance == null) - { - _instance = new StaticTiles(); - } + _instance ??= new StaticTiles(); return _instance; } } @@ -74,8 +72,8 @@ namespace stardew_access.Features /// /// Additional lambda functions can be added as needed. /// - private static readonly Dictionary> conditionals = new Dictionary> - { + private static readonly Dictionary> conditionals = new() + { ["Farm"] = (conditionType, uniqueModId) => { if (string.IsNullOrEmpty(uniqueModId)) @@ -121,36 +119,6 @@ namespace stardew_access.Features LoadTilesFiles(); SetupTilesDicts(); } - - /// - /// 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. - private 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; - } /// /// Loads the static and custom tile files. @@ -497,10 +465,7 @@ namespace stardew_access.Features /// 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) { - if (currentLocationName == null) - { - currentLocationName = Game1.currentLocation.Name; - } + currentLocationName ??= Game1.currentLocation.Name; if (customTilesDataDict != null && customTilesDataDict.TryGetValue(currentLocationName, out var customLocationDict)) { diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index a1d08ae..0c8cc19 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -5,14 +5,15 @@ 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 Dictionary ResourceClumpNames = new Dictionary - { + 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" }, @@ -29,25 +30,24 @@ namespace stardew_access.Features ///Returns the name of the object at tile alongwith it's category's name public static (string? name, string? categoryName) getNameWithCategoryNameAtTile(Vector2 tile, GameLocation? currentLocation) { - (string? name, CATEGORY? category) tileDetail = getNameWithCategoryAtTile(tile, currentLocation); + (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, GameLocation? currentLocation = null) + public static string? GetNameAtTile(Vector2 tile, GameLocation? currentLocation = null) { - if (currentLocation is null) currentLocation = Game1.currentLocation; + 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, GameLocation? currentLocation, bool lessInfo = false) { - if (currentLocation is null) currentLocation = Game1.currentLocation; + currentLocation ??= Game1.currentLocation; int x = (int)tile.X; int y = (int)tile.Y; string? toReturn = null; @@ -59,13 +59,12 @@ namespace stardew_access.Features var terrainFeature = currentLocation.terrainFeatures.FieldDict; string? door = getDoorAtTile(x, y, currentLocation); string? warp = getWarpPointAtTile(x, y, currentLocation); - (CATEGORY? category, string? name) dynamicTile = getDynamicTilesInfo(x, y, currentLocation, lessInfo); + (string? name, CATEGORY? category) dynamicTile = DynamicTiles.GetDynamicTileAt(x, y, currentLocation, lessInfo); string? junimoBundle = getJunimoBundleAt(x, y, currentLocation); string? resourceClump = getResourceClumpAtTile(x, y, currentLocation, lessInfo); string? farmAnimal = getFarmAnimalAt(currentLocation, x, y); - string? parrot = currentLocation is IslandLocation islandLocation ? getParrotPerchAtTile(x, y, islandLocation) : null; (string? name, CATEGORY category) staticTile = StaticTiles.GetStaticTileInfoAtWithCategory(x, y, currentLocation.Name); - string? bush = getBushAtTile(x, y, currentLocation, lessInfo); + string? bush = GetBushAtTile(x, y, currentLocation, lessInfo); if (currentLocation.isCharacterAtTile(tile) is NPC npc) { @@ -90,16 +89,6 @@ namespace stardew_access.Features toReturn = dynamicTile.name; category = dynamicTile.category; } - else if (currentLocation is VolcanoDungeon && ((VolcanoDungeon)currentLocation).IsCooledLava(x, y) && !lessInfo) - { - toReturn = "Cooled lava"; - category = CATEGORY.WaterTiles; - } - else if (currentLocation is VolcanoDungeon && StardewValley.Monsters.LavaLurk.IsLavaTile((VolcanoDungeon)currentLocation, x, y) && !lessInfo) - { - toReturn = "Lava"; - category = CATEGORY.WaterTiles; - } else if (currentLocation.isObjectAtTile(x, y)) { (string? name, CATEGORY? category) obj = getObjectAtTile(x, y, currentLocation, lessInfo); @@ -162,11 +151,6 @@ namespace stardew_access.Features toReturn = "Elevator"; category = CATEGORY.Doors; } - else if (parrot != null) - { - toReturn = parrot; - category = CATEGORY.Buildings; - } else if (junimoBundle != null) { toReturn = junimoBundle; @@ -179,7 +163,7 @@ namespace stardew_access.Features try { NetCollection droppedItems = currentLocation.debris; - int droppedItemsCount = droppedItems.Count(); + int droppedItemsCount = droppedItems.Count; if (droppedItemsCount > 0) { for (int i = 0; i < droppedItemsCount; i++) @@ -212,7 +196,7 @@ namespace stardew_access.Features return (toReturn, category); } - public static string? getBushAtTile(int x, int y, GameLocation currentLocation, bool lessInfo = false) + public static string? GetBushAtTile(int x, int y, GameLocation currentLocation, bool lessInfo = false) { string? toReturn = null; Bush? bush = (Bush)currentLocation.getLargeTerrainFeatureAt(x, y); @@ -269,8 +253,8 @@ namespace stardew_access.Features public static string? getJunimoBundleAt(int x, int y, GameLocation currentLocation) { - string? name = null; - if (currentLocation is CommunityCenter communityCenter) + string? name; + if (currentLocation is CommunityCenter communityCenter) { name = (x, y) switch { @@ -302,7 +286,7 @@ namespace stardew_access.Features public static bool isCollidingAtTile(int x, int y, GameLocation currentLocation) { - Rectangle rect = new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62); + Rectangle rect = new(x * 64 + 1, y * 64 + 1, 62, 62); /* Reference // 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 @@ -335,7 +319,7 @@ namespace stardew_access.Features { if (currentLocation is null) return false; - int warpsCount = currentLocation.warps.Count(); + int warpsCount = currentLocation.warps.Count; for (int i = 0; i < warpsCount; i++) { if (currentLocation.warps[i].X == x && currentLocation.warps[i].Y == y) return true; @@ -389,441 +373,132 @@ namespace stardew_access.Features } /// - /// + /// Retrieves the name and category of the terrain feature at the given tile. /// - /// - /// - /// 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, GameLocation currentLocation, bool lessInfo = false) - { - if (currentLocation.orePanPoint.Value != Point.Zero && currentLocation.orePanPoint.Value == new Point(x, y)) - { - return (CATEGORY.Interactables, "panning spot"); - } - else if (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 is not null) - { - string name = building.buildingType.Value; - - // 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}"; - } - } - - // 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 (currentLocation.currentEvent is not null) - { - string event_name = 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 (currentLocation is Town) - { - if (SpecialOrder.IsSpecialOrdersBoardUnlocked() && x == 62 && y == 93) - return (CATEGORY.Interactables, "Special quest board"); - } - else if (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 (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 (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 (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 (currentLocation is CommunityCenter communityCenter) - { - if (communityCenter.missedRewardsChestVisible.Value && x == 22 && y == 10) - return (CATEGORY.Containers, "Missed Rewards Chest"); - } - else if (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 (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 (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 (currentLocation is IslandNorth islandNorth) - { - if (islandNorth.traderActivated.Value && x == 36 && y == 71) - return (CATEGORY.Interactables, "Island Trader"); - } - } - else if (currentLocation.Name.Equals("coop", StringComparison.OrdinalIgnoreCase)) - { - if (x >= 6 && x <= 9 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); - if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (currentLocation.Name.Equals("coop2", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("big coop", StringComparison.OrdinalIgnoreCase)) - { - if (x >= 6 && x <= 13 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); - if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (currentLocation.Name.Equals("coop3", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("deluxe coop", StringComparison.OrdinalIgnoreCase)) - { - if (x >= 6 && x <= 17 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); - if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (currentLocation.Name.Equals("barn", StringComparison.OrdinalIgnoreCase)) - { - if (x >= 8 && x <= 11 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); - if (bench.name != null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (currentLocation.Name.Equals("barn2", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("big barn", StringComparison.OrdinalIgnoreCase)) - { - if (x >= 8 && x <= 15 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); - if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (currentLocation.Name.Equals("barn3", StringComparison.OrdinalIgnoreCase) || currentLocation.Name.Equals("deluxe barn", StringComparison.OrdinalIgnoreCase)) - { - if (x >= 8 && x <= 19 && y == 3) - { - (string? name, CATEGORY category) bench = getObjectAtTile(x, y, currentLocation, true); - if (bench.name is not null && bench.name.Contains("hay", StringComparison.OrdinalIgnoreCase)) - return (CATEGORY.Others, "Feeding Bench"); - else - return (CATEGORY.Others, "Empty Feeding Bench"); - } - } - else if (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); - } - + /// 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) @@ -835,21 +510,26 @@ 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 = ""; + string seedName = ""; - // Return with the name if it's one of the 3 special trees - switch (treeType) + // Handle special tree types and return their names + switch (treeType) { case 4: case 5: @@ -860,34 +540,37 @@ 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) { - switch (seedName.ToLower()) - { - case "mahogany seed": - treeName = "Mahogany"; - break; - case "acorn": - treeName = "Oak"; - break; - case "maple seed": - treeName = "Maple"; - break; - case "pine cone": - treeName = "Pine"; - break; - default: - treeName = "Coconut"; - break; - } + string treeName; + switch (seedName.ToLower()) + { + case "mahogany seed": + treeName = "Mahogany"; + break; + case "acorn": + treeName = "Oak"; + break; + case "maple seed": + treeName = "Maple"; + break; + case "pine cone": + treeName = "Pine"; + break; + default: + treeName = "Coconut"; + break; + } - if (treeStage == 1) + // Append the growth stage description to the tree name + if (treeStage == 1) treeName = $"{treeName} sprout"; else if (treeStage == 2) treeName = $"{treeName} sapling"; @@ -899,6 +582,7 @@ namespace stardew_access.Features return treeName; } + // Return the seed name if the tree is at stage 0 return seedName; } @@ -1262,7 +946,7 @@ namespace stardew_access.Features public static string? getDoorAtTile(int x, int y, GameLocation currentLocation) { // Create a Point object from the given tile coordinates - Point tilePoint = new Point(x, y); + Point tilePoint = new(x, y); // Access the doorList in the current location StardewValley.Network.NetPointDictionary doorList = currentLocation.doors; @@ -1339,64 +1023,5 @@ namespace stardew_access.Features // No matching stump found return null; } - - /// - /// 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. - /// - public static string? getParrotPerchAtTile(int x, int y, IslandLocation islandLocation) - { - // 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 - switch (foundPerch.currentState.Value) - { - case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Idle: - return foundPerch.IsAvailable() ? toSpeak : "Empty parrot perch"; - case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.StartBuilding: - return "Parrots started building request"; - case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Building: - return "Parrots building request"; - case StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Complete: - return "Request Completed"; - default: - return toSpeak; - } - } - - // If no parrot perch was found, return null - return null; - } - - /// - /// Retrieves the name of the IslandGemBird based on its item index value. - /// - /// The IslandGemBird instance. - /// A string representing the name of the IslandGemBird. - public 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 - }; - } } } diff --git a/stardew-access/Features/TileViewer.cs b/stardew-access/Features/TileViewer.cs index cedf825..a95310b 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) diff --git a/stardew-access/Features/Utils.cs b/stardew-access/Features/Utils.cs index 511e4e2..1e27c3b 100644 --- a/stardew-access/Features/Utils.cs +++ b/stardew-access/Features/Utils.cs @@ -1,3 +1,4 @@ +using System.Text.Json; namespace stardew_access.Features { /// @@ -33,7 +34,7 @@ namespace stardew_access.Features public static IReadOnlyDictionary Categories => _categories; - private static readonly Dictionary _categories = new Dictionary(StringComparer.OrdinalIgnoreCase) + private static readonly Dictionary _categories = new(StringComparer.OrdinalIgnoreCase) { {"farmer", new CATEGORY("farmer")}, {"animal", new CATEGORY("animal")}, @@ -122,11 +123,44 @@ namespace stardew_access.Features 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 static CATEGORY Others => FromString("other"); } - public enum MachineState + 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/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" + } +} From 91643e184f36ad68ea8dc6d93e7677eaca763064 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Mon, 3 Apr 2023 21:05:28 -0700 Subject: [PATCH 14/19] Finish adding documentation (with Chat GPT's help) and refactoring TileInfo.cs. Reduced code duplication with helper functions where possible. Optimized and simplified `getNameWithCategoryAtTile`. --- stardew-access/Features/ReadTile.cs | 2 +- stardew-access/Features/TileInfo.cs | 449 +++++++----------- stardew-access/Features/TileViewer.cs | 2 +- .../Patches/MiscPatches/Game1Patch.cs | 2 +- 4 files changed, 186 insertions(+), 269 deletions(-) diff --git a/stardew-access/Features/ReadTile.cs b/stardew-access/Features/ReadTile.cs index 75b37a5..a5a1a11 100644 --- a/stardew-access/Features/ReadTile.cs +++ b/stardew-access/Features/ReadTile.cs @@ -90,7 +90,7 @@ namespace stardew_access.Features } var currentLocation = Game1.currentLocation; - bool isColliding = TileInfo.isCollidingAtTile(x, y, currentLocation); + bool isColliding = TileInfo.IsCollidingAtTile(x, y, currentLocation); (string? name, string? category) info = TileInfo.getNameWithCategoryNameAtTile(tile, currentLocation); diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index 0c8cc19..f572c0c 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -50,213 +50,167 @@ namespace stardew_access.Features currentLocation ??= Game1.currentLocation; int x = (int)tile.X; int y = (int)tile.Y; - string? toReturn = null; - CATEGORY? category = CATEGORY.Others; - // Commented out; this call takes ~30 ms by itself and is usually not used. - // Called directly only in the if conditional where it is used. - //bool isColliding = isCollidingAtTile(x, y, currentLocation); var terrainFeature = currentLocation.terrainFeatures.FieldDict; - string? door = getDoorAtTile(x, y, currentLocation); - string? warp = getWarpPointAtTile(x, y, currentLocation); - (string? name, CATEGORY? category) dynamicTile = DynamicTiles.GetDynamicTileAt(x, y, currentLocation, lessInfo); - string? junimoBundle = getJunimoBundleAt(x, y, currentLocation); - string? resourceClump = getResourceClumpAtTile(x, y, currentLocation, lessInfo); - string? farmAnimal = getFarmAnimalAt(currentLocation, x, y); - (string? name, CATEGORY category) staticTile = StaticTiles.GetStaticTileInfoAtWithCategory(x, y, currentLocation.Name); - string? bush = GetBushAtTile(x, y, currentLocation, lessInfo); 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 is not 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(x, y, currentLocation, lessInfo); + if (dynamicTile.name != null) { - toReturn = dynamicTile.name; - category = dynamicTile.category; + return (dynamicTile.name, dynamicTile.category); } - else if (currentLocation.isObjectAtTile(x, y)) + + if (currentLocation.isObjectAtTile(x, y)) { (string? name, CATEGORY? category) obj = getObjectAtTile(x, y, currentLocation, lessInfo); - toReturn = obj.name; - category = obj.category; + return (obj.name, obj.category); } - else if (currentLocation.isWaterTile(x, y) && !lessInfo && isCollidingAtTile(x, y, currentLocation)) + + if (currentLocation.isWaterTile(x, y) && !lessInfo && IsCollidingAtTile(x, y, currentLocation)) { - toReturn = "Water"; - category = CATEGORY.WaterTiles; + return ("Water", CATEGORY.WaterTiles); } - else if (resourceClump != null) + + string? resourceClump = getResourceClumpAtTile(x, y, currentLocation, lessInfo); + if (resourceClump != null) { - toReturn = resourceClump; - category = CATEGORY.ResourceClumps; + return (resourceClump, CATEGORY.ResourceClumps); } - else if (terrainFeature.ContainsKey(tile)) + + if (terrainFeature.TryGetValue(tile, out var tf)) { - (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, currentLocation)) - { - toReturn = "Ladder"; - category = CATEGORY.Doors; - } - else if (isShaftAtTile(x, y, currentLocation)) - { - toReturn = "Shaft"; - category = CATEGORY.Doors; - } - else if (isMineUpLadderAtTile(x, y, currentLocation)) - { - toReturn = "Up Ladder"; - category = CATEGORY.Doors; - } - else if (isElevatorAtTile(x, y, currentLocation)) - { - toReturn = "Elevator"; - category = CATEGORY.Doors; - } - else if (junimoBundle != null) - { - toReturn = junimoBundle; - category = CATEGORY.JunimoBundle; } - #region Track dropped items + string? bush = GetBushAtTile(x, y, currentLocation, lessInfo); + if (bush != null) + { + return (bush, CATEGORY.Bush); + } + + string? door = getDoorAtTile(x, y, currentLocation); + string? warp = getWarpPointAtTile(x, y, currentLocation); + if (warp != null || door != null) + { + return (warp ?? door, CATEGORY.Doors); + } + + string? junimoBundle = GetJunimoBundleAt(x, y, currentLocation); + if (junimoBundle != null) + { + return (junimoBundle, CATEGORY.JunimoBundle); + } + + // Track dropped items if (MainClass.Config.TrackDroppedItems) { try { - NetCollection droppedItems = currentLocation.debris; - int droppedItemsCount = droppedItems.Count; - if (droppedItemsCount > 0) + foreach (var item in currentLocation.debris) { - for (int i = 0; i < droppedItemsCount; i++) - { - var item = droppedItems[i]; - 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 is null) - return ($"Dropped Item: {count} {name}", CATEGORY.DroppedItems); - else - toReturn = $"{toReturn}, Dropped Item: {count} {name}"; - item = null; - } + 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 - return (toReturn, category); + return (null, CATEGORY.Others); } + /// + /// Gets the bush at the specified tile coordinates in the provided GameLocation. + /// + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The GameLocation instance to search for bushes. + /// 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(int x, int y, GameLocation currentLocation, bool lessInfo = false) { - string? toReturn = null; Bush? bush = (Bush)currentLocation.getLargeTerrainFeatureAt(x, y); - if (bush is null) - return null; - if (lessInfo && (bush.tilePosition.Value.X != x || bush.tilePosition.Value.Y != 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(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(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, GameLocation currentLocation) + /// + /// Determines if there is a Junimo bundle at the specified tile coordinates in the provided GameLocation. + /// + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The GameLocation instance to search for Junimo bundles. + /// The name of the Junimo bundle if one is found at the specified coordinates, otherwise null. + public static string? GetJunimoBundleAt(int x, int y, GameLocation currentLocation) { - string? name; - if (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", @@ -266,66 +220,85 @@ namespace stardew_access.Features (46, 12) => "Bulletin Board", _ => null, }; + + // 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 (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 is not null) + // Bundle name was found return $"{name} bundle"; } + // No bundle was found + return null; + } + + /// + /// Determines if there is a collision at the specified tile coordinates in the provided GameLocation. + /// + /// The x-coordinate of the tile to check. + /// The y-coordinate of the tile to check. + /// The GameLocation instance to search for collisions. + /// True if a collision is detected at the specified tile coordinates, otherwise False. + public static bool IsCollidingAtTile(int x, int y, GameLocation currentLocation) + { + // 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(x, y, currentLocation) && + (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(x, y, woods) is not null)); + } + + /// + /// Returns the Warp object at the specified tile coordinates or null if not found. + /// + private static Warp? GetWarpAtTile(int x, int y, GameLocation currentLocation) + { + if (currentLocation is null) return null; + + int warpsCount = currentLocation.warps.Count; + for (int i = 0; i < warpsCount; i++) + { + if (currentLocation.warps[i].X == x && currentLocation.warps[i].Y == y) + return currentLocation.warps[i]; + } + return null; } - public static bool isCollidingAtTile(int x, int y, GameLocation currentLocation) + /// + /// Returns the name of the warp point at the specified tile coordinates, or null if not found. + /// + public static string? getWarpPointAtTile(int x, int y, GameLocation currentLocation) { - Rectangle rect = new(x * 64 + 1, y * 64 + 1, 62, 62); + Warp? warpPoint = GetWarpAtTile(x, y, currentLocation); - /* Reference - // 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, currentLocation)) return false; - - if (currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) + if (warpPoint != null) { - return true; + return $"{warpPoint.TargetName} Entrance"; } - if (currentLocation is Woods && getStumpsInWoods(x, y, currentLocation) is not null) - return true; - - return false; - */ - - // Optimized - // Sometimes warp points are 1 tile off the map for example in coops and barns; check that this is not a warp point - if (!isWarpPointAtTile(x, y, currentLocation)) - { - // not a warp point - //directly return the value of the logical comparison rather than wasting time in conditional - return currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true) || (currentLocation is Woods woods && getStumpsInWoods(x, y, woods) is not null); - } - // was a warp point; return false - return false; + return null; } - public static Boolean isWarpPointAtTile(int x, int y, GameLocation currentLocation) + /// + /// Returns true if there's a warp point at the specified tile coordinates, or false otherwise. + /// + public static bool isWarpPointAtTile(int x, int y, GameLocation currentLocation) { - if (currentLocation is null) return false; - - int warpsCount = currentLocation.warps.Count; - for (int i = 0; i < warpsCount; i++) - { - if (currentLocation.warps[i].X == x && currentLocation.warps[i].Y == y) return true; - } - - return false; + return GetWarpAtTile(x, y, currentLocation) != null; } /// @@ -807,6 +780,20 @@ namespace stardew_access.Features } #endregion + /// + /// Check if a tile with the specified index exists at the given coordinates in the specified location. + /// + /// The X coordinate of the tile. + /// The Y coordinate of the tile. + /// The current game location. + /// 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(int x, int y, GameLocation currentLocation, int targetTileIndex) + { + var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; + return tile != null && tile.TileIndex == targetTileIndex; + } + /// /// Determines if a mine down ladder is present at the specified tile location. /// @@ -816,18 +803,9 @@ namespace stardew_access.Features /// True if a mine down ladder is found at the specified tile, otherwise false. public static bool isMineDownLadderAtTile(int x, int y, GameLocation currentLocation) { - // Check if the current location is a Mine, MineShaft, or has the Name "SkullCave" - if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") - { - // Get the tile from the "Buildings" layer - var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; - - // Check if the tile is not null and its TileIndex is 173, which represents a mine down ladder - return tile != null && tile.TileIndex == 173; - } - - // No mine down ladder found at the specified tile - return false; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(x, y, currentLocation, 173) + : false; } /// @@ -839,18 +817,9 @@ namespace stardew_access.Features /// True if a mine shaft is found at the specified tile, otherwise false. public static bool isShaftAtTile(int x, int y, GameLocation currentLocation) { - // Check if the current location is a Mine, MineShaft, or has the Name "SkullCave" - if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") - { - // Get the tile from the "Buildings" layer - var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; - - // Check if the tile is not null and its TileIndex is 174, which represents a mine shaft - return tile != null && tile.TileIndex == 174; - } - - // No mine shaft found at the specified tile - return false; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(x, y, currentLocation, 174) + : false; } /// @@ -862,18 +831,9 @@ namespace stardew_access.Features /// True if a mine up ladder is found at the specified tile, otherwise false. public static bool isMineUpLadderAtTile(int x, int y, GameLocation currentLocation) { - // Check if the current location is a Mine, MineShaft, or has the Name "SkullCave" - if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") - { - // Get the tile from the "Buildings" layer - var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; - - // Check if the tile is not null and its TileIndex is 115, which represents a mine up ladder - return tile != null && tile.TileIndex == 115; - } - - // No mine up ladder found at the specified tile - return false; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(x, y, currentLocation, 115) + : false; } /// @@ -885,52 +845,9 @@ namespace stardew_access.Features /// True if an elevator is found at the specified tile, otherwise false. public static bool isElevatorAtTile(int x, int y, GameLocation currentLocation) { - // Check if the current location is a Mine, MineShaft, or has Name == "SkullCave" - // This accommodates the mod that adds the mine's elevator to the SkullCave. - if (currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave") - { - // Get the tile from the "Buildings" layer - var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; - - // Check if the tile is not null and its TileIndex is 112, which represents an elevator - return tile != null && tile.TileIndex == 112; - } - - // Location doesn't have elevators. - return false; - } - - /// - /// Get the warp point information at the specified tile location. - /// - /// The x-coordinate of the tile. - /// The y-coordinate of the tile. - /// The current GameLocation instance. - /// The warp point information as a string, or null if no warp point is found. - public static string? getWarpPointAtTile(int x, int y, GameLocation currentLocation) - { - // Check if the current location is null - if (currentLocation == null) - { - return null; - } - - // Iterate through the warp points in the current location - int warpCount = currentLocation.warps.Count; - for (int i = 0; i < warpCount; i++) - { - Warp warpPoint = currentLocation.warps[i]; - - // Check if the warp point matches the specified tile coordinates - if (warpPoint.X == x && warpPoint.Y == y) - { - // Return the warp point information - return $"{warpPoint.TargetName} Entrance"; - } - } - - // No warp point found at the specified tile - return null; + return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" + ? CheckTileIndex(x, y, currentLocation, 112) + : false; } /// diff --git a/stardew-access/Features/TileViewer.cs b/stardew-access/Features/TileViewer.cs index a95310b..0444b56 100644 --- a/stardew-access/Features/TileViewer.cs +++ b/stardew-access/Features/TileViewer.cs @@ -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, Game1.currentLocation)) + if (TileInfo.IsCollidingAtTile((int)tile.X, (int)tile.Y, Game1.currentLocation)) { name = "blocked"; } diff --git a/stardew-access/Patches/MiscPatches/Game1Patch.cs b/stardew-access/Patches/MiscPatches/Game1Patch.cs index 9eb22b8..4b52615 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, Game1.currentLocation)) + if (TileInfo.IsCollidingAtTile((int)nextTile.X, (int)nextTile.Y, Game1.currentLocation)) { if (prevTile != nextTile) { From ad0cb687b7d6451790c0b684975a3037216aa44b Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Tue, 4 Apr 2023 15:09:49 -0700 Subject: [PATCH 15/19] Readded `lessInfo` to many tiles related functions and other code cleanup (making argument ordering consist. --- stardew-access/Features/DynamicTiles.cs | 1192 +++++++++-------- stardew-access/Features/Radar.cs | 6 +- stardew-access/Features/ReadTile.cs | 2 +- stardew-access/Features/StaticTiles.cs | 2 +- stardew-access/Features/TileInfo.cs | 159 ++- stardew-access/Features/TileViewer.cs | 4 +- .../Patches/MiscPatches/Game1Patch.cs | 2 +- 7 files changed, 694 insertions(+), 673 deletions(-) diff --git a/stardew-access/Features/DynamicTiles.cs b/stardew-access/Features/DynamicTiles.cs index cc50483..2ea15c3 100644 --- a/stardew-access/Features/DynamicTiles.cs +++ b/stardew-access/Features/DynamicTiles.cs @@ -12,10 +12,10 @@ 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). - /// - /// + /// + /// 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 @@ -111,441 +111,465 @@ namespace stardew_access.Features /// - "CaptainRoom" /// - "IslandNorthCave1" /// - "QiNutRoom" - /// - public class DynamicTiles - { - // Static instance for the singleton pattern - private static DynamicTiles? _instance; + /// + 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; - } - } + /// + /// 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(); + // 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 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; + // 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(); - } + /// + /// 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"); + /// + /// 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>(); - } + 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>(); + 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 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); - } + // 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); - } + eventTiles.Add(eventName, coordinates); + } - return eventTiles; - } + 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. - /// 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) - { - 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); - } - } + /// + /// 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); - } + 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. - /// 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) - { - if (boatTunnel is null) - { - throw new ArgumentNullException(nameof(boatTunnel)); - } + /// + /// 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 (x == 4 && y == 9) - { - return ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatFixed") ? "Repair " : "") + "Ticket Machine", CATEGORY.Interactables); - } - else if (x == 6 && y == 8) - { - return ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull") ? "Repair " : "") + "Boat Hull", (!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull") ? CATEGORY.Interactables : CATEGORY.Decor)); - } - else if (x == 8 && y == 9) - { - return ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor") ? "Repair " : "") + "Boat Anchor", (!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor") ? CATEGORY.Interactables : CATEGORY.Decor)); - } + // 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"), + }; - return (null, null); - } + string itemName = (x, y) switch + { + (4, 9) => "Ticket Machine", + (6, 8) => "Boat Hull", + (8, 9) => "Boat Anchor", + _ => throw new InvalidOperationException("Unexpected (x, y) values"), + }; - /// - /// 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. - /// 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) - { - if (communityCenter.missedRewardsChestVisible.Value && x == 22 && y == 10) - { - return ("Missed Rewards Chest", CATEGORY.Containers); - } + CATEGORY category = (x, y) == (4, 9) ? CATEGORY.Interactables : (!HasMail(mail) ? CATEGORY.Interactables : CATEGORY.Decor); - return (null, null); - } + return ((!HasMail(mail) ? "Repair " : "") + itemName, category); + } - /// - /// 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. - /// 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) - { - string name = building.buildingType.Value; - int buildingTileX = building.tileX.Value; - int buildingTileY = building.tileY.Value; + return (null, null); + } - // 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}"; - } + /// + /// 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); + } - // Calculate differences in x and y coordinates - int offsetX = x - buildingTileX; - int offsetY = y - buildingTileY; + return (null, null); + } - // 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); - } - } + /// + /// 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; - // Return the building name for any other position within the building's area - return (name, CATEGORY.Buildings); - } + // 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}"; + } - /// - /// 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. - /// 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) - { - var mainMailboxPos = farm.GetMainMailboxPosition(); - Building building = farm.getBuildingAt(new Vector2(x, y)); + // Calculate differences in x and y coordinates + int offsetX = x - buildingTileX; + int offsetY = y - buildingTileY; - 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); - } + // 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 (null, null); - } + // 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 FarmHouse. - /// - /// The FarmHouse to search. - /// The x-coordinate to search. - /// The y-coordinate to search. - /// 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) - { - if (farmHouse.upgradeLevel >= 1) - { - int kitchenX = farmHouse.getKitchenStandingSpot().X; - int kitchenY = farmHouse.getKitchenStandingSpot().Y - 1; + /// + /// 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 (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); - } - } + 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); - } + 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. - /// 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) - { - 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); - } + /// + /// 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; - return (null, null); - } + 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); + } + } - /// - /// 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. - /// 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) - { - 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); + } - 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); + } - /// - /// 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. - /// 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) - { - // 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); + } - // 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 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); + } - /// - /// 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. - /// 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) - { - // 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); + } - // 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 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); + } - /// - /// 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. - /// 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) - { - 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) if no relevant object is found + return (null, null); + } - 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); + } - /// - /// 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. - /// 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) - { - object locationType = islandLocation is not null and IslandLocation ? islandLocation.Name ?? "Undefined Island Location" : islandLocation!.GetType(); + // Return (null, null) if no relevant object is found + return (null, null); + } - // Implement specific logic for named IslandLocations here, if necessary + /// + /// 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); + } + } - // 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}"); + return (null, null); + } - // Add the location to the HashSet to prevent logging it again - loggedLocations.Add(locationType); - } + /// + /// 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(); - return (null, null); - } + // Implement specific logic for named IslandLocations here, if necessary - /// - /// 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 - }; - } + // 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. @@ -564,219 +588,225 @@ namespace stardew_access.Features { 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, - }; - } + // 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. - /// 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) - { - var nutTracker = Game1.player.team.collectedNutTracker; + /// + /// 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); - } + 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), - IslandWest islandWest => GetIslandWestInfo(islandWest, x, y), - VolcanoDungeon dungeon => GetVolcanoDungeonInfo(dungeon, x, y), - _ => GetNamedIslandLocationInfo(islandLocation, x, y) - }; - } + 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. - /// 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) - { - 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 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. - /// 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) - { - if (libraryMuseum.museumPieces.TryGetValue(new Vector2(x, y), out int museumPiece)) - { - string displayName = Game1.objectInformation[museumPiece].Split('/')[0]; - return ($"{displayName} showcase", CATEGORY.Interactables); - } + /// + /// 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); + 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); - } - } + 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); - } + 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. - /// 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) - { - if (SpecialOrder.IsSpecialOrdersBoardUnlocked() && x == 62 && y == 93) - { - return ("Special quest board", CATEGORY.Interactables); - } + /// + /// 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); - } + 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. - /// 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) - { - string locationName = currentLocation.Name; + /// + /// 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(x, y, currentLocation, true); - return (name?.Contains("hay", StringComparison.OrdinalIgnoreCase) == true ? "Feeding Bench" : "Empty Feeding Bench", category); - } + 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; - } + 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. - /// 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) - { - 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... + /// + /// 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}"); + // 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); - } + // Add the location to the HashSet to prevent logging it again + loggedLocations.Add(locationType); + } - return (null, null); - } + return (null, null); + } - /// - /// Retrieves the dynamic tile information for the given coordinates in the specified location. - /// - /// The x-coordinate of the tile. - /// The y-coordinate of the tile. - /// The current GameLocation instance. - /// 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(int x, int y, GameLocation currentLocation, 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); - } - } - } + /// + /// 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), - BoatTunnel boatTunnel => GetBoatTunnelInfo(boatTunnel, x, y), - CommunityCenter communityCenter => GetCommunityCenterInfo(communityCenter, x, y), - Farm farm => GetFarmInfo(farm, x, y), - FarmHouse farmHouse => GetFarmHouseInfo(farmHouse, x, y), - Forest forest => GetForestInfo(forest, x, y), - IslandFarmHouse islandFarmHouse => GetIslandFarmHouseInfo(islandFarmHouse, x, y), - IslandLocation islandLocation => GetIslandLocationInfo(islandLocation, x, y), - LibraryMuseum libraryMuseum => GetLibraryMuseumInfo(libraryMuseum, x, y), - Town town => GetTownInfo(town, x, y), - _ => GetLocationByNameInfo(currentLocation, x, y) - }; - } - } + // 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/Radar.cs b/stardew-access/Features/Radar.cs index a2752a8..eac37e8 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -160,7 +160,7 @@ namespace stardew_access.Features watch.Stop(); var elapsedMs = watch.ElapsedMilliseconds; MainClass.DebugLog($"Search init duration: {elapsedMs}"); - watch.Reset(); + watch.Reset(); watch.Start(); while (toSearch.Count > 0) { @@ -178,7 +178,7 @@ namespace stardew_access.Features { Vector2 dir = new(item.X + dirX[i], item.Y + dirY[i]); - if (!searched.Contains(dir) && (TileInfo.isWarpPointAtTile((int)dir.X, (int)dir.Y, currentLocation) || 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); @@ -231,7 +231,7 @@ namespace stardew_access.Features { if (currentLocation.isObjectAtTile((int)position.X, (int)position.Y)) { - (string? name, CATEGORY category) objDetails = TileInfo.getObjectAtTile((int)position.X, (int)position.Y, currentLocation); + (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 = currentLocation.getObjectAtTile((int)position.X, (int)position.Y); diff --git a/stardew-access/Features/ReadTile.cs b/stardew-access/Features/ReadTile.cs index a5a1a11..661274c 100644 --- a/stardew-access/Features/ReadTile.cs +++ b/stardew-access/Features/ReadTile.cs @@ -90,7 +90,7 @@ namespace stardew_access.Features } var currentLocation = Game1.currentLocation; - bool isColliding = TileInfo.IsCollidingAtTile(x, y, currentLocation); + bool isColliding = TileInfo.IsCollidingAtTile(currentLocation, x, y); (string? name, string? category) info = TileInfo.getNameWithCategoryNameAtTile(tile, currentLocation); diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 204d562..4b2ed15 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -73,7 +73,7 @@ namespace stardew_access.Features /// Additional lambda functions can be added as needed. /// private static readonly Dictionary> conditionals = new() - { + { ["Farm"] = (conditionType, uniqueModId) => { if (string.IsNullOrEmpty(uniqueModId)) diff --git a/stardew-access/Features/TileInfo.cs b/stardew-access/Features/TileInfo.cs index f572c0c..c2712df 100644 --- a/stardew-access/Features/TileInfo.cs +++ b/stardew-access/Features/TileInfo.cs @@ -11,9 +11,9 @@ namespace stardew_access.Features { public class TileInfo { - 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 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" }, @@ -71,7 +71,7 @@ namespace stardew_access.Features return (staticTile.name, staticTile.category); } - (string? name, CATEGORY? category) dynamicTile = DynamicTiles.GetDynamicTileAt(x, y, currentLocation, lessInfo); + (string? name, CATEGORY? category) dynamicTile = DynamicTiles.GetDynamicTileAt(currentLocation, x, y, lessInfo); if (dynamicTile.name != null) { return (dynamicTile.name, dynamicTile.category); @@ -79,16 +79,16 @@ namespace stardew_access.Features if (currentLocation.isObjectAtTile(x, y)) { - (string? name, CATEGORY? category) obj = getObjectAtTile(x, y, currentLocation, lessInfo); + (string? name, CATEGORY? category) obj = getObjectAtTile(currentLocation, x, y, lessInfo); return (obj.name, obj.category); } - if (currentLocation.isWaterTile(x, y) && !lessInfo && IsCollidingAtTile(x, y, currentLocation)) + if (currentLocation.isWaterTile(x, y) && !lessInfo && IsCollidingAtTile(currentLocation, x, y)) { return ("Water", CATEGORY.WaterTiles); } - string? resourceClump = getResourceClumpAtTile(x, y, currentLocation, lessInfo); + string? resourceClump = getResourceClumpAtTile(currentLocation, x, y, lessInfo); if (resourceClump != null) { return (resourceClump, CATEGORY.ResourceClumps); @@ -103,20 +103,20 @@ namespace stardew_access.Features } } - string? bush = GetBushAtTile(x, y, currentLocation, lessInfo); + string? bush = GetBushAtTile(currentLocation, x, y, lessInfo); if (bush != null) { return (bush, CATEGORY.Bush); } - string? door = getDoorAtTile(x, y, currentLocation); - string? warp = getWarpPointAtTile(x, y, currentLocation); + 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(x, y, currentLocation); + string? junimoBundle = GetJunimoBundleAt(currentLocation, x, y); if (junimoBundle != null) { return (junimoBundle, CATEGORY.JunimoBundle); @@ -150,12 +150,12 @@ namespace stardew_access.Features /// /// 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. - /// The GameLocation instance to search for bushes. /// 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(int x, int y, GameLocation currentLocation, bool lessInfo = false) + public static string? GetBushAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { Bush? bush = (Bush)currentLocation.getLargeTerrainFeatureAt(x, y); @@ -201,11 +201,11 @@ namespace stardew_access.Features /// /// 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 GameLocation instance to search for Junimo bundles. /// The name of the Junimo bundle if one is found at the specified coordinates, otherwise null. - public static string? GetJunimoBundleAt(int x, int y, GameLocation currentLocation) + public static string? GetJunimoBundleAt(GameLocation currentLocation, int x, int y) { if (currentLocation is CommunityCenter communityCenter) { @@ -246,25 +246,25 @@ namespace stardew_access.Features /// /// 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. - /// The GameLocation instance to search for collisions. /// True if a collision is detected at the specified tile coordinates, otherwise False. - public static bool IsCollidingAtTile(int x, int y, GameLocation currentLocation) + public static bool IsCollidingAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = 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(x, y, currentLocation) && + 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(x, y, woods) is not null)); + (currentLocation is Woods woods && getStumpsInWoods(woods, x, y, lessInfo) is not null)); } /// /// Returns the Warp object at the specified tile coordinates or null if not found. /// - private static Warp? GetWarpAtTile(int x, int y, GameLocation currentLocation) + private static Warp? GetWarpAtTile(GameLocation currentLocation, int x, int y) { if (currentLocation is null) return null; @@ -281,13 +281,13 @@ namespace stardew_access.Features /// /// Returns the name of the warp point at the specified tile coordinates, or null if not found. /// - public static string? getWarpPointAtTile(int x, int y, GameLocation currentLocation) + public static string? getWarpPointAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { - Warp? warpPoint = GetWarpAtTile(x, y, currentLocation); + Warp? warpPoint = GetWarpAtTile(currentLocation, x, y); if (warpPoint != null) { - return $"{warpPoint.TargetName} Entrance"; + return lessInfo ? warpPoint.TargetName : $"{warpPoint.TargetName} Entrance"; } return null; @@ -296,9 +296,9 @@ namespace stardew_access.Features /// /// Returns true if there's a warp point at the specified tile coordinates, or false otherwise. /// - public static bool isWarpPointAtTile(int x, int y, GameLocation currentLocation) + public static bool isWarpPointAtTile(GameLocation currentLocation, int x, int y) { - return GetWarpAtTile(x, y, currentLocation) != null; + return GetWarpAtTile(currentLocation, x, y) != null; } /// @@ -311,7 +311,7 @@ namespace stardew_access.Features /// 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) + 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)) @@ -499,10 +499,10 @@ namespace stardew_access.Features { int treeType = tree.treeType.Value; int treeStage = tree.growthStage.Value; - string seedName = ""; + string seedName = ""; - // Handle special tree types and return their names - switch (treeType) + // Handle special tree types and return their names + switch (treeType) { case 4: case 5: @@ -522,28 +522,28 @@ namespace stardew_access.Features // Determine the tree name and growth stage description if (treeStage >= 1) { - string treeName; - switch (seedName.ToLower()) - { - case "mahogany seed": - treeName = "Mahogany"; - break; - case "acorn": - treeName = "Oak"; - break; - case "maple seed": - treeName = "Maple"; - break; - case "pine cone": - treeName = "Pine"; - break; - default: - treeName = "Coconut"; - break; - } + string treeName; + switch (seedName.ToLower()) + { + case "mahogany seed": + treeName = "Mahogany"; + break; + case "acorn": + treeName = "Oak"; + break; + case "maple seed": + treeName = "Maple"; + break; + case "pine cone": + treeName = "Pine"; + break; + default: + treeName = "Coconut"; + break; + } - // Append the growth stage description to the tree name - if (treeStage == 1) + // Append the growth stage description to the tree name + if (treeStage == 1) treeName = $"{treeName} sprout"; else if (treeStage == 2) treeName = $"{treeName} sapling"; @@ -563,12 +563,12 @@ namespace stardew_access.Features /// /// 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. - /// The current game location. /// 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(int x, int y, GameLocation currentLocation, bool lessInfo = false) + public static (string? name, CATEGORY category) getObjectAtTile(GameLocation currentLocation, int x, int y, bool lessInfo = false) { (string? name, CATEGORY category) toReturn = (null, CATEGORY.Others); @@ -783,12 +783,12 @@ namespace stardew_access.Features /// /// 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 current game location. /// 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(int x, int y, GameLocation currentLocation, int targetTileIndex) + private static bool CheckTileIndex(GameLocation currentLocation, int x, int y, int targetTileIndex) { var tile = currentLocation.Map.GetLayer("Buildings").Tiles[x, y]; return tile != null && tile.TileIndex == targetTileIndex; @@ -797,70 +797,67 @@ namespace stardew_access.Features /// /// 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. - /// The current GameLocation instance. /// True if a mine down ladder is found at the specified tile, otherwise false. - public static bool isMineDownLadderAtTile(int x, int y, GameLocation currentLocation) + public static bool isMineDownLadderAtTile(GameLocation currentLocation, int x, int y) { return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" - ? CheckTileIndex(x, y, currentLocation, 173) + ? CheckTileIndex(currentLocation, x, y, 173) : false; } /// /// 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. - /// The current GameLocation instance. /// True if a mine shaft is found at the specified tile, otherwise false. - public static bool isShaftAtTile(int x, int y, GameLocation currentLocation) + public static bool isShaftAtTile(GameLocation currentLocation, int x, int y) { return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" - ? CheckTileIndex(x, y, currentLocation, 174) + ? CheckTileIndex(currentLocation, x, y, 174) : false; } /// /// 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. - /// The current GameLocation instance. /// True if a mine up ladder is found at the specified tile, otherwise false. - public static bool isMineUpLadderAtTile(int x, int y, GameLocation currentLocation) + public static bool isMineUpLadderAtTile(GameLocation currentLocation, int x, int y) { return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" - ? CheckTileIndex(x, y, currentLocation, 115) + ? CheckTileIndex(currentLocation, x, y, 115) : false; } /// /// 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. - /// The current GameLocation instance. /// True if an elevator is found at the specified tile, otherwise false. - public static bool isElevatorAtTile(int x, int y, GameLocation currentLocation) + public static bool isElevatorAtTile(GameLocation currentLocation, int x, int y) { return currentLocation is Mine or MineShaft || currentLocation.Name == "SkullCave" - ? CheckTileIndex(x, y, currentLocation, 112) + ? CheckTileIndex(currentLocation, x, y, 112) : false; } /// /// 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. - /// The GameLocation where the door might be found. - /// - /// 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(int x, int y, GameLocation currentLocation) + /// 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) { // Create a Point object from the given tile coordinates Point tilePoint = new(x, y); @@ -882,19 +879,16 @@ namespace stardew_access.Features /// /// 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. - /// The GameLocation where the resource clump might be found. /// 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(int x, int y, GameLocation currentLocation, bool lessInfo = 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) { // Check if the current location is Woods and handle stumps in woods separately if (currentLocation is Woods woods) - return getStumpsInWoods(x, y, woods, lessInfo); + return getStumpsInWoods(woods, x, y, lessInfo); // 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++) @@ -916,15 +910,12 @@ namespace stardew_access.Features /// /// 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. - /// The Woods location where the stump might be found. /// 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(int x, int y, Woods woods, bool lessInfo = 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) { // Iterate through stumps in the Woods location foreach (var stump in woods.stumps) diff --git a/stardew-access/Features/TileViewer.cs b/stardew-access/Features/TileViewer.cs index 0444b56..282d0aa 100644 --- a/stardew-access/Features/TileViewer.cs +++ b/stardew-access/Features/TileViewer.cs @@ -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, Game1.currentLocation)) + if (TileInfo.IsCollidingAtTile(Game1.currentLocation, (int)tile.X, (int)tile.Y)) { name = "blocked"; } @@ -280,7 +280,7 @@ namespace stardew_access.Features { 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), currentLocation)) 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 = currentLocation.map; diff --git a/stardew-access/Patches/MiscPatches/Game1Patch.cs b/stardew-access/Patches/MiscPatches/Game1Patch.cs index 4b52615..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, Game1.currentLocation)) + if (TileInfo.IsCollidingAtTile(Game1.currentLocation, (int)nextTile.X, (int)nextTile.Y)) { if (prevTile != nextTile) { From ed405a139e9821c7774b2b44f6c735216a82b525 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Tue, 4 Apr 2023 19:21:04 -0700 Subject: [PATCH 16/19] Remove dependency on Newtonsoft.Json since we're now relying on System.Text.Json. Some optimization and refactoring in ModEntry.cs Other minor fixes --- stardew-access/Features/Utils.cs | 4 +- stardew-access/ModEntry.cs | 172 ++++++++++++++------------- stardew-access/stardew-access.csproj | 1 - 3 files changed, 93 insertions(+), 84 deletions(-) diff --git a/stardew-access/Features/Utils.cs b/stardew-access/Features/Utils.cs index 1e27c3b..ee1e90d 100644 --- a/stardew-access/Features/Utils.cs +++ b/stardew-access/Features/Utils.cs @@ -125,8 +125,8 @@ namespace stardew_access.Features public static CATEGORY DroppedItems => FromString("dropped item"); public static CATEGORY Others => FromString("other"); } - - public enum MachineState + + public enum MachineState { Ready, Busy, Waiting } diff --git a/stardew-access/ModEntry.cs b/stardew-access/ModEntry.cs index e63303d..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 { @@ -134,6 +135,12 @@ namespace stardew_access 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 @@ -142,12 +149,6 @@ namespace stardew_access ScreenReader.CloseScreenReader(); } - /// Returns the Screen Reader class for other mods to use. - public override object GetApi() - { - return new API(); - } - private void onDayStarted(object? sender, DayStartedEventArgs? e) { StaticTiles.LoadTilesFiles(); @@ -161,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(); @@ -174,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(); + } } } } @@ -204,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); } @@ -254,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; } @@ -282,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; } @@ -326,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/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 @@ - From 529a1106173771f53bb6951a7e27a8638db7b794 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Wed, 5 Apr 2023 00:44:24 -0700 Subject: [PATCH 17/19] Refactor functions that obtain current attribute values to share code. --- .../CharacterCustomizationMenuPatches.cs | 218 ++++++------------ 1 file changed, 70 insertions(+), 148 deletions(-) diff --git a/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs b/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs index 005456d..8f56f32 100644 --- a/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs +++ b/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs @@ -126,24 +126,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 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); if (characterDesignToggle) { @@ -693,136 +693,7 @@ 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 GetCurrentPetName() { if (currentComponent != null && currentComponent.name == "Pet") { @@ -833,5 +704,56 @@ namespace stardew_access.Patches return ""; } } + + private static string GetCurrentAttributeValue(string componentName, Func getValue) + { + if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == componentName)) + { + return $"{componentName}: {getValue()}"; + } + return ""; + } + + private static string GetCurrentSkin() => GetCurrentAttributeValue("Skin", () => Game1.player.skin.Value + 1); + + private static string GetCurrentHair() => GetCurrentAttributeValue("Hair", () => Game1.player.hair.Value + 1); + + private static string GetCurrentShirt() => GetCurrentAttributeValue("Shirt", () => Game1.player.shirt.Value + 1); + + private static string GetCurrentPants() => GetCurrentAttributeValue("Pants Style", () => Game1.player.pants.Value + 1); + + private static string GetCurrentAccessory() => GetCurrentAttributeValue("Acc", () => Game1.player.accessory.Value + 2); + + 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())); } } From dacbff92eaca20f3be33a0b7b8f0738172d70c7c Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Tue, 4 Apr 2023 23:35:55 -0700 Subject: [PATCH 18/19] CharacterCustomizationMenuPatches now reads descriptions of appearance options (thanks @JJpanSai for writing the descriptions!) Descriptions are stored in assets/new-character-appearance-descriptions.json. If file can't be found or is corrupted, fallback on old behavior of just reading numbers. Functions support a `lessInfo` variable that also turns off the descriptions, though the config option is not yet implemented. --- .../CharacterCustomizationMenuPatches.cs | 165 ++++++++--- ...new-character-appearance-descriptions.json | 256 ++++++++++++++++++ 2 files changed, 376 insertions(+), 45 deletions(-) create mode 100644 stardew-access/assets/new-character-appearance-descriptions.json diff --git a/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs b/stardew-access/Patches/TitleMenuPatches/CharacterCustomizationMenuPatches.cs index 8f56f32..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,7 +169,7 @@ namespace stardew_access.Patches private static string getChangesToSpeak(CharacterCustomization __instance) { string toSpeak = ""; - string currentPetName = GetCurrentPetName(); + string currentPet = GetCurrentPet(); string currentSkin = GetCurrentSkin(); string currentHair = GetCurrentHair(); string currentShirt = GetCurrentShirt(); @@ -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,36 +736,68 @@ 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 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; - private static string GetCurrentAttributeValue(string componentName, Func getValue) - { - if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == componentName)) - { - return $"{componentName}: {getValue()}"; + 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 GetCurrentSkin() => GetCurrentAttributeValue("Skin", () => Game1.player.skin.Value + 1); + private static string GetCurrentAttributeValue(string componentName, Func getValue, bool lessInfo = false) + { + if (currentComponent != null && (currentComponent.myID == 507 || currentComponent.name == componentName)) + { + int index = getValue(); - private static string GetCurrentHair() => GetCurrentAttributeValue("Hair", () => Game1.player.hair.Value + 1); + 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 GetCurrentShirt() => GetCurrentAttributeValue("Shirt", () => Game1.player.shirt.Value + 1); + private static string GetCurrentSkin(bool lessInfo = false) => GetCurrentAttributeValue("Skin", () => Game1.player.skin.Value + 1, lessInfo); - private static string GetCurrentPants() => GetCurrentAttributeValue("Pants Style", () => Game1.player.pants.Value + 1); + private static string GetCurrentHair(bool lessInfo = false) => GetCurrentAttributeValue("Hair", () => Game1.player.hair.Value + 1, lessInfo); - private static string GetCurrentAccessory() => GetCurrentAttributeValue("Acc", () => Game1.player.accessory.Value + 2); + 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) { 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 From 803bc6486878d12c0fe316602193a9a23e173a05 Mon Sep 17 00:00:00 2001 From: Katie Durden Date: Wed, 5 Apr 2023 19:17:57 -0700 Subject: [PATCH 19/19] Bump manifest version to 1.3.5-beta2. Fix error in json loading and missing static tiles. --- stardew-access/Features/Radar.cs | 20 ++++++++++---------- stardew-access/Features/StaticTiles.cs | 6 +++--- stardew-access/manifest.json | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/stardew-access/Features/Radar.cs b/stardew-access/Features/Radar.cs index eac37e8..d2216fe 100644 --- a/stardew-access/Features/Radar.cs +++ b/stardew-access/Features/Radar.cs @@ -141,8 +141,8 @@ 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() { - var watch = new Stopwatch(); - watch.Start(); + //var watch = new Stopwatch(); + //watch.Start(); var currentLocation = Game1.currentLocation; Dictionary detectedTiles = new(); Vector2 position = Vector2.Zero; @@ -157,11 +157,11 @@ 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(); + //watch.Stop(); + //var elapsedMs = watch.ElapsedMilliseconds; + //MainClass.DebugLog($"Search init duration: {elapsedMs}"); + //watch.Reset(); + //watch.Start(); while (toSearch.Count > 0) { Vector2 item = toSearch.Dequeue(); @@ -185,9 +185,9 @@ namespace stardew_access.Features } } } - watch.Stop(); - elapsedMs = watch.ElapsedMilliseconds; - MainClass.DebugLog($"Search loop duration: {elapsedMs}; {count} iterations."); + //watch.Stop(); + //elapsedMs = watch.ElapsedMilliseconds; + //MainClass.DebugLog($"Search loop duration: {elapsedMs}; {count} iterations."); searched.Clear(); return detectedTiles; } diff --git a/stardew-access/Features/StaticTiles.cs b/stardew-access/Features/StaticTiles.cs index 4b2ed15..6ee4d9c 100644 --- a/stardew-access/Features/StaticTiles.cs +++ b/stardew-access/Features/StaticTiles.cs @@ -420,7 +420,7 @@ namespace stardew_access.Features var (modConditionalLocations, modLocations, vanillaConditionalLocations, vanillaLocations) = SortLocationsByType(json); // Create a merged dictionary to store all the location dictionaries - var mergedDict = new Dictionary>(); + 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); @@ -436,7 +436,7 @@ namespace stardew_access.Features /// public static void SetupTilesDicts() { - if (staticTilesData.HasValue) + if (staticTilesData.HasValue && staticTilesData.Value.ValueKind != JsonValueKind.Undefined) { staticTilesDataDict = BuildTilesDict(staticTilesData.Value); } @@ -445,7 +445,7 @@ namespace stardew_access.Features staticTilesDataDict = new Dictionary>(); } - if (customTilesData.HasValue) + if (customTilesData.HasValue && customTilesData.Value.ValueKind != JsonValueKind.Undefined) { customTilesDataDict = BuildTilesDict(customTilesData.Value); } diff --git a/stardew-access/manifest.json b/stardew-access/manifest.json index a95415e..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.5-alpha2", + "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 +}