From 839b04fd89f5a970bc2429a3c2dd6aefdebad98a Mon Sep 17 00:00:00 2001 From: vietdungdev Date: Sun, 26 Apr 2026 13:39:16 +0700 Subject: [PATCH] update merc --- pkg/data/data.go | 25 +++--- pkg/data/npc.go | 12 +++ pkg/memory/game_reader.go | 3 + pkg/memory/mercenary.go | 183 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 pkg/memory/mercenary.go diff --git a/pkg/data/data.go b/pkg/data/data.go index 13c5d21..5bffa0a 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -30,6 +30,7 @@ type Data struct { Corpse Corpse Monsters Monsters Corpses Monsters + Mercenary OwnMercenary Game OnlineGame PlayerUnit PlayerUnit NPCs NPCs @@ -100,19 +101,21 @@ func (r Room) IsInside(p Position) bool { } func (d Data) MercHPPercent() int { - for _, m := range d.Monsters { - if m.IsMerc() { - // Hacky thing to read merc life properly - maxLife := m.Stats[stat.MaxLife] >> 8 - life := float64(m.Stats[stat.Life] >> 8) - if m.Stats[stat.Life] <= 32768 { - life = float64(m.Stats[stat.Life]) / 32768.0 * float64(maxLife) - } + // for _, m := range d.Monsters { + // if m.IsMerc() { + // // Hacky thing to read merc life properly + // maxLife := m.Stats[stat.MaxLife] >> 8 + // life := float64(m.Stats[stat.Life] >> 8) + // if m.Stats[stat.Life] <= 32768 { + // life = float64(m.Stats[stat.Life]) / 32768.0 * float64(maxLife) + // } - return int(life / float64(maxLife) * 100) - } + // return int(life / float64(maxLife) * 100) + // } + // } + if d.Mercenary.Found { + return d.Mercenary.HPPercent } - return 0 } diff --git a/pkg/data/npc.go b/pkg/data/npc.go index e380e88..63f8191 100644 --- a/pkg/data/npc.go +++ b/pkg/data/npc.go @@ -9,6 +9,18 @@ import ( "github.com/hectorgimenez/d2go/pkg/data/superunique" ) +type OwnMercenary struct { + Found bool + ClassID npc.ID + UnitID UnitID + OwnerUnitID UnitID + OwnerName string + Position Position + Mode mode.NpcMode + IsCorpse bool + HPPercent int +} + type NPC struct { ID npc.ID Name string diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index 99f0553..fdd2b71 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -147,6 +147,9 @@ func (gd *GameReader) GetData() data.Data { ActiveWeaponSlot: gd.GetActiveWeaponSlot(), } + merc, foundMerc := gd.OwnMercenary() + merc.Found = foundMerc + d.Mercenary = data.OwnMercenary(merc) return d } diff --git a/pkg/memory/mercenary.go b/pkg/memory/mercenary.go new file mode 100644 index 0000000..87b1ffd --- /dev/null +++ b/pkg/memory/mercenary.go @@ -0,0 +1,183 @@ +package memory + +import ( + "math" + "sort" + + "github.com/hectorgimenez/d2go/pkg/data" + "github.com/hectorgimenez/d2go/pkg/data/mode" + "github.com/hectorgimenez/d2go/pkg/data/npc" + "github.com/hectorgimenez/d2go/pkg/data/stat" +) + +type OwnMercenary data.OwnMercenary + +func (m OwnMercenary) Alive() bool { + return m.Found && !m.IsCorpse && m.HPPercent > 0 && m.Mode != mode.NpcDeath && m.Mode != mode.NpcDead +} + +func (gd *GameReader) OwnMercenary() (OwnMercenary, bool) { + if gd.offset.UnitTable == 0 { + gd.offset = calculateOffsets(gd.Process) + } + + ownerIDs := gd.mainPlayerOwnerIDs() + if len(ownerIDs) == 0 { + return OwnMercenary{}, false + } + + // Capture the main player name for ownership verification at the caller level. + mainPlayerName := gd.GetRawPlayerUnits().GetMainPlayer().Name + + baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + 1024 + unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8) + candidates := make([]OwnMercenary, 0) + + for bucket := 0; bucket < 128; bucket++ { + monsterOffset := uint(8 * bucket) + monsterUnitPtr := uintptr(ReadUIntFromBuffer(unitTableBuffer, monsterOffset, Uint64)) + for depth := 0; depth < 64 && monsterUnitPtr != 0; depth++ { + monsterDataBuffer := gd.Process.ReadBytesFromMemory(monsterUnitPtr, 0x1B0) + classID := npc.ID(ReadUIntFromBuffer(monsterDataBuffer, 0x04, Uint32)) + if !isMercenaryClassID(classID) { + monsterUnitPtr = uintptr(gd.Process.ReadUInt(monsterUnitPtr+0x158, Uint64)) + continue + } + + unitDataPtr := uintptr(ReadUIntFromBuffer(monsterDataBuffer, 0x10, Uint64)) + unitOwnerID := uint32(ReadUIntFromBuffer(monsterDataBuffer, 0x08, Uint32)) + unitDataOwnerID := uint32(0) + if unitDataPtr != 0 { + unitDataOwnerID = uint32(gd.Process.ReadUInt(unitDataPtr+0x54, Uint32)) + } + ownerID, ownerMatched := matchOwnerUnitID(ownerIDs, unitOwnerID, unitDataOwnerID) + if !ownerMatched { + monsterUnitPtr = uintptr(gd.Process.ReadUInt(monsterUnitPtr+0x158, Uint64)) + continue + } + + pathPtr := uintptr(ReadUIntFromBuffer(monsterDataBuffer, 0x38, Uint64)) + xPos := int(gd.Process.ReadUInt(pathPtr+0x02, Uint16)) + yPos := int(gd.Process.ReadUInt(pathPtr+0x06, Uint16)) + 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) + + candidates = append(candidates, OwnMercenary{ + Found: true, + ClassID: classID, + UnitID: data.UnitID(unitOwnerID), + OwnerUnitID: data.UnitID(ownerID), + OwnerName: mainPlayerName, + Position: data.Position{ + X: xPos, + Y: yPos, + }, + Mode: mode.NpcMode(ReadUIntFromBuffer(monsterDataBuffer, 0x0C, Uint32)), + IsCorpse: ReadUIntFromBuffer(monsterDataBuffer, 0x1AE, Uint8) != 0, + HPPercent: mercenaryHPPercent(stats), + }) + + monsterUnitPtr = uintptr(gd.Process.ReadUInt(monsterUnitPtr+0x158, Uint64)) + } + } + + if len(candidates) == 0 { + return OwnMercenary{}, false + } + + // Filter candidates to only the mercenary owned by the main (local) player. + // In a Wine/Proton party multiple players may have IsMainPlayer=true, so the + // raw candidates list can contain teammates' mercenaries. We filter by the + // owner name captured from the main player unit before sorting. + ownCandidates := make([]OwnMercenary, 0, len(candidates)) + for _, c := range candidates { + if c.OwnerName == mainPlayerName { + ownCandidates = append(ownCandidates, c) + } + } + if len(ownCandidates) == 0 { + return OwnMercenary{}, false + } + + sort.SliceStable(ownCandidates, func(i, j int) bool { + if ownCandidates[i].Alive() != ownCandidates[j].Alive() { + return ownCandidates[i].Alive() + } + if ownCandidates[i].ClassID == npc.Guard && ownCandidates[j].ClassID != npc.Guard { + return true + } + if ownCandidates[i].ClassID != npc.Guard && ownCandidates[j].ClassID == npc.Guard { + return false + } + return ownCandidates[i].HPPercent > ownCandidates[j].HPPercent + }) + + return ownCandidates[0], true +} + +func (gd *GameReader) mainPlayerOwnerIDs() map[uint32]struct{} { + rawPlayerUnits := gd.GetRawPlayerUnits() + ownerIDs := make(map[uint32]struct{}) + for _, player := range rawPlayerUnits { + if !player.IsMainPlayer || player.UnitID <= 0 { + continue + } + if player.Area > 0 && player.Position.X > 0 && player.Position.Y > 0 { + ownerIDs[uint32(player.UnitID)] = struct{}{} + } + } + if len(ownerIDs) > 0 { + return ownerIDs + } + for _, player := range rawPlayerUnits { + if player.IsMainPlayer && player.UnitID > 0 { + ownerIDs[uint32(player.UnitID)] = struct{}{} + } + } + return ownerIDs +} + +func isMercenaryClassID(classID npc.ID) bool { + switch classID { + case npc.Guard, npc.Act5Hireling1Hand, npc.Act5Hireling2Hand, npc.IronWolf, npc.Rogue2: + return true + default: + return false + } +} + +func matchOwnerUnitID(ownerIDs map[uint32]struct{}, unitOwnerID, unitDataOwnerID uint32) (uint32, bool) { + if _, ok := ownerIDs[unitDataOwnerID]; ok { + return unitDataOwnerID, true + } + if _, ok := ownerIDs[unitOwnerID]; ok { + return unitOwnerID, true + } + return 0, false +} + +func mercenaryHPPercent(stats map[stat.ID]int) int { + maxLife := stats[stat.MaxLife] >> 8 + if maxLife <= 0 { + return 0 + } + + life := float64(stats[stat.Life] >> 8) + if stats[stat.Life] <= 32768 { + life = float64(stats[stat.Life]) / 32768.0 * float64(maxLife) + } + + percentage := (life / float64(maxLife)) * 100 + if math.IsNaN(percentage) || math.IsInf(percentage, 0) { + return 0 + } + if percentage < 0 { + return 0 + } + if percentage > 100 { + return 100 + } + return int(percentage) +}