From 8ff92661316d3b6924638b3579ba50e8c6c95ac4 Mon Sep 17 00:00:00 2001 From: braccali1 <92634889+braccali1@users.noreply.github.com> Date: Fri, 21 Feb 2025 11:10:39 -0500 Subject: [PATCH] Item Pickup Improvements (#666) Co-authored-by: Arto Simonyan --- internal/action/item_pickup.go | 47 +++++++++++++++++++++----- internal/action/step/pickup_item.go | 52 ++++++++++++++++++++--------- internal/event/events.go | 12 +++++++ 3 files changed, 88 insertions(+), 23 deletions(-) diff --git a/internal/action/item_pickup.go b/internal/action/item_pickup.go index 0b41f89f5..ce70657c1 100644 --- a/internal/action/item_pickup.go +++ b/internal/action/item_pickup.go @@ -5,7 +5,6 @@ import ( "fmt" "log/slog" "slices" - "time" "github.com/hectorgimenez/d2go/pkg/data" "github.com/hectorgimenez/d2go/pkg/data/area" @@ -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 { @@ -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 @@ -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 { @@ -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)) } } @@ -119,19 +131,34 @@ 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)) @@ -139,7 +166,7 @@ func ItemPickup(maxDistance int) error { // 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 } @@ -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), diff --git a/internal/action/step/pickup_item.go b/internal/action/step/pickup_item.go index 3b5f5e319..8471d83a6 100644 --- a/internal/action/step/pickup_item.go +++ b/internal/action/step/pickup_item.go @@ -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" @@ -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 @@ -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() @@ -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) } @@ -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) @@ -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) } @@ -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 } } diff --git a/internal/event/events.go b/internal/event/events.go index c6177b0ec..991e4cc3c 100644 --- a/internal/event/events.go +++ b/internal/event/events.go @@ -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,