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
This commit is contained in:
elb
2025-02-21 11:11:27 -05:00
committed by GitHub
parent 438f80b911
commit 5455fd9772
5 changed files with 147 additions and 45 deletions

View File

@@ -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 ????"},

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}