Add some memory reading stuff, based on many other open source projects

This commit is contained in:
Héctor Giménez
2023-03-11 22:08:18 +09:00
parent 80b4732f38
commit dde3cd19b0
16 changed files with 1056 additions and 6 deletions

6
go.mod
View File

@@ -2,7 +2,11 @@ module github.com/hectorgimenez/d2go
go 1.19
require github.com/stretchr/testify v1.8.2
require (
github.com/stretchr/testify v1.8.2
github.com/winlabs/gowin32 v0.0.0-20221003142512-0d265587d3c9
golang.org/x/sys v0.6.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect

4
go.sum
View File

@@ -10,6 +10,10 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/winlabs/gowin32 v0.0.0-20221003142512-0d265587d3c9 h1:U8aCPFEMnxAEyj9IonhMVV1gSL4nzelh8uvoXp0hrq0=
github.com/winlabs/gowin32 v0.0.0-20221003142512-0d265587d3c9/go.mod h1:N51TYkG9JGR5sytj0EoPl31Xg2kuB507lxEmrwSNvfQ=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,4 +1,4 @@
package game
package data
import "strings"

View File

@@ -1,4 +1,4 @@
package game
package data
import (
"github.com/hectorgimenez/d2go/pkg/data/area"

View File

@@ -1,4 +1,4 @@
package game
package data
import (
"github.com/hectorgimenez/d2go/pkg/data/item"

View File

@@ -1,4 +1,4 @@
package game
package data
import (
"github.com/hectorgimenez/d2go/pkg/data/npc"

View File

@@ -1,4 +1,4 @@
package game
package data
import "github.com/hectorgimenez/d2go/pkg/data/object"

118
pkg/memory/game_reader.go Normal file
View File

@@ -0,0 +1,118 @@
package memory
import (
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/stat"
)
type GameReader struct {
offset Offset
Process
}
func NewGameReader(process Process) *GameReader {
return &GameReader{
offset: calculateOffsets(process),
Process: process,
}
}
var previousData *data.Data
func (gd *GameReader) GetData() data.Data {
if gd.offset.UnitTable == 0 {
gd.offset = calculateOffsets(gd.Process)
}
roster := gd.getRoster()
playerUnitPtr, corpse := gd.getPlayerUnitPtr(roster)
pu := gd.GetPlayerUnit(playerUnitPtr)
d := data.Data{
Corpse: corpse,
Monsters: gd.Monsters(pu.Position),
PlayerUnit: pu,
Items: gd.Items(pu.Position),
Objects: gd.Objects(pu.Position),
OpenMenus: gd.openMenus(),
Roster: roster,
}
if playerUnitPtr == 0 {
return *previousData
}
previousData = &d
return d
}
func (gd *GameReader) InGame() bool {
pu, _ := gd.getPlayerUnitPtr([]data.RosterMember{})
return pu > 0
}
func (gd *GameReader) openMenus() data.OpenMenus {
uiBase := gd.Process.moduleBaseAddressPtr + gd.offset.UI - 0xA
buffer := gd.Process.ReadBytesFromMemory(uiBase, 0x169)
isMapShown := gd.Process.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.UI, Uint8)
return data.OpenMenus{
Inventory: buffer[0x01] != 0,
LoadingScreen: buffer[0x168] != 0,
NPCInteract: buffer[0x08] != 0,
NPCShop: buffer[0x0B] != 0,
Stash: buffer[0x18] != 0,
Waypoint: buffer[0x13] != 0,
MapShown: isMapShown != 0,
}
}
func (gd *GameReader) hoveredData() (hoveredUnitID uint, hoveredType uint, isHovered bool) {
hoverAddressPtr := gd.Process.moduleBaseAddressPtr + gd.offset.Hover
hoverBuffer := gd.Process.ReadBytesFromMemory(hoverAddressPtr, 12)
isUnitHovered := ReadUIntFromBuffer(hoverBuffer, 0, Uint16)
if isUnitHovered > 0 {
hoveredType = ReadUIntFromBuffer(hoverBuffer, 0x04, Uint32)
hoveredUnitID = ReadUIntFromBuffer(hoverBuffer, 0x08, Uint32)
return hoveredUnitID, hoveredType, true
}
return 0, 0, false
}
func getStatData(statEnum, statValue uint) (stat.Stat, int) {
value := int(statValue)
switch stat.Stat(statEnum) {
case stat.Life,
stat.MaxLife,
stat.Mana,
stat.MaxMana,
stat.Stamina,
stat.LifePerLevel,
stat.ManaPerLevel:
value = int(statValue >> 8)
case stat.ColdLength,
stat.PoisonLength:
value = int(statValue / 25)
}
return stat.Stat(statEnum), value
}
func setProperties(item *data.Item, flags uint32) {
if 0x00400000&flags != 0 {
item.Ethereal = true
}
if 0x00000010&flags != 0 {
item.Identified = true
}
if 0x00002000&flags != 0 {
item.IsVendor = true
}
}

131
pkg/memory/item.go Normal file
View File

@@ -0,0 +1,131 @@
package memory
import (
"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/utils"
"sort"
)
func (gd *GameReader) Items(playerPosition data.Position) data.Items {
hoveredUnitID, hoveredType, isHovered := gd.hoveredData()
baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024)
unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8)
items := data.Items{}
for i := 0; i < 128; i++ {
itemOffset := 8 * i
itemUnitPtr := uintptr(ReadUIntFromBuffer(unitTableBuffer, uint(itemOffset), Uint64))
for itemUnitPtr > 0 {
itemDataBuffer := gd.Process.ReadBytesFromMemory(itemUnitPtr, 144)
itemType := ReadUIntFromBuffer(itemDataBuffer, 0x00, Uint32)
txtFileNo := ReadUIntFromBuffer(itemDataBuffer, 0x04, Uint32)
unitID := ReadUIntFromBuffer(itemDataBuffer, 0x08, Uint32)
// itemLoc = 0 in inventory, 1 equipped, 2 in belt, 3 on ground, 4 cursor, 5 dropping, 6 socketed
itemLoc := ReadUIntFromBuffer(itemDataBuffer, 0x0C, Uint32)
if itemType != 4 {
itemUnitPtr = uintptr(gd.Process.ReadUInt(itemUnitPtr+0x150, Uint64))
continue
}
unitDataPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x10, Uint64))
unitDataBuffer := gd.Process.ReadBytesFromMemory(unitDataPtr, 144)
flags := ReadUIntFromBuffer(unitDataBuffer, 0x18, Uint32)
invPage := ReadUIntFromBuffer(unitDataBuffer, 0x55, Uint8)
itemQuality := ReadUIntFromBuffer(unitDataBuffer, 0x00, Uint32)
//itemOwnerNPC := ReadUIntFromBuffer(unitDataBuffer, 0x0C, Uint32)
// Item coordinates (X, Y)
pathPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x38, Uint64))
pathBuffer := gd.Process.ReadBytesFromMemory(pathPtr, 144)
itemX := ReadUIntFromBuffer(pathBuffer, 0x10, Uint16)
itemY := ReadUIntFromBuffer(pathBuffer, 0x14, Uint16)
// Item Stats
statsListExPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x88, Uint64))
statsListExBuffer := gd.Process.ReadBytesFromMemory(statsListExPtr, 180)
statPtr := uintptr(ReadUIntFromBuffer(statsListExBuffer, 0x30, Uint64))
statCount := ReadUIntFromBuffer(statsListExBuffer, 0x38, Uint32)
statExPtr := uintptr(ReadUIntFromBuffer(statsListExBuffer, 0x88, Uint64))
statExCount := ReadUIntFromBuffer(statsListExBuffer, 0x90, Uint32)
stats := gd.getItemStats(statCount, statPtr, statExCount, statExPtr)
name := item.GetNameByEnum(txtFileNo)
itemHovered := false
if isHovered && hoveredType == 4 && hoveredUnitID == unitID {
itemHovered = true
}
itm := data.Item{
UnitID: data.UnitID(unitID),
Name: name,
Quality: item.Quality(itemQuality),
Position: data.Position{
X: int(itemX),
Y: int(itemY),
},
IsHovered: itemHovered,
Stats: stats,
}
setProperties(&itm, uint32(flags))
switch itemLoc {
case 0:
if itm.IsVendor {
items.Shop = append(items.Shop, itm)
} else if invPage == 0 {
items.Inventory = append(items.Inventory, itm)
}
case 1:
items.Equipped = append(items.Equipped, itm)
case 2:
items.Belt = append(items.Belt, itm)
case 3, 5:
items.Ground = append(items.Ground, itm)
}
itemUnitPtr = uintptr(gd.Process.ReadUInt(itemUnitPtr+0x150, Uint64))
}
}
sort.SliceStable(items.Ground, func(i, j int) bool {
distanceI := utils.DistanceFromPoint(playerPosition, items.Ground[i].Position)
distanceJ := utils.DistanceFromPoint(playerPosition, items.Ground[j].Position)
return distanceI < distanceJ
})
return items
}
func (gd *GameReader) getItemStats(statCount uint, statPtr uintptr, statExCount uint, statExPtr uintptr) map[stat.Stat]int {
stats := map[stat.Stat]int{}
if statCount < 20 && statCount > 0 {
statBuffer := gd.Process.ReadBytesFromMemory(statPtr, statCount*10)
for i := 0; i < int(statCount); i++ {
offset := uint(i * 8)
statEnum := ReadUIntFromBuffer(statBuffer, offset+0x2, Uint16)
statValue := ReadUIntFromBuffer(statBuffer, offset+0x4, Uint32)
st, value := getStatData(statEnum, statValue)
stats[st] = value
}
}
if statExCount < 20 && statCount > 0 {
statBuffer := gd.Process.ReadBytesFromMemory(statExPtr, statExCount*10)
for i := 0; i < int(statExCount); i++ {
offset := uint(i * 8)
statEnum := ReadUIntFromBuffer(statBuffer, offset+0x2, Uint16)
statValue := ReadUIntFromBuffer(statBuffer, offset+0x4, Uint32)
st, value := getStatData(statEnum, statValue)
stats[st] = value
}
}
return stats
}

201
pkg/memory/monsters.go Normal file
View File

@@ -0,0 +1,201 @@
package memory
import (
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/npc"
"github.com/hectorgimenez/d2go/pkg/data/stat"
"github.com/hectorgimenez/d2go/pkg/utils"
"sort"
)
func (gd *GameReader) Monsters(playerPosition data.Position) data.Monsters {
hoveredUnitID, hoveredType, isHovered := gd.hoveredData()
baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + 1024
unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8)
monsters := data.Monsters{}
for i := 0; i < 128; i++ {
monsterOffset := 8 * i
monsterUnitPtr := uintptr(ReadUIntFromBuffer(unitTableBuffer, uint(monsterOffset), Uint64))
for monsterUnitPtr > 0 {
monsterDataBuffer := gd.Process.ReadBytesFromMemory(monsterUnitPtr, 144)
//monsterType := ReadUIntFromBuffer(monsterDataBuffer, 0x00, Uint32)
txtFileNo := ReadUIntFromBuffer(monsterDataBuffer, 0x04, Uint32)
if !gd.shouldBeIgnored(txtFileNo) {
unitID := ReadUIntFromBuffer(monsterDataBuffer, 0x08, Uint32)
//mode := ReadUIntFromBuffer(monsterDataBuffer, 0x0C, Uint32)
unitDataPtr := uintptr(ReadUIntFromBuffer(monsterDataBuffer, 0x10, Uint64))
//isUnique := gd.Process.ReadUInt(unitDataPtr+0x18, Uint16)
flag := gd.Process.ReadBytesFromMemory(unitDataPtr+0x1A, Uint8)[0]
isCorpse := gd.Process.ReadUInt(monsterUnitPtr+0x1A6, Uint8)
//unitDataBuffer := gd.Process.ReadBytesFromMemory(unitDataPtr, 144)
// Coordinates (X, Y)
pathPtr := uintptr(gd.Process.ReadUInt(monsterUnitPtr+0x38, Uint64))
posX := gd.Process.ReadUInt(pathPtr+0x02, Uint16)
posY := gd.Process.ReadUInt(pathPtr+0x06, Uint16)
hovered := false
if isHovered && hoveredType == 1 && hoveredUnitID == unitID {
hovered = true
}
statsListExPtr := uintptr(ReadUIntFromBuffer(monsterDataBuffer, 0x88, Uint64))
statPtr := uintptr(gd.Process.ReadUInt(statsListExPtr+0x30, Uint64))
statCount := gd.Process.ReadUInt(statsListExPtr+0x38, Uint64)
stats := gd.getMonsterStats(statCount, statPtr)
m := data.Monster{
UnitID: data.UnitID(unitID),
Name: npc.ID(int(txtFileNo)),
IsHovered: hovered,
Position: data.Position{
X: int(posX),
Y: int(posY),
},
Stats: stats,
Type: getMonsterType(flag),
}
if isCorpse == 0 {
monsters = append(monsters, m)
}
}
monsterUnitPtr = uintptr(gd.Process.ReadUInt(monsterUnitPtr+0x150, Uint64))
}
}
sort.SliceStable(monsters, func(i, j int) bool {
distanceI := utils.DistanceFromPoint(playerPosition, monsters[i].Position)
distanceJ := utils.DistanceFromPoint(playerPosition, monsters[j].Position)
return distanceI < distanceJ
})
return monsters
}
func getMonsterType(typeFlag byte) data.MonsterType {
switch typeFlag {
case 10:
return data.MonsterTypeSuperUnique
case 1 << 2:
return data.MonsterTypeChampion
case 1 << 3:
return data.MonsterTypeUnique
case 1 << 4:
return data.MonsterTypeMinion
}
return data.MonsterTypeNone
}
func (gd *GameReader) getMonsterStats(statCount uint, statPtr uintptr) map[stat.Stat]int {
stats := map[stat.Stat]int{}
if statCount > 0 {
statBuffer := gd.Process.ReadBytesFromMemory(statPtr+0x2, statCount*8)
for i := 0; i < int(statCount); i++ {
offset := uint(i * 8)
statEnum := ReadUIntFromBuffer(statBuffer, offset, Uint16)
statValue := ReadUIntFromBuffer(statBuffer, offset+0x2, Uint32)
stats[stat.Stat(statEnum)] = int(statValue)
}
}
return stats
}
func (gd *GameReader) shouldBeIgnored(txtNo uint) bool {
switch npc.ID(txtNo) {
case npc.Chicken,
npc.Rat,
npc.Rogue,
npc.HellMeteor,
npc.Bird,
npc.Bird2,
npc.Bat,
npc.Act2Male,
npc.Act2Female,
npc.Act2Child,
npc.Cow,
npc.Camel,
npc.Act2Guard,
npc.Act2Vendor,
npc.Act2Vendor2,
npc.Maggot,
npc.Bug,
npc.Scorpion,
npc.Rogue2,
npc.Rogue3,
npc.Larva,
npc.Familiar,
npc.Act3Male,
npc.ClayGolem,
npc.BloodGolem,
npc.IronGolem,
npc.FireGolem,
npc.Act3Female,
npc.Snake,
npc.Parrot,
npc.Fish,
npc.EvilHole,
npc.EvilHole2,
npc.EvilHole3,
npc.EvilHole4,
npc.EvilHole5,
npc.FireboltTrap,
npc.HorzMissileTrap,
npc.VertMissileTrap,
npc.PoisonCloudTrap,
npc.LightningTrap,
npc.InvisoSpawner,
//338, //Guard
npc.MiniSper,
npc.BoneWall,
npc.Hydra,
npc.Hydra2,
npc.Hydra3,
npc.SevenTombs,
npc.Valkyrie,
npc.IronWolf,
npc.NecroSkeleton,
npc.NecroMage,
npc.CompellingOrbNpc,
npc.SpiritMummy,
npc.Act2Guard4,
npc.Act2Guard5,
npc.Window,
npc.Window2,
npc.MephistoSpirit,
npc.WakeOfDestruction,
npc.ChargedBoltSentry,
npc.LightningSentry,
npc.InvisiblePet,
npc.InfernoSentry,
npc.DeathSentry,
npc.ShadowWarrior,
npc.ShadowMaster,
npc.DruHawk,
npc.DruSpiritWolf,
npc.DruFenris,
npc.HeartOfWolverine,
npc.OakSage,
npc.DruBear,
npc.BaalThrone,
npc.InjuredBarbarian,
npc.InjuredBarbarian2,
npc.InjuredBarbarian3,
npc.DemonHole:
return true
}
return false
}

59
pkg/memory/object.go Normal file
View File

@@ -0,0 +1,59 @@
package memory
import (
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/object"
"github.com/hectorgimenez/d2go/pkg/utils"
"sort"
)
func (gd *GameReader) Objects(playerPosition data.Position) []data.Object {
hoveredUnitID, hoveredType, isHovered := gd.hoveredData()
baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (2 * 1024)
unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8)
var objects []data.Object
for i := 0; i < 128; i++ {
objectOffset := 8 * i
objectUnitPtr := uintptr(ReadUIntFromBuffer(unitTableBuffer, uint(objectOffset), Uint64))
for objectUnitPtr > 0 {
objectType := gd.Process.ReadUInt(objectUnitPtr+0x00, Uint32)
if objectType == 2 {
txtFileNo := gd.Process.ReadUInt(objectUnitPtr+0x04, Uint32)
mode := gd.Process.ReadUInt(objectUnitPtr+0x0c, Uint32)
unitID := gd.Process.ReadUInt(objectUnitPtr+0x08, Uint32)
// Coordinates (X, Y)
pathPtr := uintptr(gd.Process.ReadUInt(objectUnitPtr+0x38, Uint64))
posX := gd.Process.ReadUInt(pathPtr+0x10, Uint16)
posY := gd.Process.ReadUInt(pathPtr+0x14, Uint16)
unitDataPtr := uintptr(gd.Process.ReadUInt(objectUnitPtr+0x10, Uint64))
interactType := gd.Process.ReadUInt(unitDataPtr+0x08, Uint8)
obj := data.Object{
Name: object.Name(int(txtFileNo)),
IsHovered: unitID == hoveredUnitID && hoveredType == 2 && isHovered,
InteractType: object.InteractType(interactType),
Selectable: mode == 0,
Position: data.Position{
X: int(posX),
Y: int(posY),
},
}
objects = append(objects, obj)
}
objectUnitPtr = uintptr(gd.Process.ReadUInt(objectUnitPtr+0x150, Uint64))
}
}
sort.SliceStable(objects, func(i, j int) bool {
distanceI := utils.DistanceFromPoint(playerPosition, objects[i].Position)
distanceJ := utils.DistanceFromPoint(playerPosition, objects[j].Position)
return distanceI < distanceJ
})
return objects
}

56
pkg/memory/offset.go Normal file
View File

@@ -0,0 +1,56 @@
package memory
import "encoding/binary"
type Offset struct {
GameData uintptr
UnitTable uintptr
UI uintptr
Hover uintptr
Expansion uintptr
RosterOffset uintptr
}
func calculateOffsets(process Process) Offset {
// ignoring errors, always best practices
memory, _ := process.getProcessMemory()
// GameReader
pattern := process.FindPattern(memory, "\x44\x88\x25\x00\x00\x00\x00\x66\x44\x89\x25\x00\x00\x00\x00", "xxx????xxxx????")
bytes := process.ReadBytesFromMemory(pattern+0x3, 4)
offsetInt := uintptr(binary.LittleEndian.Uint32(bytes))
gameDataOffset := (pattern - process.moduleBaseAddressPtr) - 0x121 + offsetInt
// UnitTable
pattern = process.FindPattern(memory, "\x48\x03\xC7\x49\x8B\x8C\xC6", "xxxxxxx")
bytes = process.ReadBytesFromMemory(pattern+7, 4)
unitTableOffset := uintptr(binary.LittleEndian.Uint32(bytes))
// UI
pattern = process.FindPattern(memory, "\x40\x84\xed\x0f\x94\x05", "xxxxxx")
uiOffset := process.ReadUInt(pattern+6, Uint32)
uiOffsetPtr := (pattern - process.moduleBaseAddressPtr) + 10 + uintptr(uiOffset)
// Hover
pattern = process.FindPattern(memory, "\xc6\x84\xc2\x00\x00\x00\x00\x00\x48\x8b\x74\x24\x00", "xxx?????xxxx?")
hoverOffset := process.ReadUInt(pattern+3, Uint32) - 1
// Expansion
pattern = process.FindPattern(memory, "\x48\x8B\x05\x00\x00\x00\x00\x48\x8B\xD9\xF3\x0F\x10\x50\x00", "xxx????xxxxxxx?")
offsetPtr := uintptr(process.ReadUInt(pattern+3, Uint32))
expOffset := pattern - process.moduleBaseAddressPtr + 7 + offsetPtr
// Party members offset
pattern = process.FindPattern(memory, "\x02\x45\x33\xD2\x4D\x8B", "xxxxxx")
offsetPtr = uintptr(process.ReadUInt(pattern-3, Uint32))
rosterOffset := pattern - process.moduleBaseAddressPtr + 1 + offsetPtr
return Offset{
GameData: gameDataOffset,
UnitTable: unitTableOffset,
UI: uiOffsetPtr,
Hover: uintptr(hoverOffset),
Expansion: expOffset,
RosterOffset: rosterOffset,
}
}

271
pkg/memory/player.go Normal file
View File

@@ -0,0 +1,271 @@
package memory
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) {
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 {
pInventory := playerUnit + 0x90
inventoryAddr := uintptr(gd.Process.ReadUInt(pInventory, Uint64))
pPath := playerUnit + 0x38
pathAddress := uintptr(gd.Process.ReadUInt(pPath, Uint64))
xPos := gd.Process.ReadUInt(pathAddress+0x02, Uint16)
yPos := gd.Process.ReadUInt(pathAddress+0x06, Uint16)
// 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)
}
if baseCheck > 0 {
isCorpse := gd.Process.ReadUInt(playerUnit+0x1A6, Uint8)
if isCorpse == 1 {
unitID := gd.Process.ReadUInt(playerUnit+0x08, Uint32)
hoveredUnitID, hoveredType, isHovered := gd.hoveredData()
corpse = data.Corpse{
Found: true,
IsHovered: isHovered && hoveredUnitID == unitID && hoveredType == 0,
Position: data.Position{
X: int(xPos),
Y: int(yPos),
},
}
} else {
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),
},
}
}
}
}
playerUnit = uintptr(gd.Process.ReadUInt(playerUnit+0x150, Uint64))
}
}
return
}
func (gd *GameReader) GetPlayerUnit(playerUnit uintptr) data.PlayerUnit {
// 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
statsListExPtr := uintptr(gd.Process.ReadUInt(playerUnit+0x88, Uint64))
statPtr := gd.Process.ReadUInt(statsListExPtr+0x30, Uint64)
statCount := gd.Process.ReadUInt(statsListExPtr+0x38, Uint64)
stats := map[stat.Stat]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.Stat(statNumber) {
case stat.Life,
stat.MaxLife,
stat.Mana,
stat.MaxMana:
stats[stat.Stat(statNumber)] = int(uint32(statValue) >> 8)
default:
stats[stat.Stat(statNumber)] = int(statValue)
}
}
// States (Buff, Debuff, Auras)
states := gd.getStates(statsListExPtr)
// Skills
skills := gd.getSkills(playerUnit + 0x100)
// 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)
return data.PlayerUnit{
Name: name,
Area: area.Area(levelNo),
Position: data.Position{
X: int(xPos),
Y: int(yPos),
},
Stats: stats,
Skills: skills,
States: states,
}
}
func (gd *GameReader) getSkills(skillsPtr uintptr) map[skill.Skill]int {
skills := make(map[skill.Skill]int)
skillListPtr := uintptr(gd.Process.ReadUInt(skillsPtr, Uint64))
skillPtr := uintptr(gd.Process.ReadUInt(skillListPtr, Uint64))
for skillPtr != 0 {
skillTxtPtr := uintptr(gd.Process.ReadUInt(skillPtr, Uint64))
skillTxt := uintptr(gd.Process.ReadUInt(skillTxtPtr, Uint16))
skillLvl := gd.Process.ReadUInt(skillTxtPtr+0x34, Uint16)
skills[skill.Skill(skillTxt)] = int(skillLvl)
skillPtr = uintptr(gd.Process.ReadUInt(skillPtr+0x08, Uint64))
}
return skills
}
func (gd *GameReader) getStates(statsListExPtr uintptr) []state.State {
var states []state.State
for i := 0; i < 6; i++ {
offset := i * 4
stateByte := gd.Process.ReadUInt(statsListExPtr+0xAD0+uintptr(offset), Uint32)
offset = (32 * i) - 1
states = append(states, calculateStates(stateByte, uint(offset))...)
}
return states
}
func calculateStates(stateFlag uint, offset uint) []state.State {
var states []state.State
if 0x00000001&stateFlag != 0 {
states = append(states, state.State(1+offset))
}
if 0x00000002&stateFlag != 0 {
states = append(states, state.State(2+offset))
}
if 0x00000004&stateFlag != 0 {
states = append(states, state.State(3+offset))
}
if 0x00000008&stateFlag != 0 {
states = append(states, state.State(4+offset))
}
if 0x00000010&stateFlag != 0 {
states = append(states, state.State(5+offset))
}
if 0x00000020&stateFlag != 0 {
states = append(states, state.State(6+offset))
}
if 0x00000040&stateFlag != 0 {
states = append(states, state.State(7+offset))
}
if 0x00000080&stateFlag != 0 {
states = append(states, state.State(8+offset))
}
if 0x00000100&stateFlag != 0 {
states = append(states, state.State(9+offset))
}
if 0x00000200&stateFlag != 0 {
states = append(states, state.State(10+offset))
}
if 0x00000400&stateFlag != 0 {
states = append(states, state.State(11+offset))
}
if 0x00000800&stateFlag != 0 {
states = append(states, state.State(12+offset))
}
if 0x00001000&stateFlag != 0 {
states = append(states, state.State(13+offset))
}
if 0x00002000&stateFlag != 0 {
states = append(states, state.State(14+offset))
}
if 0x00004000&stateFlag != 0 {
states = append(states, state.State(15+offset))
}
if 0x00008000&stateFlag != 0 {
states = append(states, state.State(16+offset))
}
if 0x00010000&stateFlag != 0 {
states = append(states, state.State(17+offset))
}
if 0x00020000&stateFlag != 0 {
states = append(states, state.State(18+offset))
}
if 0x00040000&stateFlag != 0 {
states = append(states, state.State(19+offset))
}
if 0x00080000&stateFlag != 0 {
states = append(states, state.State(20+offset))
}
if 0x00100000&stateFlag != 0 {
states = append(states, state.State(21+offset))
}
if 0x00200000&stateFlag != 0 {
states = append(states, state.State(22+offset))
}
if 0x00400000&stateFlag != 0 {
states = append(states, state.State(23+offset))
}
if 0x00800000&stateFlag != 0 {
states = append(states, state.State(24+offset))
}
if 0x01000000&stateFlag != 0 {
states = append(states, state.State(25+offset))
}
if 0x02000000&stateFlag != 0 {
states = append(states, state.State(26+offset))
}
if 0x04000000&stateFlag != 0 {
states = append(states, state.State(27+offset))
}
if 0x08000000&stateFlag != 0 {
states = append(states, state.State(28+offset))
}
if 0x10000000&stateFlag != 0 {
states = append(states, state.State(29+offset))
}
if 0x20000000&stateFlag != 0 {
states = append(states, state.State(30+offset))
}
if 0x40000000&stateFlag != 0 {
states = append(states, state.State(31+offset))
}
if 0x80000000&stateFlag != 0 {
states = append(states, state.State(32+offset))
}
return states
}

166
pkg/memory/process.go Normal file
View File

@@ -0,0 +1,166 @@
package memory
import (
"bytes"
"encoding/binary"
"github.com/winlabs/gowin32"
"golang.org/x/sys/windows"
"strings"
"unsafe"
)
const moduleName = "D2R.exe"
type Process struct {
handler windows.Handle
pid uint
moduleBaseAddressPtr uintptr
moduleBaseSize uint
}
func NewProcess() (Process, error) {
module, err := getGameModule()
if err != nil {
return Process{}, err
}
h, err := windows.OpenProcess(0x0010, false, uint32(module.ProcessID))
if err != nil {
return Process{}, err
}
return Process{
handler: h,
pid: module.ProcessID,
moduleBaseAddressPtr: uintptr(unsafe.Pointer(module.ModuleBaseAddress)),
moduleBaseSize: module.ModuleBaseSize,
}, nil
}
func getGameModule() (gowin32.ModuleInfo, error) {
processes := make([]uint32, 2048)
length := uint32(0)
err := windows.EnumProcesses(processes, &length)
if err != nil {
return gowin32.ModuleInfo{}, err
}
for _, process := range processes {
module, _ := getMainModule(process)
if strings.Contains(module.ExePath, moduleName) {
return module, nil
}
}
return gowin32.ModuleInfo{}, err
}
func getMainModule(pid uint32) (gowin32.ModuleInfo, error) {
mi, err := gowin32.GetProcessModules(pid)
if err != nil {
return gowin32.ModuleInfo{}, err
}
for _, m := range mi {
if m.ModuleName == moduleName {
return m, nil
}
}
return gowin32.ModuleInfo{}, err
}
func (p Process) getProcessMemory() ([]byte, error) {
var data = make([]byte, p.moduleBaseSize)
err := windows.ReadProcessMemory(p.handler, p.moduleBaseAddressPtr, &data[0], uintptr(p.moduleBaseSize), nil)
if err != nil {
return nil, err
}
return data, nil
}
func (p Process) ReadBytesFromMemory(address uintptr, size uint) []byte {
var data = make([]byte, size)
windows.ReadProcessMemory(p.handler, address, &data[0], uintptr(size), nil)
return data
}
type IntType uint
const (
Uint8 = 1
Uint16 = 2
Uint32 = 4
Uint64 = 8
)
func (p Process) ReadUInt(address uintptr, size IntType) uint {
bytes := p.ReadBytesFromMemory(address, uint(size))
return bytesToUint(bytes, size)
}
func ReadUIntFromBuffer(bytes []byte, offset uint, size IntType) uint {
return bytesToUint(bytes[offset:offset+uint(size)], size)
}
func bytesToUint(bytes []byte, size IntType) uint {
switch size {
case Uint8:
return uint(bytes[0])
case Uint16:
return uint(binary.LittleEndian.Uint16(bytes))
case Uint32:
return uint(binary.LittleEndian.Uint32(bytes))
case Uint64:
return uint(binary.LittleEndian.Uint64(bytes))
}
return 0
}
func (p Process) ReadStringFromMemory(address uintptr, size uint) string {
if size == 0 {
for i := 1; true; i++ {
data := p.ReadBytesFromMemory(address, uint(i))
if data[i-1] == 0 {
return string(bytes.Trim(data, "\x00"))
}
}
}
return string(bytes.Trim(p.ReadBytesFromMemory(address, size), "\x00"))
}
func (p Process) findPattern(memory []byte, pattern, mask string) int {
patternLength := len(pattern)
for i := 0; i < int(p.moduleBaseSize)-patternLength; i++ {
found := true
for j := 0; j < patternLength; j++ {
if string(mask[j]) != "?" && string(pattern[j]) != string(memory[i+j]) {
found = false
break
}
}
if found {
return i
}
}
return 0
}
func (p Process) FindPattern(memory []byte, pattern, mask string) uintptr {
if offset := p.findPattern(memory, pattern, mask); offset != 0 {
return p.moduleBaseAddressPtr + uintptr(offset)
}
return 0
}
func (p Process) GetPID() uint {
return p.pid
}

27
pkg/memory/roster.go Normal file
View File

@@ -0,0 +1,27 @@
package memory
import (
"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/area"
)
func (gd *GameReader) getRoster() (roster []data.RosterMember) {
partyStruct := uintptr(gd.Process.ReadUInt(gd.Process.moduleBaseAddressPtr+gd.offset.RosterOffset, Uint64))
for partyStruct > 0 {
name := gd.Process.ReadStringFromMemory(partyStruct, 16)
a := gd.Process.ReadUInt(partyStruct+0x5C, Uint32)
xPos := gd.Process.ReadUInt(partyStruct+0x60, Uint32)
yPos := gd.Process.ReadUInt(partyStruct+0x64, Uint32)
roster = append(roster, data.RosterMember{
Name: name,
Area: area.Area(a),
Position: data.Position{X: int(xPos), Y: int(yPos)},
})
partyStruct = uintptr(gd.Process.ReadUInt(partyStruct+0x148, Uint64))
}
return
}

13
pkg/utils/utils.go Normal file
View File

@@ -0,0 +1,13 @@
package utils
import (
"github.com/hectorgimenez/d2go/pkg/data"
"math"
)
func DistanceFromPoint(from data.Position, to data.Position) int {
first := math.Pow(float64(to.X-from.X), 2)
second := math.Pow(float64(to.Y-from.Y), 2)
return int(math.Sqrt(first + second))
}