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:
@@ -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 ????"},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user