using Microsoft.Xna.Framework; using StardewValley; using StardewValley.Buildings; using StardewValley.Locations; using StardewValley.Objects; using StardewValley.TerrainFeatures; 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" }; ///Returns the name of the object at tile alongwith it's category's name public static (string? name, string? categoryName) getNameWithCategoryNameAtTile(Vector2 tile) { (string? name, CATEGORY? category) tileDetail = getNameWithCategoryAtTile(tile); if (tileDetail.category == null) tileDetail.category = CATEGORY.Others; return (tileDetail.name, tileDetail.category.ToString()); } ///Returns the name of the object at tile public static string? getNameAtTile(Vector2 tile) { return getNameWithCategoryAtTile(tile).name; } ///Returns the name of the object at tile alongwith it's category public static (string? name, CATEGORY? category) getNameWithCategoryAtTile(Vector2 tile) { int x = (int)tile.X; int y = (int)tile.Y; string? toReturn = ""; CATEGORY? category = CATEGORY.Others; bool isColliding = isCollidingAtTile(x, y); Dictionary> terrainFeature = Game1.currentLocation.terrainFeatures.FieldDict; string? door = getDoorAtTile(x, y); (CATEGORY? category, string? name) dynamicTile = getDynamicTilesInfo(x, y); string? junimoBundle = getJunimoBundleAt(x, y); string? resourceClump = getResourceClumpAtTile(x, y); string? farmAnimal = getFarmAnimalAt(Game1.currentLocation, x, y); string? parrot = getParrotPerchAtTile(x, y); (string? name, CATEGORY category) staticTile = MainClass.STiles.getStaticTileInfoAtWithCategory(x, y); if (Game1.currentLocation.isCharacterAtTile(tile) != null) { NPC npc = Game1.currentLocation.isCharacterAtTile(tile); toReturn = npc.displayName; if (npc.isVillager() || npc.CanSocialize) category = CATEGORY.Farmers; else category = CATEGORY.NPCs; } else if (farmAnimal != null) { toReturn = farmAnimal; category = CATEGORY.FarmAnimals; } else if (staticTile.name != null) { toReturn = staticTile.name; category = staticTile.category; } else if (dynamicTile.name != null) { toReturn = dynamicTile.name; category = dynamicTile.category; } else if (Game1.currentLocation is VolcanoDungeon && ((VolcanoDungeon)Game1.currentLocation).IsCooledLava(x, y)) { toReturn = "Cooled lava"; category = CATEGORY.WaterTiles; } else if (Game1.currentLocation is VolcanoDungeon && StardewValley.Monsters.LavaLurk.IsLavaTile((VolcanoDungeon)Game1.currentLocation, x, y)) { toReturn = "Lava"; category = CATEGORY.WaterTiles; } else if (Game1.currentLocation.isWaterTile(x, y) && isColliding) { toReturn = "Water"; category = CATEGORY.WaterTiles; } else if (Game1.currentLocation.isObjectAtTile(x, y)) { (string? name, CATEGORY? category) obj = getObjectAtTile(x, y); toReturn = obj.name; category = obj.category; } else if (terrainFeature.ContainsKey(tile)) { (string? name, CATEGORY category) tf = getTerrainFeatureAtTile(terrainFeature[tile]); string? terrain = tf.name; if (terrain != null) { toReturn = terrain; category = tf.category; } } else if (Game1.currentLocation.getLargeTerrainFeatureAt(x, y) != null) { toReturn = getBushAtTile(x, y); category = CATEGORY.Bush; } else if (resourceClump != null) { toReturn = resourceClump; category = CATEGORY.ResourceClumps; } else if (door != null) { toReturn = door; category = CATEGORY.Doors; } else if (isMineDownLadderAtTile(x, y)) { toReturn = "Ladder"; category = CATEGORY.Doors; } else if (isMineUpLadderAtTile(x, y)) { toReturn = "Up Ladder"; category = CATEGORY.Doors; } else if (isElevatorAtTile(x, y)) { toReturn = "Elevator"; category = CATEGORY.Doors; } else if (parrot != null) { toReturn = parrot; category = CATEGORY.Buildings; } else if (junimoBundle != null) { toReturn = junimoBundle; category = CATEGORY.JunimoBundle; } if (toReturn == "") return (null, category); return (toReturn, category); } public static string? getBushAtTile(int x, int y) { string? toReturn = null; Bush bush = (Bush)Game1.currentLocation.getLargeTerrainFeatureAt(x, y); 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)) { // Taken from the game's code string season = ((int)bush.overrideSeason.Value == -1) ? Game1.GetSeasonForLocation(Game1.currentLocation) : Utility.getSeasonNameFromNumber(bush.overrideSeason.Value); int shakeOff = -1; if (!(season == "spring")) { if (season == "fall") { shakeOff = 410; } } else { shakeOff = 296; } if ((int)size == 3) { shakeOff = 815; } if ((int)size == 4) { shakeOff = 73; } if (shakeOff == -1) { return null; } toReturn = "Harvestable"; } #endregion if (bush.townBush.Value) toReturn = $"{toReturn} Town Bush"; else if (bush.greenhouseBush.Value) toReturn = $"{toReturn} Greenhouse Bush"; else toReturn = $"{toReturn} Bush"; return toReturn; } public static string? getJunimoBundleAt(int x, int y) { string? name = null; if (Game1.currentLocation is CommunityCenter communityCenter) { name = (x, y) switch { (14, 5) => "Pantry", (14, 23) => "Crafts Room", (40, 10) => "Fish Tank", (63, 14) => "Boiler Room", (55, 6) => "Vault", (46, 11) => "Bulletin Board", _ => null, }; if (name != null && communityCenter.shouldNoteAppearInArea(CommunityCenter.getAreaNumberFromName(name))) return $"{name} bundle"; } else if (Game1.currentLocation is AbandonedJojaMart) { name = (x, y) switch { (8, 8) => "Missing", _ => null, }; if (name != null) return $"{name} bundle"; } return null; } public static bool isCollidingAtTile(int x, int y) { Rectangle rect = new Rectangle(x * 64 + 1, y * 64 + 1, 62, 62); if (Game1.currentLocation.isCollidingPosition(rect, Game1.viewport, true, 0, glider: false, Game1.player, pathfinding: true)) { return true; } if (Game1.currentLocation is Woods && getStumpsInWoods(x, y) != null) return true; return false; } public static string? getFarmAnimalAt(GameLocation? location, int x, int y, bool onlyName = false) { if (location == null) return null; if (location is not Farm && location is not AnimalHouse) return null; List? farmAnimals = null; if (location is Farm) farmAnimals = ((Farm)location).getAllFarmAnimals(); else if (location is AnimalHouse) farmAnimals = ((AnimalHouse)location).animals.Values.ToList(); if (farmAnimals == null || farmAnimals.Count <= 0) return null; for (int i = 0; i < farmAnimals.Count; i++) { int fx = farmAnimals[i].getTileX(); int fy = farmAnimals[i].getTileY(); if (fx.Equals(x) && fy.Equals(y)) { string name = farmAnimals[i].displayName; int age = farmAnimals[i].age.Value; string type = farmAnimals[i].displayType; if (onlyName) return name; return $"{name}, {type}, age {age}"; } } return null; } /// /// /// /// /// /// 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) { if (Game1.currentLocation is Farm farm) { Building building = farm.getBuildingAt(new Vector2(x, y)); if (building != null) { if ((building.humanDoor.Value.X + building.tileX.Value) == x && (building.humanDoor.Value.Y + building.tileY.Value) == y) return (CATEGORY.Doors, building.buildingType.Value + " Door"); else if ((building.animalDoor.Value.X + building.tileX.Value) == x && (building.animalDoor.Value.Y + building.tileY.Value) == y) return (CATEGORY.Doors, building.buildingType.Value + " Animal Door " + ((building.animalDoorOpen.Value) ? "Opened" : "Closed")); else return (CATEGORY.Buildings, building.buildingType.Value); } } else if (Game1.currentLocation is Town) { if (SpecialOrder.IsSpecialOrdersBoardUnlocked() && x == 62 && y == 93) return (CATEGORY.Interactables, "Special quest board"); } else if (Game1.currentLocation is FarmHouse farmHouse) { if (farmHouse.getEntryLocation().X == x && farmHouse.getEntryLocation().Y == y) return (CATEGORY.Doors, "Exit"); else if (farmHouse.upgradeLevel >= 1) if (farmHouse.getKitchenStandingSpot().X == x && (farmHouse.getKitchenStandingSpot().Y - 1) == y) // Standing spot is where the player will stand return (CATEGORY.Interactables, "Kitchen"); else if (farmHouse.fridgePosition.X == x && farmHouse.fridgePosition.Y == y) return (CATEGORY.Interactables, "Fridge"); } else if (Game1.currentLocation is IslandFarmHouse islandFarmHouse) { if (x == 14 && y == 17) return (CATEGORY.Doors, "Exit"); else if ((islandFarmHouse.fridgePosition.X - 1) == x && islandFarmHouse.fridgePosition.Y == y) return (CATEGORY.Interactables, "Kitchen"); else if (islandFarmHouse.fridgePosition.X == x && islandFarmHouse.fridgePosition.Y == y) return (CATEGORY.Interactables, "Fridge"); } else if (Game1.currentLocation is Forest forest) { if (forest.travelingMerchantDay && x == 27 && y == 11) return (CATEGORY.Interactables, "Travelling Cart"); else if (forest.log != null && x == 2 && y == 7) return (CATEGORY.Interactables, "Log"); else if (forest.log == null && x == 0 && y == 7) return (CATEGORY.Doors, "Secret Woods Entrance"); } else if (Game1.currentLocation is Beach beach) { if (x == 58 && y == 13) { if (!beach.bridgeFixed.Value) return (CATEGORY.Interactables, "Repair Bridge"); else return (CATEGORY.Bridges, "Bridge"); } } else if (Game1.currentLocation is CommunityCenter communityCenter) { if (communityCenter.missedRewardsChestVisible.Value && x == 22 && y == 10) return (CATEGORY.Chests, "Missed Rewards Chest"); } else if (Game1.currentLocation is BoatTunnel) { if (x == 4 && y == 9) return (CATEGORY.Interactables, ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatFixed")) ? "Repair " : "") + "Ticket Machine"); else if (x == 6 && y == 8) return (((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull")) ? CATEGORY.Interactables : CATEGORY.Decor), ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatHull")) ? "Repair " : "") + "Boat Hull"); else if (x == 8 && y == 9) return (((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor")) ? CATEGORY.Interactables : CATEGORY.Decor), ((!Game1.MasterPlayer.hasOrWillReceiveMail("willyBoatAnchor")) ? "Repair " : "") + "Boat Anchor"); } return (null, null); } public static (string? name, CATEGORY category) getTerrainFeatureAtTile(Netcode.NetRef terrain) { string? toReturn = null; CATEGORY category = CATEGORY.Others; if (terrain.Get() is HoeDirt) { category = CATEGORY.Crops; HoeDirt dirt = (HoeDirt)terrain.Get(); if (dirt.crop != null && !dirt.crop.forageCrop.Value) { string cropName = Game1.objectInformation[dirt.crop.indexOfHarvest.Value].Split('/')[0]; toReturn = $"{cropName}"; bool isWatered = dirt.state.Value == HoeDirt.watered; bool isHarvestable = dirt.readyForHarvest(); bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer; if (isWatered) toReturn = "Watered " + toReturn; if (isFertilized) toReturn = "Fertilized " + toReturn; if (isHarvestable) toReturn = "Harvestable " + toReturn; } else if (dirt.crop != null && dirt.crop.forageCrop.Value) { toReturn = dirt.crop.whichForageCrop.Value switch { 1 => "Spring onion", 2 => "Ginger", _ => "Forageable crop" }; } else { toReturn = "Soil"; bool isWatered = dirt.state.Value == HoeDirt.watered; bool isFertilized = dirt.fertilizer.Value != HoeDirt.noFertilizer; if (isWatered) toReturn = "Watered " + toReturn; if (isFertilized) toReturn = "Fertilized " + toReturn; } } else if (terrain.Get() is GiantCrop) { category = CATEGORY.Crops; int whichCrop = ((GiantCrop)terrain.Get()).which.Value; switch (whichCrop) { case 0: toReturn = "Cauliflower"; break; case 1: toReturn = "Melon"; break; case 2: toReturn = "Pumpkin"; break; } } else if (terrain.Get() is CosmeticPlant) { category = CATEGORY.Furnitures; CosmeticPlant cosmeticPlant = (CosmeticPlant)terrain.Get(); toReturn = cosmeticPlant.textureName().ToLower(); if (toReturn.Contains("terrain")) toReturn.Replace("terrain", ""); if (toReturn.Contains("feature")) toReturn.Replace("feature", ""); } else if (terrain.Get() is Flooring && MainClass.Config.ReadFlooring) { category = CATEGORY.Flooring; Flooring flooring = (Flooring)terrain.Get(); bool isPathway = flooring.isPathway.Get(); bool isSteppingStone = flooring.isSteppingStone.Get(); toReturn = "Flooring"; if (isPathway) toReturn = "Pathway"; if (isSteppingStone) toReturn = "Stepping Stone"; } else if (terrain.Get() is FruitTree) { category = CATEGORY.Trees; toReturn = getFruitTree((FruitTree)terrain.Get()); } else if (terrain.Get() is Grass) { category = CATEGORY.Debris; toReturn = "Grass"; } else if (terrain.Get() is Tree) { category = CATEGORY.Trees; toReturn = getTree((Tree)terrain.Get()); } else if (terrain.Get() is Quartz) { category = CATEGORY.MineItems; toReturn = "Quartz"; } return (toReturn, category); } public static string getFruitTree(FruitTree fruitTree) { int stage = fruitTree.growthStage.Value; int fruitIndex = fruitTree.indexOfFruit.Get(); string toReturn = Game1.objectInformation[fruitIndex].Split('/')[0]; if (stage == 0) toReturn = $"{toReturn} seed"; else if (stage == 1) toReturn = $"{toReturn} sprout"; else if (stage == 2) toReturn = $"{toReturn} sapling"; else if (stage == 3) toReturn = $"{toReturn} bush"; else if (stage >= 4) toReturn = $"{toReturn} tree"; if (fruitTree.fruitsOnTree.Value > 0) toReturn = $"Harvestable {toReturn}"; return toReturn; } public static string getTree(Tree tree) { int treeType = tree.treeType.Value; int treeStage = tree.growthStage.Value; string treeName = "tree"; string seedName = ""; // Return with the name if it's one of the 3 special trees switch (treeType) { case 4: case 5: return "Winter Tree"; case 6: return "Palm Tree"; case 7: return "Mushroom Tree"; } if (treeType <= 3) seedName = Game1.objectInformation[308 + treeType].Split('/')[0]; else if (treeType == 8) seedName = Game1.objectInformation[292].Split('/')[0]; 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; } if (treeStage == 1) treeName = $"{treeName} sprout"; else if (treeStage == 2) treeName = $"{treeName} sapling"; else if (treeStage == 3 || treeStage == 4) treeName = $"{treeName} bush"; else if (treeStage >= 5) treeName = $"{treeName} tree"; return treeName; } return seedName; } #region Objects public static (string? name, CATEGORY category) getObjectAtTile(int x, int y) { (string? name, CATEGORY category) toReturn = (null, CATEGORY.Others); StardewValley.Object obj = Game1.currentLocation.getObjectAtTile(x, y); int index = obj.ParentSheetIndex; toReturn.name = obj.DisplayName; // Get object names based on index (string? name, CATEGORY category) correctNameAndCategory = getCorrectNameAndCategoryFromIndex(index); if (correctNameAndCategory.name != null) toReturn = correctNameAndCategory; if (toReturn.name.ToLower().Equals("stone")) // Fix for `Busy stone` toReturn.category = CATEGORY.Debris; if (obj is Chest) { Chest chest = (Chest)obj; toReturn = (chest.DisplayName, CATEGORY.Chests); } else if (obj is Furniture) toReturn.category = CATEGORY.Furnitures; else if (obj.Type == "Crafting" && obj.bigCraftable.Value) { foreach (string machine in trackable_machines) { if (obj.Name.ToLower().Contains(machine)) { toReturn.name = obj.DisplayName; toReturn.category = CATEGORY.Machines; } } } if (toReturn.category == CATEGORY.Others) // Fix for `Harvestable table` and `Busy nodes` { MachineState machineState = GetMachineState(obj); if (machineState == MachineState.Ready) toReturn.name = $"Harvestable {toReturn.name}"; else if (machineState == MachineState.Busy) toReturn.name = $"Busy {toReturn.name}"; } return toReturn; } private static MachineState GetMachineState(StardewValley.Object machine) { if (machine is CrabPot crabPot) if (crabPot.bait.Value is not null && crabPot.heldObject.Value is null) return MachineState.Busy; return GetMachineState(machine.readyForHarvest.Value, machine.MinutesUntilReady, machine.heldObject.Value); } private static MachineState GetMachineState(bool readyForHarvest, int minutesUntilReady, StardewValley.Object? heldObject) { if (readyForHarvest || (heldObject is not null && minutesUntilReady <= 0)) return MachineState.Ready; else if (minutesUntilReady > 0) return MachineState.Busy; else return MachineState.Waiting; } private static (string? name, CATEGORY category) getCorrectNameAndCategoryFromIndex(int index) { switch (index) { case 313: case 314: case 315: case 316: case 317: case 318: case 452: case 674: case 675: case 676: case 677: case 678: case 679: case 750: case 784: case 785: case 786: return ("Weed", CATEGORY.Debris); case 792: case 793: case 794: return ("Fertile weed", CATEGORY.Debris); case 319: case 320: case 321: return ("Ice crystal", CATEGORY.Debris); case 75: return ("Geode", 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 118: case 120: case 122: case 124: return ("Barrel", CATEGORY.MineItems); case 119: case 121: case 123: case 125: return ("Item box", CATEGORY.MineItems); } if (Game1.currentLocation is Mine or MineShaft) { switch (index) { case 76: return ("Frozen geode", CATEGORY.MineItems); case 77: return ("Magma geode", CATEGORY.MineItems); case 8: case 66: return ("Amethyst node", CATEGORY.MineItems); case 14: case 62: return ("Aquamarine node", CATEGORY.MineItems); case 843: case 844: return ("Cinder shard 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 819: return ("Omni geode 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); case 290: case 850: return ("Iron node", CATEGORY.MineItems); } } return (null, CATEGORY.Others); } #endregion public static bool isMineDownLadderAtTile(int x, int y) { try { if (Game1.currentLocation is Mine or MineShaft) { int? index = null; if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] != null) index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex; if (index == 173 || index == 174) return true; } } catch (Exception) { } return false; } public static bool isMineUpLadderAtTile(int x, int y) { try { if (Game1.currentLocation is Mine or MineShaft) { int? index = null; if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] != null) index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex; if (index == 115) return true; } } catch (Exception) { } return false; } public static bool isElevatorAtTile(int x, int y) { try { if (Game1.currentLocation is Mine or MineShaft) { int? index = null; if (Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y] != null) index = Game1.currentLocation.Map.GetLayer("Buildings").Tiles[x, y].TileIndex; if (index == 112) return true; } } catch (Exception) { } return false; } public static string? getDoorAtTile(int x, int y) { Point tilePoint = new Point(x, y); StardewValley.Network.NetPointDictionary doorList = Game1.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) { if (Game1.currentLocation is Woods) return getStumpsInWoods(x, y); for (int i = 0; i < Game1.currentLocation.resourceClumps.Count; i++) { if (Game1.currentLocation.resourceClumps[i].occupiesTile(x, y)) { int index = Game1.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"; default: return "Unknown"; } } } return null; } public static string? getStumpsInWoods(int x, int y) { if (Game1.currentLocation is not Woods) return null; if ((x == 8 || x == 9) && y == 7) { return "Old Master Cannoli"; } Netcode.NetObjectList stumps = ((Woods)Game1.currentLocation).stumps; for (int i = 0; i < stumps.Count; i++) { if (stumps[i].occupiesTile(x, y)) { return "Large Stump"; } } return null; } public static string? getParrotPerchAtTile(int x, int y) { if (Game1.currentLocation is not IslandLocation islandLocation) return null; foreach (var perch in islandLocation.parrotUpgradePerches) { if (!perch.tilePosition.Value.Equals(new Point(x, y))) continue; string toSpeak = $"Parrot required nuts {perch.requiredNuts.Value}"; if (!perch.IsAvailable()) return "Empty parrot perch"; else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Idle) return toSpeak; else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.StartBuilding) return "Parrots started building request"; else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Building) return "Parrots building request"; else if (perch.currentState.Value == StardewValley.BellsAndWhistles.ParrotUpgradePerch.UpgradeState.Complete) return $"Request Completed"; else return toSpeak; } return null; } } }