From dde3cd19b0fcfd96086554fc6c1f9244e4248ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Gim=C3=A9nez?= Date: Sat, 11 Mar 2023 22:08:18 +0900 Subject: [PATCH] Add some memory reading stuff, based on many other open source projects --- go.mod | 6 +- go.sum | 4 + pkg/data/belt.go | 2 +- pkg/data/data.go | 2 +- pkg/data/items.go | 2 +- pkg/data/npc.go | 2 +- pkg/data/objects.go | 2 +- pkg/memory/game_reader.go | 118 +++++++++++++++++ pkg/memory/item.go | 131 ++++++++++++++++++ pkg/memory/monsters.go | 201 ++++++++++++++++++++++++++++ pkg/memory/object.go | 59 +++++++++ pkg/memory/offset.go | 56 ++++++++ pkg/memory/player.go | 271 ++++++++++++++++++++++++++++++++++++++ pkg/memory/process.go | 166 +++++++++++++++++++++++ pkg/memory/roster.go | 27 ++++ pkg/utils/utils.go | 13 ++ 16 files changed, 1056 insertions(+), 6 deletions(-) create mode 100644 pkg/memory/game_reader.go create mode 100644 pkg/memory/item.go create mode 100644 pkg/memory/monsters.go create mode 100644 pkg/memory/object.go create mode 100644 pkg/memory/offset.go create mode 100644 pkg/memory/player.go create mode 100644 pkg/memory/process.go create mode 100644 pkg/memory/roster.go create mode 100644 pkg/utils/utils.go diff --git a/go.mod b/go.mod index 665a708..58d837f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 6a56e69..034779d 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/data/belt.go b/pkg/data/belt.go index 2c16c4e..c3ccfb7 100644 --- a/pkg/data/belt.go +++ b/pkg/data/belt.go @@ -1,4 +1,4 @@ -package game +package data import "strings" diff --git a/pkg/data/data.go b/pkg/data/data.go index 53ebd38..2edf922 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -1,4 +1,4 @@ -package game +package data import ( "github.com/hectorgimenez/d2go/pkg/data/area" diff --git a/pkg/data/items.go b/pkg/data/items.go index e4f50fe..0f4077b 100644 --- a/pkg/data/items.go +++ b/pkg/data/items.go @@ -1,4 +1,4 @@ -package game +package data import ( "github.com/hectorgimenez/d2go/pkg/data/item" diff --git a/pkg/data/npc.go b/pkg/data/npc.go index 7f8df57..e4c2d44 100644 --- a/pkg/data/npc.go +++ b/pkg/data/npc.go @@ -1,4 +1,4 @@ -package game +package data import ( "github.com/hectorgimenez/d2go/pkg/data/npc" diff --git a/pkg/data/objects.go b/pkg/data/objects.go index 7bb974b..65d2f4e 100644 --- a/pkg/data/objects.go +++ b/pkg/data/objects.go @@ -1,4 +1,4 @@ -package game +package data import "github.com/hectorgimenez/d2go/pkg/data/object" diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go new file mode 100644 index 0000000..09ba9a4 --- /dev/null +++ b/pkg/memory/game_reader.go @@ -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 + } +} diff --git a/pkg/memory/item.go b/pkg/memory/item.go new file mode 100644 index 0000000..5e70e53 --- /dev/null +++ b/pkg/memory/item.go @@ -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 +} diff --git a/pkg/memory/monsters.go b/pkg/memory/monsters.go new file mode 100644 index 0000000..c5f4e8b --- /dev/null +++ b/pkg/memory/monsters.go @@ -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 +} diff --git a/pkg/memory/object.go b/pkg/memory/object.go new file mode 100644 index 0000000..a5aa266 --- /dev/null +++ b/pkg/memory/object.go @@ -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 +} diff --git a/pkg/memory/offset.go b/pkg/memory/offset.go new file mode 100644 index 0000000..7e57610 --- /dev/null +++ b/pkg/memory/offset.go @@ -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, + } +} diff --git a/pkg/memory/player.go b/pkg/memory/player.go new file mode 100644 index 0000000..b02574c --- /dev/null +++ b/pkg/memory/player.go @@ -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 +} diff --git a/pkg/memory/process.go b/pkg/memory/process.go new file mode 100644 index 0000000..938696b --- /dev/null +++ b/pkg/memory/process.go @@ -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 +} diff --git a/pkg/memory/roster.go b/pkg/memory/roster.go new file mode 100644 index 0000000..b952b3f --- /dev/null +++ b/pkg/memory/roster.go @@ -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 +} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..b1379e8 --- /dev/null +++ b/pkg/utils/utils.go @@ -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)) +}