Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cain identify fix + improved npc interaction #631

Merged
merged 6 commits into from
Jan 26, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions internal/action/identify.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package action

import (
"fmt"
"time"

"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -72,29 +77,40 @@ 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.PauseIfNotPriority()
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 {
step.CloseAllMenus()
}

return nil
}

func itemsToIdentify() (items []data.Item) {
Expand Down
91 changes: 55 additions & 36 deletions internal/action/step/interact_npc.go
Original file line number Diff line number Diff line change
@@ -1,69 +1,88 @@
package step

import (
"errors"
"fmt"
"time"

"github.com/hectorgimenez/d2go/pkg/data"
"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"
)

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 = 8
minMenuOpenWait = 300 * time.Millisecond
maxDistance = 15
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 ctx.Data.OpenMenus.NPCInteract {
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 {
time.Sleep(minMenuOpenWait)
return nil
}
}
}

if interactionAttempts >= maxInteractionAttempts {
return errors.New("failed interacting with NPC")
// Wrong NPC, too far, or NPC moved - close menu and retry
CloseAllMenus()
time.Sleep(200 * time.Millisecond)
targetNPCID = 0
continue
}

// Give some time before retrying the interaction
if waitingForInteraction && time.Since(lastRun) < time.Millisecond*200 {
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
}

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
}
distance := ctx.PathFinder.DistanceFromMe(townNPC.Position)
if distance > maxDistance {
return fmt.Errorf("NPC %d is too far away (distance: %d)", npcID, distance)
}

distance := ctx.PathFinder.DistanceFromMe(m.Position)
if distance > 15 {
return fmt.Errorf("NPC is too far away: %d. Current distance: %d", npcID, distance)
}
// Calculate click position
x, y := ui.GameCoordsToScreenCords(townNPC.Position.X, townNPC.Position.Y)
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()

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
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
}
currentMouseCoords = data.Position{X: x, Y: y}
ctx.HID.MovePointer(x, y)
interactionAttempts++
time.Sleep(50 * time.Millisecond)
}
}

return fmt.Errorf("failed to interact with NPC after %d attempts", maxAttempts)
}