NIP parser v2 (#17)

This commit is contained in:
Héctor Giménez
2024-05-05 12:15:08 +09:00
committed by GitHub
parent 0d5c108721
commit bb0ec2555d
28 changed files with 56618 additions and 1385 deletions

789
pkg/nip/aliases.go Normal file
View File

@@ -0,0 +1,789 @@
package nip
var typeAliases = map[string]int{
"shield": 2,
"armor": 3,
"gold": 4,
"bowquiver": 5,
"crossbowquiver": 6,
"playerbodypart": 7,
"herb": 8,
"potion": 9,
"ring": 10,
"elixir": 11,
"amulet": 12,
"charm": 13,
"notused1": 14,
"boots": 15,
"gloves": 16,
"notused2": 17,
"book": 18,
"belt": 19,
"gem": 20,
"torch": 21,
"scroll": 22,
"notused3": 23,
"scepter": 24,
"wand": 25,
"staff": 26,
"bow": 27,
"axe": 28,
"club": 29,
"sword": 30,
"hammer": 31,
"knife": 32,
"spear": 33,
"polearm": 34,
"crossbow": 35,
"mace": 36,
"helm": 37,
"missilepotion": 38,
"quest": 39,
"bodypart": 40,
"key": 41,
"throwingknife": 42,
"throwingaxe": 43,
"javelin": 44,
"weapon": 45,
"meleeweapon": 46,
"missileweapon": 47,
"thrownweapon": 48,
"comboweapon": 49,
"anyarmor": 50,
"anyshield": 51,
"miscellaneous": 52,
"socketfiller": 53,
"secondhand": 54,
"stavesandrods": 55,
"missile": 56,
"blunt": 57,
"jewel": 58,
"classspecific": 59,
"amazonitem": 60,
"barbarianitem": 61,
"necromanceritem": 62,
"paladinitem": 63,
"sorceressitem": 64,
"assassinitem": 65,
"druiditem": 66,
"handtohand": 67,
"orb": 68,
"voodooheads": 69,
"auricshields": 70,
"primalhelm": 71,
"pelt": 72,
"cloak": 73,
"rune": 74,
"circlet": 75,
"healingpotion": 76,
"manapotion": 77,
"rejuvpotion": 78,
"staminapotion": 79,
"antidotepotion": 80,
"thawingpotion": 81,
"smallcharm": 82,
"mediumcharm": 83,
"largecharm": 84,
"amazonbow": 85,
"amazonspear": 86,
"amazonjavelin": 87,
"assassinclaw": 88,
"magicbowquiv": 89,
"magicxbowquiv": 90,
"chippedgem": 91,
"flawedgem": 92,
"standardgem": 93,
"flawlessgem": 94,
"perfectgem": 95,
"amethyst": 96,
"diamond": 97,
"emerald": 98,
"ruby": 99,
"sapphire": 100,
"topaz": 101,
"skull": 102,
}
var qualityAliases = map[string]int{
"lowquality": 1,
"normal": 2,
"superior": 3,
"magic": 4,
"set": 5,
"rare": 6,
"unique": 7,
"crafted": 8,
}
var classAliases = map[string]int{
"normal": 0,
"exceptional": 1,
"elite": 2,
}
var statAliases = map[string][]int{
"strength": {0},
"energy": {1},
"dexterity": {2},
"vitality": {3},
"statpts": {4},
"newskills": {5},
"hitpoints": {6},
"maxhp": {7},
"mana": {8},
"maxmana": {9},
"stamina": {10},
"maxstamina": {11},
"level": {12},
"experience": {13},
"gold": {14},
"goldbank": {15},
"itemarmorpercent": {16, 0}, "enhanceddefense": {16, 0},
"itemmaxdamagepercent": {17, 0},
"itemmindamagepercent": {18, 0}, "enhanceddamage": {18, 0},
"tohit": {19},
"toblock": {20},
"plusmindamage": {21, 1},
"mindamage": {21},
"plusmaxdamage": {22, 1},
"maxdamage": {22},
"secondarymindamage": {23},
"secondarymaxdamage": {24},
"damagepercent": {25},
"manarecovery": {26},
"manarecoverybonus": {27},
"staminarecoverybonus": {28},
"lastexp": {29},
"nextexp": {30},
"armorclass": {31}, "defense": {31},
"plusdefense": {31, 0},
"armorclassvsmissile": {32},
"armorclassvshth": {33},
"normaldamagereduction": {34},
"magicdamagereduction": {35},
"damageresist": {36},
"magicresist": {37},
"maxmagicresist": {38},
"fireresist": {39},
"maxfireresist": {40},
"lightresist": {41},
"maxlightresist": {42},
"coldresist": {43},
"maxcoldresist": {44},
"poisonresist": {45},
"maxpoisonresist": {46},
"damageaura": {47},
"firemindam": {48},
"firemaxdam": {49},
"lightmindam": {50},
"lightmaxdam": {51},
"magicmindam": {52},
"magicmaxdam": {53},
"coldmindam": {54},
"coldmaxdam": {55},
"coldlength": {56},
"poisondamage": {57, 1},
"poisonmindam": {57},
"poisonmaxdam": {58},
"poisonlength": {59},
"lifedrainmindam": {60}, "lifeleech": {60},
"lifedrainmaxdam": {61},
"manadrainmindam": {62}, "manaleech": {62},
"manadrainmaxdam": {63},
"stamdrainmindam": {64},
"stamdrainmaxdam": {65},
"stunlength": {66},
"velocitypercent": {67},
"attackrate": {68},
"otheranimrate": {69},
"quantity": {70},
"value": {71},
"durability": {72},
"maxdurability": {73},
"hpregen": {74},
"itemmaxdurabilitypercent": {75},
"itemmaxhppercent": {76},
"itemmaxmanapercent": {77},
"itemattackertakesdamage": {78},
"itemgoldbonus": {79},
"itemmagicbonus": {80},
"itemknockback": {81},
"itemtimeduration": {82},
"itemaddclassskills": {83},
"itemaddamazonskills": {83, 0}, "amazonskills": {83, 0},
"itemaddsorceressskills": {83, 1}, "sorceressskills": {83, 1},
"itemaddnecromancerskills": {83, 2}, "necromancerskills": {83, 2},
"itemaddpaladinskills": {83, 3}, "paladinskills": {83, 3},
"itemaddbarbarianskills": {83, 4}, "barbarianskills": {83, 4},
"itemadddruidskills": {83, 5}, "druidskills": {83, 5},
"itemaddassassinskills": {83, 6}, "assassinskills": {83, 6},
"unsentparam1": {84},
"itemaddexperience": {85},
"itemhealafterkill": {86},
"itemreducedprices": {87},
"itemdoubleherbduration": {88},
"itemlightradius": {89},
"itemlightcolor": {90},
"itemreqpercent": {91},
"itemlevelreq": {92},
"itemfasterattackrate": {93}, "ias": {93},
"itemlevelreqpct": {94},
"lastblockframe": {95},
"itemfastermovevelocity": {96}, "frw": {96},
// oskill
"itemnonclassskill": {97},
// Amazon
"plusskillcriticalstrike": {97, 9},
"plusskillguidedarrow": {97, 22},
// Sorceress
"plusskillteleport": {97, 54},
// Barbarian
"plusskillbattleorders": {97, 149},
"plusskillbattlecommand": {97, 155},
"plusskillbattlecry": {97, 146},
// Druid
"plusskillwerewolf": {97, 223},
"plusskillshapeshifting": {97, 224}, "plusskilllycanthropy": {97, 224},
"plusskillsummonspiritwolf": {97, 227},
"plusskillferalrage": {97, 232},
"state": {98},
"itemfastergethitrate": {99}, "fhr": {99},
"monsterplayercount": {100},
"skillpoisonoverridelength": {101},
"itemfasterblockrate": {102}, "fbr": {102},
"skillbypassundead": {103},
"skillbypassdemons": {104},
"itemfastercastrate": {105}, "fcr": {105},
"skillbypassbeasts": {106},
"itemsingleskill": {107},
// Amazon skills
"skillmagicarrow": {107, 6},
"skillfirearrow": {107, 7},
"skillinnersight": {107, 8},
"skillcriticalstrike": {107, 9},
"skilljab": {107, 10},
"skillcoldarrow": {107, 11},
"skillmultipleshot": {107, 12},
"skilldodge": {107, 13},
"skillpowerstrike": {107, 14},
"skillpoisonjavelin": {107, 15},
"skillexplodingarrow": {107, 16},
"skillslowmissiles": {107, 17},
"skillavoid": {107, 18},
"skillimpale": {107, 19},
"skilllightningbolt": {107, 20},
"skillicearrow": {107, 21},
"skillguidedarrow": {107, 22},
"skillpenetrate": {107, 23},
"skillchargedstrike": {107, 24},
"skillplaguejavelin": {107, 25},
"skillstrafe": {107, 26},
"skillimmolationarrow": {107, 27},
"skilldecoy": {107, 28},
"skillevade": {107, 29},
"skillfend": {107, 30},
"skillfreezingarrow": {107, 31},
"skillvalkyrie": {107, 32},
"skillpierce": {107, 33},
"skilllightningstrike": {107, 34},
"skilllightningfury": {107, 35},
// Sorceress skills
"skillfirebolt": {107, 36},
"skillwarmth": {107, 37},
"skillchargedbolt": {107, 38},
"skillicebolt": {107, 39},
"skillfrozenarmor": {107, 40},
"skillinferno": {107, 41},
"skillstaticfield": {107, 42},
"skilltelekinesis": {107, 43},
"skillfrostnova": {107, 44},
"skilliceblast": {107, 45},
"skillblaze": {107, 46},
"skillfireball": {107, 47},
"skillnova": {107, 48},
"skilllightning": {107, 49},
"skillshiverarmor": {107, 50},
"skillfirewall": {107, 51},
"skillenchant": {107, 52},
"skillchainlightning": {107, 53},
"skillteleport": {107, 54},
"skillglacialspike": {107, 55},
"skillmeteor": {107, 56},
"skillthunderstorm": {107, 57},
"skillenergyshield": {107, 58},
"skillblizzard": {107, 59},
"skillchillingarmor": {107, 60},
"skillfiremastery": {107, 61},
"skillhydra": {107, 62},
"skilllightningmastery": {107, 63},
"skillfrozenorb": {107, 64},
"skillcoldmastery": {107, 65},
// Necromancer skills
"skillamplifydamage": {107, 66},
"skillteeth": {107, 67},
"skillbonearmor": {107, 68},
"skillskeletonmastery": {107, 69},
"skillraiseskeleton": {107, 70},
"skilldimvision": {107, 71},
"skillweaken": {107, 72},
"skillpoisondagger": {107, 73},
"skillcorpseexplosion": {107, 74},
"skillclaygolem": {107, 75},
"skillironmaiden": {107, 76},
"skillterror": {107, 77},
"skillbonewall": {107, 78},
"skillgolemmastery": {107, 79},
"skillskeletalmage": {107, 80},
"skillconfuse": {107, 81},
"skilllifetap": {107, 82},
"skillpoisonexplosion": {107, 83},
"skillbonespear": {107, 84},
"skillbloodgolem": {107, 85},
"skillattract": {107, 86},
"skilldecrepify": {107, 87},
"skillboneprison": {107, 88},
"skillsummonresist": {107, 89},
"skillirongolem": {107, 90},
"skilllowerresist": {107, 91},
"skillpoisonnova": {107, 92},
"skillbonespirit": {107, 93},
"skillfiregolem": {107, 94},
"skillrevive": {107, 95},
// Paladin skills
"skillsacrifice": {107, 96},
"skillsmite": {107, 97},
"skillmight": {107, 98},
"skillprayer": {107, 99},
"skillresistfire": {107, 100},
"skillholybolt": {107, 101},
"skillholyfire": {107, 102},
"skillthorns": {107, 103},
"skilldefiance": {107, 104},
"skillresistcold": {107, 105},
"skillzeal": {107, 106},
"skillcharge": {107, 107},
"skillblessedaim": {107, 108},
"skillcleansing": {107, 109},
"skillresistlightning": {107, 110},
"skillvengeance": {107, 111},
"skillblessedhammer": {107, 112},
"skillconcentration": {107, 113},
"skillholyfreeze": {107, 114},
"skillvigor": {107, 115},
"skillconversion": {107, 116},
"skillholyshield": {107, 117},
"skillholyshock": {107, 118},
"skillsanctuary": {107, 119},
"skillmeditation": {107, 120},
"skillfistoftheheavens": {107, 121},
"skillfanaticism": {107, 122},
"skillconviction": {107, 123},
"skillredemption": {107, 124},
"skillsalvation": {107, 125},
// Barbarian skills
"skillbash": {107, 126},
"skillswordmastery": {107, 127},
"skillaxemastery": {107, 128},
"skillmacemastery": {107, 129},
"skillhowl": {107, 130},
"skillfindpotion": {107, 131},
"skillleap": {107, 132},
"skilldoubleswing": {107, 133},
"skillpolearmmastery": {107, 134},
"skillthrowingmastery": {107, 135},
"skillspearmastery": {107, 136},
"skilltaunt": {107, 137},
"skillshout": {107, 138},
"skillstun": {107, 139},
"skilldoublethrow": {107, 140},
"skillincreasedstamina": {107, 141},
"skillfinditem": {107, 142},
"skillleapattack": {107, 143},
"skillconcentrate": {107, 144},
"skillironskin": {107, 145},
"skillbattlecry": {107, 146},
"skillfrenzy": {107, 147},
"skillincreasedspeed": {107, 148},
"skillbattleorders": {107, 149},
"skillgrimward": {107, 150},
"skillwhirlwind": {107, 151},
"skillberserk": {107, 152},
"skillnaturalresistance": {107, 153},
"skillwarcry": {107, 154},
"skillbattlecommand": {107, 155},
// Druid skills
"skillraven": {107, 221},
"skillpoisoncreeper": {107, 222},
"skillwerewolf": {107, 223},
"skilllycanthropy": {107, 224},
"skillfirestorm": {107, 225},
"skilloaksage": {107, 226},
"skillsummonspiritwolf": {107, 227},
"skillwerebear": {107, 228},
"skillmoltenboulder": {107, 229},
"skillarcticblast": {107, 230},
"skillcarrionvine": {107, 231},
"skillferalrage": {107, 232},
"skillmaul": {107, 233},
"skillfissure": {107, 234},
"skillcyclonearmor": {107, 235},
"skillheartofwolverine": {107, 236},
"skillsummondirewolf": {107, 237},
"skillrabies": {107, 238},
"skillfireclaws": {107, 239},
"skilltwister": {107, 240},
"skillsolarcreeper": {107, 241},
"skillhunger": {107, 242},
"skillshockwave": {107, 243},
"skillvolcano": {107, 244},
"skilltornado": {107, 245},
"skillspiritofbarbs": {107, 246},
"skillsummongrizzly": {107, 247},
"skillfury": {107, 248},
"skillarmageddon": {107, 249},
"skillhurricane": {107, 250},
// Assassin skills
"skillfireblast": {107, 251},
"skillclawmastery": {107, 252},
"skillpsychichammer": {107, 253},
"skilltigerstrike": {107, 254},
"skilldragontalon": {107, 255},
"skillshockweb": {107, 256},
"skillbladesentinel": {107, 257},
"skillburstofspeed": {107, 258},
"skillfistsoffire": {107, 259},
"skilldragonclaw": {107, 260},
"skillchargedboltsentry": {107, 261},
"skillwakeoffire": {107, 262},
"skillweaponblock": {107, 263},
"skillcloakofshadows": {107, 264},
"skillcobrastrike": {107, 265},
"skillbladefury": {107, 266},
"skillfade": {107, 267},
"skillshadowwarrior": {107, 268},
"skillclawsofthunder": {107, 269},
"skilldragontail": {107, 270},
"skilllightningsentry": {107, 271},
"skillwakeofinferno": {107, 272},
"skillmindblast": {107, 273},
"skillbladesofice": {107, 274},
"skilldragonflight": {107, 275},
"skilldeathsentry": {107, 276},
"skillbladeshield": {107, 277},
"skillvenom": {107, 278},
"skillshadowmaster": {107, 279},
"skillphoenixstrike": {107, 280},
"itemrestinpeace": {108},
"curseresistance": {109},
"itempoisonlengthresist": {110},
"itemnormaldamage": {111},
"itemhowl": {112},
"itemstupidity": {113},
"itemdamagetomana": {114},
"itemignoretargetac": {115},
"itemfractionaltargetac": {116},
"itempreventheal": {117},
"itemhalffreezeduration": {118},
"itemtohitpercent": {119},
"itemdamagetargetac": {120},
"itemdemondamagepercent": {121},
"itemundeaddamagepercent": {122},
"itemdemontohit": {123},
"itemundeadtohit": {124},
"itemthrowable": {125},
"itemelemskill": {126},
"itemallskills": {127},
"itemattackertakeslightdamage": {128},
"ironmaidenlevel": {129},
"lifetaplevel": {130},
"thornspercent": {131},
"bonearmor": {132},
"bonearmormax": {133},
"itemfreeze": {134},
"itemopenwounds": {135},
"itemcrushingblow": {136},
"itemkickdamage": {137},
"itemmanaafterkill": {138},
"itemhealafterdemonkill": {139},
"itemextrablood": {140},
"itemdeadlystrike": {141},
"itemabsorbfirepercent": {142},
"itemabsorbfire": {143},
"itemabsorblightpercent": {144},
"itemabsorblight": {145},
"itemabsorbmagicpercent": {146},
"itemabsorbmagic": {147},
"itemabsorbcoldpercent": {148},
"itemabsorbcold": {149},
"itemslow": {150},
"itemaura": {151},
"mightaura": {151, 98},
"holyfireaura": {151, 102},
"thornsaura": {151, 103},
"defianceaura": {151, 104},
"concentrationaura": {151, 113},
"holyfreezeaura": {151, 114},
"vigoraura": {151, 115},
"holyshockaura": {151, 118},
"sanctuaryaura": {151, 119},
"meditationaura": {151, 120},
"fanaticismaura": {151, 122},
"convictionaura": {151, 123},
"redemptionaura": {151, 124},
"itemindestructible": {152},
"itemcannotbefrozen": {153},
"itemstaminadrainpct": {154},
"itemreanimate": {155},
"itempierce": {156},
"itemmagicarrow": {157},
"itemexplosivearrow": {158},
"itemthrowmindamage": {159},
"itemthrowmaxdamage": {160},
"itemskillhandofathena": {161},
"itemskillstaminapercent": {162},
"itemskillpassivestaminapercent": {163},
"itemskillconcentration": {164},
"itemskillenchant": {165},
"itemskillpierce": {166},
"itemskillconviction": {167},
"itemskillchillingarmor": {168},
"itemskillfrenzy": {169},
"itemskilldecrepify": {170},
"itemskillarmorpercent": {171},
"alignment": {172},
"target0": {173},
"target1": {174},
"goldlost": {175},
"conversionlevel": {176},
"conversionmaxhp": {177},
"unitdooverlay": {178},
"attackvsmontype": {179},
"damagevsmontype": {180},
"fade": {181},
"armoroverridepercent": {182},
"unused183": {183},
"unused184": {184},
"unused185": {185},
"unused186": {186},
"unused187": {187},
"itemaddskilltab": {188},
"itemaddbowandcrossbowskilltab": {188, 0}, "bowandcrossbowskilltab": {188, 0},
"itemaddpassiveandmagicskilltab": {188, 1}, "passiveandmagicskilltab": {188, 1},
"itemaddjavelinandspearskilltab": {188, 2}, "javelinandspearskilltab": {188, 2},
"itemaddfireskilltab": {188, 8}, "fireskilltab": {188, 8},
"itemaddlightningskilltab": {188, 9}, "lightningskilltab": {188, 9},
"itemaddcoldskilltab": {188, 10}, "coldskilltab": {188, 10},
"itemaddcursesskilltab": {188, 16}, "cursesskilltab": {188, 16},
"itemaddpoisonandboneskilltab": {188, 17}, "poisonandboneskilltab": {188, 17},
"itemaddnecromancersummoningskilltab": {188, 18}, "necromancersummoningskilltab": {188, 18},
"itemaddpalicombatskilltab": {188, 24}, "palicombatskilltab": {188, 24},
"itemaddoffensiveaurasskilltab": {188, 25}, "offensiveaurasskilltab": {188, 25},
"itemadddefensiveaurasskilltab": {188, 26}, "defensiveaurasskilltab": {188, 26},
"itemaddbarbcombatskilltab": {188, 32}, "barbcombatskilltab": {188, 32},
"itemaddmasteriesskilltab": {188, 33}, "masteriesskilltab": {188, 33},
"itemaddwarcriesskilltab": {188, 34}, "warcriesskilltab": {188, 34},
"itemadddruidsummoningskilltab": {188, 40}, "druidsummoningskilltab": {188, 40},
"itemaddshapeshiftingskilltab": {188, 41}, "shapeshiftingskilltab": {188, 41},
"itemaddelementalskilltab": {188, 42}, "elementalskilltab": {188, 42},
"itemaddtrapsskilltab": {188, 48}, "trapsskilltab": {188, 48},
"itemaddshadowdisciplinesskilltab": {188, 49}, "shadowdisciplinesskilltab": {188, 49},
"itemaddmartialartsskilltab": {188, 50}, "martialartsskilltab": {188, 50},
"unused189": {189},
"unused190": {190},
"unused191": {191},
"unused192": {192},
"unused193": {193},
"itemnumsockets": {194}, "sockets": {194},
"itemskillonattack": {195, 1},
"itemskillonattacklevel": {195, 2},
"itemskillonkill": {196, 1},
"itemskillonkilllevel": {196, 2},
"itemskillondeath": {197, 1},
"itemskillondeathlevel": {197, 2},
"itemskillonhit": {198, 1},
"itemskillonhitlevel": {198, 2},
"amplifydamageonhit": {198, 4225},
"itemskillonlevelup": {199, 1},
"itemskillonleveluplevel": {199, 2},
"unused200": {200},
"itemskillongethit": {201, 1},
"itemskillongethitlevel": {201, 2},
"unused202": {202},
"unused203": {203},
"itemchargedskill": {204, 1},
"itemchargedskilllevel": {204, 2},
"teleportcharges": {204, 3461},
"unused204": {205},
"unused205": {206},
"unused206": {207},
"unused207": {208},
"unused208": {209},
"unused209": {210},
"unused210": {211},
"unused211": {212},
"unused212": {213},
"itemarmorperlevel": {214},
"itemarmorpercentperlevel": {215},
"itemhpperlevel": {216},
"itemmanaperlevel": {217},
"itemmaxdamageperlevel": {218},
"itemmaxdamagepercentperlevel": {219},
"itemstrengthperlevel": {220},
"itemdexterityperlevel": {221},
"itemenergyperlevel": {222},
"itemvitalityperlevel": {223},
"itemtohitperlevel": {224},
"itemtohitpercentperlevel": {225},
"itemcolddamagemaxperlevel": {226},
"itemfiredamagemaxperlevel": {227},
"itemltngdamagemaxperlevel": {228},
"itempoisdamagemaxperlevel": {229},
"itemresistcoldperlevel": {230},
"itemresistfireperlevel": {231},
"itemresistltngperlevel": {232},
"itemresistpoisperlevel": {233},
"itemabsorbcoldperlevel": {234},
"itemabsorbfireperlevel": {235},
"itemabsorbltngperlevel": {236},
"itemabsorbpoisperlevel": {237},
"itemthornsperlevel": {238},
"itemfindgoldperlevel": {239},
"itemfindmagicperlevel": {240},
"itemregenstaminaperlevel": {241},
"itemstaminaperlevel": {242},
"itemdamagedemonperlevel": {243},
"itemdamageundeadperlevel": {244},
"itemtohitdemonperlevel": {245},
"itemtohitundeadperlevel": {246},
"itemcrushingblowperlevel": {247},
"itemopenwoundsperlevel": {248},
"itemkickdamageperlevel": {249},
"itemdeadlystrikeperlevel": {250},
"itemfindgemsperlevel": {251},
"itemreplenishdurability": {252},
"itemreplenishquantity": {253},
"itemextrastack": {254},
"itemfinditem": {255},
"itemslashdamage": {256},
"itemslashdamagepercent": {257},
"itemcrushdamage": {258},
"itemcrushdamagepercent": {259},
"itemthrustdamage": {260},
"itemthrustdamagepercent": {261},
"itemabsorbslash": {262},
"itemabsorbcrush": {263},
"itemabsorbthrust": {264},
"itemabsorbslashpercent": {265},
"itemabsorbcrushpercent": {266},
"itemabsorbthrustpercent": {267},
"itemarmorbytime": {268},
"itemarmorpercentbytime": {269},
"itemhpbytime": {270},
"itemmanabytime": {271},
"itemmaxdamagebytime": {272},
"itemmaxdamagepercentbytime": {273},
"itemstrengthbytime": {274},
"itemdexteritybytime": {275},
"itemenergybytime": {276},
"itemvitalitybytime": {277},
"itemtohitbytime": {278},
"itemtohitpercentbytime": {279},
"itemcolddamagemaxbytime": {280},
"itemfiredamagemaxbytime": {281},
"itemltngdamagemaxbytime": {282},
"itempoisdamagemaxbytime": {283},
"itemresistcoldbytime": {284},
"itemresistfirebytime": {285},
"itemresistltngbytime": {286},
"itemresistpoisbytime": {287},
"itemabsorbcoldbytime": {288},
"itemabsorbfirebytime": {289},
"itemabsorbltngbytime": {290},
"itemabsorbpoisbytime": {291},
"itemfindgoldbytime": {292},
"itemfindmagicbytime": {293},
"itemregenstaminabytime": {294},
"itemstaminabytime": {295},
"itemdamagedemonbytime": {296},
"itemdamageundeadbytime": {297},
"itemtohitdemonbytime": {298},
"itemtohitundeadbytime": {299},
"itemcrushingblowbytime": {300},
"itemopenwoundsbytime": {301},
"itemkickdamagebytime": {302},
"itemdeadlystrikebytime": {303},
"itemfindgemsbytime": {304},
"itempiercecold": {305},
"itempiercefire": {306},
"itempierceltng": {307},
"itempiercepois": {308},
"itemdamagevsmonster": {309},
"itemdamagepercentvsmonster": {310},
"itemtohitvsmonster": {311},
"itemtohitpercentvsmonster": {312},
"itemacvsmonster": {313},
"itemacpercentvsmonster": {314},
"firelength": {315},
"burningmin": {316},
"burningmax": {317},
"progressivedamage": {318},
"progressivesteal": {319},
"progressiveother": {320},
"progressivefire": {321},
"progressivecold": {322},
"progressivelightning": {323},
"itemextracharges": {324},
"progressivetohit": {325},
"poisoncount": {326},
"damageframerate": {327},
"pierceidx": {328},
"passivefiremastery": {329},
"passiveltngmastery": {330},
"passivecoldmastery": {331},
"passivepoismastery": {332},
"passivefirepierce": {333},
"passiveltngpierce": {334},
"passivecoldpierce": {335},
"passivepoispierce": {336},
"passivecriticalstrike": {337},
"passivedodge": {338},
"passiveavoid": {339},
"passiveevade": {340},
"passivewarmth": {341},
"passivemasterymeleeth": {342},
"passivemasterymeleedmg": {343},
"passivemasterymeleecrit": {344},
"passivemasterythrowth": {345},
"passivemasterythrowdmg": {346},
"passivemasterythrowcrit": {347},
"passiveweaponblock": {348},
"passivesummonresist": {349},
"modifierlistskill": {350},
"modifierlistlevel": {351},
"lastsenthppct": {352},
"sourceunittype": {353},
"sourceunitid": {354},
"shortparam1": {355},
"questitemdifficulty": {356},
"passivemagmastery": {357},
"passivemagpierce": {358},
// Doesnt really exists, but is calculated in getStatEx
"allres": {555},
}

8
pkg/nip/errors.go Normal file
View File

@@ -0,0 +1,8 @@
package nip
import "errors"
var (
// ErrEmptyRule is returned when a rule is empty
ErrEmptyRule = errors.New("empty rule")
)

View File

@@ -6,9 +6,12 @@ import (
"fmt"
"os"
"strings"
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item"
)
func ReadDir(path string) ([]Rule, error) {
func ReadDir(path string) (Rules, error) {
files, err := os.ReadDir(path)
if err != nil {
return nil, err
@@ -31,7 +34,7 @@ func ReadDir(path string) ([]Rule, error) {
return rules, nil
}
func ParseNIPFile(filePath string) ([]Rule, error) {
func ParseNIPFile(filePath string) (Rules, error) {
fileReader, err := os.Open(filePath)
if err != nil {
return nil, err
@@ -43,18 +46,44 @@ func ParseNIPFile(filePath string) ([]Rule, error) {
rules := make([]Rule, 0)
lineNumber := 0
dummyItem := data.Item{
ID: 516,
Name: "healingpotion",
Quality: item.QualityNormal,
}
for fileScanner.Scan() {
lineNumber++
rule, err := ParseLine(fileScanner.Text(), filePath, lineNumber)
if errors.Is(err, errEmptyLine) {
rule, err := New(fileScanner.Text(), filePath, lineNumber)
if errors.Is(err, ErrEmptyRule) {
continue
}
if err != nil {
return nil, fmt.Errorf("error reading %s file at line %d: %w", filePath, lineNumber, err)
}
// We evaluate all the rules at startup to ensure no format errors, if there is a format error we will throw it now instead of during runtime
_, err = rule.Evaluate(dummyItem)
if err != nil {
return nil, fmt.Errorf("error testing rule on [%s:%d]: %w", filePath, lineNumber, err)
}
rules = append(rules, rule)
}
return rules, nil
}
func sanitizeLine(rawLine string) string {
l := strings.Split(rawLine, "//")
line := strings.TrimSpace(l[0])
line = strings.Join(strings.Fields(line), " ")
line = strings.ReplaceAll(line, "'", "")
// Fix possible wrong formatted lines
line = strings.ReplaceAll(line, "=>", ">=")
line = strings.ReplaceAll(line, "=<", "<=")
line = strings.TrimSpace(strings.Trim(line, "&&"))
return strings.ToLower(line)
}

View File

@@ -0,0 +1,15 @@
package nip
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseNIPFile(t *testing.T) {
path := "mock/rare.nip"
rules, err := ParseNIPFile(path)
assert.NoError(t, err)
assert.NotEmpty(t, rules)
}

View File

@@ -0,0 +1,21 @@
package nip
// These stats are currently NOT supported, rules can not contain them otherwise it will return an error
var notSupportedStats = []string{
"plusmindamage",
"mindamage",
"plusmaxdamage",
"maxdamage",
"enhanceddamage",
"itemarmorpercent",
"itemmindamagepercent",
"itemslashdamage",
"itemslashdamagepercent",
"itemcrushdamage",
"itemcrushdamagepercent",
"itemthrustdamage",
"itemthrustdamagepercent",
"secondarymindamage",
"secondarymaxdamage",
"damagepercent",
}

103
pkg/nip/mock/rare.nip Normal file
View File

@@ -0,0 +1,103 @@
// rare jewels
[type] == jewel
// rare rings
[type] == ring && [quality] == rare # [fcr] >= 10 && [strength] + [dexterity] + [maxhp] + [maxmana] + [fireresist] + [lightresist] + [coldresist] + [poisonresist] >= 50
// rare amulets
[type] == amulet && [quality] == rare # [itemaddclassskills] >= 2
// rare circlets
[type] == circlet && [quality] == rare && [flag] != ethereal # [itemaddclassskills] >= 2 && [dexterity]+[strength] >= 10 && ([itemtohitpercentperlevel] >= 1 || [fhr]+[frw] >= 10 || [maxhp]+[maxmana] >= 10 ||[coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10)
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [itemaddclassskills] >= 2 && [dexterity]+[strength] >= 10 && ([itemtohitpercentperlevel] >= 1 || [fhr]+[frw] >= 10 || [maxhp]+[maxmana] >= 10 ||[coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10)
[type] == circlet && [quality] == rare && [flag] != ethereal # [amazonskills] >= 2 && [frw] >= 10 && ([dexterity]+[strength] >= 10 || [maxhp] >= 20) && (([itemtohitpercentperlevel] >= 1) || ([dexterity]+[strength] >= 20) || ([maxhp] >= 10) || ([fhr] >= 10) || ([fcr] >= 20) || ([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10))
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [amazonskills] >= 2 && [frw] >= 10 && ([dexterity]+[strength] >= 10 || [maxhp] >= 20) && (([itemtohitpercentperlevel] >= 1) || ([dexterity]+[strength] >= 20) || ([maxhp] >= 10) || ([fhr] >= 10) || ([fcr] >= 10) || ([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 10))
[type] == circlet && [quality] == rare && [flag] != ethereal # [paladinskills] >= 2
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [paladinskills] >= 2
[type] == circlet && [quality] == rare && [flag] != ethereal # [necromancerskills] >= 2
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [necromancerskills] >= 2
[type] == circlet && [quality] == rare && [flag] != ethereal # [itemgoldbonus] >= 40 && [sockets] >= 2
[type] == circlet && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [itemgoldbonus] >= 40 && [sockets] >= 2
// rare sorceress orbs
[type] == orb && [quality] == rare # [fcr] >= 20 && [sorceressskills]+[skilllightning] >= 5
[type] == orb && [quality] == rare # [fcr] >= 20 && [sorceressskills]+[skillfrozenorb]+[skillblizzard] >= 5
[type] == orb && [quality] == rare # [fcr] >= 20 && [sorceressskills]+[skillfireball]+[skillhydra] >= 5
// rare barb helms
[type] == primalhelm && [quality] == rare # [barbarianskills]+[skillbattleorders] >= 5
[type] == primalhelm && [quality] == rare # [itemtohitpercentperlevel] >= 1 || [sockets] == 2
[type] == primalhelm && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([itemtohitpercentperlevel] >= 1 || [sockets] == 2)
// rare wands
[type] == wand && [quality] == rare # [necromancerskills]+[poisonandboneskilltab]+[skillbonespear] +[skillpoisonnova] + [skillrevive] >= 4
// rare necro shields
[type] == voodooheads && [quality] == rare # [necromancerskills] == 2 && ([skillbonespear] >= 3 || [skillpoisonnova] >= 3) && ([sockets] >= 2 || ([fbr] >= 30 && ([maxhp] >= 20 || [fhr] >= 17)))
[type] == voodooheads && [quality] == rare # [necromancerskills]+[skillbonespear]+[skillpoisonnova]+[skillrevive]+[skilllowerresist]+[skilldecrepify]+[poisonandboneskilltab]+[necromancersummoningskilltab]+[skillbonespirit] >= 5
// rare helm
[type] == helm && [quality] == rare && [flag] != ethereal # [itemtohitpercentperlevel] >= 1 && [maxhp] >= 39 && [sockets] == 2 && [fhr] >= 10
[type] == helm && [quality] == rare && [flag] == ethereal # [itemtohitpercentperlevel] >= 1 && [maxhp] >= 35 && [sockets] == 2 && ([fhr] >= 10 || [itemtohitpercent] >= 5 ) && [itemreplenishdurability] >=1
[type] == helm && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([itemtohitpercentperlevel] >= 1 || [sockets] == 2)
// rare armor
//([name] == archonplate || [name] == mageplate || [name] == lightplate || [name] == gothicplate || [name] == ancientarmor || [name] == wirefleece)
//[type] == armor && [quality] == rare && [flag] != ethereal # [sockets] == 2 && [maxhp] >= 55 && ([dexterity] >= 9 || [strength] >= 17) && (([coldresist]+[fireresist]+[lightresist] >= 20) || [fhr] >= 17 || [hpregen] >= 4)
[type] == armor && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [maxhp] >= 50 && [dexterity]+[strength] >= 7 && [itemreplenishdurability] >= 1
[type] == armor && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [itemreplenishdurability] >=1
[type] == armor && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [itemreplenishdurability] >=1 && ([maxhp] >= 20 || [dexterity]+[strength] >= 7 || [hpregen] >= 4)
// rare shields
[type] == shield && [quality] == rare && [flag] == ethereal # [sockets] == 2 && [itemreplenishdurability] >=1
[type] == shield && [quality] == rare && [flag] != ethereal # [fhr] >= 17 && [sockets] == 2 && [fbr] >= 30 && ([maxhp] >= 30 || [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40)
[type] == shield && [quality] == rare && [flag] == ethereal # [fhr] >= 17 && [sockets] == 2 && [fbr] >= 30 && ([maxhp] >= 30 || [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40) && [itemreplenishdurability] >= 1
[type] == shield && [quality] == rare && [flag] != ethereal # [maxhp] >= 30 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40 && [sockets] == 2 && [fbr] >= 30
[type] == shield && [quality] == rare && [flag] == ethereal # [maxhp] >= 30 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 40 && [sockets] == 2 && [fbr] >= 30 && [itemreplenishdurability] >= 1
// rare gloves
[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [itemreplenishdurability] >=1
[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2)
[type] == gloves && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([enhanceddefense] >= 200 || [ias] >= 20 || ([dexterity]+[strength] >= 10) || [hpregen] >= 5 || [passiveandmagicskilltab] == 2 || [javelinandspearskilltab] == 2 || ([coldresist]+[fireresist]+[lightresist] >= 20))
[type] == gloves && [quality] == rare && [flag] != ethereal # [dexterity]+[strength] >= 25 && [coldresist]+[fireresist]+[lightresist] >= 55
[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] == 20 && ([dexterity] >= 15 || [strength] >= 15) && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [coldresist]+[fireresist]+[lightresist] >= 25
//[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && (([coldresist]+[fireresist]+[lightresist] >= 35) || ([coldresist]+[fireresist] >= 40) || ([coldresist]+[lightresist] >= 40) || ([fireresist]+[lightresist] >= 40))
[type] == gloves && [quality] == rare && [flag] != ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2)
[type] == gloves && [quality] == rare && [flag] == ethereal # [dexterity]+[strength] >= 25 && [coldresist]+[fireresist]+[lightresist] >= 60 && [itemreplenishdurability] >= 1
[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] == 20 && ([dexterity] >= 15 || [strength] >= 15) && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [coldresist]+[fireresist]+[lightresist] >= 25 && [itemreplenishdurability] >= 1
//[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && (([coldresist]+[fireresist]+[lightresist] >= 35) || ([coldresist]+[fireresist] >= 40) || ([coldresist]+[lightresist] >= 40) || ([fireresist]+[lightresist] >= 40)) && [itemreplenishdurability] >= 1
[type] == gloves && [quality] == rare && [flag] == ethereal # [ias] == 20 && [dexterity]+[strength] >= 20 && ([bowandcrossbowskilltab] == 2 || [javelinandspearskilltab] == 2) && [itemreplenishdurability] >= 1
// rare boots
[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && ([enhanceddefense] >= 200 || [fhr] >= 10 || [dexterity] >= 7 || [hpregen] >= 5 || [frw] >= 30 || ([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 60))
[type] == boots && [quality] == rare && [flag] != ethereal # [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 120
[type] == boots && [quality] == rare && [flag] != ethereal # [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 105 && ([fhr] >= 10 || [frw] >= 30 || [dexterity] >= 7 || [hpregen] >= 5)
//[type] == boots && [quality] == rare && [flag] != ethereal # [dexterity] >= 7 && ([frw] >= 30 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
//[type] == boots && [quality] == rare && [flag] != ethereal # [frw] >= 30 && ([fhr] >= 10 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
//[type] == boots && [quality] == rare && [flag] != ethereal # [fhr] >= 10 && ([dexterity] >= 7 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 120
[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 105 && ([fhr] >= 10 || [frw] >= 30 || [dexterity] >= 7 || [hpregen] >= 5)
//[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [dexterity] >= 7 && ([frw] >= 30 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
//[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [frw] >= 30 && ([fhr] >= 10 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
//[type] == boots && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [fhr] >= 10 && ([dexterity] >= 7 || [hpregen] >= 5) && (([coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 90) || ([coldresist]+[fireresist] >= 79) || ([coldresist]+[lightresist] >= 79) || ([lightresist]+[fireresist] >= 79))
// rare belts
[type] == belt && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >=1 && [fhr] >= 1 && [coldresist]+[fireresist]+[lightresist] >= 25
[type] == belt && [quality] == rare && [flag] != ethereal # [fhr] == 24 && [strength] >= 25 && [maxhp] >= 45 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 22
[type] == belt && [quality] == rare && [flag] != ethereal # [fhr] == 24 && [strength] >= 30 && [maxhp] >= 60
[type] == belt && [quality] == rare && [flag] == ethereal # [fhr] == 24 && [strength] >= 25 && [maxhp] >= 45 && [coldresist]+[fireresist]+[lightresist]+[poisonresist] >= 22 && [itemreplenishdurability] >= 1
[type] == belt && [quality] == rare && [flag] == ethereal # [fhr] == 24 && [strength] >= 29 && [maxhp] >= 55 && [itemreplenishdurability] >= 1
// scepters
[type] == scepter && [quality] == rare && [flag] == ethereal # [itemreplenishdurability] >= 1 && [skillfistoftheheavens] + [paladinskills] + [palicombatskilltab] + [sockets] + [skillblessedhammer] + [skillconcentration]+[skillholyshield] >= 5
[type] == scepter && [quality] == rare && [flag] != ethereal # [skillfistoftheheavens] + [paladinskills] + [palicombatskilltab] + [sockets] + [skillblessedhammer] + [skillconcentration]+[skillholyshield] >= 5
// polearms
[type] == polearm && [quality] == rare && [flag] == ethereal # [itemtohitperlevel] >= 1

View File

@@ -1,175 +0,0 @@
package nip
import (
"errors"
"regexp"
"strconv"
"strings"
)
var (
errEmptyLine = errors.New("empty line")
operandRegex = regexp.MustCompile(`(\|{2}|\&{2})`)
comparableRegex = regexp.MustCompile(`(\={2}|\<\=|\>\=|\>|\<|\!\=)`)
propertyNameRegex = regexp.MustCompile(`\[(.*)\]`)
)
func ParseLine(line string, filename string, linenum int) (Rule, error) {
line = lineCleanup(line)
if line == "" {
return Rule{}, errEmptyLine
}
lineSplit := strings.Split(line, "#")
properties, err := buildGroups(lineSplit[0])
if err != nil {
return Rule{}, err
}
var stats []Group
if len(lineSplit) > 1 && strings.TrimSpace(lineSplit[1]) != "" {
stats, err = buildGroups(lineSplit[1])
if err != nil {
return Rule{}, err
}
}
var maxQuantity []Group
if len(lineSplit) > 2 && strings.TrimSpace(lineSplit[2]) != "" {
maxQuantity, err = buildGroups(lineSplit[2])
if err != nil {
return Rule{}, err
}
}
return Rule{
Properties: properties,
Stats: stats,
MaxQuantity: maxQuantity,
Filename: filename,
LineNumber: linenum,
Enabled: true,
}, nil
}
func lineCleanup(line string) string {
l := strings.Split(line, "//")
line = strings.TrimSpace(l[0])
return strings.ToLower(line)
}
func buildGroups(block string) ([]Group, error) {
operands := operandRegex.FindAllString(block, -1)
groupLines := operandRegex.Split(block, -1)
operands, groupLines = reGroupParentheses(operands, groupLines)
if len(operands) != len(groupLines)-1 {
return nil, errors.New("syntax error")
}
groups := make([]Group, 0)
for i, groupLine := range groupLines {
cmp, err := buildGroupComparables(groupLine)
if err != nil {
return nil, err
}
group := Group{Comparable: cmp}
if i < len(operands) {
group.Operand = Operand(operands[i])
}
groups = append(groups, group)
}
return groups, nil
}
func reGroupParentheses(operands, groups []string) ([]string, []string) {
cleanOperands := make([]string, 0)
cleanGroups := make([]string, 0)
inGroup := false
currentGroupContent := ""
for i, group := range groups {
group = strings.TrimSpace(group)
if strings.Contains(group, "(") && strings.Contains(group, ")") {
group = strings.ReplaceAll(group, "(", "")
group = strings.ReplaceAll(group, ")", "")
cleanGroups = append(cleanGroups, group)
if i < len(operands) {
cleanOperands = append(cleanOperands, operands[i])
}
continue
}
if strings.Contains(group, "(") || (inGroup && !strings.Contains(group, ")")) {
inGroup = true
currentGroupContent += strings.ReplaceAll(group, "(", "") + operands[i]
continue
}
if strings.Contains(group, ")") {
inGroup = false
currentGroupContent += strings.ReplaceAll(group, ")", "")
cleanGroups = append(cleanGroups, currentGroupContent)
currentGroupContent = ""
if i < len(operands) {
cleanOperands = append(cleanOperands, operands[i])
}
continue
}
if !inGroup {
cleanGroups = append(cleanGroups, group)
if i < len(operands) {
cleanOperands = append(cleanOperands, operands[i])
}
}
}
return cleanOperands, cleanGroups
}
func buildGroupComparables(group string) ([]Comparable, error) {
operands := operandRegex.FindAllString(group, -1)
properties := operandRegex.Split(group, -1)
if len(properties)-1 != len(operands) {
return nil, errors.New("invalid NIP block")
}
comparables := make([]Comparable, 0)
for i, prop := range properties {
comparisonSymbol := comparableRegex.FindString(prop)
values := comparableRegex.Split(prop, -1)
if len(values) != 2 {
return nil, errors.New("invalid NIP line")
}
aggregatedProperties := strings.Split(values[0], "+")
for propNum, prop := range aggregatedProperties {
propertyNameGr := propertyNameRegex.FindStringSubmatch(prop)
propertyName := strings.TrimSpace(propertyNameGr[1])
cmp := Comparable{
Keyword: propertyName,
Comparison: Operand(strings.TrimSpace(comparisonSymbol)),
}
if len(aggregatedProperties) > 0 && propNum < len(aggregatedProperties)-1 {
cmp.Operand = OperandOr
} else if i < len(operands) {
cmp.Operand = Operand(operands[i])
}
value := strings.TrimSpace(values[1])
if val, err := strconv.Atoi(value); err == nil {
cmp.ValueInt = val
} else {
cmp.ValueString = value
}
comparables = append(comparables, cmp)
}
}
return comparables, nil
}

View File

@@ -1,96 +0,0 @@
package nip
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const nipLine = "[type] == boots && [quality] == rare # [frw] >= 10 && [fireresist] >= 10 && ([lightresist]+[coldresist] >= 10 && [dexterity] >= 1 && [fireresist]+[poisonresist] >= 10) // this is a comment"
func Test_parseLine(t *testing.T) {
rules, err := ParseLine(nipLine, "dummy", 0)
require.NoError(t, err)
expected := Rule{
Properties: []Group{
{
Comparable: []Comparable{
{
Keyword: "type",
Comparison: OperandEqual,
ValueString: "boots",
},
},
Operand: OperandAnd,
},
{
Comparable: []Comparable{
{
Keyword: "quality",
Comparison: OperandEqual,
ValueString: "rare",
},
},
},
},
Stats: []Group{
{
Comparable: []Comparable{
{
Keyword: "frw",
Comparison: OperandGreaterOrEqualTo,
ValueInt: 10,
},
},
Operand: OperandAnd,
},
{
Comparable: []Comparable{
{
Keyword: "fireresist",
Comparison: OperandGreaterOrEqualTo,
ValueInt: 10,
},
},
Operand: OperandAnd,
},
{
Comparable: []Comparable{
{
Keyword: "lightresist",
Comparison: OperandGreaterOrEqualTo,
ValueInt: 10,
Operand: OperandOr,
},
{
Keyword: "coldresist",
Comparison: OperandGreaterOrEqualTo,
ValueInt: 10,
Operand: OperandAnd,
},
{
Keyword: "dexterity",
Comparison: OperandGreaterOrEqualTo,
ValueInt: 1,
Operand: OperandAnd,
},
{
Keyword: "fireresist",
Comparison: OperandGreaterOrEqualTo,
ValueInt: 10,
Operand: OperandOr,
},
{
Keyword: "poisonresist",
Comparison: OperandGreaterOrEqualTo,
ValueInt: 10,
},
},
},
},
}
assert.Equal(t, expected, rules)
}

View File

@@ -1,28 +1,14 @@
package nip
const (
// Properties
PropertyType = "type"
PropertyName = "name"
PropertyClass = "class"
PropertyQuality = "quality"
PropertyFlag = "flag"
PropertyLevel = "level"
PropertyPrefix = "prefix"
PropertySuffix = "suffix"
import "github.com/hectorgimenez/d2go/pkg/data/item"
// Types
TypeNormal = "normal"
TypeExceptional = "exceptional"
TypeElite = "elite"
// Quality
QualityLowQuality = "lowquality"
QualityNormal = "normal"
QualitySuperior = "superior"
QualityMagic = "magic"
QualitySet = "set"
QualityRare = "rare"
QualityUnique = "unique"
QualityCrafted = "crafted"
)
var quality = map[string]item.Quality{
"LowQuality": item.QualityLowQuality,
"Normal": item.QualityNormal,
"Superior": item.QualitySuperior,
"Magic": item.QualityMagic,
"Set": item.QualitySet,
"Rare": item.QualityRare,
"Unique": item.QualityUnique,
"Crafted": item.QualityCrafted,
}

View File

@@ -1,38 +1,223 @@
package nip
const (
OperandEqual Operand = "=="
OperandGreaterThan Operand = ">"
OperandGreaterOrEqualTo Operand = ">="
OperandLessThan Operand = "<"
OperandLessThanOrEqualTo Operand = "<="
OperandNotEqualTo Operand = "!="
OperandAnd Operand = "&&"
OperandOr Operand = "||"
OperandNone Operand = ""
import (
"fmt"
"math"
"regexp"
"slices"
"strconv"
"strings"
"github.com/expr-lang/expr"
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item"
"github.com/hectorgimenez/d2go/pkg/data/stat"
)
var (
fixedPropsRegexp = regexp.MustCompile(`(\[(type|quality|class|name|flag|color)]\s*(<=|<|>|>=|!=|==)\s*([a-zA-Z]+))`)
statsRegexp = regexp.MustCompile(`\[(.*?)]`)
maxQtyRegexp = regexp.MustCompile(`(\[maxquantity]\s*(<=|<|>|>=|!=|==)\s*([0-9]+))`)
)
type Rule struct {
Properties []Group
Stats []Group
MaxQuantity []Group
RawLine string // Original line, don't use it for evaluation
Stages [3]string
Filename string
LineNumber int
Enabled bool
maxQuantity int
}
type Keyword string
type Operand string
type Rules []Rule
type Comparable struct {
Keyword string
Comparison Operand
ValueInt int
ValueString string
Operand
type RuleResult int
var (
RuleResultFullMatch RuleResult = 1
RuleResultPartial RuleResult = 2
RuleResultNoMatch RuleResult = 3
)
func (r Rules) EvaluateAll(it data.Item) (Rule, RuleResult) {
bestMatch := RuleResultNoMatch
bestMatchingRule := Rule{}
for _, rule := range r {
if rule.Enabled {
result, err := rule.Evaluate(it)
if err != nil {
continue
}
if result == RuleResultFullMatch {
return rule, result
}
if result == RuleResultPartial {
bestMatch = result
bestMatchingRule = rule
}
}
}
return bestMatchingRule, bestMatch
}
type Group struct {
Comparable []Comparable
Operand
func New(rawRule string, filename string, lineNumber int) (Rule, error) {
rule := sanitizeLine(rawRule)
// Check for not supported stats
for _, prop := range statsRegexp.FindAllStringSubmatch(rule, -1) {
if slices.Contains(notSupportedStats, prop[1]) {
return Rule{}, fmt.Errorf("property %s is not supported, please remove it", prop[1])
}
}
// Try to get the maxquantity value and purge it from the rule, we can not evaluate it
maxQuantity := 0
for _, prop := range maxQtyRegexp.FindAllStringSubmatch(rule, -1) {
mxQty, err := strconv.Atoi(prop[3])
if err != nil {
return Rule{}, fmt.Errorf("error parsing maxquantity value %s: %w", prop[3], err)
}
maxQuantity = mxQty
rule = strings.ReplaceAll(rule, prop[0], "")
}
// Sanitize again, just in case we messed up the rule while parsing maxquantity
rule = sanitizeLine(rule)
if rule == "" {
return Rule{}, ErrEmptyRule
}
stages := [3]string{}
for i, stg := range strings.Split(rule, "#") {
stages[i] = strings.TrimSpace(stg)
}
return Rule{
RawLine: rawRule,
Stages: stages,
Filename: filename,
LineNumber: lineNumber,
Enabled: true,
maxQuantity: maxQuantity,
}, nil
}
func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
stage1 := r.Stages[0]
baseProperties := fixedPropsRegexp.FindAllStringSubmatch(stage1, -1)
for _, prop := range baseProperties {
switch prop[2] {
case "type":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", typeAliases[it.TypeAsString()]))
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", typeAliases[prop[4]]))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "quality":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", it.Quality))
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", qualityAliases[prop[4]]))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "class":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", it.Desc().Tier()))
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", classAliases[prop[4]]))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "name":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", item.GetIDByName(string(it.Name))))
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", item.GetIDByName(prop[4])))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "flag":
partialReplace := strings.ReplaceAll(prop[0], fmt.Sprintf("[%s]", prop[2]), fmt.Sprintf("%d", map[bool]int{true: 1, false: 0}[it.Ethereal]))
partialReplace = strings.ReplaceAll(partialReplace, prop[4], fmt.Sprintf("%d", 1))
stage1 = strings.ReplaceAll(stage1, prop[0], partialReplace)
case "color":
// TODO: Not supported yet
}
}
// Let's evaluate first stage
stage1Result, err := expr.Eval(stage1, nil)
if err != nil {
return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err)
}
// Let's go with other stats now
// TODO: properties are missing (enhanceddefense, enhanceddamage, etc)
stage2 := r.Stages[1]
stage2Result := true
if stage2 != "" {
for _, statName := range statsRegexp.FindAllStringSubmatch(stage2, -1) {
statData, found := statAliases[statName[1]]
if !found {
return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName[1])
}
// Exception for enhanceddefense, is not accurate
if statName[0] == "[enhanceddefense]" {
enhancedDefense := r.calculateEnhancedDefense(it)
stage2 = strings.ReplaceAll(stage2, statName[0], fmt.Sprintf("%d", enhancedDefense))
continue
}
layer := 0
if len(statData) > 1 {
layer = statData[1]
}
itemStat, _ := it.FindStat(stat.ID(statData[0]), layer)
// By default, value will be 0 is stat is not found, it's okay for evaluation purposes.
stage2 = strings.ReplaceAll(stage2, statName[0], fmt.Sprintf("%d", itemStat.Value))
}
res, err := expr.Eval(stage2, nil)
if err != nil {
return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err)
}
stage2Result = res.(bool)
}
// 100% rule match, we can return here
if stage1Result.(bool) && stage2Result {
return RuleResultFullMatch, nil
}
// If Stage 1 matches and the item is NOT identified, let's return a partial match, we can not be 100% sure if all the stats are matching
if stage1Result.(bool) && !it.Identified {
return RuleResultPartial, nil
}
return RuleResultNoMatch, nil
}
// MaxQuantity returns the maximum quantity of items that character can have, 0 means no limit
func (r Rule) MaxQuantity() int {
return r.maxQuantity
}
func (r Rule) calculateEnhancedDefense(i data.Item) int {
defenseStat, found := i.FindStat(stat.Defense, 0)
// This is special case for weapons and other stuff without base Defense and with EnhancedDefense, we can return it directly
// since EnhancedDefense it's a stat.
if !found {
if edStat, edStatFound := i.FindStat(stat.EnhancedDefense, 0); edStatFound {
return edStat.Value
}
}
// Make it only work for white base items
if i.Quality != item.QualitySuperior {
return 0
}
if i.TypeAsString() != "armor" {
return 0
}
itemTypeMaxDefense := i.Desc().MaxDefense
if defenseStat.Value <= itemTypeMaxDefense {
return 0
}
res := float64(defenseStat.Value-itemTypeMaxDefense) / float64(itemTypeMaxDefense) * 100
return int(math.Ceil(res))
}

213
pkg/nip/rule_test.go Normal file
View File

@@ -0,0 +1,213 @@
package nip
import (
"testing"
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item"
"github.com/hectorgimenez/d2go/pkg/data/stat"
"github.com/stretchr/testify/require"
)
func TestRule_Evaluate(t *testing.T) {
type fields struct {
RawLine string
Filename string
LineNumber int
Enabled bool
}
type args struct {
item data.Item
}
tests := []struct {
name string
fields fields
args args
want RuleResult
wantErr bool
}{
{
name: "Basic rule with posion dmg, ethereal is not specified as a condition so it should be ignored",
fields: fields{
RawLine: "[name] == smallcharm && [quality] == magic # (([poisonlength]*25)*[poisonmaxdam])/256 >= 123",
Filename: "test.nip",
LineNumber: 1,
Enabled: true,
},
args: args{
item: data.Item{
Name: "SmAlLCharM",
Quality: item.QualityMagic,
Ethereal: true,
Stats: []stat.Data{
{ID: stat.PoisonLength, Value: 20},
{ID: stat.PoisonMaxDamage, Value: 100},
},
},
},
want: RuleResultFullMatch,
},
{
name: "Complex rule with flags",
fields: fields{
RawLine: "[type] == armor && [quality] <= superior && [flag] != ethereal # ([itemmaxdurabilitypercent] == 0 || [itemmaxdurabilitypercent] == 15) && ([sockets] == 0 || [sockets] == 3 || [sockets] == 4)",
Filename: "test.nip",
LineNumber: 1,
Enabled: true,
},
args: args{
item: data.Item{
Name: "mageplate",
Quality: item.QualitySuperior,
Ethereal: false,
Stats: []stat.Data{
{ID: stat.MaxDurabilityPercent, Value: 15},
{ID: stat.NumSockets, Value: 4},
},
},
},
want: RuleResultFullMatch,
},
{
name: "Armor with +3 Sorc skills",
fields: fields{
RawLine: "[type] == armor # [sorceressskills] >= 3",
Enabled: true,
},
args: args{
item: data.Item{
Name: "mageplate",
Stats: []stat.Data{
{ID: stat.AddClassSkills, Value: 3, Layer: 1},
},
},
},
want: RuleResultFullMatch,
},
{
name: "Armor with +3 Glacial Spike",
fields: fields{
RawLine: "[type] == armor # [skillglacialspike] >= 3",
Enabled: true,
},
args: args{
item: data.Item{
Name: "mageplate",
Stats: []stat.Data{
{ID: stat.SingleSkill, Value: 3, Layer: 55},
},
},
},
want: RuleResultFullMatch,
},
{
name: "Ensure [color] returns error, not supported yet",
fields: fields{
RawLine: "[type] == armor && [color] == 1000 && [quality] == magic",
Enabled: true,
},
args: args{
item: data.Item{
Name: "mageplate",
Quality: item.QualityMagic,
},
},
wantErr: true,
},
{
name: "Unid item matching base stats should return partial match",
fields: fields{
RawLine: "[type] == armor && [quality] == magic # [defense] == 200",
Enabled: true,
},
args: args{
item: data.Item{
Identified: false,
Name: "mageplate",
Quality: item.QualityMagic,
},
},
want: RuleResultPartial,
},
{
name: "Basic rule without stats or maxquantity",
fields: fields{
RawLine: "[type] == armor && [quality] == magic # #",
Enabled: true,
},
args: args{
item: data.Item{
Identified: false,
Name: "mageplate",
Quality: item.QualityMagic,
},
},
want: RuleResultFullMatch,
},
{
name: "Basic rule for a white superior item with enhanceddefense",
fields: fields{
RawLine: "[type] == armor && [quality] == superior # [enhanceddefense] >= 15 #",
Enabled: true,
},
args: args{
item: data.Item{
Identified: true,
ID: 373,
Name: "mageplate",
Quality: item.QualitySuperior,
Stats: []stat.Data{
{ID: stat.Defense, Value: 301},
},
},
},
want: RuleResultFullMatch,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, err := New(tt.fields.RawLine, tt.fields.Filename, tt.fields.LineNumber)
require.NoError(t, err)
got, err := r.Evaluate(tt.args.item)
if !tt.wantErr {
require.NoError(t, err)
require.Equal(t, tt.want, got)
} else {
require.Error(t, err)
}
})
}
}
func TestNew(t *testing.T) {
type args struct {
rawRule string
filename string
lineNumber int
}
tests := []struct {
name string
args args
want Rule
wantErr bool
}{
{
name: "Enhanced Damage should throw an error",
args: args{
rawRule: "[type] == armor # [enhanceddamage] >= 15",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := New(tt.args.rawRule, tt.args.filename, tt.args.lineNumber)
if tt.wantErr {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NotNil(t, got)
}
})
}
}