Procurement-PoE / Procurement

Path Of Exile Character & Stash Management Tool
Artistic License 2.0
330 stars 133 forks source link

Support recipe settings with blacklists/whitelists #873

Open thailyn opened 6 years ago

thailyn commented 6 years ago

I've been accumulating a bunch of recipe customizations that have improved my experience with using Procurement, but they are not settings that everyone should or would want to use. Because of that, I have been planning out adding a section in the Settings.xml file for recipe settings. This can have basic configuration knobs, like whether recipes are enabled or disabled, as well as setting generic values, such as minimumMatchPercentage.

But the biggest feature would be to support whitelists and blacklists for what items are considered for each recipe. I haven't nailed down yet the exact syntax we should use, but I have a general idea and I'd welcome any feedback. I've included an example below for how I currently envision it. The values themselves are mostly based off of the customizations I've done for myself that I mentioned earlier.

We can use reflection to match RecipeSettings to each Recipe subclass, and we can add to the Recipe class two lists of IFilter, one for holding whitelist filters and blacklist filters. The white/blacklist entries themselves can reuse the filtering mechanics to find items in the stash and add items to the trading shop. I'll need to generalize them slightly, I think, since I want to add more generic parameters. For example, we should have a filter that is based on an item's quality. But, it shouldn't just be a single binary test for a single filter, because then we would have an inordinate number of filters, or the filters would be too unwieldly to be used. I'd like to have a generic "comparison" we can specify, we we indicate 1) the property being used in the comparison, 2) the value to compare against, and 3) the operator used to compare the two values.

I still need to think about the best way to specify the operator, since there can be a lot. We have a bunch of mathematical comparisons, but also various string operations, like StartsWith or Contains or whatever regex we might want. The comparison operator might be ternary or an even higher order, so perhaps the Comparison element should have the value(s) specified in a repeated list of Parameter child elements, where the Parameter elements can (should) have something like an index attribute to indicate how to order the parameters.

Obviously, we should ultimately also be able to configure the recipes via the GUI, but 1) we'll need to save the settings to the Settings.xml file anyway, and 2) I've already stated I'm not too fond of working with GUIs, and adding support won't be in the first version, at least.

  <RecipesSettings>
    <RecipeSettings alias="It's a Same Base Type Recipe" type="SameBaseTypeRecipe"
                    inheritParentConditions="True">
      <Properties>
        <!-- Restating the default value. -->
        <Property name="minimumMatchPercentage" value="60" />
      </Properties>
      <Whitelist>
        <!-- An empty whitelist/blacklist section is ignored. -->
      </Whitelist>
      <Blacklist>
        <!-- Each filter in a Whitelist/Blacklist section is implicitly OR-ed. -->
        <Filter alias="Exclude belts!" type="BeltFilter" />
        <!-- The 'alias' attribute is optional and is meant to differentiate elements of the same
             type for the user. -->
        <Filter type="AmuletFilter" />
        <Filter type="RingFilter" />
        <Filter type="TabFilter">
          <!-- Don't look at items in the "Supes" tab. -->
          <Comparison name="name" operator="equals" value="Supes" />
        </Filter>
      </Blacklist>
    </RecipeSettings>
    <!-- RecipeSettings and Filter types are not case sensitive. -->
    <RecipeSettings type="raresetRecipe" inheritParentConditions="True">
      <Properties>
        <!-- Use a lower (60) minimumMatchPercentage than normal (80), so sets are shown
             earlier. -->
        <Property name="minimumMatchPercentage" value="60" />
      </Properties>
      <Whitelist>
        <Filter type="QualityFilter">
          <Comparison name="quality" operator="lessThan" value="20" />
        </Filter>
      </Whitelist>
      <Blacklist>
        <Filter type="ElderItemFilter" />
        <Filter type="ShaperItemFilter" />
      </Blacklist>
    </RecipeSettings>
    <RecipeSettings type="MinimumQualityRecipe">
      <Whitelist>
        <Filter type="QualityFilter">
          <Comparison name="quality" operator="equals" value="20" invert="True" />
        </Filter>
      </Whitelist>
      <Blacklist>
        <Filter type="TabFilter">
          <!-- Ignore items in tabs whose name starts with the string "+20%". -->
          <Comparison name="name" operator="startsWith" value="+20%" />
        </Filter>
      </Blacklist>
    </RecipeSettings>
    <!-- Allow using 20% Quality items in this version of the recipe. -->
    <RecipeSettings type="GlassblowersBaubleRecipe" inheritParentConditions="False" />
    <!-- Disable a recipe so it does not show up in the UI. -->
    <RecipeSettings type="CartographersChiselRecipe" enabled="False" />
  </RecipesSettings>
aydjay commented 6 years ago

I would favour serialisation of settings in JSON using Json.Net.

https://www.newtonsoft.com/json

Just make a recipe settings class and save it out to file. Much easier imo. Once that is done I can spin up a VM and a View to the "Model" to give you something to visualise it if you wish.

I just need to finish off the fragment stash. https://github.com/aydjay/procurement/tree/fragmentStash

thailyn commented 6 years ago

Thanks for the feedback! I agree using JSON to store the settings would be more efficient than XML. But, if we're considering an alternate storage implementation, I'll also suggest protocol buffers (and not just because I'm biased).

Beyond how the recipe settings are stored in the settings file, I'm also concerned with how these settings are used by the filters. I might be trying to overengineering the situation however. But I'd like to avoid having, say, a separate filter for each kind of operation we can do (e.g., a StartsWithFilter, a ContainsFilter, a GreaterThanFilter, a NotEqualToFilter, and so on. Each of the possible operations will need to be specified somewhere, though. It just seems silly that they might end up being simple wrappers around a class function call of the same name. We'd also want some type checking based on the filter. That is, we wouldn't want to allow something like using numeric comparisons against a TabFilter/TabNameFilter. That would probably require templating the filters or having them inherit from some interface or class (IStringComparisonFilter or such).

And right now these questions about how the filters will work can impact how the settings are saved. Is an operator a property of a filter? Or does an operator take a filter as one of its arguments? (Though that is treating the filters more like a glorified property getter, which doesn't seem right.) The layout of the settings would probably mirror this relationship.

(In the back of my mind I'm also seeing if this can be used to allow more complicated trading filters. The syntax would be a bit different, but the concept of operators would still apply.)

I need to take a little time and go back and worth though some of these examples more fully, and see if I can figure out some logical structure that works in the general case.

aydjay commented 6 years ago

You know what they say. KISS 👍