bugfixing, performance improvements and added casting frames formula

This commit is contained in:
Héctor Giménez
2024-05-09 20:27:31 +09:00
parent 0c6a2c17ab
commit c398062b57
14 changed files with 293 additions and 216 deletions

View File

@@ -10,11 +10,11 @@ imported, along with data structures and some other tools.
- [data](https://github.com/hectorgimenez/d2go/tree/main/pkg/data) - D2R Game data structures - [data](https://github.com/hectorgimenez/d2go/tree/main/pkg/data) - D2R Game data structures
- [memory](https://github.com/hectorgimenez/d2go/tree/main/pkg/memory) - D2R memory reader (it provides the data - [memory](https://github.com/hectorgimenez/d2go/tree/main/pkg/memory) - D2R memory reader (it provides the data
structures) structures)
- [nip](https://github.com/hectorgimenez/d2go/tree/main/pkg/nip) - A very basic NIP file parser - [nip](https://github.com/hectorgimenez/d2go/tree/main/pkg/nip) - [NIP](https://github.com/blizzhackers/pickits/blob/master/NipGuide.md) file parser and rule evaluator, used by the itemwatcher item filter.
- [itemfilter](https://github.com/hectorgimenez/d2go/tree/main/pkg/itemfilter) - Based on game data, it provides an item
pickup filtering
### Tools ### Tools
- [cmd/itemwatcher](https://github.com/hectorgimenez/d2go/tree/main/cmd/itemwatcher) - Small tool that plays a sound - [cmd/itemwatcher](https://github.com/hectorgimenez/d2go/tree/main/cmd/itemwatcher) - Small tool that plays a sound
when an item passing the filtering process is dropped when an item passing the filtering process is dropped
- [cmd/txttocode](https://github.com/hectorgimenez/d2go/tree/main/cmd/txttocode) - Static code generator, takes game .txt files and generates Go code to be used by the data package, it provides
static data like item names, item types, skill details, etc.

2
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/hectorgimenez/d2go
go 1.22 go 1.22
require ( require (
github.com/expr-lang/expr v1.16.6 github.com/expr-lang/expr v1.16.7
github.com/faiface/beep v1.1.0 github.com/faiface/beep v1.1.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
golang.org/x/sys v0.20.0 golang.org/x/sys v0.20.0

4
go.sum
View File

@@ -2,8 +2,8 @@ github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.16.6 h1:u1mrPXbwHtWAih5ZP24ZDG+ht8CB5xB0aBvagkAWPY0= github.com/expr-lang/expr v1.16.7 h1:gCIiHt5ODA0xIaDbD0DPKyZpM9Drph3b3lolYAYq2Kw=
github.com/expr-lang/expr v1.16.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/expr-lang/expr v1.16.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c= github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4= github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=

View File

@@ -1,6 +1,7 @@
package data package data
import ( import (
"math"
"strings" "strings"
"github.com/hectorgimenez/d2go/pkg/data/quest" "github.com/hectorgimenez/d2go/pkg/data/quest"
@@ -137,8 +138,8 @@ type PlayerUnit struct {
ID UnitID ID UnitID
Area area.ID Area area.ID
Position Position Position Position
Stats map[stat.ID]int Stats stat.Stats
BaseStats map[stat.ID]int BaseStats stat.Stats
Skills map[skill.ID]skill.Points Skills map[skill.ID]skill.Points
States state.States States state.States
Class Class Class Class
@@ -147,21 +148,76 @@ type PlayerUnit struct {
AvailableWaypoints []area.ID // Is only filled when WP menu is open and only for the specific selected tab AvailableWaypoints []area.ID // Is only filled when WP menu is open and only for the specific selected tab
} }
func (pu PlayerUnit) FindStat(id stat.ID, layer int) (stat.Data, bool) {
st, found := pu.Stats.FindStat(id, layer)
if found {
return st, true
}
return pu.BaseStats.FindStat(id, layer)
}
func (pu PlayerUnit) MaxGold() int { func (pu PlayerUnit) MaxGold() int {
return goldPerLevel * pu.Stats[stat.Level] lvl, _ := pu.FindStat(stat.Level, 0)
return goldPerLevel * lvl.Value
} }
// TotalGold returns the amount of gold, including inventory and stash // TotalGold returns the amount of gold, including inventory and stash
func (pu PlayerUnit) TotalGold() int { func (pu PlayerUnit) TotalGold() int {
return pu.Stats[stat.Gold] + pu.Stats[stat.StashGold] gold, _ := pu.FindStat(stat.Gold, 0)
stashGold, _ := pu.FindStat(stat.StashGold, 0)
return gold.Value + stashGold.Value
} }
func (pu PlayerUnit) HPPercent() int { func (pu PlayerUnit) HPPercent() int {
return int((float64(pu.Stats[stat.Life]) / float64(pu.Stats[stat.MaxLife])) * 100) life, _ := pu.FindStat(stat.Life, 0)
maxLife, _ := pu.FindStat(stat.MaxLife, 0)
return int((float64(life.Value) / float64(maxLife.Value)) * 100)
} }
func (pu PlayerUnit) MPPercent() int { func (pu PlayerUnit) MPPercent() int {
return int((float64(pu.Stats[stat.Mana]) / float64(pu.Stats[stat.MaxMana])) * 100) mana, _ := pu.FindStat(stat.Mana, 0)
maxMana, _ := pu.FindStat(stat.MaxMana, 0)
return int((float64(mana.Value) / float64(maxMana.Value)) * 100)
}
func (pu PlayerUnit) CastingFrames() int {
// Formula detailed here: https://diablo.fandom.com/wiki/Faster_Cast_Rate
fcr, _ := pu.FindStat(stat.FasterCastRate, 0)
baseAnimation := pu.getCastingBaseSpeed()
ecfr := math.Floor(float64(fcr.Value*120) / float64(fcr.Value+120))
if ecfr > 75 {
ecfr = 75
}
// Animation speed for druids is different
cf := math.Ceil(256*baseAnimation/math.Floor(float64(256*(100+ecfr))/100.0)) - 1
return int(cf)
}
func (pu PlayerUnit) getCastingBaseSpeed() float64 {
// TODO: Implement logic for Lightning?
switch pu.Class {
case Amazon:
return 20
case Assassin:
return 17
case Barbarian:
return 14
case Necromancer, Paladin:
return 16
case Druid:
return 15
case Sorceress:
return 14
}
return 16
} }
func (pu PlayerUnit) HasDebuff() bool { func (pu PlayerUnit) HasDebuff() bool {

View File

@@ -54,6 +54,7 @@ type Item struct {
Quality item.Quality Quality item.Quality
Position Position Position Position
Location item.Location Location item.Location
Page int // Used for shared stash
Ethereal bool Ethereal bool
IsHovered bool IsHovered bool
BaseStats stat.Stats BaseStats stat.Stats

View File

@@ -23,10 +23,14 @@ func (gd *GameReader) GetData() data.Data {
gd.offset = calculateOffsets(gd.Process) gd.offset = calculateOffsets(gd.Process)
} }
roster := gd.getRoster() rawPlayerUnits := gd.GetRawPlayerUnits()
playerUnitPtr, corpse := gd.GetPlayerUnitPtr(roster) roster := gd.getRoster(rawPlayerUnits)
mainPlayerUnit := rawPlayerUnits.GetMainPlayer()
if mainPlayerUnit.Address == 0 {
return gd.previousRead
}
pu := gd.GetPlayerUnit(playerUnitPtr) pu := gd.GetPlayerUnit(mainPlayerUnit)
hover := gd.hoveredData() hover := gd.hoveredData()
// Quests // Quests
@@ -36,11 +40,16 @@ func (gd *GameReader) GetData() data.Data {
gameQuestsBytes = gameQuestsBytes[3:] gameQuestsBytes = gameQuestsBytes[3:]
corpseUnit := rawPlayerUnits.GetCorpse()
d := data.Data{ d := data.Data{
Corpse: corpse, Corpse: data.Corpse{
Found: corpseUnit.Address != 0,
IsHovered: corpseUnit.IsHovered,
Position: corpseUnit.Position,
},
Monsters: gd.Monsters(pu.Position, hover), Monsters: gd.Monsters(pu.Position, hover),
PlayerUnit: pu, PlayerUnit: pu,
Items: gd.Items(pu, hover), Items: gd.Items(rawPlayerUnits, hover),
Objects: gd.Objects(pu.Position, hover), Objects: gd.Objects(pu.Position, hover),
OpenMenus: gd.openMenus(), OpenMenus: gd.openMenus(),
Roster: roster, Roster: roster,
@@ -50,19 +59,15 @@ func (gd *GameReader) GetData() data.Data {
KeyBindings: gd.GetKeyBindings(), KeyBindings: gd.GetKeyBindings(),
} }
if playerUnitPtr == 0 {
return gd.previousRead
}
gd.previousRead = d gd.previousRead = d
return d return d
} }
func (gd *GameReader) InGame() bool { func (gd *GameReader) InGame() bool {
pu, _ := gd.GetPlayerUnitPtr([]data.RosterMember{}) playerUnits := gd.GetRawPlayerUnits()
return pu > 0 return playerUnits.GetMainPlayer().UnitID > 0
} }
func (gd *GameReader) openMenus() data.OpenMenus { func (gd *GameReader) openMenus() data.OpenMenus {
@@ -108,8 +113,9 @@ func (gd *GameReader) hoveredData() data.HoverData {
} }
func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats { func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats {
statList := gd.Process.ReadUInt(statListPtr, Uint64) statsListBuffer := gd.ReadBytesFromMemory(statListPtr, 0x10)
statCount := gd.Process.ReadUInt(statListPtr+0x8, Uint64) statList := ReadUIntFromBuffer(statsListBuffer, 0, Uint64)
statCount := ReadUIntFromBuffer(statsListBuffer, 0x08, Uint64)
if statCount == 0 { if statCount == 0 {
return []stat.Data{} return []stat.Data{}
} }
@@ -129,12 +135,38 @@ func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats {
stat.Mana, stat.Mana,
stat.MaxMana, stat.MaxMana,
stat.Stamina, stat.Stamina,
stat.LifePerLevel, stat.MaxStamina:
stat.ManaPerLevel:
value = int(statValue >> 8) value = int(statValue >> 8)
case stat.ColdLength, case stat.ColdLength,
stat.PoisonLength: stat.PoisonLength:
value = int(statValue / 25) value = int(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)
case stat.MagicFindPerLevel,
stat.ExtraGoldPerLevel,
stat.DamageDemonPerLevel,
stat.DamageUndeadPerLevel,
stat.DefensePerLevel,
stat.MaxDamagePerLevel,
stat.MaxDamagePercentPerLevel,
stat.StrengthPerLevel,
stat.DexterityPerLevel,
stat.VitalityPerLevel,
stat.ThornsPerLevel:
value = int(statValue / 8)
case stat.LifePerLevel,
stat.ManaPerLevel:
value = int(statValue / 2048)
case stat.ReplenishDurability, stat.ReplenishQuantity:
value = 2 / int(statValue)
case stat.RegenStaminaPerLevel:
value = int(statValue) * 10
case stat.LevelRequirePercent:
value = int(statValue) * -1
} }
stats = append(stats, stat.Data{ stats = append(stats, stat.Data{

View File

@@ -0,0 +1,20 @@
package memory
import (
"testing"
"github.com/stretchr/testify/require"
)
func BenchmarkDataReader(b *testing.B) {
process, err := NewProcess()
require.NoError(b, err)
gr := NewGameReader(process)
gr.GetData()
b.ResetTimer()
for n := 0; n < b.N; n++ {
gr.GetData()
}
}

View File

@@ -1,18 +1,32 @@
package memory package memory
import ( import (
"slices"
"sort" "sort"
"github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item" "github.com/hectorgimenez/d2go/pkg/data/item"
"github.com/hectorgimenez/d2go/pkg/data/stat" "github.com/hectorgimenez/d2go/pkg/data/stat"
"github.com/hectorgimenez/d2go/pkg/data/state"
"github.com/hectorgimenez/d2go/pkg/utils" "github.com/hectorgimenez/d2go/pkg/utils"
) )
func (gd *GameReader) Items(pu data.PlayerUnit, hover data.HoverData) data.Items { func (gd *GameReader) Items(rawPlayerUnits RawPlayerUnits, hover data.HoverData) data.Items {
mainPlayer := rawPlayerUnits.GetMainPlayer()
baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024) baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024)
unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8) unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8)
stashPlayerUnits := make(map[uint]RawPlayerUnit)
stashPlayerUnitOrder := make([]uint, 0)
for _, pu := range rawPlayerUnits {
if pu.States.HasState(state.Sharedstash) {
order := gd.ReadUInt(pu.Address+0xD8, Uint64)
stashPlayerUnitOrder = append(stashPlayerUnitOrder, order)
stashPlayerUnits[order] = pu
}
}
slices.Sort(stashPlayerUnitOrder)
items := data.Items{} items := data.Items{}
belt := data.Belt{} belt := data.Belt{}
for i := 0; i < 128; i++ { for i := 0; i < 128; i++ {
@@ -73,31 +87,31 @@ func (gd *GameReader) Items(pu data.PlayerUnit, hover data.HoverData) data.Items
location := item.LocationUnknown location := item.LocationUnknown
switch itemLoc { switch itemLoc {
case 0: case 0:
if 0x00002000&flags != 0 || (data.UnitID(itemOwnerNPC) != pu.ID) { // Offline only
if itemOwnerNPC == 2 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[0]].UnitID) {
location = item.LocationSharedStash1
break
}
if itemOwnerNPC == 3 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[1]].UnitID) {
location = item.LocationSharedStash2
break
}
if itemOwnerNPC == 4 || itemOwnerNPC == uint(stashPlayerUnits[stashPlayerUnitOrder[2]].UnitID) {
location = item.LocationSharedStash3
break
}
if 0x00002000&flags != 0 && itemOwnerNPC == 4294967295 {
location = item.LocationVendor location = item.LocationVendor
break break
} else if invPage == 0 { } else if invPage == 0 {
location = item.LocationInventory location = item.LocationInventory
break break
} }
if data.UnitID(itemOwnerNPC) == pu.ID || itemOwnerNPC == 1 { if data.UnitID(itemOwnerNPC) == mainPlayer.UnitID || itemOwnerNPC == 1 {
location = item.LocationStash location = item.LocationStash
break break
} }
// Offline only
if itemOwnerNPC == 2 {
location = item.LocationSharedStash1
break
}
if itemOwnerNPC == 3 {
location = item.LocationSharedStash2
break
}
if itemOwnerNPC == 4 {
location = item.LocationSharedStash3
break
}
case 1: case 1:
location = item.LocationEquipped location = item.LocationEquipped
if itm.Type().Code == item.TypeBelt { if itm.Type().Code == item.TypeBelt {
@@ -115,10 +129,13 @@ func (gd *GameReader) Items(pu data.PlayerUnit, hover data.HoverData) data.Items
itm.Location = location itm.Location = location
if location == item.LocationBelt { // We don't care about the items we don't know where they are, probably previous games or random crap
belt.Items = append(belt.Items, itm) if location != item.LocationUnknown {
} else { if location == item.LocationBelt {
items.AllItems = append(items.AllItems, itm) belt.Items = append(belt.Items, itm)
} else {
items.AllItems = append(items.AllItems, itm)
}
} }
itemUnitPtr = uintptr(gd.Process.ReadUInt(itemUnitPtr+0x150, Uint64)) itemUnitPtr = uintptr(gd.Process.ReadUInt(itemUnitPtr+0x150, Uint64))
@@ -128,8 +145,8 @@ func (gd *GameReader) Items(pu data.PlayerUnit, hover data.HoverData) data.Items
items.Belt = belt items.Belt = belt
sort.SliceStable(items.AllItems, func(i, j int) bool { sort.SliceStable(items.AllItems, func(i, j int) bool {
distanceI := utils.DistanceFromPoint(pu.Position, items.AllItems[i].Position) distanceI := utils.DistanceFromPoint(mainPlayer.Position, items.AllItems[i].Position)
distanceJ := utils.DistanceFromPoint(pu.Position, items.AllItems[j].Position) distanceJ := utils.DistanceFromPoint(mainPlayer.Position, items.AllItems[j].Position)
return distanceI < distanceJ return distanceI < distanceJ
}) })
@@ -142,8 +159,8 @@ func (gd *GameReader) getItemStats(statsListExPtr uintptr) (stat.Stats, stat.Sta
baseStats := gd.getStatsList(statsListExPtr + 0x30) baseStats := gd.getStatsList(statsListExPtr + 0x30)
flags := gd.Process.ReadUInt(statsListExPtr+0x1C, Uint64) flags := gd.Process.ReadUInt(statsListExPtr+0x1C, Uint64)
lastStatsList := uintptr(gd.Process.ReadUInt(statsListExPtr+0x70, Uint64)) lastStatsList := uintptr(gd.Process.ReadUInt(statsListExPtr+0x70, Uint64))
if (flags & 0x80000000) == 0 { if (flags & 0x80000000) == 0 {
return baseStats, fullStats return baseStats, fullStats
} }

View File

@@ -6,137 +6,74 @@ import (
"github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/area" "github.com/hectorgimenez/d2go/pkg/data/area"
"github.com/hectorgimenez/d2go/pkg/data/skill" "github.com/hectorgimenez/d2go/pkg/data/skill"
"github.com/hectorgimenez/d2go/pkg/data/stat"
"github.com/hectorgimenez/d2go/pkg/data/state" "github.com/hectorgimenez/d2go/pkg/data/state"
) )
func (gd *GameReader) GetPlayerUnitPtr(roster data.Roster) (playerUnitPtr uintptr, corpse data.Corpse) { func (gd *GameReader) GetRawPlayerUnits() RawPlayerUnits {
rawPlayerUnits := make(RawPlayerUnits, 0)
hover := gd.hoveredData()
for i := 0; i < 128; i++ { for i := 0; i < 128; i++ {
unitOffset := gd.offset.UnitTable + uintptr(i*8) unitOffset := gd.offset.UnitTable + uintptr(i*8)
playerUnitAddr := gd.Process.moduleBaseAddressPtr + unitOffset playerUnitAddr := gd.Process.moduleBaseAddressPtr + unitOffset
playerUnit := uintptr(gd.Process.ReadUInt(playerUnitAddr, Uint64)) playerUnit := uintptr(gd.Process.ReadUInt(playerUnitAddr, Uint64))
for playerUnit > 0 { for playerUnit > 0 {
unitID := gd.Process.ReadUInt(playerUnit+0x08, Uint32)
pInventory := playerUnit + 0x90 pInventory := playerUnit + 0x90
inventoryAddr := uintptr(gd.Process.ReadUInt(pInventory, Uint64)) inventoryAddr := uintptr(gd.Process.ReadUInt(pInventory, Uint64))
pPath := playerUnit + 0x38 pPath := playerUnit + 0x38
pathAddress := uintptr(gd.Process.ReadUInt(pPath, Uint64)) pathAddress := uintptr(gd.Process.ReadUInt(pPath, Uint64))
room1Ptr := uintptr(gd.Process.ReadUInt(pathAddress+0x20, Uint64))
room2Ptr := uintptr(gd.Process.ReadUInt(room1Ptr+0x18, Uint64))
levelPtr := uintptr(gd.Process.ReadUInt(room2Ptr+0x90, Uint64))
levelNo := gd.Process.ReadUInt(levelPtr+0x1F8, Uint32)
xPos := gd.Process.ReadUInt(pathAddress+0x02, Uint16) xPos := gd.Process.ReadUInt(pathAddress+0x02, Uint16)
yPos := gd.Process.ReadUInt(pathAddress+0x06, Uint16) yPos := gd.Process.ReadUInt(pathAddress+0x06, Uint16)
pUnitData := playerUnit + 0x10
playerNameAddr := uintptr(gd.Process.ReadUInt(pUnitData, Uint64))
name := gd.Process.ReadStringFromMemory(playerNameAddr, 0)
// Only current player has inventory expCharPtr := uintptr(gd.Process.ReadUInt(gd.moduleBaseAddressPtr+gd.offset.Expansion, Uint64))
if inventoryAddr > 0 && xPos > 0 && yPos > 0 { expChar := gd.Process.ReadUInt(expCharPtr+0x5C, Uint16)
expCharPtr := uintptr(gd.Process.ReadUInt(gd.moduleBaseAddressPtr+gd.offset.Expansion, Uint64)) isMainPlayer := gd.Process.ReadUInt(inventoryAddr+0x30, Uint16)
expChar := gd.Process.ReadUInt(expCharPtr+0x5C, Uint16) if expChar > 0 {
baseCheck := gd.Process.ReadUInt(inventoryAddr+0x30, Uint16) isMainPlayer = gd.Process.ReadUInt(inventoryAddr+0x70, Uint16)
if expChar > 0 {
baseCheck = gd.Process.ReadUInt(inventoryAddr+0x70, Uint16)
}
isCorpse := gd.Process.ReadUInt(playerUnit+0x1A6, Uint8)
if isCorpse == 1 {
unitID := gd.Process.ReadUInt(playerUnit+0x08, Uint32)
hover := gd.hoveredData()
corpse = data.Corpse{
Found: true,
IsHovered: hover.IsHovered && hover.UnitID == data.UnitID(unitID) && hover.UnitType == 0,
Position: data.Position{
X: int(xPos),
Y: int(yPos),
},
}
}
if baseCheck > 0 {
playerUnitPtr = playerUnit
} else {
pUnitData := playerUnit + 0x10
playerNameAddr := uintptr(gd.Process.ReadUInt(pUnitData, Uint64))
name := gd.Process.ReadStringFromMemory(playerNameAddr, 0)
for k, rm := range roster {
if name != rm.Name {
continue
}
roster[k] = data.RosterMember{
Name: name,
Area: rm.Area,
Position: data.Position{
X: int(xPos),
Y: int(yPos),
},
}
}
}
} }
isCorpse := gd.Process.ReadUInt(playerUnit+0x1A6, Uint8)
statsListExPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x88, Uint64))
states := gd.getStates(statsListExPtr)
rawPlayerUnits = append(rawPlayerUnits, RawPlayerUnit{
UnitID: data.UnitID(unitID),
Address: playerUnit,
Name: name,
IsMainPlayer: isMainPlayer > 0,
IsCorpse: isCorpse == 1 && inventoryAddr > 0 && xPos > 0 && yPos > 0,
Area: area.ID(levelNo),
Position: data.Position{
X: int(xPos),
Y: int(yPos),
},
IsHovered: hover.IsHovered && hover.UnitID == data.UnitID(unitID) && hover.UnitType == 0,
States: states,
})
playerUnit = uintptr(gd.Process.ReadUInt(playerUnit+0x150, Uint64)) playerUnit = uintptr(gd.Process.ReadUInt(playerUnit+0x150, Uint64))
} }
} }
return return rawPlayerUnits
} }
func (gd *GameReader) GetPlayerUnit(playerUnit uintptr) data.PlayerUnit { func (gd *GameReader) GetPlayerUnit(mainPlayerUnit RawPlayerUnit) data.PlayerUnit {
unitID := gd.Process.ReadUInt(playerUnit+0x08, Uint32)
// Read X and Y Positions
pPath := playerUnit + 0x38
pathAddress := uintptr(gd.Process.ReadUInt(pPath, Uint64))
xPos := gd.Process.ReadUInt(pathAddress+0x02, Uint16)
yPos := gd.Process.ReadUInt(pathAddress+0x06, Uint16)
// Player name
pUnitData := playerUnit + 0x10
playerNameAddr := uintptr(gd.Process.ReadUInt(pUnitData, Uint64))
name := gd.Process.ReadStringFromMemory(playerNameAddr, 0)
// Get Stats // Get Stats
statsListExPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x88, Uint64)) statsListExPtr := uintptr(gd.Process.ReadUInt(mainPlayerUnit.Address+0x88, Uint64))
baseStatPtr := gd.Process.ReadUInt(statsListExPtr+0x30, Uint64) baseStats := gd.getStatsList(statsListExPtr + 0x30)
baseStatsCount := gd.Process.ReadUInt(statsListExPtr+0x38, Uint64) stats := gd.getStatsList(statsListExPtr + 0x88)
statPtr := gd.Process.ReadUInt(statsListExPtr+0x88, Uint64)
statCount := gd.Process.ReadUInt(statsListExPtr+0x90, Uint64)
stats := map[stat.ID]int{}
for j := 0; j < int(statCount); j++ {
statOffset := uintptr(statPtr) + 0x2 + uintptr(j*8)
statNumber := gd.Process.ReadUInt(statOffset, Uint16)
statValue := gd.Process.ReadUInt(statOffset+0x02, Uint32)
switch stat.ID(statNumber) {
case stat.Life,
stat.MaxLife,
stat.Mana,
stat.MaxMana:
stats[stat.ID(statNumber)] = int(uint32(statValue) >> 8)
default:
stats[stat.ID(statNumber)] = int(statValue)
}
}
baseStats := map[stat.ID]int{}
for j := 0; j < int(baseStatsCount); j++ {
statOffset := uintptr(baseStatPtr) + 0x2 + uintptr(j*8)
statNumber := gd.Process.ReadUInt(statOffset, Uint16)
statValue := gd.Process.ReadUInt(statOffset+0x02, Uint32)
switch stat.ID(statNumber) {
case stat.Life,
stat.MaxLife,
stat.Mana,
stat.MaxMana:
baseStats[stat.ID(statNumber)] = int(uint32(statValue) >> 8)
default:
baseStats[stat.ID(statNumber)] = int(statValue)
}
}
// States (Buff, Debuff, Auras)
states := gd.getStates(statsListExPtr)
// Skills // Skills
skillListPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x100, Uint64)) skillListPtr := uintptr(gd.Process.ReadUInt(mainPlayerUnit.Address+0x100, Uint64))
skills := gd.getSkills(skillListPtr) skills := gd.getSkills(skillListPtr)
leftSkillPtr := gd.Process.ReadUInt(skillListPtr+0x08, Uint64) leftSkillPtr := gd.Process.ReadUInt(skillListPtr+0x08, Uint64)
@@ -148,14 +85,7 @@ func (gd *GameReader) GetPlayerUnit(playerUnit uintptr) data.PlayerUnit {
rightSkillId := uintptr(gd.Process.ReadUInt(rightSkillTxtPtr, Uint16)) rightSkillId := uintptr(gd.Process.ReadUInt(rightSkillTxtPtr, Uint16))
// Class // Class
class := data.Class(gd.Process.ReadUInt(playerUnit+0x174, Uint32)) class := data.Class(gd.Process.ReadUInt(mainPlayerUnit.Address+0x174, Uint32))
// Level number
pathPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x38, Uint64))
room1Ptr := uintptr(gd.Process.ReadUInt(pathPtr+0x20, Uint64))
room2Ptr := uintptr(gd.Process.ReadUInt(room1Ptr+0x18, Uint64))
levelPtr := uintptr(gd.Process.ReadUInt(room2Ptr+0x90, Uint64))
levelNo := gd.Process.ReadUInt(levelPtr+0x1F8, Uint32)
availableWPs := make([]area.ID, 0) availableWPs := make([]area.ID, 0)
// Probably there is a better place to pick up those values, since this seems to be very tied to the UI // Probably there is a better place to pick up those values, since this seems to be very tied to the UI
@@ -163,22 +93,20 @@ func (gd *GameReader) GetPlayerUnit(playerUnit uintptr) data.PlayerUnit {
for i := 0; i < 0x48; i = i + 8 { for i := 0; i < 0x48; i = i + 8 {
a := binary.LittleEndian.Uint32(wpList[i : i+4]) a := binary.LittleEndian.Uint32(wpList[i : i+4])
available := binary.LittleEndian.Uint32(wpList[i+4 : i+8]) available := binary.LittleEndian.Uint32(wpList[i+4 : i+8])
if available == 1 || area.ID(levelNo) == area.ID(a) { if available == 1 || mainPlayerUnit.Area == area.ID(a) {
availableWPs = append(availableWPs, area.ID(a)) availableWPs = append(availableWPs, area.ID(a))
} }
} }
d := data.PlayerUnit{ d := data.PlayerUnit{
Name: name, Name: mainPlayerUnit.Name,
ID: data.UnitID(unitID), ID: mainPlayerUnit.UnitID,
Area: area.ID(levelNo), Area: mainPlayerUnit.Area,
Position: data.Position{ Position: mainPlayerUnit.Position,
X: int(xPos),
Y: int(yPos),
},
Stats: stats, Stats: stats,
BaseStats: baseStats,
Skills: skills, Skills: skills,
States: states, States: mainPlayerUnit.States,
Class: class, Class: class,
LeftSkill: skill.ID(leftSkillId), LeftSkill: skill.ID(leftSkillId),
RightSkill: skill.ID(rightSkillId), RightSkill: skill.ID(rightSkillId),

39
pkg/memory/player_unit.go Normal file
View File

@@ -0,0 +1,39 @@
package memory
import (
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/area"
"github.com/hectorgimenez/d2go/pkg/data/state"
)
type RawPlayerUnit struct {
data.UnitID
Address uintptr
Name string
IsMainPlayer bool
IsCorpse bool
Area area.ID
Position data.Position
IsHovered bool
States state.States
}
type RawPlayerUnits []RawPlayerUnit
func (pu RawPlayerUnits) GetMainPlayer() RawPlayerUnit {
for _, p := range pu {
if p.IsMainPlayer {
return p
}
}
return RawPlayerUnit{}
}
func (pu RawPlayerUnits) GetCorpse() RawPlayerUnit {
for _, p := range pu {
if p.IsCorpse {
return p
}
}
return RawPlayerUnit{}
}

View File

@@ -4,10 +4,11 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"errors" "errors"
"golang.org/x/sys/windows"
"strings" "strings"
"syscall" "syscall"
"unsafe" "unsafe"
"golang.org/x/sys/windows"
) )
const moduleName = "d2r.exe" const moduleName = "d2r.exe"

View File

@@ -5,23 +5,41 @@ import (
"github.com/hectorgimenez/d2go/pkg/data/area" "github.com/hectorgimenez/d2go/pkg/data/area"
) )
func (gd *GameReader) getRoster() (roster []data.RosterMember) { func (gd *GameReader) getRoster(rawPlayerUnits RawPlayerUnits) (roster []data.RosterMember) {
partyStruct := uintptr(gd.Process.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.RosterOffset, Uint64)) partyStruct := uintptr(gd.Process.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.RosterOffset, Uint64))
// We skip the first position because it's the main player, and we already have the information (+0x148 is the next party member)
partyStruct = uintptr(gd.Process.ReadUInt(partyStruct+0x148, Uint64))
for partyStruct > 0 { for partyStruct > 0 {
name := gd.Process.ReadStringFromMemory(partyStruct, 16) name := gd.Process.ReadStringFromMemory(partyStruct, 16)
a := gd.Process.ReadUInt(partyStruct+0x5C, Uint32) a := area.ID(gd.Process.ReadUInt(partyStruct+0x5C, Uint32))
xPos := gd.Process.ReadUInt(partyStruct+0x60, Uint32) xPos := int(gd.Process.ReadUInt(partyStruct+0x60, Uint32))
yPos := gd.Process.ReadUInt(partyStruct+0x64, Uint32) yPos := int(gd.Process.ReadUInt(partyStruct+0x64, Uint32))
// When the player is in town, roster data is not updated, so we need to get the area from the player unit that match the same name
for _, pu := range rawPlayerUnits {
if pu.Name == name {
xPos = pu.Position.X
yPos = pu.Position.Y
a = pu.Area
break
}
}
roster = append(roster, data.RosterMember{ roster = append(roster, data.RosterMember{
Name: name, Name: name,
Area: area.ID(a), Area: a,
Position: data.Position{X: int(xPos), Y: int(yPos)}, Position: data.Position{X: xPos, Y: yPos},
}) })
partyStruct = uintptr(gd.Process.ReadUInt(partyStruct+0x148, Uint64)) partyStruct = uintptr(gd.Process.ReadUInt(partyStruct+0x148, Uint64))
} }
return mainPlayerUnit := rawPlayerUnits.GetMainPlayer()
return append([]data.RosterMember{{
Name: mainPlayerUnit.Name,
Area: mainPlayerUnit.Area,
Position: mainPlayerUnit.Position,
}}, roster...)
} }

View File

@@ -175,13 +175,6 @@ func (r Rule) Evaluate(it data.Item) (RuleResult, error) {
return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName) return RuleResultNoMatch, fmt.Errorf("property %s is not valid or not supported", statName)
} }
// Exception for enhanceddefense, is not accurate
if strings.EqualFold(statName, "enhanceddefense") {
enhancedDefense := r.calculateEnhancedDefense(it)
stage2Props[statName] = enhancedDefense
continue
}
layer := 0 layer := 0
if len(statData) > 1 { if len(statData) > 1 {
layer = statData[1] layer = statData[1]
@@ -249,32 +242,3 @@ func getRequiredStatsForRule(line string) []string {
func (r Rule) MaxQuantity() int { func (r Rule) MaxQuantity() int {
return r.maxQuantity 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.Type().IsType(item.TypeArmor) {
return 0
}
itemTypeMaxDefense := i.Desc().MaxDefense
if defenseStat.Value <= itemTypeMaxDefense {
return 0
}
return int(float64(defenseStat.Value-itemTypeMaxDefense) / float64(itemTypeMaxDefense) * 100)
}

View File

@@ -149,6 +149,7 @@ func TestRule_Evaluate(t *testing.T) {
Name: "mageplate", Name: "mageplate",
Quality: item.QualitySuperior, Quality: item.QualitySuperior,
Stats: []stat.Data{ Stats: []stat.Data{
{ID: stat.EnhancedDefense, Value: 15},
{ID: stat.Defense, Value: 301}, {ID: stat.Defense, Value: 301},
}, },
}, },