From 5455fd977276a9dd405d56cc5849a972ee8de8ea Mon Sep 17 00:00:00 2001 From: elb <51070858+elobo91@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:11:27 -0500 Subject: [PATCH] Fix broken negative stats, update some stat description (#79) * Fix broken negative stats, update some stat description * ### * #2 * curses is necro, frw on armor is velocity percent ( unused) * Update rules to accept negative value fix a bug with unique grand charm tz [name] == grandcharm && [quality] == unique # ([fireresist] + [coldresist] + [lightresist]) >= -75 Single stat rules like [coldresist] >= -75 require the stat to exist expressions like ([fireresist] + [coldresist] + [lightresist]) >= -75 treat missing stats as 0 Unidentified items with any stat requirements need identification first * LevelReq + identifiedName added for all Crafted items * Fix issue with rule.go * Update stat throw damage and basename: GrandCharm -- Grand Charm * Adjusting rule for ItemLevelReq = 0 * update rule again --- pkg/data/stat/stats.go | 42 +++++++++++++----------- pkg/memory/game_reader.go | 12 ++++--- pkg/memory/item.go | 47 ++++++++++++++++++++++++--- pkg/memory/process.go | 23 +++++++++++++ pkg/nip/rule.go | 68 ++++++++++++++++++++++++++++++--------- 5 files changed, 147 insertions(+), 45 deletions(-) diff --git a/pkg/data/stat/stats.go b/pkg/data/stat/stats.go index 7f8b69f..a1d239a 100644 --- a/pkg/data/stat/stats.go +++ b/pkg/data/stat/stats.go @@ -47,7 +47,7 @@ const ( Gold StashGold EnhancedDefense - EnhancedDamageMax + EnhancedDamageMin EnhancedDamage AttackRating ChanceToBlock @@ -411,7 +411,7 @@ var StringStats = []string{ "gold", "stashgold", "enhanceddefense", - "enhanceddamagemax", + "enhanceddamagemin", "enhanceddamage", "attackrating", "chancetoblock", @@ -779,11 +779,11 @@ var StatStringMap = map[int]map[int]string{ 13: {0: "Experience: #"}, 14: {0: "Gold: #"}, 15: {0: "Gold in Bank: #"}, - 16: {0: "+#% Enhanced Defense"}, // enhanceddefense - 17: {0: "+#% Enhanced Maximum Damage (Based on Character Level)"}, // itemmaxdamagepercent - 18: {0: "+#% Enhanced Damage"}, // enhanceddamage - 19: {0: "+# to Attack Rating"}, // tohit - 20: {0: "#% Increased Chance of Blocking"}, // toblock + 16: {0: "+#% Enhanced Defense"}, // enhanceddefense + 17: {0: "+#% Enhanced Damage (Max)"}, // enhanceddamagemax + 18: {0: "+#% Enhanced Damage (Min)"}, // enhanceddamagemin + 19: {0: "+# to Attack Rating"}, // tohit + 20: {0: "#% Increased Chance of Blocking"}, // toblock 21: { 0: "#-# Damage", // mindamage 1: "+# to Minimum Damage", // plusmindamage @@ -839,13 +839,13 @@ var StatStringMap = map[int]map[int]string{ 64: {0: "stamdrainmindam ????"}, 65: {0: "stamdrainmaxdam ????"}, 66: {0: "stunlength ????"}, - 67: {0: "+#% Faster Run/Walk"}, + 67: {0: "#% Velocity"}, 68: {0: "#"}, // attackrate 69: {0: "otheranimrate ????"}, 70: {0: "Quantity: #"}, 71: {0: "Value: #"}, 72: {0: "Durability: # of #"}, // durability - 73: {0: "#"}, // maxdurability + 73: {0: "# Max Durability"}, // maxdurability 74: {0: "Replenish Life +#"}, // hpregen 75: {0: "Increase Maximum Durability #%"}, //itemmaxdurabilitypercent 76: {0: "Increase Maximum Life #%"}, //itemmaxmanapercent @@ -1178,8 +1178,8 @@ var StatStringMap = map[int]map[int]string{ 156: {0: "Piercing Attack"}, // itempierce 157: {0: "Fires Magic Arrows"}, // itemmagicarrow 158: {0: "Fires Explosive Arrows or Bolts"}, // itemexplosivearrow - 159: {0: "itemthrowmindamage ????"}, - 160: {0: "itemthrowmaxdamage ????"}, + 159: {0: "# To Minimum Damage"}, + 160: {0: "+# Maximum Damage"}, 161: {0: "itemskillhandofathena ????"}, 162: {0: "itemskillstaminapercent ????"}, 163: {0: "itemskillpassivestaminapercent ????"}, @@ -1214,7 +1214,7 @@ var StatStringMap = map[int]map[int]string{ 8: "+# to Fire Skills (Sorceress only)", // fireskilltab 9: "+# to Lightning Skills (Sorceress only)", // lightningskilltab 10: "+# to Cold Skills (Sorceress only)", // coldskilltab - 16: "+# to Curses Skills (Sorceress only)", // cursesskilltab + 16: "+# to Curses Skills (Necromancer only)", // cursesskilltab 17: "+# to Poison and Bone Skills (Necromancer only)", // poisonandboneskilltab 18: "+# to Summoning Skills (Necromancer only)", // necromancersummoningskilltab 24: "+# to Paladin Combat Skills (Paladin only)", // palicombatskilltab @@ -1237,8 +1237,9 @@ var StatStringMap = map[int]map[int]string{ 193: {0: "Monster Magic Immunity is Sundered"}, // itempiercemagicimmunity 194: {0: "Socketed (#)"}, 195: { - 1: "#% Chance to cast level # [Skill] on attack", //itemskillonattack - 2: "itemskillonattacklevel ????", + 1: "#% Chance to cast level # [Skill] on attack", //itemskillonattack + 2: "itemskillonattacklevel ????", + 3395: "#% Chance to cast level 3 Chain Lightning on attack", }, 196: { 1: "#% Chance to cast level # [Skill] when you Kill an Enemy", //itemskillonkill @@ -1259,15 +1260,18 @@ var StatStringMap = map[int]map[int]string{ }, 200: {0: "unused200 ????"}, 201: { - 1: "#% Chance to cast level # [Skill] when struck", //itemskillongethit - 2: "itemskillongethitlevel ????", + 1: "#% Chance to cast level # [Skill] when struck", //itemskillongethit + 2: "itemskillongethitlevel ????", + 5903: "#% Chance to cast level 15 Poison Nova when struck", + 7751: "#% Chance to cast level 7 Fist of Heavens when struck", }, 202: {0: "unused202 ????"}, 203: {0: "unused203 ????"}, 204: { - 1: "itemchargedskill ????", - 2: "itemchargedskilllevel ????", - 3461: "Teleport (charged)", + 1: "itemchargedskill ????", + 2: "itemchargedskilllevel ????", + 3461: "Teleport (charged)", + 17795: "Venom level 3 (charged)", }, 205: {0: "unused204 ????"}, 206: {0: "unused205 ????"}, diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index 395b0b4..1a42b00 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -146,14 +146,16 @@ func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats { } var stats = make([]stat.Data, 0) + statBuffer := gd.Process.ReadBytesFromMemory(uintptr(statList), statCount*10) for i := 0; i < int(statCount); i++ { offset := uint(i * 8) + statLayer := ReadUIntFromBuffer(statBuffer, offset, Uint16) statEnum := ReadUIntFromBuffer(statBuffer, offset+0x2, Uint16) - statValue := ReadUIntFromBuffer(statBuffer, offset+0x4, Uint32) + statValue := ReadIntFromBuffer(statBuffer, offset+0x4, Uint32) - value := int(statValue) + value := statValue switch stat.ID(statEnum) { case stat.Life, stat.MaxLife, @@ -161,16 +163,16 @@ func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats { stat.MaxMana, stat.Stamina, stat.MaxStamina: - value = int(statValue >> 8) + value = statValue >> 8 case stat.ColdLength, stat.PoisonLength: - value = int(statValue / 25) + value = statValue / 25 case stat.DeadlyStrikePerLevel: value = int(float64(statValue) / .8) case stat.HitCausesMonsterToFlee: value = int(float64(statValue) / 1.28) case stat.AttackRatingUndeadPerLevel: - value = int(statValue / 2) + value = statValue / 2 case stat.MagicFindPerLevel, stat.ExtraGoldPerLevel, stat.DamageDemonPerLevel, diff --git a/pkg/memory/item.go b/pkg/memory/item.go index eb18598..a49ffab 100644 --- a/pkg/memory/item.go +++ b/pkg/memory/item.go @@ -182,7 +182,7 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD break } } - case item.QualityRare: + case item.QualityRare, item.QualityCrafted: // Set item name from rare affixes if prefix, exists := item.RarePrefixDesc[int(rarePrefix)]; exists { if suffix, exists := item.RareSuffixDesc[int(rareSuffix)]; exists { @@ -233,7 +233,7 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD if len(prefixParts) > 0 { nameParts = append(nameParts, prefixParts...) } - nameParts = append(nameParts, string(itm.Name)) + nameParts = append(nameParts, itm.Desc().Name) if len(suffixParts) > 0 { nameParts = append(nameParts, suffixParts...) } @@ -436,7 +436,7 @@ func (gd *GameReader) Inventory(rawPlayerUnits RawPlayerUnits, hover data.HoverD maxReq := calculateItemLevelReq(itm, baseDesc) // Check magic/rare affixes - if itm.Identified && (itm.Quality == item.QualityMagic || itm.Quality == item.QualityRare) { + if itm.Identified && (itm.Quality == item.QualityMagic || itm.Quality == item.QualityRare || itm.Quality == item.QualityCrafted) { maxReq = updateMaxReqFromAffixes(maxReq, itm.Affixes) } @@ -631,6 +631,43 @@ func calculateItemLevelReq(itm *data.Item, baseDesc item.Description) int { // Start with base item's requirement maxReq := baseDesc.RequiredLevel + // Handle Crafted items first as they have special calculation rules + if itm.Quality == item.QualityCrafted { + // Count number of affixes and find highest level requirement + affixCount := 0 + maxAffixReq := 0 + + for _, prefixID := range itm.Affixes.Magic.Prefixes { + if prefix, exists := item.MagicPrefixDesc[int(prefixID)]; exists && prefixID != 0 { + affixCount++ + if prefix.LevelReq > maxAffixReq { + maxAffixReq = prefix.LevelReq + } + } + } + for _, suffixID := range itm.Affixes.Magic.Suffixes { + if suffix, exists := item.MagicSuffixDesc[int(suffixID)]; exists && suffixID != 0 { + affixCount++ + if suffix.LevelReq > maxAffixReq { + maxAffixReq = suffix.LevelReq + } + } + } + + // Base calculation + levelReq := maxAffixReq + (3 * affixCount) + // Add 10 to final level req + levelReq += 10 + + // Cap at 98 if needed (Maximum possible) + if levelReq > 99 { + levelReq = 98 + } + + return levelReq + } + + // Handle Unique/Set items and their special upgrade cases if itm.Quality == item.QualityUnique || itm.Quality == item.QualitySet { itemCode := baseDesc.Code normalCode := baseDesc.NormalCode @@ -655,6 +692,7 @@ func calculateItemLevelReq(itm *data.Item, baseDesc item.Description) int { } } + // Apply upgrade requirements if itemCode == eliteCode { if originalCode == normalCode { // Double upgraded: Normal -> Exceptional -> Elite @@ -679,7 +717,7 @@ func calculateItemLevelReq(itm *data.Item, baseDesc item.Description) int { break } } - } else { // Set item + } else if itm.Quality == item.QualitySet { for _, setItemInfo := range item.SetItems { if setItemInfo.ID == int(itm.UniqueSetID) { if setItemInfo.LevelReq > maxReq { @@ -691,6 +729,5 @@ func calculateItemLevelReq(itm *data.Item, baseDesc item.Description) int { } } } - return maxReq } diff --git a/pkg/memory/process.go b/pkg/memory/process.go index 67bee9f..184e83b 100644 --- a/pkg/memory/process.go +++ b/pkg/memory/process.go @@ -20,6 +20,13 @@ type Process struct { moduleBaseSize uint32 } +const ( + Int8 = 1 // signed 8-bit integer + Int16 = 2 // signed 16-bit integer + Int32 = 4 // signed 32-bit integer + Int64 = 8 // signed 64-bit integer +) + func NewProcess() (Process, error) { module, err := getGameModule() if err != nil { @@ -144,6 +151,22 @@ func bytesToUint(bytes []byte, size IntType) uint { return 0 } +func ReadIntFromBuffer(bytes []byte, offset uint, size IntType) int { + return bytesToInt(bytes[offset:offset+uint(size)], size) +} +func bytesToInt(bytes []byte, size IntType) int { + switch size { + case Int8: + return int(int8(bytes[0])) + case Int16: + return int(int16(binary.LittleEndian.Uint16(bytes))) + case Int32: + return int(int32(binary.LittleEndian.Uint32(bytes))) + case Int64: + return int(int64(binary.LittleEndian.Uint64(bytes))) + } + return 0 +} func (p Process) ReadStringFromMemory(address uintptr, size uint) string { if size == 0 { diff --git a/pkg/nip/rule.go b/pkg/nip/rule.go index 451a835..d097bfb 100644 --- a/pkg/nip/rule.go +++ b/pkg/nip/rule.go @@ -136,6 +136,7 @@ func NewRule(rawRule string, filename string, lineNumber int) (Rule, error) { } func (r Rule) Evaluate(it data.Item) (RuleResult, error) { + // Stage 1: Basic properties evaluation stage1Props := make(map[string]int) for prop := range fixedPropsList { switch prop { @@ -185,39 +186,74 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) { return RuleResultNoMatch, nil } - stage2Result := true - if r.stage2 != nil { - stage2Props := make(map[string]int) + // If we have no stage2 (no stat requirements), allow full match even for unidentified items + if r.stage2 == nil { + return RuleResultFullMatch, nil + } + + // From here on we have stat requirements - return partial match for unidentified items + if !it.Identified { + return RuleResultPartial, nil + } + + stage2Props := make(map[string]int) + + // We only want to default to 0 if we find at least one of the required resists + hasAnyResist := false + stage2 := strings.ToLower(strings.Split(r.RawLine, "#")[1]) + isResistSum := strings.Contains(stage2, "resist") && (strings.Contains(stage2, "+") || strings.Contains(stage2, "-")) + + // First pass - check if we have any of the required resists + if isResistSum { for _, statName := range r.requiredStats { statData, found := statAliases[statName] if !found { - return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName) + continue } - layer := 0 if len(statData) > 1 { layer = statData[1] } - itemStat, _ := it.FindStat(stat.ID(statData[0]), layer) - stage2Props[statName] = itemStat.Value + if _, found := it.FindStat(stat.ID(statData[0]), layer); found { + hasAnyResist = true + break + } + } + } + + // Second pass - evaluate stats + for _, statName := range r.requiredStats { + statData, found := statAliases[statName] + if !found { + return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName) } - res, err := expr.Run(r.stage2, stage2Props) - if err != nil { - return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err) + layer := 0 + if len(statData) > 1 { + layer = statData[1] } - stage2Result = res.(bool) + + itemStat, found := it.FindStat(stat.ID(statData[0]), layer) + if found { + stage2Props[statName] = itemStat.Value + } else if isResistSum && hasAnyResist { + // Only default to 0 for resist sums if we found at least one resist + stage2Props[statName] = 0 + } else { + return RuleResultNoMatch, nil + } + } + + res, err := expr.Run(r.stage2, stage2Props) + if err != nil { + return RuleResultNoMatch, fmt.Errorf("error evaluating rule: %w", err) } // 100% rule match, we can return here - if stage1Result.(bool) && stage2Result { + if res.(bool) { 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 }