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)