From c0e6ffd3145a768b715e1b658f17f6e8fe9bb705 Mon Sep 17 00:00:00 2001 From: See1Duck <61428716+see1duck@users.noreply.github.com> Date: Sat, 6 Sep 2025 21:43:30 +0200 Subject: [PATCH 1/3] LootScript: feat: refactor looting logic to use Rs2LootEngine for improved item handling --- .../microbot/aiofighter/loot/LootScript.java | 231 +++++----------- .../util/grounditem/Rs2GroundItem.java | 6 +- .../util/grounditem/Rs2LootEngine.java | 256 ++++++++++++++++++ 3 files changed, 331 insertions(+), 162 deletions(-) create mode 100644 runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java index 7f73dc7e2b7..dec675621cd 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java @@ -1,6 +1,7 @@ package net.runelite.client.plugins.microbot.aiofighter.loot; import lombok.extern.slf4j.Slf4j; +import net.runelite.client.plugins.grounditems.GroundItem; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.aiofighter.AIOFighterConfig; @@ -9,199 +10,111 @@ import net.runelite.client.plugins.microbot.aiofighter.enums.State; import net.runelite.client.plugins.microbot.util.grounditem.LootingParameters; import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; +import net.runelite.client.plugins.microbot.util.grounditem.Rs2LootEngine; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; @Slf4j public class LootScript extends Script { - int minFreeSlots = 0; - public LootScript() { + private static final int DEFAULT_MIN_STACK_EXCLUSIVE_ARROWS = 9; // allow 2+ + private static final int DEFAULT_MIN_STACK_EXCLUSIVE_RUNES = 1; // allow 2+ - } + private int minFreeSlots = 0; + public LootScript() {} public boolean run(AIOFighterConfig config) { - mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { minFreeSlots = config.bank() ? config.minFreeSlots() : 0; + if (!super.run()) return; if (!Microbot.isLoggedIn()) return; if (!config.toggleLootItems()) return; - if (AIOFighterPlugin.getState().equals(State.BANKING) || AIOFighterPlugin.getState().equals(State.WALKING)) return; - if (((Rs2Inventory.isFull() || Rs2Inventory.getEmptySlots() <= minFreeSlots) && !config.eatFoodForSpace()) || (Rs2Player.isInCombat() && !config.toggleForceLoot())) { - return; - } + final State st = AIOFighterPlugin.getState(); + if (st == State.BANKING || st == State.WALKING) return; - if (config.looterStyle().equals(DefaultLooterStyle.MIXED) || config.looterStyle().equals(DefaultLooterStyle.ITEM_LIST)) { - lootItemsOnName(config); + if (((Rs2Inventory.isFull() || Rs2Inventory.getEmptySlots() <= minFreeSlots) && !config.eatFoodForSpace()) + || (Rs2Player.isInCombat() && !config.toggleForceLoot())) { + return; } - if (config.looterStyle().equals(DefaultLooterStyle.GE_PRICE_RANGE) || config.looterStyle().equals(DefaultLooterStyle.MIXED)) { - lootItemsByValue(config); + LootingParameters params = new LootingParameters( + config.minPriceOfItemsToLoot(), + config.maxPriceOfItemsToLoot(), + config.attackRadius(), + /* minQuantity */ 1, + /* minInvSlots */ minFreeSlots, + config.toggleDelayedLooting(), + config.toggleOnlyLootMyItems() + ); + params.setEatFoodForSpace(config.eatFoodForSpace()); + + Rs2LootEngine.Builder builder = Rs2LootEngine.with(params) + .withLootAction(Rs2GroundItem::coreLoot); + + // custom filter + if (config.looterStyle() == DefaultLooterStyle.ITEM_LIST || config.looterStyle() == DefaultLooterStyle.MIXED) { + addCustomNames(builder, config.listOfItemsToLoot()); } - lootBones(config); - lootAshes(config); - lootRunes(config); - lootCoins(config); - lootUntradeableItems(config); - lootArrows(config); - - } catch(Exception ex) { - Microbot.log("Looterscript: " + ex.getMessage()); - } - - }, 0, 200, TimeUnit.MILLISECONDS); - return true; - } - - private void lootArrows(AIOFighterConfig config) { - if (config.toggleLootArrows()) { - LootingParameters arrowParams = new LootingParameters( - config.attackRadius(), - 1, - 10, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems(), - "arrow" - ); - arrowParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootItemsBasedOnNames(arrowParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); - } - } - } - - private void lootBones(AIOFighterConfig config) { - if (config.toggleBuryBones()) { - LootingParameters bonesParams = new LootingParameters( - config.attackRadius(), - 1, - 1, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems(), - "bones" - ); - bonesParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootItemsBasedOnNames(bonesParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); - } - } - } - private void lootAshes(AIOFighterConfig config) { - if (config.toggleScatter()) { - LootingParameters ashesParams = new LootingParameters( - config.attackRadius(), - 1, - 1, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems(), - " ashes" - ); - ashesParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootItemsBasedOnNames(ashesParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); - } - } - } + if (config.looterStyle() == DefaultLooterStyle.GE_PRICE_RANGE || config.looterStyle() == DefaultLooterStyle.MIXED) builder.addByValue(); + if (config.toggleBuryBones()) builder.addBones(); + if (config.toggleScatter()) builder.addAshes(); + if (config.toggleLootCoins()) builder.addCoins(); + if (config.toggleLootUntradables()) builder.addUntradables(); + if (config.toggleLootArrows()) builder.addArrows(DEFAULT_MIN_STACK_EXCLUSIVE_ARROWS); + if (config.toggleLootRunes()) builder.addRunes(DEFAULT_MIN_STACK_EXCLUSIVE_RUNES); + + // Execute one combined, distance-sorted looting pass + boolean looted = builder.loot(); + if (looted) { + Microbot.pauseAllScripts.compareAndSet(true, false); + } - // loot runes - private void lootRunes(AIOFighterConfig config) { - if (config.toggleLootRunes()) { - LootingParameters runesParams = new LootingParameters( - config.attackRadius(), - 1, - 1, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems(), - " rune" - ); - runesParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootItemsBasedOnNames(runesParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); + } catch (Exception ex) { + Microbot.log("LootScript: " + ex.getMessage()); } - } - } + }, 0, 200, TimeUnit.MILLISECONDS); - // loot coins - private void lootCoins(AIOFighterConfig config) { - if (config.toggleLootCoins()) { - LootingParameters coinsParams = new LootingParameters( - config.attackRadius(), - 1, - 1, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems(), - "coins" - ); - coinsParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootCoins(coinsParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); - } - } + return true; } - // loot untradeable items - private void lootUntradeableItems(AIOFighterConfig config) { - if (config.toggleLootUntradables()) { - LootingParameters untradeableItemsParams = new LootingParameters( - config.attackRadius(), - 1, - 1, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems(), - "untradeable" - ); - untradeableItemsParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootUntradables(untradeableItemsParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); + /** + * Adds a custom "by names" intent sourced from the config's comma-separated list. + * (We use a custom predicate so we don't depend on params.getNames()). + */ + private void addCustomNames(Rs2LootEngine.Builder builder, String csvNames) { + if (csvNames == null) return; + final Set needles = new HashSet<>(); + Arrays.stream(csvNames.split(",")) + .map(s -> s == null ? "" : s.trim().toLowerCase()) + .filter(s -> !s.isEmpty()) + .forEach(needles::add); + + if (needles.isEmpty()) return; + + Predicate byNames = gi -> { + final String n = gi.getName() == null ? "" : gi.getName().trim().toLowerCase(); + for (String needle : needles) { + if (n.contains(needle)) return true; } - } - } + return false; + }; - private void lootItemsByValue(AIOFighterConfig config) { - LootingParameters valueParams = new LootingParameters( - config.minPriceOfItemsToLoot(), - config.maxPriceOfItemsToLoot(), - config.attackRadius(), - 1, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems() - ); - valueParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootItemBasedOnValue(valueParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); - } + builder.addCustom("names", byNames, /*ignoredLower*/ null); } - private void lootItemsOnName(AIOFighterConfig config) { - LootingParameters valueParams = new LootingParameters( - config.attackRadius(), - 1, - 1, - minFreeSlots, - config.toggleDelayedLooting(), - config.toggleOnlyLootMyItems(), - config.listOfItemsToLoot().trim().split(",") - ); - valueParams.setEatFoodForSpace(config.eatFoodForSpace()); - if (Rs2GroundItem.lootItemsBasedOnNames(valueParams)) { - Microbot.pauseAllScripts.compareAndSet(true, false); - } - } + @Override public void shutdown() { super.shutdown(); } diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java index 436f22b4f24..10f66852f6e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2GroundItem.java @@ -34,7 +34,7 @@ public class Rs2GroundItem { private static final int DESPAWN_DELAY_THRESHOLD_TICKS = 150; - private static boolean runWhilePaused(BooleanSupplier booleanSupplier) { + public static boolean runWhilePaused(BooleanSupplier booleanSupplier) { final boolean paused = Microbot.pauseAllScripts.getAndSet(true); final boolean success = booleanSupplier.getAsBoolean(); if (!paused && !Microbot.pauseAllScripts.compareAndSet(true, false)) { @@ -262,7 +262,7 @@ public static boolean waitForGroundItemDespawn(Runnable actionWhileWaiting,Groun return groundItem != getGroundItems().get(groundItem.getLocation(), groundItem.getId()); } - private static boolean coreLoot(GroundItem groundItem) { + public static boolean coreLoot(GroundItem groundItem) { int quantity = Math.min(groundItem.isStackable() ? 1 : groundItem.getQuantity(), Rs2Inventory.emptySlotCount()); @@ -552,7 +552,7 @@ public static Table getGroundItems() { return GroundItemsPlugin.getCollectedGroundItems(); } - private static boolean canTakeGroundItem(GroundItem groundItem) { + public static boolean canTakeGroundItem(GroundItem groundItem) { int maxQuantity = groundItem.isStackable() ? 1 : groundItem.getQuantity(); int availableSlots = Rs2Inventory.emptySlotCount(); int quantity = Math.min(maxQuantity, availableSlots); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java new file mode 100644 index 00000000000..856cbdedb1e --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java @@ -0,0 +1,256 @@ +package net.runelite.client.plugins.microbot.util.grounditem; + +import net.runelite.api.ItemID; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.grounditems.GroundItem; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static net.runelite.api.TileItem.OWNERSHIP_SELF; +import static net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem.*; + +// import static your.package.OwnershipConstants.OWNERSHIP_SELF; + +public final class Rs2LootEngine { + private static final int DESPAWN_DELAY_THRESHOLD_TICKS = 150; + + private Rs2LootEngine() {} + + public static Builder with(LootingParameters params) { + return new Builder(params); + } + + public static final class Builder { + private final LootingParameters params; + private Consumer lootAction = gi -> {}; + private final Map> candidateBuckets = new LinkedHashMap<>(); + + private Builder(LootingParameters params) { + this.params = Objects.requireNonNull(params, "params"); + } + + public Builder withLootAction(Consumer lootAction) { + this.lootAction = Objects.requireNonNull(lootAction, "lootAction"); + return this; + } + + /** Existing intents (kept for completeness) */ + public Builder addByValue() { + Predicate byValue = gi -> { + final int qty = Math.max(1, gi.getQuantity()); + final int price = gi.getGePrice(); + return price > params.getMinValue() && (price / qty) < params.getMaxValue(); + }; + final Set ignoredLower = toLowerTrimmedSet(params.getIgnoredNames()); + collect("byValue", byValue, ignoredLower); + return this; + } + + public Builder addByNames() { + final Set needles = toLowerTrimmedSet(params.getNames()); + if (needles.isEmpty()) return this; + + Predicate byNames = gi -> { + final String n = safeLower(gi.getName()); + for (String needle : needles) { + if (n.contains(needle)) return true; + } + return false; + }; + collect("byNames", byNames, null); + return this; + } + + public Builder addUntradables() { + Predicate untradables = gi -> !gi.isTradeable() && gi.getId() != ItemID.COINS_995; + collect("untradables", untradables, null); + return this; + } + + public Builder addCoins() { + Predicate coins = gi -> gi.getId() == ItemID.COINS_995; + collect("coins", coins, null); + return this; + } + + /** ── NEW INTENTS ───────────────────────────────────────────────────────── */ + + /** Arrows: by name contains "arrow". Avoid singleton stacks by default (qty > 1 on stackables). */ + public Builder addArrows() { return addArrows(1); } + + /** + * Arrows with custom exclusive min stack threshold for stackables. + * Example: minStackExclusive=1 → allow only stacks with qty >= 2. + */ + public Builder addArrows(int minStackExclusive) { + Predicate arrows = gi -> { + final String n = safeLower(gi.getName()); + if (!n.contains("arrow")) return false; + // Only apply stack filter if the item is actually stackable + return !gi.isStackable() || gi.getQuantity() > minStackExclusive; + }; + collect("arrows[min>" + minStackExclusive + "]", arrows, null); + return this; + } + + /** Bones: by name contains "bones". */ + public Builder addBones() { + Predicate bones = gi -> safeLower(gi.getName()).contains("bones"); + collect("bones", bones, null); + return this; + } + + /** Ashes: by name contains " ashes" OR equals "ashes" (to catch the exact item). */ + public Builder addAshes() { + Predicate ashes = gi -> { + final String n = safeLower(gi.getName()); + return n.equals("ashes") || n.contains(" ashes"); + }; + collect("ashes", ashes, null); + return this; + } + + /** Runes: by name contains " rune" (leading space so it won't hit "rune scimitar"). */ + public Builder addRunes() { return addRunes(1); } + + /** + * Runes with custom exclusive min stack threshold for stackables. + * Example: minStackExclusive=1 → avoid stacks of 1 rune. + */ + public Builder addRunes(int minStackExclusive) { + Predicate runes = gi -> { + final String n = safeLower(gi.getName()); + if (!n.contains(" rune")) return false; + return !gi.isStackable() || gi.getQuantity() > minStackExclusive; + }; + collect("runes[min>" + minStackExclusive + "]", runes, null); + return this; + } + + /** Add any custom predicate-based intent. */ + public Builder addCustom(String label, Predicate predicate, Set ignoredLower) { + collect(label == null ? "custom" : label, predicate, ignoredLower); + return this; + } + + /** Final combined looting pass. */ + public boolean loot() { + final WorldPoint me = Microbot.getClient().getLocalPlayer().getWorldLocation(); + + final Map unique = new LinkedHashMap<>(); + for (List list : candidateBuckets.values()) { + for (GroundItem gi : list) { + unique.putIfAbsent(uniqueKey(gi), gi); + } + } + + final List toLoot = new ArrayList<>(unique.values()); + toLoot.sort(Comparator.comparingInt(gi -> gi.getLocation().distanceTo(me))); + + return runWhilePaused(() -> { + for (GroundItem gi : toLoot) { + if (gi.getQuantity() < params.getMinQuantity()) continue; + if (!ensureSpaceFor(gi, params)) continue; + lootAction.accept(gi); + } + final Predicate base = baseRangeAndOwnershipFilter(params); + return validateLoot(base); + }); + } + + /** Internal collector that applies base filters, delayed gate, and per-item prechecks. */ + private void collect(String label, Predicate itemPredicate, Set ignoredLower) { + final Predicate base = baseRangeAndOwnershipFilter(params); + final Predicate combined = base.and(itemPredicate); + + List groundItems = getGroundItems().values().stream() + .filter(combined) + .collect(Collectors.toList()); + + if (groundItems.size() < params.getMinItems()) { + candidateBuckets.put(label, Collections.emptyList()); + return; + } + + if (params.isDelayedLooting()) { + final GroundItem soonest = groundItems.stream() + .min(Comparator.comparingInt(gi -> calculateDespawnTime(gi))) + .orElse(null); + if (soonest == null || calculateDespawnTime(soonest) > DESPAWN_DELAY_THRESHOLD_TICKS) { + candidateBuckets.put(label, Collections.emptyList()); + return; + } + } + + final List filtered = new ArrayList<>(groundItems.size()); + for (GroundItem gi : groundItems) { + if (gi.getQuantity() < params.getMinQuantity()) continue; + if (!passesIgnoredNames(gi, ignoredLower)) continue; + filtered.add(gi); + } + candidateBuckets.put(label, filtered); + } + } + + // ------------------ shared helpers (same logic as before) ------------------ + + private static Predicate baseRangeAndOwnershipFilter(LootingParameters params) { + final WorldPoint me = Microbot.getClient().getLocalPlayer().getWorldLocation(); + final boolean anti = params.isAntiLureProtection(); + return gi -> + gi.getLocation().distanceTo(me) < params.getRange() + && (!anti || gi.getOwnership() == OWNERSHIP_SELF); + } + + private static boolean passesIgnoredNames(GroundItem gi, Set ignoredLower) { + if (ignoredLower == null || ignoredLower.isEmpty()) return true; + final String name = safeLower(gi.getName()); + for (String needle : ignoredLower) { + if (name.contains(needle)) return false; + } + return true; + } + + private static boolean ensureSpaceFor(GroundItem gi, LootingParameters params) { + if (Rs2Inventory.emptySlotCount() > params.getMinInvSlots()) { + return true; + } + if (params.isEatFoodForSpace() && !canTakeGroundItem(gi) && !Rs2Inventory.getInventoryFood().isEmpty()) { + if (Rs2Player.eatAt(100)) { + Rs2Player.waitForAnimation(); + } + } + return canTakeGroundItem(gi); + } + + private static Set toLowerTrimmedSet(String[] arr) { + if (arr == null || arr.length == 0) return Collections.emptySet(); + Set out = new HashSet<>(arr.length); + for (String s : arr) { + if (s != null) { + final String t = s.trim().toLowerCase(); + if (!t.isEmpty()) out.add(t); + } + } + return out; + } + + private static String safeLower(String s) { + return s == null ? "" : s.trim().toLowerCase(); + } + + private static boolean validateLoot(Predicate filter) { + return getGroundItems().values().stream().noneMatch(filter); + } + + private static String uniqueKey(GroundItem gi) { + final WorldPoint wp = gi.getLocation(); + return gi.getId() + "@" + wp.getX() + "," + wp.getY() + "," + wp.getPlane(); + } +} From 61595eb29e2cbe85a37e3b581d1563fc1e433af5 Mon Sep 17 00:00:00 2001 From: S-1D <61428716+See1Duck@users.noreply.github.com> Date: Sat, 6 Sep 2025 23:07:25 +0200 Subject: [PATCH 2/3] Update runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../client/plugins/microbot/aiofighter/loot/LootScript.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java index dec675621cd..23505898952 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/aiofighter/loot/LootScript.java @@ -75,10 +75,7 @@ public boolean run(AIOFighterConfig config) { if (config.toggleLootRunes()) builder.addRunes(DEFAULT_MIN_STACK_EXCLUSIVE_RUNES); // Execute one combined, distance-sorted looting pass - boolean looted = builder.loot(); - if (looted) { - Microbot.pauseAllScripts.compareAndSet(true, false); - } + builder.loot(); } catch (Exception ex) { Microbot.log("LootScript: " + ex.getMessage()); From 11c05c6c9e4e12912e18d0759515a059d06cee70 Mon Sep 17 00:00:00 2001 From: S-1D <61428716+See1Duck@users.noreply.github.com> Date: Sat, 6 Sep 2025 23:13:31 +0200 Subject: [PATCH 3/3] Update runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../plugins/microbot/util/grounditem/Rs2LootEngine.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java index 856cbdedb1e..055941efc11 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/grounditem/Rs2LootEngine.java @@ -159,8 +159,13 @@ public boolean loot() { if (!ensureSpaceFor(gi, params)) continue; lootAction.accept(gi); } - final Predicate base = baseRangeAndOwnershipFilter(params); - return validateLoot(base); + // Validate only items we targeted in this pass + final Set targetKeys = candidateBuckets.values().stream() + .flatMap(List::stream) + .map(Rs2LootEngine::uniqueKey) + .collect(Collectors.toSet()); + return getGroundItems().values().stream() + .noneMatch(gi -> targetKeys.contains(uniqueKey(gi))); }); }