diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeConfig.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeConfig.java index 9de8f22aa9..3f025323f0 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeConfig.java @@ -63,4 +63,12 @@ public interface DroFirecapeConfig extends Config { default boolean recordRun(){return true;} default boolean diagnostics(){return recordRun();} default boolean exitOnDamage(){return false;} + enum AfterExitBehavior { + LOGOUT("Log out"), GET_ANOTHER_CAPE("Get another cape"); + private final String label; + AfterExitBehavior(String label) { this.label = label; } + @Override public String toString() { return label; } + } + @ConfigItem(keyName="afterExitBehavior",name="After exit",description="After a cape or a failed cave attempt: log out, or bank/regear and start another cape. Missing supplies always stop and log out.",position=1000) + default AfterExitBehavior afterExitBehavior(){return AfterExitBehavior.LOGOUT;} } diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapePlugin.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapePlugin.java index 862750fee8..f62422af3f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapePlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapePlugin.java @@ -15,7 +15,7 @@ import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.overlay.OverlayManager; -@PluginDescriptor(name = PluginConstants.DRO + "Firecape",version=DroFirecapePlugin.version,minClientVersion="2.6.30",enabledByDefault=PluginConstants.DEFAULT_ENABLED, +@PluginDescriptor(name = PluginConstants.DRO + "Firecape",version=DroFirecapePlugin.version,minClientVersion="2.6.32",enabledByDefault=PluginConstants.DEFAULT_ENABLED, description="Fight Caves with preserved regular combat and isolated pure positioning / native tick prayers", authors={"droplugins"}, iconUrl="https://chsami.github.io/Microbot-Hub/DroFirecapePlugin/assets/icon.png", @@ -23,7 +23,7 @@ isExternal=PluginConstants.IS_EXTERNAL, tags={"microbot","dro","firecape","fight caves","jad","boss"}) public final class DroFirecapePlugin extends Plugin { - public static final String version="0.3.71"; + public static final String version="0.3.74"; @Inject private FcControllers script; @Inject private DroFirecapeConfig config; @Inject private DroFirecapeOverlay overlay; diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeScript.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeScript.java index df80529d0e..b7eb569d8b 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/DroFirecapeScript.java @@ -149,12 +149,44 @@ enum State { START,TELEPORT,WALK_BANK,BANK_EQUIPMENT,BANK_INVENTORY,BANK_VERIFY, protected boolean usesSharedLoginReadiness(){return false;} + protected volatile String exhaustedSupply; + private final net.runelite.client.plugins.microbot.util.player.TerminalLogoutPrompt terminalLogout = + new net.runelite.client.plugins.microbot.util.player.TerminalLogoutPrompt(); + + protected void failForSupplies(String supply) { + exhaustedSupply = supply; + fail(Microbot.supplyMessage(supply)); + } + + private void stopForMissingSupply(String supply) { + exhaustedSupply = supply; + fail(Microbot.supplyMessage(supply)); + } + + protected void afterExit() { + if (observeOnly()) return; + FcFrame outsideFrame = FcActions.read(this::captureFrame, null); + if (outsideFrame == null || outsideFrame.cave || outsideFrame.hp <= 0) { + status = "Waiting for confirmed cave exit / respawn"; + return; + } + if (FcAfterExit.repeat(config.afterExitBehavior(), hadCave, exhaustedSupply != null, false)) { + trace.event("repeat", failed ? "Retry after failed cave attempt" : "Get another cape"); + run(config); + return; + } + terminalLogout.begin(exhaustedSupply != null ? Microbot.supplyMessage(exhaustedSupply) + : state == State.COMPLETE ? "Fire cape completed; logged out" + : "Firecape stopped: " + failureReason); + } + public boolean run(DroFirecapeConfig configuration) { enabled=false; + terminalLogout.reset();exhaustedSupply=null; if(mainScheduledFuture!=null)mainScheduledFuture.cancel(true); if(scheduledFuture!=null)scheduledFuture.cancel(true); FcTickPrayers previous=tickPrayers;if(previous!=null)previous.detach(); - config=configuration;keybindings.reset();if(pointerLease!=null)pointerLease.close(); + config=FcRuntimeConfig.resolve(configuration);keybindings.reset();if(pointerLease!=null)pointerLease.close(); meleeMode=config.meleeCape(); pointerLease=null;resetVariantRun();lureResetAfterReturn=false;rangePrepot.reset();sweetCancelTick=sweetProgressTick=sweetRetryTick=-1;supplyAck.reset();supplyPreparation.reset();jadActions.reset();lastFailedMoveTick=-1;movement.reset();lures.reset();bowPrayer.reset();requestedCaveProtection=Protection.NONE;caveBaseX=caveBaseY=Integer.MIN_VALUE;cavePlane=-1;actions.reset();actions.observeOnly(observeOnly()); tickPrayers=createTickPrayers(); @@ -204,7 +236,7 @@ public boolean run(DroFirecapeConfig configuration) { Microbot.log("[Dro Firecape] Full-run controller "+DroFirecapePlugin.version+" (recorded rock lures / visible protection switches / tick prayers / retained startup, supplies and thralls) started. Trace: "+trace.location()+startupDetail()); return true; } - protected void setState(State next,String text){if(state!=next){state=next;stateAt=System.currentTimeMillis();trace.event("state",next+": "+text);}status=text;} + protected void setState(State next,String text){if(next==State.STOPPED&&failureReason.isEmpty())failureReason=text;if(state!=next){state=next;stateAt=System.currentTimeMillis();trace.event("state",next+": "+text);}status=text;} protected void logStatus() { long now=System.currentTimeMillis(); String current=state+": "+status; @@ -226,7 +258,12 @@ protected void fail(String reason) { protected void loop() { if(!enabled||Thread.currentThread().isInterrupted())return; for(String e;(e=events.poll())!=null;)trace.event("game",e); - if(state==State.STOPPED||state==State.COMPLETE)return; + if (terminalLogout.active()) { + status = Microbot.status = terminalLogout.message(); + if (terminalLogout.tick()) shutdown(); + return; + } + if(state==State.STOPPED||state==State.COMPLETE){afterExit();return;} if(!Microbot.isLoggedIn()) { status="Waiting for login / world transition; preserving run"; if(!observeOnly())super.run(); @@ -404,7 +441,9 @@ protected void outside(FcFrame f) { break; } case WALK_BANK: { - if(f.location.distanceTo2D(BANK)<=6){setup=new Rs2InventorySetup(config.inventorySetup(),mainScheduledFuture);setState(State.BANK_EQUIPMENT,"Opening TzHaar bank");return;} + if(f.location.distanceTo2D(BANK)<=6){setup=new Rs2InventorySetup(config.inventorySetup(),mainScheduledFuture) + .withMissingSupplyPolicy(Rs2InventorySetup.MissingSupplyPolicy.LOGOUT_AND_STOP) + .withMissingSupplyHandler(this::stopForMissingSupply);setState(State.BANK_EQUIPMENT,"Opening TzHaar bank");return;} travel(f,BANK);if(elapsed(90_000))fail("Unable to reach the TzHaar bank"); break; } @@ -432,7 +471,7 @@ protected void outside(FcFrame f) { public void withdraw(String name){Rs2Bank.withdrawOne(name,true);} }); status=rangePrepot.status(); - if(rangePrepot.failed()){fail(status);return;} + if(rangePrepot.failed()){if(rangePrepot.missingSupply()!=null)failForSupplies(rangePrepot.missingSupply());else fail(status);return;} if(done){actions.releaseTab();setState(State.CAMERA,"Pre-dose and full inventory confirmed");} return; } @@ -1339,7 +1378,7 @@ protected void supplyRequested(FcFrame f,FcFrame.ItemSlot item) { protected boolean emergencyPrayer(FcFrame f) { if(observeOnly()||supplyAck.pending()||System.currentTimeMillis()-lastSupplyAt<1400)return false; FcFrame.ItemSlot dose=f.inventory.stream().filter(i->isPrayerPotion(i.name())).findFirst().orElse(null); - if(dose==null){if(!keepsFightingWhenDepleted())fail("Prayer exhausted: no restore dose remains");return false;} + if(dose==null){if(!keepsFightingWhenDepleted())failForSupplies("Prayer potions or super restores");return false;} long now=System.currentTimeMillis(); if(!supplyPreparation.ready(dose.id(),now))return false; FcActions.ItemResult result; @@ -1750,7 +1789,7 @@ protected SupplyChoice prioritySupply(FcFrame f,boolean sweets,boolean healing) .findFirst().orElse(null); if(item==null&&f.prayer<=config.restorePrayer())item=f.inventory.stream() .filter(i->isPrayerPotion(i.name())).findFirst().orElse(null); - if(item==null&&f.prayer<=3){fail("Prayer supplies exhausted");return null;} + if(item==null&&f.prayer<=3){failForSupplies("Prayer potions or super restores");return null;} } if(item==null&&f.hp*100<=f.maxHp*config.eatPercent()) { item=f.inventory.stream().filter(i->i.hasAction("Eat")&&!FcSupplyPolicy.sweet(i.name())).findFirst().orElse(null);action="Eat"; @@ -1857,7 +1896,7 @@ public void onChatMessage(ChatMessage e) { } if(e.getType()==ChatMessageType.GAMEMESSAGE&&hadCave&&(message.contains("Your TzTok-Jad kill count")||message.toLowerCase(Locale.ROOT).contains("you are awarded a fire cape"))){rewardConfirmed=true;events.add(message);} if(e.getType()==ChatMessageType.GAMEMESSAGE&&(message.toLowerCase(Locale.ROOT).contains("not enough ammo")||message.toLowerCase(Locale.ROOT).contains("run out of darts") - ||message.toLowerCase(Locale.ROOT).contains("run out of scales"))){events.add("AMMUNITION FAILURE: "+message);if(!keepsFightingWhenDepleted()){exitRequested=true;failed=true;failureReason="Weapon ammunition/charges exhausted";} + ||message.toLowerCase(Locale.ROOT).contains("run out of scales"))){events.add("AMMUNITION FAILURE: "+message);if(!keepsFightingWhenDepleted()){exitRequested=true;failed=true;exhaustedSupply="Weapon ammunition or charges";failureReason=Microbot.supplyMessage(exhaustedSupply);} else warning="Weapon ammunition/charges exhausted; staying in the cave";} } public void onNpcSpawned(NpcSpawned e) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcActions.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcActions.java index 13fbc8f54d..54c7b5b7e8 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcActions.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcActions.java @@ -338,7 +338,6 @@ protected Click inventoryClick(FcFrame.ItemSlot item,String action) { if(slot==null||slot.getIndex()!=item.slot()||slot.isHidden())continue; Rectangle bounds=FcPrayerUi.visibleBounds(slot.getBounds(),client().getCanvasWidth(),client().getCanvasHeight()); int option=actionIndex(slot.getActions(),action); - if(option<0)option=actionIndex(client().getItemDefinition(item.id()).getInventoryActions(),action); if(bounds==null||option<0)return null; return new Click(new NewMenuEntry().option(action).target(item.name()).param0(item.slot()) .param1(inventory.getId()).type(MenuAction.CC_OP).identifier(option+1).itemId(item.id()),bounds); diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcAfterExit.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcAfterExit.java new file mode 100644 index 0000000000..38787305c8 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcAfterExit.java @@ -0,0 +1,10 @@ +package net.runelite.client.plugins.microbot.drofirecape; + +/** Startup/configuration failures and supply exhaustion cannot create automatic retry loops. */ +final class FcAfterExit { + static boolean repeat(DroFirecapeConfig.AfterExitBehavior behavior, boolean enteredCave, + boolean missingSupplies, boolean observeOnly) { + return behavior == DroFirecapeConfig.AfterExitBehavior.GET_ANOTHER_CAPE + && enteredCave && !missingSupplies && !observeOnly; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcRangePrepot.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcRangePrepot.java index 7d8424e626..4f57fd6426 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcRangePrepot.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcRangePrepot.java @@ -21,12 +21,13 @@ interface Input { } private enum Phase { START, DRINK, DOSE, OPEN, DEPOSIT, DEPOSITED, WITHDRAW, FULL, DONE } private Phase phase=Phase.START; - private String full,partial,status=""; + private String full,partial,status="",missingSupply; private int fullCount,partialCount,at=-1; private boolean failed; - void reset(){phase=Phase.START;full=partial=null;at=-1;failed=false;status="";} + void reset(){phase=Phase.START;full=partial=null;at=-1;failed=false;status="";missingSupply=null;} String status(){return status;} boolean failed(){return failed;} + String missingSupply(){return missingSupply;} private void next(Phase p,int tick){phase=p;at=tick;} private static int count(List inv,String name){return inv.stream().filter(i->i.name().equalsIgnoreCase(name)).mapToInt(FcFrame.ItemSlot::quantity).sum();} boolean step(int tick,int ranged,int base,List inv,Input input) { @@ -50,7 +51,7 @@ boolean step(int tick,int ranged,int base,List inv,Input input return false; case OPEN: if(!input.bankOpen()){input.openBank();return false;} - if(!input.stocked(full)){failed=true;status="Bank needs a "+full+" to replace the pre-dose";return false;} + if(!input.stocked(full)){missingSupply=full;failed=true;status="Bank needs a "+full+" to replace the pre-dose";return false;} next(Phase.DEPOSIT,tick);return false; case DEPOSIT: if(!input.bankOpen()){input.openBank();return false;} diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcRuntimeConfig.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcRuntimeConfig.java new file mode 100644 index 0000000000..3e80e63bd4 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/FcRuntimeConfig.java @@ -0,0 +1,44 @@ +/* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */ +package net.runelite.client.plugins.microbot.drofirecape; + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import net.runelite.client.config.ConfigItem; + +/** RuneLite proxies require ConfigItem; removed settings are fixed Java defaults. */ +final class FcRuntimeConfig implements InvocationHandler { + private final DroFirecapeConfig settings; + private FcRuntimeConfig(DroFirecapeConfig settings) { this.settings=settings; } + + static DroFirecapeConfig resolve(DroFirecapeConfig settings) { + if (Proxy.isProxyClass(settings.getClass()) && Proxy.getInvocationHandler(settings) instanceof FcRuntimeConfig) { + return settings; + } + return (DroFirecapeConfig) Proxy.newProxyInstance(DroFirecapeConfig.class.getClassLoader(), + new Class[]{DroFirecapeConfig.class}, new FcRuntimeConfig(settings)); + } + + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getDeclaringClass() == Object.class) { + switch (method.getName()) { + case "equals": return proxy == args[0]; + case "hashCode": return System.identityHashCode(proxy); + case "toString": return settings.toString(); + default: throw new IllegalStateException("Unexpected Object method: " + method); + } + } + if (method.isDefault() && method.getAnnotation(ConfigItem.class) == null) { + return MethodHandles.privateLookupIn(method.getDeclaringClass(), MethodHandles.lookup()) + .unreflectSpecial(method, method.getDeclaringClass()).bindTo(proxy) + .invokeWithArguments(args == null ? new Object[0] : args); + } + try { + return method.invoke(settings, args); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/OptionalFirecapeScript.java b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/OptionalFirecapeScript.java index 856baf9000..f2803975c7 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drofirecape/OptionalFirecapeScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drofirecape/OptionalFirecapeScript.java @@ -200,7 +200,7 @@ else if(healerPlan.targetIndex()>=0) { .findFirst().orElse(null); if(item==null&&f.prayer<=config.restorePrayer())item=f.inventory.stream() .filter(i->isPrayerPotion(i.name())).findFirst().orElse(null); - if(item==null&&f.prayer<=3&&!config.pureMode()){fail("Prayer supplies exhausted");return null;} + if(item==null&&f.prayer<=3&&!config.pureMode()){failForSupplies("Prayer potions or super restores");return null;} } if(item==null&&healing) { if(brewAllowed(f))item=f.inventory.stream() diff --git a/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdPlugin.java index 13d0631b7a..63461941a1 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdPlugin.java @@ -15,11 +15,11 @@ name = PluginConstants.DRO + "KBD", description = "King Black Dragon trips with Lava Maze travel, Wilderness safety, and death recovery.", tags = {"kbd", "king black dragon", "boss", "wilderness"}, - version = "1.1.7", + version = "1.1.8", authors = {"droplugins"}, iconUrl = "https://chsami.github.io/Microbot-Hub/DroKbdPlugin/assets/icon.png", cardUrl = "https://chsami.github.io/Microbot-Hub/DroKbdPlugin/assets/card.png", - minClientVersion = "2.6.25", + minClientVersion = "2.6.32", enabledByDefault = PluginConstants.DEFAULT_ENABLED, isExternal = PluginConstants.IS_EXTERNAL ) diff --git a/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdScript.java b/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdScript.java index 173db2e688..fc964fa891 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drokbd/DroKbdScript.java @@ -253,9 +253,18 @@ public class DroKbdScript extends Script private long sessionStartedAt; private int startingRangedXp; + private final net.runelite.client.plugins.microbot.util.player.TerminalLogoutPrompt terminalLogout = + new net.runelite.client.plugins.microbot.util.player.TerminalLogoutPrompt(); + + private void stopForMissingSupply(String supply) { + terminalLogout.begin(Microbot.supplyMessage(supply)); + status = Microbot.status = terminalLogout.message(); + } + public boolean run(DroKbdConfig config) { this.config = config; + terminalLogout.reset(); bankCleared = false; inventorySetup = null; combatMouseHandled = false; @@ -322,6 +331,11 @@ public boolean run(DroKbdConfig config) mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { + if (terminalLogout.active()) { + status = Microbot.status = terminalLogout.message(); + if (terminalLogout.tick()) shutdown(); + return; + } if (stopping || !Microbot.isLoggedIn() || !super.run() || threatLogoutActive.get()) return; tick(); } @@ -486,6 +500,8 @@ private void prepareTrip(WorldPoint location) if (inventorySetup == null) { inventorySetup = new Rs2InventorySetup(sanitizeTripSetup(selectedSetup), mainScheduledFuture); + inventorySetup.withMissingSupplyPolicy(Rs2InventorySetup.MissingSupplyPolicy.LOGOUT_AND_STOP) + .withMissingSupplyHandler(this::stopForMissingSupply); } if (!inventorySetup.loadEquipment()) return; @@ -552,7 +568,7 @@ private boolean validateTripLoadout() { if (!hasBurningAmulet()) { - status = "Loadout needs a charged Burning amulet"; + stopForMissingSupply("Charged Burning amulet in selected Inventory Setup"); return false; } if (selectedCombatMode() != DroKbdAmmo.TOXIC_BLOWPIPE @@ -567,20 +583,20 @@ private boolean validateTripLoadout() hasDuelingRing |= Rs2Equipment.isWearing(ring) || Rs2Inventory.contains(ring); if (!hasDuelingRing) { - status = "Loadout needs a charged Ring of dueling"; + stopForMissingSupply("Charged Ring of dueling in selected Inventory Setup"); return false; } if (selectedCombatMode() == DroKbdAmmo.TOXIC_BLOWPIPE) { if (!Rs2Player.hasSuperAntiFireActive() && !hasExtendedSuperAntifireDose()) { - status = "Blowpipe mode needs Extended super antifire (4)-(1)"; + stopForMissingSupply("Extended super antifire (4)-(1)"); return false; } } else if (!Rs2Player.hasAntiFireActive() && !Rs2Inventory.contains(false, "antifire")) { - status = "Loadout needs an antifire potion"; + stopForMissingSupply("Antifire potion"); return false; } if (selectedCombatMode().isAmmunitionBootstrap() @@ -1822,7 +1838,7 @@ private boolean prepareAmmoBeforeInventorySetup(InventorySetup setup) { if (!Rs2Bank.hasItem(ammoId)) { - status = "Missing " + ammoName; + stopForMissingSupply(ammoName); return false; } @@ -2010,7 +2026,10 @@ private void escape(String reason) } else { - status = "Missing charged Ring of dueling for Ferox"; + boolean hasRing = Arrays.stream(RINGS_OF_DUELING) + .anyMatch(id -> Rs2Inventory.contains(id) || Rs2Equipment.isWearing(id)); + if (!hasRing) stopForMissingSupply("Charged Ring of dueling for Ferox"); + else status = "Ferox teleport not dispatched; retrying"; } delay(1000); } diff --git a/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahPlugin.java index 628afc647b..7363c166f9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahPlugin.java @@ -18,8 +18,8 @@ @PluginDescriptor( name=PluginConstants.DRO + "Zulrah", - version="1.10.17", - minClientVersion = "2.6.26", + version="1.10.19", + minClientVersion = "2.6.32", description="Inventory-setup driven Zulrah trips, rotations, prayer, switches, thralls and regear", tags={"microbot","zulrah","dro"}, authors={"droplugins"}, diff --git a/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahScript.java b/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahScript.java index 078992d609..89c7d8b8aa 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahScript.java @@ -181,9 +181,18 @@ public class DroZulrahScript extends Script private int startingMagicXp; private int startingRangedXp; + private final net.runelite.client.plugins.microbot.util.player.TerminalLogoutPrompt terminalLogout = + new net.runelite.client.plugins.microbot.util.player.TerminalLogoutPrompt(); + + private void stopForMissingSupply(String supply) { + terminalLogout.begin(Microbot.supplyMessage(supply)); + status = Microbot.status = terminalLogout.message(); + } + public boolean run(DroZulrahConfig config) { this.config = config; + terminalLogout.reset(); this.startupBankTrip = false; this.emergencyActive = false; this.emergencyBrews = 0; @@ -272,6 +281,11 @@ public boolean run(DroZulrahConfig config) loopEnteredAt = System.currentTimeMillis(); try { + if (terminalLogout.active()) { + status = Microbot.status = terminalLogout.message(); + if (terminalLogout.tick()) shutdown(); + return; + } if (System.currentTimeMillis() < nextErrorRetryAt) return; GameState gameState = clientRead(() -> Microbot.getClient().getGameState(), GameState.UNKNOWN); if (gameState == GameState.LOADING || gameState == GameState.HOPPING @@ -747,11 +761,12 @@ private void tickDeathRecovery(WorldPoint location) { + " | status=" + status + " | location=" + location); if (recovery.stopped()) { String reason = recovery.status(); - shutdown(); state = DroZulrahState.OUT_OF_SUPPLIES; - status = Microbot.status = reason; - Microbot.log("[Dro] Zulrah: " + reason); - javax.swing.SwingUtilities.invokeLater(() -> Microbot.showMessage(reason)); + if (reason.contains("Ring of dueling")) stopForMissingSupply("Charged Ring of dueling"); + else if (reason.contains("no Zul-andra teleports")) stopForMissingSupply("Zul-andra teleports"); + else terminalLogout.begin(reason); + status = Microbot.status = terminalLogout.message(); + Microbot.log("[Dro] Zulrah: " + status); } else if (recovery.done()) { deathRecovery = null; startupBankTrip = false; @@ -834,6 +849,8 @@ private void prepareTrip(WorldPoint location) if (inventorySetup == null) { inventorySetup = new Rs2InventorySetup(selectedSetup, mainScheduledFuture); + inventorySetup.withMissingSupplyPolicy(Rs2InventorySetup.MissingSupplyPolicy.LOGOUT_AND_STOP) + .withMissingSupplyHandler(this::stopForMissingSupply); } // Identical completion contract to DroKBD. loadEquipment already equips items; @@ -887,12 +904,12 @@ private boolean validateTripLoadout() { if (!Rs2Inventory.contains(ZUL_ANDRA_TELEPORT)) { - status = "Inventory Setup needs Zul-andra teleport [12938]"; + stopForMissingSupply("Zul-andra teleport in selected Inventory Setup"); return false; } if (!hasRingOfDueling()) { - status = "Inventory Setup needs a charged Ring of dueling"; + stopForMissingSupply("Charged Ring of dueling in selected Inventory Setup"); return false; } if (!hasMagicWeapon && !hasRangeWeapon) @@ -1401,7 +1418,6 @@ private int dispatchGearBatch(Map plan) Widget child = inventory.getChild(item.getSlot()); if (child == null || child.getItemId() != item.getId()) continue; String[] actions = child.getActions(); - if (actions == null) actions = item.getInventoryActions(); if (actions == null) continue; for (int i = 0; i < actions.length; i++) { @@ -1635,7 +1651,6 @@ private boolean dispatchConsumable(Rs2ItemModel item, String wanted) Widget child = inventory == null ? null : inventory.getChild(item.getSlot()); if (child == null || child.getItemId() != item.getId()) return false; String[] actions = child.getActions(); - if (actions == null) actions = item.getInventoryActions(); if (actions == null) return false; for (int i = 0; i < actions.length; i++) if (wanted.equalsIgnoreCase(actions[i])) @@ -1884,8 +1899,10 @@ private void returnToFerox(String reason) } else { - state = DroZulrahState.OUT_OF_SUPPLIES; - status = "Missing charged Ring of dueling for Ferox"; + if (!hasRingOfDueling()) { + state = DroZulrahState.OUT_OF_SUPPLIES; + stopForMissingSupply("Charged Ring of dueling for Ferox"); + } else status = "Ferox teleport not dispatched; retrying"; } } diff --git a/src/main/resources/net/runelite/client/plugins/microbot/drofirecape/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/drofirecape/docs/README.md index 9d6461174d..3b8e9e10b6 100644 --- a/src/main/resources/net/runelite/client/plugins/microbot/drofirecape/docs/README.md +++ b/src/main/resources/net/runelite/client/plugins/microbot/drofirecape/docs/README.md @@ -5,7 +5,7 @@ ![Dro Firecape](assets/card.png) -Automated Fight Caves for Microbot: all **63 waves and 15 rotations**, TzHaar travel, banking, equipment, positioning, protection prayers, potions and supported thralls. Version **0.3.71**, requiring **Microbot 2.6.30**. +Automated Fight Caves for Microbot: all **63 waves and 15 rotations**, TzHaar travel, banking, equipment, positioning, protection prayers, potions and supported thralls. Version **0.3.73**, requiring the terminal-logout APIs planned for **Microbot 2.6.32 or newer**. **There is no guarantee of a Fire cape. Better gear and adequate supplies improve the chances. Test under supervision.** Normal ranged mode has completed capes; Pure has substantial live testing but no confirmed completed cape in the retained evidence. Melee remains untested. @@ -131,3 +131,9 @@ Copyright (c) 2026, **DRO (droplugins)**. See LICENSE.txt for the full BSD-2-Cla - **Rick:** thank you for extensive Pure-mode testing and practical feedback. Third-party attribution and license notices are retained. Redistribution should credit DRO by retaining its copyright and license notices; the Codex and Rick acknowledgements are included in CREDITS.txt. + +## After exit + +The bottom **After exit** option defaults to **Log out**. **Get another cape** banks, regears and begins a new attempt after a cape or failed cave attempt. Startup failures and missing required supplies stop and log out instead of repeating. Supply messages appear after logout is confirmed; terminal supply stops suppress Auto Login. End-of-wave recovery pauses remain separate and still resume by world-hop. + +Version .72 fixes the startup config-proxy failure; .73 adds the After exit control and confirmed terminal logout. These changes have automated validation, not a new live cape run. diff --git a/src/main/resources/net/runelite/client/plugins/microbot/drokbd/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/drokbd/docs/README.md index 5e3f507fe3..8b6df5b9e5 100644 --- a/src/main/resources/net/runelite/client/plugins/microbot/drokbd/docs/README.md +++ b/src/main/resources/net/runelite/client/plugins/microbot/drokbd/docs/README.md @@ -29,3 +29,7 @@ Nearby attackable players trigger a logout attempt before combat. Once another p The plugin uses the combat antiban template while running and restores the previous global antiban settings and activity on stop. If global settings are configured to override script settings, the plugin respects that choice. If a future client changes the fields needed to capture the global antiban profile, KBD keeps that profile unchanged and skips its combat-template override rather than failing startup. + +## Required supply shortages + +Missing required Inventory Setup gear, ammunition or travel/protection supplies stop the run. The script closes the bank, waits for confirmed logout, then reports the missing supply. Auto Login stays suppressed until another script starts, the user logs in manually, or Auto Login restarts. Failed teleport clicks with the required item present remain retryable. diff --git a/src/main/resources/net/runelite/client/plugins/microbot/drozulrah/docs/README.md b/src/main/resources/net/runelite/client/plugins/microbot/drozulrah/docs/README.md index d2d68e9340..0bca181280 100644 --- a/src/main/resources/net/runelite/client/plugins/microbot/drozulrah/docs/README.md +++ b/src/main/resources/net/runelite/client/plugins/microbot/drozulrah/docs/README.md @@ -182,3 +182,7 @@ The overlay, visible prayer clicks and new break controls have compile/focused-t DroZulrah is explicitly marked as an external Hub plugin and disabled by default, allowing installed-plugin refresh, update and uninstall to recognize the loaded instance. The displayed name remains **[Dro] Zulrah**, with its prefix defined in the plugin-local PluginConstants. Author and catalog-image metadata are included, with icon/card assets in this documentation directory. The startup build label comes from the descriptor version. The minimum client version is **2.6.26**, matching the current verified client. Compilation, JAR packaging and all 41 focused DroZulrah tests passed in Microbot-Hub. This metadata update does not change combat, travel, mouse or break behavior. The known full-Hub CI issue in unrelated plugins is unchanged. + +## Required supply shortages + +Missing required Inventory Setup gear or travel supplies stop the run. The script closes the bank, waits for confirmed logout, then reports the shortage. Auto Login stays suppressed until another script starts, the user logs in manually, or Auto Login restarts. Failed teleport clicks with the required item present remain retryable. diff --git a/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FcAfterExitTest.java b/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FcAfterExitTest.java new file mode 100644 index 0000000000..abe4f4b6c5 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FcAfterExitTest.java @@ -0,0 +1,21 @@ +/* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */ +package net.runelite.client.plugins.microbot.drofirecape; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class FcAfterExitTest { + @Test public void defaultStopsRatherThanRepeats() { + DroFirecapeConfig config = new DroFirecapeConfig() { }; + assertEquals(DroFirecapeConfig.AfterExitBehavior.LOGOUT, config.afterExitBehavior()); + assertFalse(FcAfterExit.repeat(config.afterExitBehavior(), true, false, false)); + } + + @Test public void repeatRequiresAnActualCaveAttemptAndNoSupplyFailure() { + DroFirecapeConfig.AfterExitBehavior repeat = DroFirecapeConfig.AfterExitBehavior.GET_ANOTHER_CAPE; + assertTrue(FcAfterExit.repeat(repeat, true, false, false)); + assertFalse(FcAfterExit.repeat(repeat, false, false, false)); + assertFalse(FcAfterExit.repeat(repeat, true, true, false)); + assertFalse(FcAfterExit.repeat(repeat, true, false, true)); + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FcRuntimeConfigTest.java b/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FcRuntimeConfigTest.java new file mode 100644 index 0000000000..32726c87f1 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FcRuntimeConfigTest.java @@ -0,0 +1,80 @@ +/* Copyright (c) 2026, DRO (droplugins). SPDX-License-Identifier: BSD-2-Clause */ +package net.runelite.client.plugins.microbot.drofirecape; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigManager; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class FcRuntimeConfigTest { + private DroFirecapeConfig realProxy(ConfigManager manager) throws Exception { + Constructor ctor=Class.forName("net.runelite.client.config.ConfigInvocationHandler") + .getDeclaredConstructor(ConfigManager.class); + ctor.setAccessible(true); + return (DroFirecapeConfig)Proxy.newProxyInstance(DroFirecapeConfig.class.getClassLoader(), + new Class[]{DroFirecapeConfig.class},(InvocationHandler)ctor.newInstance(manager)); + } + + @Test void internalDefaultsWorkWithRealRuneLiteProxyAndIgnoreRemovedSavedSettings() throws Exception { + ConfigManager manager=mock(ConfigManager.class); + when(manager.getConfiguration(eq(DroFirecapeConfig.GROUP),anyString())).thenReturn("true"); + DroFirecapeConfig raw=realProxy(manager); + assertThrows(NullPointerException.class,raw::observeOnly); + DroFirecapeConfig resolved=FcRuntimeConfig.resolve(raw); + assertSame(resolved,FcRuntimeConfig.resolve(resolved),"Repeat runs must not stack config adapters"); + DroFirecapeConfig defaults=new DroFirecapeConfig(){}; + for(Method method:DroFirecapeConfig.class.getDeclaredMethods()) { + if(method.isDefault()&&method.getAnnotation(ConfigItem.class)==null&&!method.getName().equals("diagnostics")) { + assertEquals(method.invoke(defaults),method.invoke(resolved),method.getName()); + verify(manager,never()).getConfiguration(DroFirecapeConfig.GROUP,method.getName()); + } + } + } + + @Test void visibleSettingsAndDiagnosticAliasRemainLive() throws Exception { + ConfigManager manager=mock(ConfigManager.class); + when(manager.getConfiguration(DroFirecapeConfig.GROUP,"recordRun")).thenReturn("false"); + Method conversion=ConfigManager.class.getDeclaredMethod("stringToObject",String.class,java.lang.reflect.Type.class); + conversion.setAccessible(true); + when(conversion.invoke(manager,"false",boolean.class)).thenReturn(false); + DroFirecapeConfig resolved=FcRuntimeConfig.resolve(realProxy(manager)); + assertFalse(resolved.recordRun()); + assertFalse(resolved.diagnostics()); + verify(manager,atLeastOnce()).getConfiguration(DroFirecapeConfig.GROUP,"recordRun"); + assertFalse(resolved.pureMode()); + assertFalse(resolved.nativeTickPrayers()); + } + + @Test void regularAndOptionalControllerDefaultsAreUsableWithRealProxy() throws Exception { + DroFirecapeConfig resolved=FcRuntimeConfig.resolve(realProxy(mock(ConfigManager.class))); + for(DroFirecapeScript script:new DroFirecapeScript[]{new DroFirecapeScript(),new OptionalFirecapeScript()}) { + script.config=resolved; + assertFalse(script.observeOnly()); + assertFalse(script.strictZeroExposure()); + assertEquals(0,script.weaponRangeSetting()); + assertTrue(script.rangingPotion()); + assertTrue(script.demonstrationLures()); + assertTrue(script.blowpipeSpecial()); + assertTrue(script.useThralls()); + assertFalse(script.exitOnDamage()); + assertTrue(script.traceEnabled()); + } + } + + @Test void adapterHasStableIdentityAndCanBeReusedForRepeatedStarts() throws Exception { + DroFirecapeConfig raw=realProxy(mock(ConfigManager.class)); + DroFirecapeConfig resolved=FcRuntimeConfig.resolve(raw); + assertEquals(resolved,resolved); + assertNotEquals(resolved,raw); + assertNotEquals(resolved,null); + assertEquals(System.identityHashCode(resolved),resolved.hashCode()); + for(int i=0;i<100;i++)resolved=FcRuntimeConfig.resolve(resolved); + assertFalse(resolved.observeOnly()); + assertEquals("DroFirecapeConfig",resolved.toString()); + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FirecapeInventoryWidgetTest.java b/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FirecapeInventoryWidgetTest.java new file mode 100644 index 0000000000..8cec7c5f44 --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/drofirecape/FirecapeInventoryWidgetTest.java @@ -0,0 +1,37 @@ +package net.runelite.client.plugins.microbot.drofirecape; + +import java.awt.Rectangle; +import java.util.*; +import java.util.concurrent.Callable; +import net.runelite.api.*; +import net.runelite.api.widgets.Widget; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class FirecapeInventoryWidgetTest { + @Test void liveActionIsRequiredAndDeterminesTheOperation() throws Exception { + try(var micro=mockStatic(Microbot.class);var inventory=mockStatic(Rs2Inventory.class)) { + Client client=mock(Client.class); ClientThread thread=mock(ClientThread.class); + micro.when(Microbot::getClient).thenReturn(client); micro.when(Microbot::getClientThread).thenReturn(thread); + when(thread.runOnClientThreadOptional(any())).thenAnswer(i->Optional.ofNullable(((Callable)i.getArgument(0)).call())); + ItemContainer items=mock(ItemContainer.class); when(client.getItemContainer(InventoryID.INVENTORY)).thenReturn(items); + when(items.getItem(2)).thenReturn(new Item(123,1)); + Widget root=mock(Widget.class),slot=mock(Widget.class); inventory.when(Rs2Inventory::getInventory).thenReturn(root); + when(root.getChildren()).thenReturn(new Widget[]{slot}); when(root.getId()).thenReturn(9764864); + when(slot.getIndex()).thenReturn(2); when(slot.getBounds()).thenReturn(new Rectangle(600,300,32,32)); + when(client.getCanvasWidth()).thenReturn(800); when(client.getCanvasHeight()).thenReturn(600); + ItemComposition definition=mock(ItemComposition.class); when(client.getItemDefinition(123)).thenReturn(definition); + when(definition.getInventoryActions()).thenReturn(new String[]{"Eat"}); + FcActions actions=new FcActions(); FcFrame.ItemSlot item=new FcFrame.ItemSlot(2,123,1,"Food",List.of("Eat")); + assertNull(actions.inventoryClick(item,"Eat")); + when(slot.getActions()).thenReturn(new String[]{"Drop"}); assertNull(actions.inventoryClick(item,"Eat")); + when(slot.getActions()).thenReturn(new String[]{"Drop",null,"Eat"}); + var click=actions.inventoryClick(item,"Eat"); assertNotNull(click); assertEquals(3,click.entry().getIdentifier()); + } + } +} diff --git a/src/test/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahDescriptorTest.java b/src/test/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahDescriptorTest.java index 142e445cdf..727c6c1222 100644 --- a/src/test/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahDescriptorTest.java +++ b/src/test/java/net/runelite/client/plugins/microbot/drozulrah/DroZulrahDescriptorTest.java @@ -13,11 +13,11 @@ public class DroZulrahDescriptorTest @Test public void hubRefreshAndUnloadCanRecognizeThisExternalPlugin() { assertTrue(descriptor.isExternal(), "Hub sync and unload filter on the external descriptor flag"); assertFalse(descriptor.enabledByDefault()); - assertEquals("2.6.26", descriptor.minClientVersion()); + assertEquals("2.6.32", descriptor.minClientVersion()); } @Test public void runtimeLogReportsThePublishedPluginVersion() { - assertEquals("1.10.17", descriptor.version()); + assertEquals("1.10.19", descriptor.version()); assertEquals(descriptor.version(), DroZulrahScript.BUILD); } diff --git a/src/test/java/net/runelite/client/plugins/microbot/drozulrah/ZulrahInventoryWidgetTest.java b/src/test/java/net/runelite/client/plugins/microbot/drozulrah/ZulrahInventoryWidgetTest.java new file mode 100644 index 0000000000..4ceca69cdf --- /dev/null +++ b/src/test/java/net/runelite/client/plugins/microbot/drozulrah/ZulrahInventoryWidgetTest.java @@ -0,0 +1,69 @@ +package net.runelite.client.plugins.microbot.drozulrah; + +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.Callable; +import net.runelite.api.*; +import net.runelite.api.widgets.*; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.inventory.*; +import net.runelite.client.plugins.microbot.util.tabs.Rs2Tab; +import org.junit.jupiter.api.*; +import org.mockito.MockedStatic; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +class ZulrahInventoryWidgetTest { + private Client client; + private Widget cell; + private Rs2ItemModel item; + private DroZulrahScript script; + private final List> mocks = new ArrayList<>(); + private MockedStatic open(Class type) { var m = mockStatic(type); mocks.add(m); return m; } + @BeforeEach void setUp() throws Exception { + client = mock(Client.class); cell = mock(Widget.class); item = mock(Rs2ItemModel.class); + script = mock(DroZulrahScript.class, CALLS_REAL_METHODS); + var thread = mock(ClientThread.class); + when(thread.runOnClientThreadOptional(any())).thenAnswer(i -> Optional.ofNullable(((Callable)i.getArgument(0)).call())); + var micro = open(Microbot.class); micro.when(Microbot::getClient).thenReturn(client); micro.when(Microbot::getClientThread).thenReturn(thread); + var inv = open(Rs2Inventory.class); inv.when(() -> Rs2Inventory.get(123)).thenReturn(item); inv.when(() -> Rs2Inventory.contains(123)).thenReturn(true); + open(Rs2Equipment.class); open(Rs2Tab.class); + var container = mock(Widget.class); when(client.getWidget(ComponentID.INVENTORY_CONTAINER)).thenReturn(container); + when(container.getChild(2)).thenReturn(cell); when(cell.getItemId()).thenReturn(123); + when(item.getId()).thenReturn(123); when(item.getSlot()).thenReturn(2); when(item.getName()).thenReturn("Item"); + when(item.getInventoryActions()).thenReturn(new String[]{"Eat", "Wear"}); + when(client.getGameState()).thenReturn(GameState.LOGGED_IN); + } + @AfterEach void close() { Collections.reverse(mocks); mocks.forEach(MockedStatic::close); } + private boolean consume() throws Exception { + Method m = DroZulrahScript.class.getDeclaredMethod("dispatchConsumable",Rs2ItemModel.class,String.class); + m.setAccessible(true); return (boolean)m.invoke(script,item,"Eat"); + } + private int gear() throws Exception { + Class type = Class.forName(DroZulrahScript.class.getName()+"$GearChoice"); + Constructor constructor = type.getDeclaredConstructor(int.class,String.class,int.class,boolean.class,int.class,int.class); + constructor.setAccessible(true); + Object choice = constructor.newInstance(123,"Item",0,false,0,0); + Method m = DroZulrahScript.class.getDeclaredMethod("dispatchGearBatch",Map.class); m.setAccessible(true); + return (int)m.invoke(script,Map.of(0,choice)); + } + @Test void missingLiveActionsCannotEmitDefinitionBasedEatOrWear() throws Exception { + assertFalse(consume()); assertEquals(0,gear()); + verify(client,never()).menuAction(anyInt(),anyInt(),any(),anyInt(),anyInt(),anyString(),anyString()); + } + @Test void reorderedLiveActionsControlBothConsumableAndGearOperations() throws Exception { + when(cell.getActions()).thenReturn(new String[]{"Drop", null, "Eat", "Wear"}); + assertTrue(consume()); assertEquals(1,gear()); + verify(client).menuAction(2,ComponentID.INVENTORY_CONTAINER,MenuAction.CC_OP,3,123,"Eat","Item"); + verify(client).menuAction(2,ComponentID.INVENTORY_CONTAINER,MenuAction.CC_OP,4,123,"Wear","Item"); + } + @Test void missingActionAndChangedSlotDeclineWithoutDispatch() throws Exception { + when(cell.getActions()).thenReturn(new String[]{"Drop"}); assertFalse(consume()); assertEquals(0,gear()); + when(cell.getActions()).thenReturn(new String[]{"Eat","Wear"}); when(cell.getItemId()).thenReturn(456); + assertFalse(consume()); assertEquals(0,gear()); + verify(client,never()).menuAction(anyInt(),anyInt(),any(),anyInt(),anyInt(),anyString(),anyString()); + } +}