diff --git a/cmd/itemwatcher/internal/watcher.go b/cmd/itemwatcher/internal/watcher.go index 8b871c0..ee37826 100644 --- a/cmd/itemwatcher/internal/watcher.go +++ b/cmd/itemwatcher/internal/watcher.go @@ -2,6 +2,10 @@ package itemwatcher import ( "context" + "log" + "os" + "time" + "github.com/faiface/beep" "github.com/faiface/beep/speaker" "github.com/faiface/beep/wav" @@ -11,9 +15,6 @@ import ( "github.com/hectorgimenez/d2go/pkg/itemfilter" "github.com/hectorgimenez/d2go/pkg/memory" "github.com/hectorgimenez/d2go/pkg/nip" - "log" - "os" - "time" ) type Watcher struct { @@ -53,7 +54,7 @@ func (w *Watcher) Start(ctx context.Context) error { time.Sleep(100 * time.Millisecond) d := w.gr.GetData() - for _, i := range d.Items.Ground { + for _, i := range d.Items.ByLocation(item.LocationGround) { if !itemfilter.Evaluate(i, w.rules) { continue } @@ -85,7 +86,7 @@ func (w *Watcher) Start(ctx context.Context) error { purgedNotifiedItems := make([]itemFootprint, 0) for _, t := range w.alreadyNotifiedItemIDs { found := false - for _, it := range d.Items.Ground { + for _, it := range d.Items.ByLocation(item.LocationGround) { if t.Match(d.PlayerUnit.Area, it) { found = true } diff --git a/pkg/data/area/area.go b/pkg/data/area/area.go index 5c1dfdd..fd32111 100644 --- a/pkg/data/area/area.go +++ b/pkg/data/area/area.go @@ -11,6 +11,23 @@ func (a Area) IsTown() bool { return false } +func (a Area) Act() int { + if a < 40 { + return 1 + } + if a >= 40 && a < 75 { + return 2 + } + if a >= 75 && a < 103 { + return 3 + } + if a >= 103 && a < 109 { + return 4 + } + + return 5 +} + const ( Abaddon Area = 125 AncientTunnels Area = 65 diff --git a/pkg/data/data.go b/pkg/data/data.go index a3ad207..c30fc1c 100644 --- a/pkg/data/data.go +++ b/pkg/data/data.go @@ -40,6 +40,7 @@ type Data struct { Rooms []Room OpenMenus OpenMenus Roster Roster + HoverData HoverData } type Room struct { @@ -48,6 +49,12 @@ type Room struct { Height int } +type HoverData struct { + IsHovered bool + UnitID + UnitType int +} + func (r Room) GetCenter() Position { return Position{ X: r.Position.X + r.Width/2, @@ -98,10 +105,9 @@ func (r Roster) FindByName(name string) (RosterMember, bool) { } type Level struct { - Area area.Area - Position Position - IsGoodExit bool - CanInteract bool + Area area.Area + Position Position + IsEntrance bool // This means the area can not be accessed just walking through it, needs to be clicked } type Class uint @@ -129,6 +135,7 @@ type Position struct { type PlayerUnit struct { Name string + ID UnitID Area area.Area Position Position Stats map[stat.ID]int @@ -226,4 +233,5 @@ type OpenMenus struct { QuitMenu bool Cube bool SkillSelect bool + Anvil bool } diff --git a/pkg/data/item/location.go b/pkg/data/item/location.go new file mode 100644 index 0000000..cbf6a62 --- /dev/null +++ b/pkg/data/item/location.go @@ -0,0 +1,18 @@ +package item + +type Location string + +const ( + LocationEquipped Location = "equipped" + LocationStash Location = "stash" + LocationSharedStash1 Location = "shared_stash_1" + LocationSharedStash2 Location = "shared_stash_2" + LocationSharedStash3 Location = "shared_stash_3" + LocationBelt Location = "belt" + LocationInventory Location = "inventory" + LocationCube Location = "cube" + LocationVendor Location = "vendor" + LocationGround Location = "ground" + LocationSocket Location = "socket" + LocationUnknown Location = "unknown" +) diff --git a/pkg/data/item/type.go b/pkg/data/item/type.go index 41b4c83..bb1c21d 100644 --- a/pkg/data/item/type.go +++ b/pkg/data/item/type.go @@ -41,6 +41,26 @@ var typeMapping = map[string][]string{ "voodooheads": {"PreservedHead", "ZombieHead", "UnravellerHead", "GargoyleHead", "DemonHeadShield", "MummifiedTrophy", "FetishTrophy", "SextonTrophy", "CantorTrophy", "HierophantTrophy", "MinionSkull", "HellspawnSkull", "OverseerSkull", "SuccubusSkull", "BloodlordSkull"}, } +var QuestItems = []string{ + "TheGidbinn", + "WirtsLeg", + "HoradricMalus", + "HellforgeHammer", + "HoradricStaff", + "StaffOfKings", + "KhalimsFlail", + "AmuletOfTheViper", + "KhalimsEye", + "KhalimsHeart", + "KhalimsBrain", + "KhalimsWill", + "ScrollOfInifuss", + "HoradricCube", + "HoradricScroll", + "MephistosSoulstone", + "BookOfSkill", +} + func TypeForItemName(itemName string) (string, bool) { for t, itemNames := range typeMapping { for _, name := range itemNames { diff --git a/pkg/data/items.go b/pkg/data/items.go index fc1fc42..d27c9ad 100644 --- a/pkg/data/items.go +++ b/pkg/data/items.go @@ -8,14 +8,43 @@ import ( ) type Items struct { - Belt Belt - Inventory Inventory - Shop []Item - Ground []Item - Equipped []Item + Belt Belt + AllItems []Item +} + +func (i Items) Find(name item.Name, locations ...item.Location) (Item, bool) { + for _, it := range i.AllItems { + if strings.EqualFold(string(it.Name), string(name)) { + // If no locations are specified, return the first item found + if len(locations) == 0 { + return it, true + } + + for _, l := range locations { + if it.Location == l { + return it, true + } + } + } + } + + return Item{}, false +} + +func (i Items) ByLocation(locations ...item.Location) []Item { + var items []Item + + for _, it := range i.AllItems { + for _, l := range locations { + if it.Location == l { + items = append(items, it) + } + } + } + + return items } -type Inventory []Item type UnitID int type Item struct { @@ -23,11 +52,11 @@ type Item struct { Name item.Name Quality item.Quality Position Position + Location item.Location Ethereal bool IsHovered bool Stats map[stat.ID]stat.Data Identified bool - IsVendor bool } func (i Item) Type() string { @@ -47,6 +76,17 @@ func (i Item) IsHealingPotion() bool { func (i Item) IsManaPotion() bool { return strings.Contains(string(i.Name), string(ManaPotion)) } + func (i Item) IsRejuvPotion() bool { return strings.Contains(string(i.Name), string(RejuvenationPotion)) } + +func (i Item) IsFromQuest() bool { + for _, q := range item.QuestItems { + if strings.EqualFold(string(i.Name), q) { + return true + } + } + + return false +} diff --git a/pkg/data/npc.go b/pkg/data/npc.go index 886d95b..d8f8f37 100644 --- a/pkg/data/npc.go +++ b/pkg/data/npc.go @@ -50,7 +50,7 @@ func (m Monsters) FindOne(id npc.ID, t MonsterType) (Monster, bool) { func (m Monsters) Enemies(filters ...MonsterFilter) []Monster { monsters := make([]Monster, 0) for _, mo := range m { - if !mo.IsMerc() && !mo.IsGoodNPC() { + if !mo.IsMerc() && !mo.IsGoodNPC() && mo.Stats[stat.Life] > 0 { monsters = append(monsters, mo) } } diff --git a/pkg/data/objects.go b/pkg/data/objects.go index 98b01df..428d260 100644 --- a/pkg/data/objects.go +++ b/pkg/data/objects.go @@ -29,6 +29,16 @@ func (o Object) IsWaypoint() bool { object.Act2Waypoint, object.Act3TownWaypoint, object.PandamoniumFortressWaypoint, + object.Act2CellerWaypoint, + object.Act2SewerWaypoint, + object.Act3TravincalWaypoint, + object.ValleyWaypoint, + object.WorldstoneWaypoint, + object.ExpansionWildernessWaypoint, + object.IceCaveWaypoint, + object.TempleWaypoint, + object.InnerHellWaypoint, + object.WaypointH, object.ExpansionWaypoint: return true } @@ -85,7 +95,8 @@ func (o Object) IsDoor() bool { object.AndarielDoor, object.PenBreakableDoor, object.ArreatSummitDoorToWorldstone, - object.SecretDoor1: + object.SecretDoor1, + object.ExpansionTownGate: return true } diff --git a/pkg/memory/game_reader.go b/pkg/memory/game_reader.go index 9394fb8..d3f6824 100644 --- a/pkg/memory/game_reader.go +++ b/pkg/memory/game_reader.go @@ -28,15 +28,17 @@ func (gd *GameReader) GetData() data.Data { playerUnitPtr, corpse := gd.GetPlayerUnitPtr(roster) pu := gd.GetPlayerUnit(playerUnitPtr) + hover := gd.hoveredData() d := data.Data{ Corpse: corpse, - Monsters: gd.Monsters(pu.Position), + Monsters: gd.Monsters(pu.Position, hover), PlayerUnit: pu, - Items: gd.Items(pu.Position), - Objects: gd.Objects(pu.Position), + Items: gd.Items(pu, hover), + Objects: gd.Objects(pu.Position, hover), OpenMenus: gd.openMenus(), Roster: roster, + HoverData: hover, } if playerUnitPtr == 0 { @@ -74,21 +76,26 @@ func (gd *GameReader) openMenus() data.OpenMenus { QuitMenu: buffer[0x09] != 0, Cube: buffer[0x19] != 0, SkillSelect: buffer[0x03] != 0, + Anvil: buffer[0x0D] != 0, } } -func (gd *GameReader) hoveredData() (hoveredUnitID uint, hoveredType uint, isHovered bool) { +func (gd *GameReader) hoveredData() data.HoverData { 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) + hoveredType := ReadUIntFromBuffer(hoverBuffer, 0x04, Uint32) + hoveredUnitID := ReadUIntFromBuffer(hoverBuffer, 0x08, Uint32) - return hoveredUnitID, hoveredType, true + return data.HoverData{ + IsHovered: true, + UnitID: data.UnitID(hoveredUnitID), + UnitType: int(hoveredType), + } } - return 0, 0, false + return data.HoverData{} } func (gd *GameReader) getStatsData(statCount uint, statPtr uintptr) []stat.Data { @@ -124,15 +131,3 @@ func (gd *GameReader) getStatsData(statCount uint, statPtr uintptr) []stat.Data return stats } - -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 index a5af34d..7344aab 100644 --- a/pkg/memory/item.go +++ b/pkg/memory/item.go @@ -9,9 +9,7 @@ import ( "github.com/hectorgimenez/d2go/pkg/utils" ) -func (gd *GameReader) Items(playerPosition data.Position) data.Items { - hoveredUnitID, hoveredType, isHovered := gd.hoveredData() - +func (gd *GameReader) Items(pu data.PlayerUnit, hover data.HoverData) data.Items { baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (4 * 1024) unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8) @@ -38,7 +36,7 @@ func (gd *GameReader) Items(playerPosition data.Position) data.Items { flags := ReadUIntFromBuffer(unitDataBuffer, 0x18, Uint32) invPage := ReadUIntFromBuffer(unitDataBuffer, 0x55, Uint8) itemQuality := ReadUIntFromBuffer(unitDataBuffer, 0x00, Uint32) - //itemOwnerNPC := ReadUIntFromBuffer(unitDataBuffer, 0x0C, Uint32) + itemOwnerNPC := ReadUIntFromBuffer(unitDataBuffer, 0x0C, Uint32) // Item coordinates (X, Y) pathPtr := uintptr(ReadUIntFromBuffer(itemDataBuffer, 0x38, Uint64)) @@ -58,7 +56,7 @@ func (gd *GameReader) Items(playerPosition data.Position) data.Items { name := item.GetNameByEnum(txtFileNo) itemHovered := false - if isHovered && hoveredType == 4 && hoveredUnitID == unitID { + if hover.IsHovered && hover.UnitType == 4 && hover.UnitID == data.UnitID(unitID) { itemHovered = true } @@ -75,22 +73,53 @@ func (gd *GameReader) Items(playerPosition data.Position) data.Items { } setProperties(&itm, uint32(flags)) + location := item.LocationUnknown switch itemLoc { case 0: - if itm.IsVendor { - items.Shop = append(items.Shop, itm) + if 0x00002000&flags != 0 { + location = item.LocationVendor + break } else if invPage == 0 { - items.Inventory = append(items.Inventory, itm) + location = item.LocationInventory + break + } + if data.UnitID(itemOwnerNPC) == pu.ID || 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: - items.Equipped = append(items.Equipped, itm) + location = item.LocationEquipped if itm.Type() == "belt" { belt.Name = itm.Name } case 2: - belt.Items = append(belt.Items, itm) + location = item.LocationBelt case 3, 5: - items.Ground = append(items.Ground, itm) + location = item.LocationGround + case 6: + location = item.LocationSocket + } + + itm.Location = location + + 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)) @@ -99,9 +128,9 @@ func (gd *GameReader) Items(playerPosition data.Position) data.Items { items.Belt = belt - 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) + 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) return distanceI < distanceJ }) @@ -127,3 +156,12 @@ func (gd *GameReader) getItemStats(statCount uint, statPtr uintptr, statExCount return stats } + +func setProperties(item *data.Item, flags uint32) { + if 0x00400000&flags != 0 { + item.Ethereal = true + } + if 0x00000010&flags != 0 { + item.Identified = true + } +} diff --git a/pkg/memory/monsters.go b/pkg/memory/monsters.go index c2e8e83..7b11a75 100644 --- a/pkg/memory/monsters.go +++ b/pkg/memory/monsters.go @@ -1,16 +1,15 @@ package memory import ( + "sort" + "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() - +func (gd *GameReader) Monsters(playerPosition data.Position, hover data.HoverData) data.Monsters { baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + 1024 unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8) @@ -41,7 +40,7 @@ func (gd *GameReader) Monsters(playerPosition data.Position) data.Monsters { posY := gd.Process.ReadUInt(pathPtr+0x06, Uint16) hovered := false - if isHovered && hoveredType == 1 && hoveredUnitID == unitID { + if hover.IsHovered && hover.UnitType == 1 && hover.UnitID == data.UnitID(unitID) { hovered = true } diff --git a/pkg/memory/object.go b/pkg/memory/object.go index a5aa266..f7ea632 100644 --- a/pkg/memory/object.go +++ b/pkg/memory/object.go @@ -1,15 +1,14 @@ package memory import ( + "sort" + "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() - +func (gd *GameReader) Objects(playerPosition data.Position, hover data.HoverData) []data.Object { baseAddr := gd.Process.moduleBaseAddressPtr + gd.offset.UnitTable + (2 * 1024) unitTableBuffer := gd.Process.ReadBytesFromMemory(baseAddr, 128*8) @@ -34,7 +33,7 @@ func (gd *GameReader) Objects(playerPosition data.Position) []data.Object { obj := data.Object{ Name: object.Name(int(txtFileNo)), - IsHovered: unitID == hoveredUnitID && hoveredType == 2 && isHovered, + IsHovered: data.UnitID(unitID) == hover.UnitID && hover.UnitType == 2 && hover.IsHovered, InteractType: object.InteractType(interactType), Selectable: mode == 0, Position: data.Position{ diff --git a/pkg/memory/player.go b/pkg/memory/player.go index d3657b4..f8a7328 100644 --- a/pkg/memory/player.go +++ b/pkg/memory/player.go @@ -34,10 +34,10 @@ func (gd *GameReader) GetPlayerUnitPtr(roster data.Roster) (playerUnitPtr uintpt isCorpse := gd.Process.ReadUInt(playerUnit+0x1A6, Uint8) if isCorpse == 1 { unitID := gd.Process.ReadUInt(playerUnit+0x08, Uint32) - hoveredUnitID, hoveredType, isHovered := gd.hoveredData() + hover := gd.hoveredData() corpse = data.Corpse{ Found: true, - IsHovered: isHovered && hoveredUnitID == unitID && hoveredType == 0, + IsHovered: hover.IsHovered && hover.UnitID == data.UnitID(unitID) && hover.UnitType == 0, Position: data.Position{ X: int(xPos), Y: int(yPos), @@ -76,6 +76,8 @@ func (gd *GameReader) GetPlayerUnitPtr(roster data.Roster) (playerUnitPtr uintpt } 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)) @@ -127,6 +129,7 @@ func (gd *GameReader) GetPlayerUnit(playerUnit uintptr) data.PlayerUnit { return data.PlayerUnit{ Name: name, + ID: data.UnitID(unitID), Area: area.Area(levelNo), Position: data.Position{ X: int(xPos),