harbingerofme / DebugToolkit

Debugging commands for Risk of Rain 2. Previously known as RoR2Cheats.
https://thunderstore.io/package/IHarbHD/DebugToolkit/
BSD 3-Clause "New" or "Revised" License
14 stars 8 forks source link

Code Quality: Refactor StringFinder partial matching methods #162

Closed SChinchi closed 9 months ago

SChinchi commented 10 months ago

The majority of partial matching methods have two versions; one for returning the first match and the other to return a list of all matches, e.g., GetItemFromPartial and GetItemsFromPartial. The code in both cases is extremely similar, where one returns early and the other collects all results. These can be combined into one method of returning a lazy iterator, where one can choose to either iterate all matches or get the first one only.

There is a strong argument to be made about matching an index to the exact match only so as the avoid the scenario of "CrippleWardOnLevel" and "WardOnLevel" both existing in the item collection and due to the alphabetical order the latter would never be matched (the language text can make a distinction here). This is something that also happens with GetMasterName where "VoidRaidCrab" is after "MiniVoidRaidCrabPhaseX" and can only be selected with an index or exact string match.

Put together, the result would look like

public IEnumerable<ItemIndex> GetItemsFromPartial(string name) {
    if (int.TryParse(name, out var index) && ItemCatalog.IsIndexValid((ItemIndex)index)) {
        yield return (ItemIndex)index;
        yield break;
    }
    name = name.ToUpperInvariant();
    foreach (var item in ItemCatalog.allItemDefs) {
        if (item.name.ToUpperInvariant().Contains(name) /* and langInvariant checks */) {
            yield return item.itemIndex;
        }
    }
}

public ItemIndex GetItemFromPartial(string name) {
    return GetItemsFromPartial(name).DefaultIfEmpty(ItemIndex.None).First();
}

And the list command

private static CCListItem(ConCommandArgs args) {
    var sb = new StringBuilder();
    var indices = StringFinder.Instance.GetItemsFromPartial(args.Count > 0 ? args[0] : "");
    foreach (var index in indices) {
        sb.AppendLine(...);
    }
    var s = sb.Count > 0 ? sb.ToString() : "No match";
    Log(...);
}

This would also bring in line the order of results from both the partial matching methods and the list command. As mentioned above, list_ai voidraidcrab currently lists the exact result as last after all the mini versions, but it is the selected match for spawn_ai voidraidcrab. This can create confusion to users who may expect to use the former to predict what would be selected for the latter if there are more than one partial matches.

Match ordering metric

We can instead sort the ordering based on how close a match the partial is. This is similar in spirit to #61 and how the code already prioritises exact matches. For example

internal static IEnumerable<ItemIndex> GetItemsFromPartial(string name) {
    // keep the index part the same
    name = name.ToUpperInvariant();
    var matches = new List<KeyValuePair<ItemIndex, int>>();
    foreach (var item in ItemCatalog.allItemDefs) {
        if (item.name.ToUpperInvariant().Contains(name)) {
            // A value of 0 is a perfect match, a negative is how many chars away it is from that
            matches.Add(new KeyValuePair<ItemIndex, int>(item.itemIndex, name.Length - item.name.Length));
        }
    }
    foreach (var kvp in matches.OrderByDescending(i => i.Value)) {
        yield return kvp.Key;
    }
}