From a28abfe80536e7369a0b562fab83da23587e9624 Mon Sep 17 00:00:00 2001 From: elb Date: Thu, 16 Jan 2025 11:13:15 -0500 Subject: [PATCH 1/4] Cain identify fix + improved npc interaction --- internal/action/identify.go | 41 +++++++---- internal/action/step/interact_npc.go | 103 ++++++++++++++++++--------- 2 files changed, 97 insertions(+), 47 deletions(-) diff --git a/internal/action/identify.go b/internal/action/identify.go index 8d46f51c..9c70c774 100644 --- a/internal/action/identify.go +++ b/internal/action/identify.go @@ -2,6 +2,7 @@ package action import ( "fmt" + "time" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/item" @@ -30,6 +31,10 @@ func IdentifyAll(skipIdentify bool) error { if ctx.CharacterCfg.Game.UseCainIdentify { ctx.Logger.Debug("Identifying all item with Cain...") + // Close any open menus first + step.CloseAllMenus() + utils.Sleep(500) + err := CainIdentify() // if identifying with cain fails then we should continue to identify using tome if err == nil { @@ -72,29 +77,39 @@ func CainIdentify() error { stayAwhileAndListen := town.GetTownByArea(ctx.Data.PlayerUnit.Area).IdentifyNPC() + // Close any open menus first + step.CloseAllMenus() + utils.Sleep(200) + err := InteractNPC(stayAwhileAndListen) if err != nil { - ctx.Logger.Error("Error interacting with Cain: ", "error", err.Error()) - return err + return fmt.Errorf("error interacting with Cain: %w", err) } - // Select the identify option - ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) - utils.Sleep(500) - - if len(itemsToIdentify()) > 0 { - - // Close the NPC interact menu if it's open + // Verify menu opened + menuWait := time.Now().Add(2 * time.Second) + for time.Now().Before(menuWait) { + ctx.RefreshGameData() if ctx.Data.OpenMenus.NPCInteract { - ctx.HID.KeySequence(win.VK_ESCAPE) + break } + utils.Sleep(100) + } - return fmt.Errorf("failed to identify items") + if !ctx.Data.OpenMenus.NPCInteract { + return fmt.Errorf("NPC menu did not open") } - utils.Sleep(500) + // Select identify option + ctx.HID.KeySequence(win.VK_HOME, win.VK_DOWN, win.VK_RETURN) + utils.Sleep(800) - return step.CloseAllMenus() + // Close menu if still open + if ctx.Data.OpenMenus.NPCInteract { + ctx.HID.PressKey(win.VK_ESCAPE) + } + + return nil } func itemsToIdentify() (items []data.Item) { diff --git a/internal/action/step/interact_npc.go b/internal/action/step/interact_npc.go index c73b1988..76ec2ef6 100644 --- a/internal/action/step/interact_npc.go +++ b/internal/action/step/interact_npc.go @@ -13,57 +13,92 @@ import ( ) func InteractNPC(npcID npc.ID) error { - maxInteractionAttempts := 5 - interactionAttempts := 0 - waitingForInteraction := false - currentMouseCoords := data.Position{} - lastRun := time.Time{} - ctx := context.Get() ctx.SetLastStep("InteractNPC") - for { - ctx.RefreshGameData() + const ( + maxAttempts = 5 + interactionTimeout = 3 * time.Second + minMenuOpenWait = 200 * time.Millisecond + ) + + var lastInteractionTime time.Time + var currentMouseCoords data.Position - // Pause the execution if the priority is not the same as the execution priority + for attempts := 0; attempts < maxAttempts; attempts++ { + ctx.RefreshGameData() ctx.PauseIfNotPriority() - if ctx.Data.OpenMenus.NPCInteract { - return nil + // Clear last interaction if we've waited long enough + if !lastInteractionTime.IsZero() && time.Since(lastInteractionTime) > interactionTimeout { + lastInteractionTime = time.Time{} } - if interactionAttempts >= maxInteractionAttempts { - return errors.New("failed interacting with NPC") + // Check if interaction succeeded + if ctx.Data.OpenMenus.NPCInteract { + // Verify we're interacting with the right NPC by checking distance + if townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found { + if ctx.PathFinder.DistanceFromMe(townNPC.Position) <= 15 { + // Wait a minimum time to ensure menu is fully open + time.Sleep(minMenuOpenWait) + return nil + } + } + // Wrong NPC or too far - close menu and retry + ctx.HID.PressKey(0x1B) // ESC + time.Sleep(200 * time.Millisecond) + continue } - // Give some time before retrying the interaction - if waitingForInteraction && time.Since(lastRun) < time.Millisecond*200 { + // Don't attempt new interaction if we're waiting for previous one + if !lastInteractionTime.IsZero() && time.Since(lastInteractionTime) < minMenuOpenWait { + time.Sleep(50 * time.Millisecond) continue } - lastRun = time.Now() - m, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone) - if found { - if m.IsHovered { - ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) - waitingForInteraction = true - interactionAttempts++ - continue + // Find and interact with NPC + townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone) + if !found { + if attempts == maxAttempts-1 { + return fmt.Errorf("NPC %d not found after %d attempts", npcID, maxAttempts) } + time.Sleep(200 * time.Millisecond) + continue + } - distance := ctx.PathFinder.DistanceFromMe(m.Position) - if distance > 15 { - return fmt.Errorf("NPC is too far away: %d. Current distance: %d", npcID, distance) - } + distance := ctx.PathFinder.DistanceFromMe(townNPC.Position) + if distance > 15 { + return fmt.Errorf("NPC %d is too far away (distance: %d)", npcID, distance) + } + + x, y := ui.GameCoordsToScreenCords(townNPC.Position.X, townNPC.Position.Y) + // Act 4 Tyrael has a super weird hitbox + if npcID == npc.ID(240) { + y = y - 40 + } + + currentMouseCoords = data.Position{X: x, Y: y} + ctx.HID.MovePointer(x, y) - x, y := ui.GameCoordsToScreenCords(m.Position.X, m.Position.Y) - // Act 4 Tyrael has a super weird hitbox - if npcID == npc.Tyrael2 { - y = y - 40 + // Wait for hover + hoverWaitStart := time.Now() + hoverFound := false + for time.Since(hoverWaitStart) < 500*time.Millisecond { + ctx.RefreshGameData() + if townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found && townNPC.IsHovered { + hoverFound = true + break } - currentMouseCoords = data.Position{X: x, Y: y} - ctx.HID.MovePointer(x, y) - interactionAttempts++ + time.Sleep(50 * time.Millisecond) } + + if !hoverFound { + continue + } + + ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) + lastInteractionTime = time.Now() } + + return errors.New("failed to interact with NPC after all attempts") } From 76d3c078981b73adfe09910a4d87cd2d7a1ea3cd Mon Sep 17 00:00:00 2001 From: elb Date: Sun, 19 Jan 2025 16:10:46 -0500 Subject: [PATCH 2/4] some optimization? --- internal/action/identify.go | 2 +- internal/action/step/interact_npc.go | 80 +++++++++++++++++++--------- 2 files changed, 55 insertions(+), 27 deletions(-) diff --git a/internal/action/identify.go b/internal/action/identify.go index 9c70c774..0dd20af7 100644 --- a/internal/action/identify.go +++ b/internal/action/identify.go @@ -106,7 +106,7 @@ func CainIdentify() error { // Close menu if still open if ctx.Data.OpenMenus.NPCInteract { - ctx.HID.PressKey(win.VK_ESCAPE) + step.CloseAllMenus() } return nil diff --git a/internal/action/step/interact_npc.go b/internal/action/step/interact_npc.go index 76ec2ef6..ec4da3cb 100644 --- a/internal/action/step/interact_npc.go +++ b/internal/action/step/interact_npc.go @@ -9,6 +9,7 @@ import ( "github.com/hectorgimenez/d2go/pkg/data/npc" "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/game" + "github.com/hectorgimenez/koolo/internal/pather" "github.com/hectorgimenez/koolo/internal/ui" ) @@ -17,36 +18,43 @@ func InteractNPC(npcID npc.ID) error { ctx.SetLastStep("InteractNPC") const ( - maxAttempts = 5 + maxAttempts = 8 interactionTimeout = 3 * time.Second - minMenuOpenWait = 200 * time.Millisecond + minMenuOpenWait = 300 * time.Millisecond + maxDistance = 15 + hoverTimeout = 800 * time.Millisecond ) var lastInteractionTime time.Time - var currentMouseCoords data.Position + var targetNPCID data.UnitID for attempts := 0; attempts < maxAttempts; attempts++ { - ctx.RefreshGameData() ctx.PauseIfNotPriority() - // Clear last interaction if we've waited long enough + // Clear last interaction if we've waited too long if !lastInteractionTime.IsZero() && time.Since(lastInteractionTime) > interactionTimeout { lastInteractionTime = time.Time{} + targetNPCID = 0 } - // Check if interaction succeeded - if ctx.Data.OpenMenus.NPCInteract { - // Verify we're interacting with the right NPC by checking distance - if townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found { - if ctx.PathFinder.DistanceFromMe(townNPC.Position) <= 15 { - // Wait a minimum time to ensure menu is fully open - time.Sleep(minMenuOpenWait) - return nil + // Check if interaction succeeded and menu is open + if ctx.Data.OpenMenus.NPCInteract || ctx.Data.OpenMenus.NPCShop { + // Find current NPC position + if targetNPCID != 0 { + if currentNPC, found := ctx.Data.Monsters.FindByID(targetNPCID); found { + currentDistance := pather.DistanceFromPoint(currentNPC.Position, ctx.Data.PlayerUnit.Position) + if currentDistance <= maxDistance { + // Success - wait minimum time for menu to fully open + time.Sleep(minMenuOpenWait) + return nil + } } } - // Wrong NPC or too far - close menu and retry - ctx.HID.PressKey(0x1B) // ESC + + // Wrong NPC, too far, or NPC moved away - close menu and retry + CloseAllMenus() time.Sleep(200 * time.Millisecond) + targetNPCID = 0 continue } @@ -56,7 +64,7 @@ func InteractNPC(npcID npc.ID) error { continue } - // Find and interact with NPC + // Find and validate target NPC townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone) if !found { if attempts == maxAttempts-1 { @@ -67,27 +75,42 @@ func InteractNPC(npcID npc.ID) error { } distance := ctx.PathFinder.DistanceFromMe(townNPC.Position) - if distance > 15 { + if distance > maxDistance { return fmt.Errorf("NPC %d is too far away (distance: %d)", npcID, distance) } + // Calculate screen coordinates based on current NPC position x, y := ui.GameCoordsToScreenCords(townNPC.Position.X, townNPC.Position.Y) - // Act 4 Tyrael has a super weird hitbox + // Special case for Tyrael's hitbox if npcID == npc.ID(240) { y = y - 40 } - currentMouseCoords = data.Position{X: x, Y: y} ctx.HID.MovePointer(x, y) - // Wait for hover + // Wait for hover before clicking hoverWaitStart := time.Now() hoverFound := false - for time.Since(hoverWaitStart) < 500*time.Millisecond { - ctx.RefreshGameData() - if townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found && townNPC.IsHovered { - hoverFound = true - break + var hoveredNPC data.Monster + + for time.Since(hoverWaitStart) < hoverTimeout { + // Get fresh NPC position in case they moved + if currentNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found { + if currentNPC.IsHovered { + hoveredNPC = currentNPC + hoverFound = true + break + } + + // Update mouse position if NPC moved + newX, newY := ui.GameCoordsToScreenCords(currentNPC.Position.X, currentNPC.Position.Y) + if newX != x || newY != y { + if npcID == npc.ID(240) { + newY = newY - 40 + } + ctx.HID.MovePointer(newX, newY) + x, y = newX, newY + } } time.Sleep(50 * time.Millisecond) } @@ -96,8 +119,13 @@ func InteractNPC(npcID npc.ID) error { continue } - ctx.HID.Click(game.LeftButton, currentMouseCoords.X, currentMouseCoords.Y) + // Store the NPC ID we're interacting with + targetNPCID = hoveredNPC.UnitID + ctx.HID.Click(game.LeftButton, x, y) lastInteractionTime = time.Now() + + // Wait a bit for the menu to open + time.Sleep(minMenuOpenWait) } return errors.New("failed to interact with NPC after all attempts") From 4b795917ac3845a06033ddf8c1f6b9d79674dd86 Mon Sep 17 00:00:00 2001 From: elb Date: Sat, 25 Jan 2025 11:41:52 -0500 Subject: [PATCH 3/4] fix tyrael, simplified logic --- internal/action/identify.go | 1 + internal/action/step/interact_npc.go | 100 +++++---------------------- 2 files changed, 20 insertions(+), 81 deletions(-) diff --git a/internal/action/identify.go b/internal/action/identify.go index 0dd20af7..5f21d7c3 100644 --- a/internal/action/identify.go +++ b/internal/action/identify.go @@ -89,6 +89,7 @@ func CainIdentify() error { // Verify menu opened menuWait := time.Now().Add(2 * time.Second) for time.Now().Before(menuWait) { + ctx.PauseIfNotPriority() ctx.RefreshGameData() if ctx.Data.OpenMenus.NPCInteract { break diff --git a/internal/action/step/interact_npc.go b/internal/action/step/interact_npc.go index ec4da3cb..f5ffe004 100644 --- a/internal/action/step/interact_npc.go +++ b/internal/action/step/interact_npc.go @@ -1,7 +1,6 @@ package step import ( - "errors" "fmt" "time" @@ -9,7 +8,6 @@ import ( "github.com/hectorgimenez/d2go/pkg/data/npc" "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/game" - "github.com/hectorgimenez/koolo/internal/pather" "github.com/hectorgimenez/koolo/internal/ui" ) @@ -18,53 +16,22 @@ func InteractNPC(npcID npc.ID) error { ctx.SetLastStep("InteractNPC") const ( - maxAttempts = 8 - interactionTimeout = 3 * time.Second - minMenuOpenWait = 300 * time.Millisecond - maxDistance = 15 - hoverTimeout = 800 * time.Millisecond + maxAttempts = 8 + minMenuOpenWait = 300 * time.Millisecond + maxDistance = 15 + hoverWait = 800 * time.Millisecond ) - var lastInteractionTime time.Time - var targetNPCID data.UnitID - for attempts := 0; attempts < maxAttempts; attempts++ { + // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() - // Clear last interaction if we've waited too long - if !lastInteractionTime.IsZero() && time.Since(lastInteractionTime) > interactionTimeout { - lastInteractionTime = time.Time{} - targetNPCID = 0 - } - - // Check if interaction succeeded and menu is open + // If menu is already open and distance is good, we're done if ctx.Data.OpenMenus.NPCInteract || ctx.Data.OpenMenus.NPCShop { - // Find current NPC position - if targetNPCID != 0 { - if currentNPC, found := ctx.Data.Monsters.FindByID(targetNPCID); found { - currentDistance := pather.DistanceFromPoint(currentNPC.Position, ctx.Data.PlayerUnit.Position) - if currentDistance <= maxDistance { - // Success - wait minimum time for menu to fully open - time.Sleep(minMenuOpenWait) - return nil - } - } - } - - // Wrong NPC, too far, or NPC moved away - close menu and retry - CloseAllMenus() - time.Sleep(200 * time.Millisecond) - targetNPCID = 0 - continue - } - - // Don't attempt new interaction if we're waiting for previous one - if !lastInteractionTime.IsZero() && time.Since(lastInteractionTime) < minMenuOpenWait { - time.Sleep(50 * time.Millisecond) - continue + time.Sleep(minMenuOpenWait) + return nil } - // Find and validate target NPC townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone) if !found { if attempts == maxAttempts-1 { @@ -79,54 +46,25 @@ func InteractNPC(npcID npc.ID) error { return fmt.Errorf("NPC %d is too far away (distance: %d)", npcID, distance) } - // Calculate screen coordinates based on current NPC position + // Calculate click position x, y := ui.GameCoordsToScreenCords(townNPC.Position.X, townNPC.Position.Y) - // Special case for Tyrael's hitbox - if npcID == npc.ID(240) { - y = y - 40 + if npcID == npc.Tyrael2 { + y = y - 40 // Act 4 Tyrael has a super weird hitbox } + // Move mouse and wait for hover ctx.HID.MovePointer(x, y) + hoverStart := time.Now() - // Wait for hover before clicking - hoverWaitStart := time.Now() - hoverFound := false - var hoveredNPC data.Monster - - for time.Since(hoverWaitStart) < hoverTimeout { - // Get fresh NPC position in case they moved - if currentNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found { - if currentNPC.IsHovered { - hoveredNPC = currentNPC - hoverFound = true - break - } - - // Update mouse position if NPC moved - newX, newY := ui.GameCoordsToScreenCords(currentNPC.Position.X, currentNPC.Position.Y) - if newX != x || newY != y { - if npcID == npc.ID(240) { - newY = newY - 40 - } - ctx.HID.MovePointer(newX, newY) - x, y = newX, newY - } + for time.Since(hoverStart) < hoverWait { + if currentNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found && currentNPC.IsHovered { + ctx.HID.Click(game.LeftButton, x, y) + time.Sleep(minMenuOpenWait) + break } time.Sleep(50 * time.Millisecond) } - - if !hoverFound { - continue - } - - // Store the NPC ID we're interacting with - targetNPCID = hoveredNPC.UnitID - ctx.HID.Click(game.LeftButton, x, y) - lastInteractionTime = time.Now() - - // Wait a bit for the menu to open - time.Sleep(minMenuOpenWait) } - return errors.New("failed to interact with NPC after all attempts") + return fmt.Errorf("failed to interact with NPC after %d attempts", maxAttempts) } From ca5880b26c107912189b41e97d41b7e1ae34f629 Mon Sep 17 00:00:00 2001 From: elb Date: Sat, 25 Jan 2025 12:07:20 -0500 Subject: [PATCH 4/4] fix issue with a5 vendor --- internal/action/step/interact_npc.go | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/internal/action/step/interact_npc.go b/internal/action/step/interact_npc.go index f5ffe004..f044e9f8 100644 --- a/internal/action/step/interact_npc.go +++ b/internal/action/step/interact_npc.go @@ -8,6 +8,7 @@ import ( "github.com/hectorgimenez/d2go/pkg/data/npc" "github.com/hectorgimenez/koolo/internal/context" "github.com/hectorgimenez/koolo/internal/game" + "github.com/hectorgimenez/koolo/internal/pather" "github.com/hectorgimenez/koolo/internal/ui" ) @@ -22,14 +23,30 @@ func InteractNPC(npcID npc.ID) error { hoverWait = 800 * time.Millisecond ) + var targetNPCID data.UnitID + for attempts := 0; attempts < maxAttempts; attempts++ { // Pause the execution if the priority is not the same as the execution priority ctx.PauseIfNotPriority() - // If menu is already open and distance is good, we're done + // Check if interaction succeeded and menu is open if ctx.Data.OpenMenus.NPCInteract || ctx.Data.OpenMenus.NPCShop { - time.Sleep(minMenuOpenWait) - return nil + // Find current NPC position + if targetNPCID != 0 { + if currentNPC, found := ctx.Data.Monsters.FindByID(targetNPCID); found { + currentDistance := pather.DistanceFromPoint(currentNPC.Position, ctx.Data.PlayerUnit.Position) + if currentDistance <= maxDistance { + time.Sleep(minMenuOpenWait) + return nil + } + } + } + + // Wrong NPC, too far, or NPC moved - close menu and retry + CloseAllMenus() + time.Sleep(200 * time.Millisecond) + targetNPCID = 0 + continue } townNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone) @@ -58,6 +75,7 @@ func InteractNPC(npcID npc.ID) error { for time.Since(hoverStart) < hoverWait { if currentNPC, found := ctx.Data.Monsters.FindOne(npcID, data.MonsterTypeNone); found && currentNPC.IsHovered { + targetNPCID = currentNPC.UnitID ctx.HID.Click(game.LeftButton, x, y) time.Sleep(minMenuOpenWait) break