using System; using System.Collections.Generic; using System.Globalization; using System.Linq; namespace NHSE.Core { /// /// Logic for retrieving details based off input strings. /// public static class ItemParser { /// /// Invert the recipe dictionary so we can look up recipe IDs from an input item ID. /// public static readonly IReadOnlyDictionary InvertedRecipeDictionary = RecipeList.Recipes.ToDictionary(z => z.Value, z => z.Key); // Users can put spaces between item codes, or newlines. Recognize both! private static readonly string[] SplittersHex = {" ", "\n", "\r\n"}; private static readonly string[] SplittersName = {",", "\n", "\r\n"}; /// /// Gets a list of items from the requested hex string(s). /// /// /// If the first input is a language code (2 characters), the logic will try to parse item names for that language instead of item IDs. /// /// 8 byte hex item values (u64 format) /// Options for packaging items public static IReadOnlyCollection GetItemsFromUserInput(string requestHex, IConfigItem cfg) { try { // having a language 2char code will cause an exception in parsing; this is fine and is handled by our catch statement. var split = requestHex.Split(SplittersHex, StringSplitOptions.RemoveEmptyEntries); return GetItemsHexCode(split, cfg); } #pragma warning disable CA1031 // Do not catch general exception types catch #pragma warning restore CA1031 // Do not catch general exception types { var split = requestHex.Split(SplittersName, StringSplitOptions.RemoveEmptyEntries); return GetItemsLanguage(split, cfg, GameLanguage.DefaultLanguage); } } /// /// Gets a list of DIY item cards from the requested list of DIY IDs. /// /// /// If the first input is a language code (2 characters), the logic will try to parse item names for that language instead of DIY IDs. /// /// 8 byte hex item values (u64 format) public static IReadOnlyCollection GetDIYsFromUserInput(string requestHex) { try { // having a language 2char code will cause an exception in parsing; this is fine and is handled by our catch statement. var split = requestHex.Split(SplittersHex, StringSplitOptions.RemoveEmptyEntries); return GetDIYItemsHexCode(split); } #pragma warning disable CA1031 // Do not catch general exception types catch #pragma warning restore CA1031 // Do not catch general exception types { var split = requestHex.Split(SplittersName, StringSplitOptions.RemoveEmptyEntries); return GetDIYItemsLanguage(split); } } /// /// Gets a list of items from the requested list of DIY hex code strings. /// /// /// If a hex code parse fails or a recipe ID does not exist, exceptions will be thrown. /// /// List of recipe IDs as u16 hex public static IReadOnlyCollection GetDIYItemsHexCode(IReadOnlyList split) { var result = new Item[split.Count]; for (int i = 0; i < result.Length; i++) { var text = split[i].Trim(); bool parse = ulong.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out var value); if (!parse) throw new Exception($"Item value out of expected range ({text})."); if (!RecipeList.Recipes.TryGetValue((ushort)value, out _)) throw new Exception($"DIY recipe appears to be invalid ({text})."); result[i] = new Item(Item.DIYRecipe) { Count = (ushort)value }; } return result; } /// /// Gets a list of DIY item cards from the requested list of item name strings. /// /// /// If a item name parse fails or a recipe ID does not exist, exceptions will be thrown. /// /// List of item names /// Language code to parse with. If the first entry in is a language code, it will be used instead of . public static IReadOnlyCollection GetDIYItemsLanguage(IReadOnlyList split, string lang = GameLanguage.DefaultLanguage) { if (split.Count > 1 && split[0].Length < 3) { var langIndex = GameLanguage.GetLanguageIndex(split[0]); lang = GameLanguage.Language2Char(langIndex); split = split.Skip(1).ToArray(); } var result = new Item[split.Count]; for (int i = 0; i < result.Length; i++) { var text = split[i].Trim(); var item = GetItem(text, lang); if (!InvertedRecipeDictionary.TryGetValue(item.ItemId, out var diy)) throw new Exception($"DIY recipe appears to be invalid ({text})."); result[i] = new Item(Item.DIYRecipe) { Count = diy }; } return result; } /// /// Gets a list of items from the requested list of item name strings. /// /// /// If a item name parse fails or the item ID does not exist as a known item, exceptions will be thrown. /// /// List of item names /// Item packaging options /// Language code to parse with. If the first entry in is a language code, it will be used instead of . public static IReadOnlyCollection GetItemsLanguage(IReadOnlyList split, IConfigItem config, string lang = GameLanguage.DefaultLanguage) { if (split.Count > 1 && split[0].Length < 3) { var langIndex = GameLanguage.GetLanguageIndex(split[0]); lang = GameLanguage.Language2Char(langIndex); split = split.Skip(1).ToArray(); } var strings = GameInfo.Strings.itemlistdisplay; var result = new Item[split.Count]; for (int i = 0; i < result.Length; i++) { var text = split[i].Trim(); var item = CreateItem(text, i, config, lang); if (item.ItemId >= strings.Length) throw new Exception($"Item requested is out of expected range ({item.ItemId:X4} > {strings.Length:X4})."); if (string.IsNullOrWhiteSpace(strings[item.ItemId])) throw new Exception($"Item requested does not have a valid name ({item.ItemId:X4})."); result[i] = item; } return result; } /// /// Gets a list of items from the requested list of item hex code strings. /// /// /// If a hex code parse fails or a recipe ID does not exist, exceptions will be thrown. /// /// List of recipe IDs as u16 hex /// Item packaging options public static IReadOnlyCollection GetItemsHexCode(IReadOnlyList split, IConfigItem config) { var strings = GameInfo.Strings.itemlistdisplay; var result = new Item[split.Count]; for (int i = 0; i < result.Length; i++) { var text = split[i].Trim(); var convert = GetBytesFromString(text); var item = CreateItem(convert, i, config); if (item.ItemId >= strings.Length) throw new Exception($"Item requested is out of expected range ({item.ItemId:X4} > {strings.Length:X4})."); if (string.IsNullOrWhiteSpace(strings[item.ItemId])) throw new Exception($"Item requested does not have a valid name ({item.ItemId:X4})."); result[i] = item; } return result; } private static byte[] GetBytesFromString(string text) { if (!ulong.TryParse(text, NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out var value)) return Item.NONE.ToBytes(); return BitConverter.GetBytes(value); } private static Item CreateItem(string name, int requestIndex, IConfigItem config, string lang = "en") { var item = GetItem(name, lang); if (item.IsNone) throw new Exception($"Failed to convert item (index {requestIndex}: {name}) for Language {lang}."); if (!ItemInfo.IsSaneItemForDrop(item)) throw new Exception($"Unsupported item: (index {requestIndex}: {name})."); if (config.WrapAllItems && item.ShouldWrapItem()) item.SetWrapping(ItemWrapping.WrappingPaper, config.WrappingPaper, true); return item; } private static Item CreateItem(byte[] convert, int requestIndex, IConfigItem config) { Item item; try { item = convert.ToClass(); } catch (Exception ex) { throw new Exception($"Failed to convert item (index {requestIndex}: {ex.Message})."); } if (!ItemInfo.IsSaneItemForDrop(item) || convert.Length != Item.SIZE) throw new Exception($"Unsupported item: (index {requestIndex})."); if (config.WrapAllItems && item.ShouldWrapItem()) item.SetWrapping(ItemWrapping.WrappingPaper, config.WrappingPaper, true); return item; } private static readonly CompareInfo Comparer = CultureInfo.InvariantCulture.CompareInfo; private const CompareOptions opt = CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreSymbols | CompareOptions.IgnoreWidth; /// /// Gets the first item name-value that contains the (case insensitive). /// /// Requested Item /// Game strings language to fetch with /// Returns if no match found. public static Item GetItem(string itemName, string lang = "en") { var strings = GameInfo.GetStrings(lang).ItemDataSource; return GetItem(itemName, strings); } /// /// Gets the first item name-value that contains the (case insensitive). /// /// Requested Item /// Game strings /// Returns if no match found. public static Item GetItem(string itemName, IEnumerable strings) { if (TryGetItem(itemName, strings, out var id)) return new Item(id); return Item.NO_ITEM; } /// /// Gets the first item name-value that contains the (case insensitive). /// /// Requested Item /// List of item name-values /// Item ID, if found. Otherwise, 0 /// True if found, false if none. public static bool TryGetItem(string itemName, IEnumerable strings, out ushort value) { foreach (var item in strings) { var result = Comparer.Compare(item.Text, 0, itemName, 0, opt); if (result != 0) continue; value = (ushort)item.Value; return true; } value = Item.NONE; return false; } /// /// Gets an enumerable list of item key-value pairs that contain (case insensitive) the requested . /// /// Item name /// Item names (and their Item ID values) public static IEnumerable GetItemsMatching(string itemName, IReadOnlyList strings) { foreach (var item in strings) { var result = Comparer.IndexOf(item.Text, itemName, opt); if (result < 0) continue; yield return item; } } /// /// Gets an enumerable list of item key-value pairs that contain (case insensitive) the requested . /// /// /// Orders the items based on the closest match (). /// /// Item name /// Item names (and their Item ID values) public static IEnumerable GetItemsMatchingOrdered(string itemName, IReadOnlyList strings) { var matches = GetItemsMatching(itemName, strings); return GetItemsClosestOrdered(itemName, matches); } /// /// Gets an enumerable list of item key-value pairs ordered by the closest for the requested . /// /// Item name /// Item names (and their Item ID values) public static IEnumerable GetItemsClosestOrdered(string itemName, IEnumerable strings) { return strings.OrderBy(z => LevenshteinDistance.Compute(z.Text, itemName)); } /// /// Gets the Item Name and raw 8-byte value as a string. /// /// Item value public static string GetItemText(Item item) { var value = BitConverter.ToUInt64(item.ToBytesClass(), 0); var name = GameInfo.Strings.GetItemName(item.ItemId); return $"{name}: {value:X16}"; } /// /// Gets the u16 item ID from the input hex code. /// /// Hex code for the item (preferably 4 digits) public static ushort GetID(string text) { if (!ulong.TryParse(text.Trim(), NumberStyles.AllowHexSpecifier, CultureInfo.CurrentCulture, out var value)) return Item.NONE; return (ushort)value; } } }