Skip to content

Commit

Permalink
Item Pickup Improvements (#666)
Browse files Browse the repository at this point in the history
Co-authored-by: Arto Simonyan <artosimonyan@protonmail.com>
  • Loading branch information
braccali1 and artosimonyan authored Feb 21, 2025
1 parent 015e607 commit 8ff9266
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 23 deletions.
47 changes: 39 additions & 8 deletions internal/action/item_pickup.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"log/slog"
"slices"
"time"

"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/area"
Expand All @@ -14,6 +13,7 @@ import (
"github.com/hectorgimenez/d2go/pkg/nip"
"github.com/hectorgimenez/koolo/internal/action/step"
"github.com/hectorgimenez/koolo/internal/context"
"github.com/hectorgimenez/koolo/internal/event"
)

func itemFitsInventory(i data.Item) bool {
Expand Down Expand Up @@ -47,9 +47,12 @@ func ItemPickup(maxDistance int) error {
ctx := context.Get()
ctx.SetLastAction("ItemPickup")

const maxRetries = 3
const maxRetries = 5
const maxItemTooFarAttempts = 5

for {
ctx.PauseIfNotPriority()

itemsToPickup := GetItemsToPickup(maxDistance)
if len(itemsToPickup) == 0 {
return nil
Expand Down Expand Up @@ -81,12 +84,14 @@ func ItemPickup(maxDistance int) error {
// Try to pick up the item with retries
var lastError error
attempt := 1
attemptItemTooFar := 1
for attempt <= maxRetries {
// Clear monsters on each attempt
ClearAreaAroundPosition(itemToPickup.Position, 4, data.MonsterAnyFilter())

// Calculate position to move to based on attempt number
// on 2nd and 3rd attempt try position left/right of item
// on 4th and 5th attempt try position further away
pickupPosition := itemToPickup.Position
moveDistance := 3
if attempt > 1 {
Expand All @@ -101,6 +106,13 @@ func ItemPickup(maxDistance int) error {
X: itemToPickup.Position.X - moveDistance,
Y: itemToPickup.Position.Y + 1,
}
case 4:
pickupPosition = data.Position{
X: itemToPickup.Position.X + moveDistance + 2,
Y: itemToPickup.Position.Y - 3,
}
case 5:
MoveToCoords(ctx.PathFinder.BeyondPosition(ctx.Data.PlayerUnit.Position, itemToPickup.Position, 4))
}
}

Expand All @@ -119,27 +131,42 @@ func ItemPickup(maxDistance int) error {
}

// Try to pick up the item
err := step.PickupItem(itemToPickup)
err := step.PickupItem(itemToPickup, attempt)
if err == nil {
break // Success!
}

lastError = err
// Skip logging when casting moving error and don't count these specific errors as retry attempts
if errors.Is(err, step.ErrCastingMoving) {
continue
}
ctx.Logger.Debug(fmt.Sprintf("Pickup attempt %d failed: %v", attempt, err))

// Don't count these specific errors as retry attempts
if errors.Is(err, step.ErrMonsterAroundItem) || errors.Is(err, step.ErrItemTooFar) {
if errors.Is(err, step.ErrMonsterAroundItem) {
continue
}

// Item too far retry logic
if errors.Is(err, step.ErrItemTooFar) {
// Use default retries first, if we hit last attempt retry add random movement and continue until maxItemTooFarAttempts
if attempt >= maxRetries && attemptItemTooFar <= maxItemTooFarAttempts {
ctx.Logger.Debug(fmt.Sprintf("Item too far pickup attempt %d", attemptItemTooFar))
attemptItemTooFar++
ctx.PathFinder.RandomMovement()
continue
}
}

if errors.Is(err, step.ErrNoLOSToItem) {
ctx.Logger.Debug("No line of sight to item, moving closer",
slog.String("item", itemToPickup.Desc().Name))

// Try moving beyond the item for better line of sight
beyondPos := ctx.PathFinder.BeyondPosition(ctx.Data.PlayerUnit.Position, itemToPickup.Position, 2+attempt)
if mvErr := MoveToCoords(beyondPos); mvErr == nil {
err = step.PickupItem(itemToPickup)
err = step.PickupItem(itemToPickup, attempt)
if err == nil {
break
}
Expand All @@ -151,14 +178,18 @@ func ItemPickup(maxDistance int) error {

attempt++

if attempt <= maxRetries {
time.Sleep(150 * time.Duration(attempt-1) * time.Millisecond)
}
}

// If all attempts failed, blacklist the item
if attempt > maxRetries && lastError != nil {
ctx.CurrentGame.BlacklistedItems = append(ctx.CurrentGame.BlacklistedItems, itemToPickup)

// Screenshot with show items on
ctx.HID.KeyDown(ctx.Data.KeyBindings.ShowItems)
screenshot := ctx.GameReader.Screenshot()
event.Send(event.ItemBlackListed(event.WithScreenshot(ctx.Name, fmt.Sprintf("Item %s [%s] BlackListed in Area:%s", itemToPickup.Name, itemToPickup.Quality.ToString(), ctx.Data.PlayerUnit.Area.Area().Name), screenshot), data.Drop{Item: itemToPickup}))
ctx.HID.KeyUp(ctx.Data.KeyBindings.ShowItems)

ctx.Logger.Warn(
"Failed picking up item after all attempts, blacklisting it",
slog.String("itemName", itemToPickup.Desc().Name),
Expand Down
52 changes: 37 additions & 15 deletions internal/action/step/pickup_item.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/hectorgimenez/d2go/pkg/data"
"github.com/hectorgimenez/d2go/pkg/data/item"
"github.com/hectorgimenez/d2go/pkg/data/mode"
"github.com/hectorgimenez/d2go/pkg/data/stat"
"github.com/hectorgimenez/koolo/internal/context"
"github.com/hectorgimenez/koolo/internal/game"
Expand All @@ -15,25 +16,45 @@ import (
)

const (
maxInteractions = 45
spiralDelay = 50 * time.Millisecond
clickDelay = 25 * time.Millisecond
pickupTimeout = 3 * time.Second
clickDelay = 25 * time.Millisecond
spiralDelay = 25 * time.Millisecond
pickupTimeout = 3 * time.Second
)

var (
maxInteractions = 24 // 25 attempts since we start at 0
ErrItemTooFar = errors.New("item is too far away")
ErrNoLOSToItem = errors.New("no line of sight to item")
ErrMonsterAroundItem = errors.New("monsters detected around item")
ErrCastingMoving = errors.New("char casting or moving")
)

func PickupItem(it data.Item) error {
func PickupItem(it data.Item, itemPickupAttempt int) error {
ctx := context.Get()
ctx.SetLastStep("PickupItem")

// Casting skill/moving return back
for ctx.Data.PlayerUnit.Mode == mode.CastingSkill || ctx.Data.PlayerUnit.Mode == mode.Running || ctx.Data.PlayerUnit.Mode == mode.Walking || ctx.Data.PlayerUnit.Mode == mode.WalkingInTown {
time.Sleep(25 * time.Millisecond)
return ErrCastingMoving
}

// Calculate base screen position for item
baseX := it.Position.X - 1
baseY := it.Position.Y - 1
switch itemPickupAttempt {
case 3:
baseX = baseX + 1
case 4:
maxInteractions = 44
baseY = baseY + 1
case 5:
maxInteractions = 44
baseX = baseX - 1
baseY = baseY - 1
default:
maxInteractions = 24
}
baseScreenX, baseScreenY := ctx.PathFinder.GameCoordsToScreenCords(baseX, baseY)

// Check for monsters first
Expand All @@ -59,7 +80,7 @@ func PickupItem(it data.Item) error {
spiralAttempt := 0
targetItem := it
lastMonsterCheck := time.Now()
const monsterCheckInterval = 250 * time.Millisecond
const monsterCheckInterval = 150 * time.Millisecond

startTime := time.Now()

Expand All @@ -78,15 +99,18 @@ func PickupItem(it data.Item) error {
// Check if item still exists
currentItem, exists := findItemOnGround(targetItem.UnitID)
if !exists {
ctx.Logger.Info(fmt.Sprintf("Picked up: %s [%s]", targetItem.Desc().Name, targetItem.Quality.ToString()))

ctx.Logger.Info(fmt.Sprintf("Picked up: %s [%s] | Item Pickup Attempt:%d | Spiral Attempt:%d", targetItem.Desc().Name, targetItem.Quality.ToString(), itemPickupAttempt, spiralAttempt))

ctx.CurrentGame.PickedUpItems[int(targetItem.UnitID)] = int(ctx.Data.PlayerUnit.Area.Area().ID)

return nil // Success!
}

// Check timeout conditions
if spiralAttempt > maxInteractions ||
(!waitingForInteraction.IsZero() && time.Since(waitingForInteraction) > pickupTimeout) ||
time.Since(startTime) > pickupTimeout*2 {
time.Since(startTime) > pickupTimeout {
return fmt.Errorf("failed to pick up %s after %d attempts", it.Desc().Name, spiralAttempt)
}

Expand All @@ -98,10 +122,8 @@ func PickupItem(it data.Item) error {
ctx.HID.MovePointer(cursorX, cursorY)
time.Sleep(spiralDelay)

// Refresh game state and check hover
ctx.RefreshGameData()

if currentItem.IsHovered {
// Click on item if mouse is hovering over
if currentItem.UnitID == ctx.GameReader.GameReader.GetData().HoverData.UnitID {
ctx.HID.Click(game.LeftButton, cursorX, cursorY)
time.Sleep(clickDelay)

Expand All @@ -113,7 +135,7 @@ func PickupItem(it data.Item) error {

// Sometimes we got stuck because mouse is hovering a chest and item is in behind, it usually happens a lot
// on Andariel, so we open it
if isChestHovered() {
if isChestorShrineHovered() {
ctx.HID.Click(game.LeftButton, cursorX, cursorY)
time.Sleep(50 * time.Millisecond)
}
Expand Down Expand Up @@ -143,11 +165,11 @@ func findItemOnGround(targetID data.UnitID) (data.Item, bool) {
return data.Item{}, false
}

func isChestHovered() bool {
func isChestorShrineHovered() bool {
ctx := context.Get()

for _, o := range ctx.Data.Objects {
if o.IsChest() && o.IsHovered {
if (o.IsChest() || o.IsShrine()) && o.IsHovered {
return true
}
}
Expand Down
12 changes: 12 additions & 0 deletions internal/event/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ type RunStartedEvent struct {
RunName string
}

type ItemBlackListedEvent struct {
BaseEvent
Item data.Drop
}

func ItemBlackListed(be BaseEvent, drop data.Drop) ItemBlackListedEvent {
return ItemBlackListedEvent{
BaseEvent: be,
Item: drop,
}
}

func RunStarted(be BaseEvent, runName string) RunStartedEvent {
return RunStartedEvent{
BaseEvent: be,
Expand Down

0 comments on commit 8ff9266

Please sign in to comment.