diff --git a/README.md b/README.md index 6ec6634..29f8a92 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/go.mod b/go.mod index 832be36..fadf4c0 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 445ff4e..d3c8a38 100644 --- a/go.sum +++ b/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= diff --git a/pkg/data/data.go b/pkg/data/data.go index db744b3..a436ce7 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -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 { diff --git a/pkg/data/items.go b/pkg/data/items.go index 7ebd36c..412c859 100644 --- a/pkg/data/items.go +++ b/pkg/data/items.go @@ -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 diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index 48f6d92..c3f4271 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -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{ diff --git a/pkg/memory/game_reader_test.go b/pkg/memory/game_reader_test.go new file mode 100644 index 0000000..c13826f --- /dev/null +++ b/pkg/memory/game_reader_test.go @@ -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() + } +} diff --git a/pkg/memory/item.go b/pkg/memory/item.go index fcc4057..592a655 100644 --- a/pkg/memory/item.go +++ b/pkg/memory/item.go @@ -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 } diff --git a/pkg/memory/player.go b/pkg/memory/player.go index bf30554..a7dfdd6 100644 --- a/pkg/memory/player.go +++ b/pkg/memory/player.go @@ -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), diff --git a/pkg/memory/player_unit.go b/pkg/memory/player_unit.go new file mode 100644 index 0000000..f1e0607 --- /dev/null +++ b/pkg/memory/player_unit.go @@ -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{} +} diff --git a/pkg/memory/process.go b/pkg/memory/process.go index caa1d82..63a87c3 100644 --- a/pkg/memory/process.go +++ b/pkg/memory/process.go @@ -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" diff --git a/pkg/memory/roster.go b/pkg/memory/roster.go index 46b340d..b26adf2 100644 --- a/pkg/memory/roster.go +++ b/pkg/memory/roster.go @@ -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...) } diff --git a/pkg/nip/rule.go b/pkg/nip/rule.go index 9845282..842fc68 100644 --- a/pkg/nip/rule.go +++ b/pkg/nip/rule.go @@ -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) -} diff --git a/pkg/nip/rule_test.go b/pkg/nip/rule_test.go index 0aa7558..f2d837b 100644 --- a/pkg/nip/rule_test.go +++ b/pkg/nip/rule_test.go @@ -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}, }, },