From 66dd506ba7a8f349a58935d6c855d0792e306d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Gim=C3=A9nez?= Date: Sun, 12 Mar 2023 19:32:50 +0900 Subject: [PATCH] basic item filter wip --- pkg/data/item/quality.go | 16 +++-- pkg/data/item/{class.go => type.go} | 18 ++---- pkg/data/items.go | 9 ++- pkg/itemfilter/chain.go | 45 +++++++++++++++ pkg/itemfilter/chain_test.go | 64 ++++++++++++++++++++ pkg/itemfilter/itemfilter.go | 90 +++++++++++++++++++++++++++++ pkg/itemfilter/mapping.go | 17 ++++++ pkg/nip/file_reader.go | 2 +- pkg/nip/nip_parser.go | 2 +- pkg/nip/nip_parser_test.go | 5 +- pkg/nip/properties.go | 28 +++++++++ pkg/nip/rule.go | 3 +- 12 files changed, 273 insertions(+), 26 deletions(-) rename pkg/data/item/{class.go => type.go} (96%) create mode 100644 pkg/itemfilter/chain.go create mode 100644 pkg/itemfilter/chain_test.go create mode 100644 pkg/itemfilter/itemfilter.go create mode 100644 pkg/itemfilter/mapping.go create mode 100644 pkg/nip/properties.go diff --git a/pkg/data/item/quality.go b/pkg/data/item/quality.go index 30b68b5..7e3fa68 100644 --- a/pkg/data/item/quality.go +++ b/pkg/data/item/quality.go @@ -3,16 +3,20 @@ package item type Quality int const ( - QualityNormal Quality = 0x02 - QualitySuperior Quality = 0x03 - QualityMagic Quality = 0x04 - QualitySet Quality = 0x05 - QualityRare Quality = 0x06 - QualityUnique Quality = 0x07 + QualityLowQuality Quality = 0x01 + QualityNormal Quality = 0x02 + QualitySuperior Quality = 0x03 + QualityMagic Quality = 0x04 + QualitySet Quality = 0x05 + QualityRare Quality = 0x06 + QualityUnique Quality = 0x07 + QualityCrafted Quality = 0x08 ) func (q Quality) ToString() string { switch q { + case QualityLowQuality: + return "LowQuality" case QualityNormal: return "Normal" case QualitySuperior: diff --git a/pkg/data/item/class.go b/pkg/data/item/type.go similarity index 96% rename from pkg/data/item/class.go rename to pkg/data/item/type.go index 9aa0eb1..067e916 100644 --- a/pkg/data/item/class.go +++ b/pkg/data/item/type.go @@ -2,7 +2,7 @@ package item import "strings" -var classMapping = map[string][]string{ +var typeMapping = map[string][]string{ "Axes": {"HandAxe", "Axe", "DoubleAxe", "MilitaryPick", "WarAxe", "LargeAxe", "BroadAxe", "BattleAxe", "GreatAxe", "GiantAxe", "Hatchet", "Cleaver", "TwinAxe", "Crowbill", "Naga", "MilitaryAxe", "BeardedAxe", "Tabar", "GothicAxe", "AncientAxe", "Tomahawk", "SmallCrescent", "EttinAxe", "WarSpike", "BerserkerAxe", "FeralAxe", "SilverEdgedAxe", "Decapitator", "ChampionAxe", "GloriousAxe"}, "Wands": {"Wand", "YewWand", "BoneWand", "GrimWand", "BurntWand", "PetrifiedWand", "TombWand", "GraveWand", "PolishedWand", "GhostWand", "LichWand", "UnearthedWand"}, "Clubs": {"Club", "SpikedClub", "Cudgel", "BarbedClub", "Truncheon", "TyrantClub"}, @@ -37,21 +37,11 @@ var classMapping = map[string][]string{ "NecromancerShields": {"PreservedHead", "ZombieHead", "UnravellerHead", "GargoyleHead", "DemonHeadShield", "MummifiedTrophy", "FetishTrophy", "SextonTrophy", "CantorTrophy", "HierophantTrophy", "MinionSkull", "HellspawnSkull", "OverseerSkull", "SuccubusSkull", "BloodlordSkull"}, } -func IsClass(itemClass string) bool { - for class := range classMapping { - if strings.EqualFold(itemClass, class) { - return true - } - } - - return false -} - -func ClassForItemName(itemName string) (string, bool) { - for class, itemNames := range classMapping { +func TypeForItemName(itemName string) (string, bool) { + for t, itemNames := range typeMapping { for _, name := range itemNames { if strings.EqualFold(itemName, name) { - return class, true + return t, true } } } diff --git a/pkg/data/items.go b/pkg/data/items.go index 0f4077b..29137b7 100644 --- a/pkg/data/items.go +++ b/pkg/data/items.go @@ -1,9 +1,10 @@ package data import ( + "strings" + "github.com/hectorgimenez/d2go/pkg/data/item" "github.com/hectorgimenez/d2go/pkg/data/stat" - "strings" ) type Items struct { @@ -29,6 +30,12 @@ type Item struct { IsVendor bool } +func (i Item) Type() string { + t, _ := item.TypeForItemName(string(i.Name)) + + return t +} + func (i Item) IsPotion() bool { return i.IsHealingPotion() || i.IsManaPotion() || i.IsRejuvPotion() } diff --git a/pkg/itemfilter/chain.go b/pkg/itemfilter/chain.go new file mode 100644 index 0000000..e0d7f8c --- /dev/null +++ b/pkg/itemfilter/chain.go @@ -0,0 +1,45 @@ +package itemfilter + +import "github.com/hectorgimenez/d2go/pkg/nip" + +type evaluationChain struct { + links []link +} + +type link struct { + Result bool + Operand nip.Operand +} + +func (ch *evaluationChain) Add(result bool, operand nip.Operand) { + ch.links = append(ch.links, link{ + Result: result, + Operand: operand, + }) +} + +func (ch *evaluationChain) Evaluate() bool { + if len(ch.links) == 1 { + return ch.links[0].Result + } + + result := ch.links[0].Result + operand := ch.links[0].Operand + for i, eval := range ch.links { + if i == 0 { + continue + } + + if i < len(ch.links) { + switch operand { + case nip.OperandAnd: + result = result && eval.Result + case nip.OperandOr: + result = result || eval.Result + } + } + operand = eval.Operand + } + + return result +} diff --git a/pkg/itemfilter/chain_test.go b/pkg/itemfilter/chain_test.go new file mode 100644 index 0000000..791fac0 --- /dev/null +++ b/pkg/itemfilter/chain_test.go @@ -0,0 +1,64 @@ +package itemfilter + +import ( + "testing" + + "github.com/hectorgimenez/d2go/pkg/nip" +) + +func Test_evaluationChain_Evaluate(t *testing.T) { + tests := []struct { + name string + links []link + want bool + }{ + { + name: "Given one single link with true value should return true", + links: []link{ + {true, nip.OperandNone}, + }, + want: true, + }, + { + name: "Given one single link with false value should return false", + links: []link{ + {false, nip.OperandNone}, + }, + want: false, + }, + { + name: "Given different links using OR and one valid value should be true", + links: []link{ + {false, nip.OperandOr}, + {true, nip.OperandNone}, + }, + want: true, + }, + { + name: "Given different links using OR and no valid value should be false", + links: []link{ + {false, nip.OperandOr}, + {false, nip.OperandNone}, + }, + want: false, + }, + { + name: "Given different links using AND and one valid value should be false", + links: []link{ + {true, nip.OperandAnd}, + {false, nip.OperandNone}, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ch := &evaluationChain{ + links: tt.links, + } + if got := ch.Evaluate(); got != tt.want { + t.Errorf("Evaluate() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/itemfilter/itemfilter.go b/pkg/itemfilter/itemfilter.go new file mode 100644 index 0000000..65fba7a --- /dev/null +++ b/pkg/itemfilter/itemfilter.go @@ -0,0 +1,90 @@ +package itemfilter + +import ( + "strings" + + "github.com/hectorgimenez/d2go/pkg/data" + "github.com/hectorgimenez/d2go/pkg/nip" +) + +func Evaluate(i data.Item, rules []nip.Rule) bool { + for _, r := range rules { + if !checkProperties(i, r.Properties) { + // Properties not matching, skipping + continue + } + + // We can not check stats, item is not identified, but properties matching + if !i.Identified { + return true + } + + } + + return false +} + +func checkProperties(i data.Item, properties []nip.Group) bool { + groupChain := evaluationChain{} + for _, propGroup := range properties { + propChain := evaluationChain{} + for _, prop := range propGroup.Comparable { + propChain.Add(checkProperty(i, prop), prop.Operand) + } + groupChain.Add(propChain.Evaluate(), propGroup.Operand) + } + + return groupChain.Evaluate() +} + +func checkProperty(i data.Item, prop nip.Comparable) bool { + switch prop.Keyword { + case nip.PropertyType: + return strings.EqualFold(i.Type(), prop.ValueString) + case nip.PropertyName: + return strings.EqualFold(string(i.Name), prop.ValueString) + case nip.PropertyClass: + // TODO: Implement + case nip.PropertyQuality: + quality, found := qualities[prop.ValueString] + if !found { + return false + } + + return compare(int(i.Quality), int(quality), prop.Comparison) + case nip.PropertyFlag: + if prop.Comparison == nip.OperandEqual && !i.Ethereal { + return false + } + if prop.Comparison == nip.OperandNotEqualTo && i.Ethereal { + return false + } + case nip.PropertyLevel: + // TODO: Implement + case nip.PropertyPrefix: + // TODO: Implement + case nip.PropertySuffix: + // TODO: Implement + } + + return true +} + +func compare(val1, val2 int, operand nip.Operand) bool { + switch operand { + case nip.OperandEqual: + return val1 == val2 + case nip.OperandGreaterThan: + return val1 > val2 + case nip.OperandGreaterOrEqualTo: + return val1 >= val2 + case nip.OperandLessThan: + return val1 < val2 + case nip.OperandLessThanOrEqualTo: + return val1 <= val2 + case nip.OperandNotEqualTo: + return val1 != val2 + } + + return false +} diff --git a/pkg/itemfilter/mapping.go b/pkg/itemfilter/mapping.go new file mode 100644 index 0000000..3830a8c --- /dev/null +++ b/pkg/itemfilter/mapping.go @@ -0,0 +1,17 @@ +package itemfilter + +import ( + "github.com/hectorgimenez/d2go/pkg/data/item" + "github.com/hectorgimenez/d2go/pkg/nip" +) + +var qualities = map[string]item.Quality{ + nip.QualityLowQuality: item.QualityLowQuality, + nip.QualityNormal: item.QualityNormal, + nip.QualitySuperior: item.QualitySuperior, + nip.QualityMagic: item.QualityMagic, + nip.QualitySet: item.QualitySet, + nip.QualityRare: item.QualityRare, + nip.QualityUnique: item.QualityUnique, + nip.QualityCrafted: item.QualityCrafted, +} diff --git a/pkg/nip/file_reader.go b/pkg/nip/file_reader.go index fdc635e..24198f0 100644 --- a/pkg/nip/file_reader.go +++ b/pkg/nip/file_reader.go @@ -1,4 +1,4 @@ -package pickit +package nip import ( "bufio" diff --git a/pkg/nip/nip_parser.go b/pkg/nip/nip_parser.go index 3b1c4a8..1e6eaeb 100644 --- a/pkg/nip/nip_parser.go +++ b/pkg/nip/nip_parser.go @@ -1,4 +1,4 @@ -package pickit +package nip import ( "errors" diff --git a/pkg/nip/nip_parser_test.go b/pkg/nip/nip_parser_test.go index b1aaf0a..0cca470 100644 --- a/pkg/nip/nip_parser_test.go +++ b/pkg/nip/nip_parser_test.go @@ -1,9 +1,10 @@ -package pickit +package nip import ( + "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" ) const nipLine = "[type] == boots && [quality] == rare # [frw] >= 10 && [fireresist] >= 10 && ([lightresist]+[coldresist] >= 10 && [dexterity] >= 1 && [fireresist]+[poisonresist] >= 10) // this is a comment" diff --git a/pkg/nip/properties.go b/pkg/nip/properties.go new file mode 100644 index 0000000..4e565b6 --- /dev/null +++ b/pkg/nip/properties.go @@ -0,0 +1,28 @@ +package nip + +const ( + // Properties + PropertyType = "type" + PropertyName = "name" + PropertyClass = "class" + PropertyQuality = "quality" + PropertyFlag = "flag" + PropertyLevel = "level" + PropertyPrefix = "prefix" + PropertySuffix = "suffix" + + // Types + TypeNormal = "normal" + TypeExceptional = "exceptional" + TypeElite = "elite" + + // Quality + QualityLowQuality = "lowquality" + QualityNormal = "normal" + QualitySuperior = "superior" + QualityMagic = "magic" + QualitySet = "set" + QualityRare = "rare" + QualityUnique = "unique" + QualityCrafted = "crafted" +) diff --git a/pkg/nip/rule.go b/pkg/nip/rule.go index 48589d3..8548d43 100644 --- a/pkg/nip/rule.go +++ b/pkg/nip/rule.go @@ -1,4 +1,4 @@ -package pickit +package nip const ( OperandEqual Operand = "==" @@ -9,6 +9,7 @@ const ( OperandNotEqualTo Operand = "!=" OperandAnd Operand = "&&" OperandOr Operand = "||" + OperandNone Operand = "" ) type Rule struct {