bugfixing, performance improvements and added casting frames formula
This commit is contained in:
@@ -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
|
||||
- [memory](https://github.com/hectorgimenez/d2go/tree/main/pkg/memory) - D2R memory reader (it provides the data
|
||||
structures)
|
||||
- [nip](https://github.com/hectorgimenez/d2go/tree/main/pkg/nip) - A very basic NIP file parser
|
||||
- [itemfilter](https://github.com/hectorgimenez/d2go/tree/main/pkg/itemfilter) - Based on game data, it provides an item
|
||||
pickup filtering
|
||||
- [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.
|
||||
|
||||
### Tools
|
||||
|
||||
- [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
|
||||
- [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
2
go.mod
@@ -3,7 +3,7 @@ module github.com/hectorgimenez/d2go
|
||||
go 1.22
|
||||
|
||||
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/stretchr/testify v1.9.0
|
||||
golang.org/x/sys v0.20.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -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/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/expr-lang/expr v1.16.6 h1:u1mrPXbwHtWAih5ZP24ZDG+ht8CB5xB0aBvagkAWPY0=
|
||||
github.com/expr-lang/expr v1.16.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
|
||||
github.com/expr-lang/expr v1.16.7 h1:gCIiHt5ODA0xIaDbD0DPKyZpM9Drph3b3lolYAYq2Kw=
|
||||
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/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/hectorgimenez/d2go/pkg/data/quest"
|
||||
@@ -137,8 +138,8 @@ type PlayerUnit struct {
|
||||
ID UnitID
|
||||
Area area.ID
|
||||
Position Position
|
||||
Stats map[stat.ID]int
|
||||
BaseStats map[stat.ID]int
|
||||
Stats stat.Stats
|
||||
BaseStats stat.Stats
|
||||
Skills map[skill.ID]skill.Points
|
||||
States state.States
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
|
||||
@@ -54,6 +54,7 @@ type Item struct {
|
||||
Quality item.Quality
|
||||
Position Position
|
||||
Location item.Location
|
||||
Page int // Used for shared stash
|
||||
Ethereal bool
|
||||
IsHovered bool
|
||||
BaseStats stat.Stats
|
||||
|
||||
@@ -23,10 +23,14 @@ func (gd *GameReader) GetData() data.Data {
|
||||
gd.offset = calculateOffsets(gd.Process)
|
||||
}
|
||||
|
||||
roster := gd.getRoster()
|
||||
playerUnitPtr, corpse := gd.GetPlayerUnitPtr(roster)
|
||||
rawPlayerUnits := gd.GetRawPlayerUnits()
|
||||
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()
|
||||
|
||||
// Quests
|
||||
@@ -36,11 +40,16 @@ func (gd *GameReader) GetData() data.Data {
|
||||
|
||||
gameQuestsBytes = gameQuestsBytes[3:]
|
||||
|
||||
corpseUnit := rawPlayerUnits.GetCorpse()
|
||||
d := data.Data{
|
||||
Corpse: corpse,
|
||||
Corpse: data.Corpse{
|
||||
Found: corpseUnit.Address != 0,
|
||||
IsHovered: corpseUnit.IsHovered,
|
||||
Position: corpseUnit.Position,
|
||||
},
|
||||
Monsters: gd.Monsters(pu.Position, hover),
|
||||
PlayerUnit: pu,
|
||||
Items: gd.Items(pu, hover),
|
||||
Items: gd.Items(rawPlayerUnits, hover),
|
||||
Objects: gd.Objects(pu.Position, hover),
|
||||
OpenMenus: gd.openMenus(),
|
||||
Roster: roster,
|
||||
@@ -50,19 +59,15 @@ func (gd *GameReader) GetData() data.Data {
|
||||
KeyBindings: gd.GetKeyBindings(),
|
||||
}
|
||||
|
||||
if playerUnitPtr == 0 {
|
||||
return gd.previousRead
|
||||
}
|
||||
|
||||
gd.previousRead = d
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -108,8 +113,9 @@ func (gd *GameReader) hoveredData() data.HoverData {
|
||||
}
|
||||
|
||||
func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats {
|
||||
statList := gd.Process.ReadUInt(statListPtr, Uint64)
|
||||
statCount := gd.Process.ReadUInt(statListPtr+0x8, Uint64)
|
||||
statsListBuffer := gd.ReadBytesFromMemory(statListPtr, 0x10)
|
||||
statList := ReadUIntFromBuffer(statsListBuffer, 0, Uint64)
|
||||
statCount := ReadUIntFromBuffer(statsListBuffer, 0x08, Uint64)
|
||||
if statCount == 0 {
|
||||
return []stat.Data{}
|
||||
}
|
||||
@@ -129,12 +135,38 @@ func (gd *GameReader) getStatsList(statListPtr uintptr) stat.Stats {
|
||||
stat.Mana,
|
||||
stat.MaxMana,
|
||||
stat.Stamina,
|
||||
stat.LifePerLevel,
|
||||
stat.ManaPerLevel:
|
||||
stat.MaxStamina:
|
||||
value = int(statValue >> 8)
|
||||
case stat.ColdLength,
|
||||
stat.PoisonLength:
|
||||
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{
|
||||
|
||||
20
pkg/memory/game_reader_test.go
Normal file
20
pkg/memory/game_reader_test.go
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,32 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sort"
|
||||
|
||||
"github.com/hectorgimenez/d2go/pkg/data"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/item"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/stat"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/state"
|
||||
"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)
|
||||
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{}
|
||||
belt := data.Belt{}
|
||||
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
|
||||
switch itemLoc {
|
||||
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
|
||||
break
|
||||
} else if invPage == 0 {
|
||||
location = item.LocationInventory
|
||||
break
|
||||
}
|
||||
if data.UnitID(itemOwnerNPC) == pu.ID || itemOwnerNPC == 1 {
|
||||
if data.UnitID(itemOwnerNPC) == mainPlayer.UnitID || itemOwnerNPC == 1 {
|
||||
location = item.LocationStash
|
||||
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:
|
||||
location = item.LocationEquipped
|
||||
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
|
||||
|
||||
if location == item.LocationBelt {
|
||||
belt.Items = append(belt.Items, itm)
|
||||
} else {
|
||||
items.AllItems = append(items.AllItems, itm)
|
||||
// We don't care about the items we don't know where they are, probably previous games or random crap
|
||||
if location != item.LocationUnknown {
|
||||
if location == item.LocationBelt {
|
||||
belt.Items = append(belt.Items, itm)
|
||||
} else {
|
||||
items.AllItems = append(items.AllItems, itm)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
sort.SliceStable(items.AllItems, func(i, j int) bool {
|
||||
distanceI := utils.DistanceFromPoint(pu.Position, items.AllItems[i].Position)
|
||||
distanceJ := utils.DistanceFromPoint(pu.Position, items.AllItems[j].Position)
|
||||
distanceI := utils.DistanceFromPoint(mainPlayer.Position, items.AllItems[i].Position)
|
||||
distanceJ := utils.DistanceFromPoint(mainPlayer.Position, items.AllItems[j].Position)
|
||||
|
||||
return distanceI < distanceJ
|
||||
})
|
||||
@@ -142,8 +159,8 @@ func (gd *GameReader) getItemStats(statsListExPtr uintptr) (stat.Stats, stat.Sta
|
||||
baseStats := gd.getStatsList(statsListExPtr + 0x30)
|
||||
|
||||
flags := gd.Process.ReadUInt(statsListExPtr+0x1C, Uint64)
|
||||
|
||||
lastStatsList := uintptr(gd.Process.ReadUInt(statsListExPtr+0x70, Uint64))
|
||||
|
||||
if (flags & 0x80000000) == 0 {
|
||||
return baseStats, fullStats
|
||||
}
|
||||
|
||||
@@ -6,137 +6,74 @@ import (
|
||||
"github.com/hectorgimenez/d2go/pkg/data"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/area"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/skill"
|
||||
"github.com/hectorgimenez/d2go/pkg/data/stat"
|
||||
"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++ {
|
||||
unitOffset := gd.offset.UnitTable + uintptr(i*8)
|
||||
playerUnitAddr := gd.Process.moduleBaseAddressPtr + unitOffset
|
||||
playerUnit := uintptr(gd.Process.ReadUInt(playerUnitAddr, Uint64))
|
||||
for playerUnit > 0 {
|
||||
unitID := gd.Process.ReadUInt(playerUnit+0x08, Uint32)
|
||||
pInventory := playerUnit + 0x90
|
||||
inventoryAddr := uintptr(gd.Process.ReadUInt(pInventory, Uint64))
|
||||
|
||||
pPath := playerUnit + 0x38
|
||||
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)
|
||||
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
|
||||
if inventoryAddr > 0 && xPos > 0 && yPos > 0 {
|
||||
expCharPtr := uintptr(gd.Process.ReadUInt(gd.moduleBaseAddressPtr+gd.offset.Expansion, Uint64))
|
||||
expChar := gd.Process.ReadUInt(expCharPtr+0x5C, Uint16)
|
||||
baseCheck := gd.Process.ReadUInt(inventoryAddr+0x30, 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
expCharPtr := uintptr(gd.Process.ReadUInt(gd.moduleBaseAddressPtr+gd.offset.Expansion, Uint64))
|
||||
expChar := gd.Process.ReadUInt(expCharPtr+0x5C, Uint16)
|
||||
isMainPlayer := gd.Process.ReadUInt(inventoryAddr+0x30, Uint16)
|
||||
if expChar > 0 {
|
||||
isMainPlayer = gd.Process.ReadUInt(inventoryAddr+0x70, Uint16)
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return rawPlayerUnits
|
||||
}
|
||||
|
||||
func (gd *GameReader) GetPlayerUnit(playerUnit uintptr) 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)
|
||||
|
||||
func (gd *GameReader) GetPlayerUnit(mainPlayerUnit RawPlayerUnit) data.PlayerUnit {
|
||||
// Get Stats
|
||||
statsListExPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x88, Uint64))
|
||||
baseStatPtr := gd.Process.ReadUInt(statsListExPtr+0x30, Uint64)
|
||||
baseStatsCount := gd.Process.ReadUInt(statsListExPtr+0x38, Uint64)
|
||||
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)
|
||||
statsListExPtr := uintptr(gd.Process.ReadUInt(mainPlayerUnit.Address+0x88, Uint64))
|
||||
baseStats := gd.getStatsList(statsListExPtr + 0x30)
|
||||
stats := gd.getStatsList(statsListExPtr + 0x88)
|
||||
|
||||
// Skills
|
||||
skillListPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x100, Uint64))
|
||||
skillListPtr := uintptr(gd.Process.ReadUInt(mainPlayerUnit.Address+0x100, Uint64))
|
||||
skills := gd.getSkills(skillListPtr)
|
||||
|
||||
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))
|
||||
|
||||
// Class
|
||||
class := data.Class(gd.Process.ReadUInt(playerUnit+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)
|
||||
class := data.Class(gd.Process.ReadUInt(mainPlayerUnit.Address+0x174, Uint32))
|
||||
|
||||
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
|
||||
@@ -163,22 +93,20 @@ func (gd *GameReader) GetPlayerUnit(playerUnit uintptr) data.PlayerUnit {
|
||||
for i := 0; i < 0x48; i = i + 8 {
|
||||
a := binary.LittleEndian.Uint32(wpList[i : i+4])
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
d := data.PlayerUnit{
|
||||
Name: name,
|
||||
ID: data.UnitID(unitID),
|
||||
Area: area.ID(levelNo),
|
||||
Position: data.Position{
|
||||
X: int(xPos),
|
||||
Y: int(yPos),
|
||||
},
|
||||
Name: mainPlayerUnit.Name,
|
||||
ID: mainPlayerUnit.UnitID,
|
||||
Area: mainPlayerUnit.Area,
|
||||
Position: mainPlayerUnit.Position,
|
||||
Stats: stats,
|
||||
BaseStats: baseStats,
|
||||
Skills: skills,
|
||||
States: states,
|
||||
States: mainPlayerUnit.States,
|
||||
Class: class,
|
||||
LeftSkill: skill.ID(leftSkillId),
|
||||
RightSkill: skill.ID(rightSkillId),
|
||||
|
||||
39
pkg/memory/player_unit.go
Normal file
39
pkg/memory/player_unit.go
Normal 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{}
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"golang.org/x/sys/windows"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const moduleName = "d2r.exe"
|
||||
|
||||
@@ -5,23 +5,41 @@ import (
|
||||
"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))
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
yPos := gd.Process.ReadUInt(partyStruct+0x64, Uint32)
|
||||
xPos := int(gd.Process.ReadUInt(partyStruct+0x60, 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{
|
||||
Name: name,
|
||||
Area: area.ID(a),
|
||||
Position: data.Position{X: int(xPos), Y: int(yPos)},
|
||||
Area: a,
|
||||
Position: data.Position{X: xPos, Y: yPos},
|
||||
})
|
||||
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...)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
// Exception for enhanceddefense, is not accurate
|
||||
if strings.EqualFold(statName, "enhanceddefense") {
|
||||
enhancedDefense := r.calculateEnhancedDefense(it)
|
||||
stage2Props[statName] = enhancedDefense
|
||||
continue
|
||||
}
|
||||
|
||||
layer := 0
|
||||
if len(statData) > 1 {
|
||||
layer = statData[1]
|
||||
@@ -249,32 +242,3 @@ func getRequiredStatsForRule(line string) []string {
|
||||
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.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)
|
||||
}
|
||||
|
||||
@@ -149,6 +149,7 @@ func TestRule_Evaluate(t *testing.T) {
|
||||
Name: "mageplate",
|
||||
Quality: item.QualitySuperior,
|
||||
Stats: []stat.Data{
|
||||
{ID: stat.EnhancedDefense, Value: 15},
|
||||
{ID: stat.Defense, Value: 301},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user