Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@
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",
cardUrl="https://chsami.github.io/Microbot-Hub/DroFirecapePlugin/assets/card.png",
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<FcFrame.ItemSlot> 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<FcFrame.ItemSlot> inv,Input input) {
Expand All @@ -50,7 +51,7 @@ boolean step(int tick,int ranged,int base,List<FcFrame.ItemSlot> 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;}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Loading
Loading