/// <summary>
        /// Tag meta docs user command.
        /// </summary>
        public void CMD_Tag(string[] cmds, SocketMessage message)
        {
            List <string> secondarySearches = new List <string>();

            if (cmds.Length > 0)
            {
                cmds[0] = MetaTag.CleanTag(cmds[0]);
                int dotIndex = cmds[0].IndexOf('.');
                if (dotIndex > 0)
                {
                    string tagBase   = cmds[0].Substring(0, dotIndex);
                    string tagSuffix = cmds[0].Substring(dotIndex);
                    if (!tagBase.EndsWith("tag"))
                    {
                        secondarySearches.Add(tagBase + "tag" + tagSuffix);
                    }
                    string tagBaseLow = tagBase.ToLowerFast();
                    if (tagBaseLow == "player" || tagBaseLow == "npc" || tagBaseLow == "playertag" || tagBaseLow == "npctag")
                    {
                        secondarySearches.Add("entitytag" + tagSuffix);
                    }
                    secondarySearches.Add("elementtag" + tagSuffix);
                }
            }
            int getDistanceTo(MetaTag tag)
            {
                int dist1 = StringConversionHelper.GetLevenshteinDistance(cmds[0], tag.CleanedName);
                int dist2 = StringConversionHelper.GetLevenshteinDistance(cmds[0], tag.AfterDotCleaned);
                int dist  = Math.Min(dist1, dist2);

                foreach (string secondSearch in secondarySearches)
                {
                    int dist3 = StringConversionHelper.GetLevenshteinDistance(secondSearch, tag.CleanedName);
                    dist = Math.Min(dist, dist3);
                }
                return(dist);
            }

            string findClosestTag()
            {
                int    lowestDistance = 20;
                string lowestStr      = null;

                foreach (MetaTag tag in Program.CurrentMeta.Tags.Values)
                {
                    int currentDistance = getDistanceTo(tag);
                    if (currentDistance < lowestDistance)
                    {
                        lowestDistance = currentDistance;
                        lowestStr      = tag.CleanedName;
                    }
                }
                return(lowestStr);
            }

            AutoMetaCommand(Program.CurrentMeta.Tags, MetaDocs.META_TYPE_TAG, cmds, message, secondarySearches, altFindClosest: findClosestTag,
                            altMatchOrderer: (list) => list.OrderBy(getDistanceTo).ToList());
        }
        /// <summary>
        /// Automatically processes a meta search command.
        /// </summary>
        /// <typeparam name="T">The meta object type.</typeparam>
        /// <param name="docs">The docs mapping.</param>
        /// <param name="type">The meta type.</param>
        /// <param name="cmds">The command args.</param>
        /// <param name="message">The Discord message object.</param>
        /// <param name="secondarySearches">A list of secondary search strings if the first fails.</param>
        /// <param name="secondaryMatcher">A secondary matching function if needed.</param>
        /// <param name="altSingleOutput">An alternate method of processing the single-item-result.</param>
        /// <param name="altFindClosest">Alternate method to find the closest result.</param>
        /// <returns>How close of an answer was gotten (0 = perfect, -1 = no match needed, 1000 = none).</returns>
        public int AutoMetaCommand <T>(Dictionary <string, T> docs, MetaType type, string[] cmds, SocketMessage message,
                                       List <string> secondarySearches = null, Func <T, bool> secondaryMatcher            = null, Action <T> altSingleOutput = null,
                                       Func <string> altFindClosest    = null, Func <List <T>, List <T> > altMatchOrderer = null) where T : MetaObject
        {
            if (CheckMetaDenied(message))
            {
                return(-1);
            }
            if (cmds.Length == 0)
            {
                SendErrorMessageReply(message, $"Need input for '{type.Name}' command",
                                      $"Please specify a {type.Name} to search, like `!{type.Name} Some{type.Name}Here`. Or, use `!{type.Name} all` to view all documented {type.Name.ToLowerFast()}s.");
                return(-1);
            }
            string search = cmds[0].ToLowerFast();

            if (search == "all")
            {
                SendGenericPositiveMessageReply(message, $"All {type.Name}s", $"Find all {type.Name}s at {Constants.DOCS_URL_BASE}{type.WebPath}/");
                return(-1);
            }
            if (altSingleOutput == null)
            {
                altSingleOutput = (singleObj) => SendReply(message, singleObj.GetEmbed().Build());
            }
            if (altFindClosest == null)
            {
                altFindClosest = () =>
                {
                    string initialPossibleResult = StringConversionHelper.FindClosestString(docs.Keys, search, out int lowestDistance, 20);
                    string lowestStr             = null;
                    foreach (string possibleName in docs.Values.Where(o => o.HasMultipleNames).SelectMany(o => o.MultiNames))
                    {
                        int currentDistance = StringConversionHelper.GetLevenshteinDistance(search, possibleName);
                        if (currentDistance < lowestDistance)
                        {
                            lowestDistance = currentDistance;
                            lowestStr      = possibleName;
                        }
                    }
                    return(lowestStr);
                };
            }
            if (altMatchOrderer == null)
            {
                altMatchOrderer = (list) => list.OrderBy((mat) => StringConversionHelper.GetLevenshteinDistance(search, mat.CleanName)).ToList();
            }
            if (docs.TryGetValue(search, out T obj))
            {
                string multiNameData = string.Join("', '", obj.MultiNames);
                Console.WriteLine($"Meta-Command for '{type.Name}' found perfect match for search '{search}': '{obj.CleanName}', multi={obj.HasMultipleNames}='{multiNameData}'");
                altSingleOutput(obj);
                return(0);
            }
            if (secondarySearches != null)
            {
                secondarySearches = secondarySearches.Select(s => s.ToLowerFast()).ToList();
                foreach (string secondSearch in secondarySearches)
                {
                    if (docs.TryGetValue(secondSearch, out obj))
                    {
                        Console.WriteLine($"Meta-Command for '{type.Name}' found perfect match for secondary search '{secondSearch}': '{obj.CleanName}', multi={obj.HasMultipleNames}");
                        altSingleOutput(obj);
                        return(0);
                    }
                }
            }
            List <T> matched       = new List <T>();
            List <T> strongMatched = new List <T>();

            int tryProcesSingleMatch(T objVal, string objName, int min)
            {
                if (objName.Contains(search))
                {
                    Console.WriteLine($"Meta-Command for '{type.Name}' found a strong match (main contains) for search '{search}': '{objName}'");
                    strongMatched.Add(objVal);
                    return(2);
                }
                if (secondarySearches != null)
                {
                    foreach (string secondSearch in secondarySearches)
                    {
                        if (objName.Contains(secondSearch))
                        {
                            Console.WriteLine($"Meta-Command for '{type.Name}' found a strong match (secondary contains) for search '{secondSearch}': '{objName}'");
                            strongMatched.Add(objVal);
                            return(2);
                        }
                    }
                }
                if (min < 1 && secondaryMatcher != null && secondaryMatcher(objVal))
                {
                    Console.WriteLine($"Meta-Command for '{type.Name}' found a weak match (secondaryMatcher) for search '{search}': '{objName}'");
                    matched.Add(objVal);
                    return(1);
                }
                return(min);
            }

            foreach (KeyValuePair <string, T> objPair in docs)
            {
                if (objPair.Value.HasMultipleNames)
                {
                    int matchQuality = 0;
                    foreach (string name in objPair.Value.MultiNames)
                    {
                        matchQuality = tryProcesSingleMatch(objPair.Value, name, matchQuality);
                        if (matchQuality == 2)
                        {
                            break;
                        }
                    }
                }
                else
                {
                    tryProcesSingleMatch(objPair.Value, objPair.Key, 0);
                }
            }
            if (strongMatched.Count > 0)
            {
                matched = strongMatched;
            }
            if (matched.Count == 0)
            {
                string closeName = altFindClosest();
                SendErrorMessageReply(message, $"Cannot Find Searched {type.Name}", $"Unknown {type.Name.ToLowerFast()}." + (closeName == null ? "" : $" Did you mean `{closeName}`?"));
                return(closeName == null ? 1000 : StringConversionHelper.GetLevenshteinDistance(search, closeName));
            }
            else if (matched.Count > 1)
            {
                matched = altMatchOrderer(matched);
                string suffix = ".";
                if (matched.Count > 20)
                {
                    matched = matched.GetRange(0, 20);
                    suffix  = ", ...";
                }
                string listText = string.Join("`, `", matched.Select((m) => m.Name));
                SendErrorMessageReply(message, $"Cannot Specify Searched {type.Name}", $"Multiple possible {type.Name.ToLowerFast()}s: `{listText}`{suffix}");
                return(StringConversionHelper.GetLevenshteinDistance(search, matched[0].CleanName));
            }
            else // Count == 1
            {
                obj = matched[0];
                Console.WriteLine($"Meta-Command for '{type.Name}' found imperfect single match for search '{search}': '{obj.CleanName}', multi={obj.HasMultipleNames}");
                altSingleOutput(obj);
                return(0);
            }
        }
        /// <summary>
        /// Meta docs total search command.
        /// </summary>
        public void CMD_Search(string[] cmds, SocketMessage message)
        {
            if (CheckMetaDenied(message))
            {
                return;
            }
            if (cmds.Length == 0)
            {
                SendErrorMessageReply(message, "Need input for Search command", "Please specify some text to search, like `!search someobjecthere`.");
                return;
            }
            for (int i = 0; i < cmds.Length; i++)
            {
                cmds[i] = cmds[i].ToLowerFast();
            }
            string            fullSearch         = string.Join(' ', cmds);
            List <MetaObject> strongMatch        = new List <MetaObject>();
            List <MetaObject> partialStrongMatch = new List <MetaObject>();
            List <MetaObject> weakMatch          = new List <MetaObject>();
            List <MetaObject> partialWeakMatch   = new List <MetaObject>();

            foreach (MetaObject obj in Program.CurrentMeta.AllMetaObjects())
            {
                if (obj.CleanName.Contains(fullSearch))
                {
                    strongMatch.Add(obj);
                    continue;
                }
                foreach (string word in cmds)
                {
                    if (obj.CleanName.Contains(word))
                    {
                        partialStrongMatch.Add(obj);
                        goto fullContinue;
                    }
                }
                if (obj.Searchable.Contains(fullSearch))
                {
                    weakMatch.Add(obj);
                    continue;
                }
                if (fullSearch.Contains(obj.CleanName))
                {
                    partialWeakMatch.Add(obj);
                    continue;
                }
                foreach (string word in cmds)
                {
                    if (obj.Searchable.Contains(word))
                    {
                        partialWeakMatch.Add(obj);
                        goto fullContinue;
                    }
                }
fullContinue:
                continue;
            }
            if (strongMatch.IsEmpty() && partialStrongMatch.IsEmpty() && weakMatch.IsEmpty() && partialWeakMatch.IsEmpty())
            {
                SendErrorMessageReply(message, "Search Command Has No Results", "Input search text could not be found.");
                return;
            }
            string suffix = ".";

            void listWrangle(string typeShort, string typeLong, List <MetaObject> objs)
            {
                objs   = objs.OrderBy((obj) => StringConversionHelper.GetLevenshteinDistance(fullSearch, obj.CleanName)).ToList();
                suffix = ".";
                if (objs.Count > 20)
                {
                    objs   = objs.GetRange(0, 20);
                    suffix = ", ...";
                }
                string listText = string.Join("`, `", objs.Select((obj) => $"!{obj.Type.Name} {obj.CleanName}"));

                SendGenericPositiveMessageReply(message, $"{typeShort} Search Results", $"{typeShort} ({typeLong}) search results: `{listText}`{suffix}");
            }

            if (strongMatch.Any())
            {
                listWrangle("Best", "very close", strongMatch);
            }
            if (partialStrongMatch.Any())
            {
                listWrangle("Probable", "close but imperfect", partialStrongMatch);
                if (strongMatch.Any())
                {
                    return;
                }
            }
            if (weakMatch.Any())
            {
                listWrangle("Possible", "might be related", weakMatch);
                if (strongMatch.Any() || partialStrongMatch.Any())
                {
                    return;
                }
            }
            if (partialWeakMatch.Any())
            {
                listWrangle("Weak", "if nothing else, some chance of being related", partialWeakMatch);
            }
        }