From d292c802c9f68e7bd9a9e10c2f1547ef5a5d50d9 Mon Sep 17 00:00:00 2001 From: Haluzer Date: Fri, 14 Aug 2026 21:31:13 +0100 Subject: [PATCH 01/11] Create ReviveLoopWatchdog.java --- .../ReviveLoopWatchdog.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java diff --git a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java new file mode 100644 index 00000000..067d12fb --- /dev/null +++ b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java @@ -0,0 +1,121 @@ +package dev.shared.halizeur.revive_loop_watchdog; + +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.config.annotations.Configuration; +import eu.darkbot.api.config.annotations.Number; +import eu.darkbot.api.extensions.Behavior; +import eu.darkbot.api.extensions.Configurable; +import eu.darkbot.api.extensions.Feature; +import eu.darkbot.api.managers.BotAPI; +import eu.darkbot.api.managers.HeroAPI; +import eu.darkbot.api.managers.RepairAPI; + +import java.time.Instant; +import java.time.Duration; +import java.time.format.DateTimeFormatter; +import java.time.ZoneId; + +/** + * Detects DarkBot's known "stuck on revive" refresh loop bug + * (see: github.com/darkbot-reloaded/DarkBot issue #391 / #455). + * + * If the ship stays destroyed for far longer than a real revive should ever take, + * this assumes the bot is stuck refreshing uselessly, pauses it (so it stops + * burning hours doing nothing), and keeps watching in the background. Once the + * game genuinely finishes loading again AND the ship is confirmed alive, it + * resumes the bot automatically. Every state change is logged to the console/log + * file so it's visible after the fact even if nobody was watching live. + */ +@Feature(name = "Revive Loop Watchdog", description = + "Detects the stuck-on-revive refresh loop bug, pauses the bot, and auto-resumes once it recovers.", + enabledByDefault = true) +public class ReviveLoopWatchdog implements Behavior, Configurable { + + private static final DateTimeFormatter TIME_FORMAT = + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").withZone(ZoneId.systemDefault()); + + private final HeroAPI hero; + private final BotAPI bot; + private final RepairAPI repair; + + private Config config; + + // When the ship was first observed destroyed in the current death, null if alive + private Instant deadSince = null; + + // True only if THIS plugin paused the bot (so we never touch a manual user pause) + private boolean pausedByWatchdog = false; + + public ReviveLoopWatchdog(HeroAPI hero, BotAPI bot, RepairAPI repair) { + this.hero = hero; + this.bot = bot; + this.repair = repair; + } + + @Configuration("revive_loop_watchdog.config") + public static class Config { + @Number(min = 1, max = 30, step = 1) + public int STUCK_THRESHOLD_MINUTES = 3; + } + + @Override + public void setConfig(ConfigSetting config) { + this.config = config.getValue(); + } + + // Runs while the bot is actively working normally. + @Override + public void onTickBehavior() { + // Ship is alive & bot is ticking normally: nothing is wrong, clear any stale death timer. + deadSince = null; + } + + // Runs whenever onTickBehavior wouldn't: bot stopped/paused, ship destroyed, refreshing, or still loading. + // This is the only place we can watch a stuck-revive situation, since the ship being + // "destroyed" is itself one of the conditions that routes ticks here instead of onTickBehavior. + @Override + public void onStoppedBehavior() { + if (pausedByWatchdog) { + checkForRecovery(); + return; + } + + if (repair.isDestroyed()) { + if (deadSince == null) { + deadSince = Instant.now(); + } + + long stuckMinutes = Duration.between(deadSince, Instant.now()).toMinutes(); + if (stuckMinutes >= config.STUCK_THRESHOLD_MINUTES) { + pauseForStuckLoop(stuckMinutes); + } + } else { + // Not destroyed, and we didn't pause it - just a normal stop/refresh/loading moment. + deadSince = null; + } + } + + private void pauseForStuckLoop(long stuckMinutes) { + log("Ship has been destroyed for " + stuckMinutes + "+ minute(s) with no successful revive. " + + "Assuming the known DarkBot stuck-on-revive refresh loop bug. Pausing bot to stop wasted refreshing."); + bot.setRunning(false); + pausedByWatchdog = true; + } + + private void checkForRecovery() { + boolean loaded = hero.getLocationInfo() != null && hero.getLocationInfo().isInitialized(); + boolean alive = !repair.isDestroyed(); + + if (loaded && alive) { + log("Game has finished loading and ship is confirmed alive again. Resuming bot automatically."); + pausedByWatchdog = false; + deadSince = null; + bot.setRunning(true); + } + // else: keep waiting, still stuck or still loading - checked again next tick. + } + + private void log(String message) { + System.out.println("[" + TIME_FORMAT.format(Instant.now()) + " | ReviveLoopWatchdog] " + message); + } +} From 3430408e27c8c9baeec36d4815eb4efd5dc5fe6d Mon Sep 17 00:00:00 2001 From: Haluzer Date: Fri, 14 Aug 2026 21:32:04 +0100 Subject: [PATCH 02/11] Create DeathRateProfileSwitcher.java --- .../DeathRateProfileSwitcher.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java diff --git a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java new file mode 100644 index 00000000..b7f8feed --- /dev/null +++ b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java @@ -0,0 +1,159 @@ +package dev.shared.halizeur.death_rate_switcher; + +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.config.annotations.Configuration; +import eu.darkbot.api.config.annotations.Dropdown; +import eu.darkbot.api.config.annotations.Number; +import eu.darkbot.api.extensions.Behavior; +import eu.darkbot.api.extensions.Configurable; +import eu.darkbot.api.extensions.Feature; +import eu.darkbot.api.managers.BotAPI; +import eu.darkbot.api.managers.ConfigAPI; +import eu.darkbot.api.managers.RepairAPI; + +import java.time.Instant; +import java.time.Duration; +import java.time.format.DateTimeFormatter; +import java.time.ZoneId; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; + +/** + * Watches death rate on a configured "home" profile (Config A). If deaths spike + * past a threshold within a rolling 60 minute window - i.e. you're being PvP hunted - + * it switches to a configured "safe" profile (Config B) for 60 minutes. + * + * While on Config B: + * - if deaths spike again before the 60 minutes are up, it's assumed you're + * being hunted there too, so it reverts to Config A immediately. + * - if 60 minutes pass with no further spike, it reverts to Config A as scheduled. + * + * Either way it always ends back on Config A, ready to trip again later. + */ +@Feature(name = "Death Rate Profile Switcher", description = + "Switches to a safer config profile when deaths/hour spike (PvP hunting), and reverts after a quiet hour.", + enabledByDefault = true) +public class DeathRateProfileSwitcher implements Behavior, Configurable { + + private static final DateTimeFormatter TIME_FORMAT = + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").withZone(ZoneId.systemDefault()); + private static final Duration WINDOW = Duration.ofMinutes(60); + + private final ConfigAPI configAPI; + private final RepairAPI repair; + + private Config config; + + private final Deque recentDeaths = new ArrayDeque<>(); + private boolean wasDestroyed = false; + + // Set once we've switched to TO_PROFILE, holds when the 60 min revert is due. + private Instant revertAt = null; + + public DeathRateProfileSwitcher(ConfigAPI configAPI, RepairAPI repair, BotAPI bot) { + this.configAPI = configAPI; + this.repair = repair; + } + + @Configuration("death_rate_profile_switcher.config") + public static class Config { + @Dropdown(options = ProfileConfigOptions.class) + public String FROM_PROFILE = ""; + + @Dropdown(options = ProfileConfigOptions.class) + public String TO_PROFILE = ""; + + @Number(min = 1, max = 100, step = 1) + public int DEATHS_PER_HOUR_THRESHOLD = 20; + } + + public static class ProfileConfigOptions implements Dropdown.Options { + private final ConfigAPI configAPI; + + public ProfileConfigOptions(ConfigAPI configAPI) { + this.configAPI = configAPI; + } + + @Override + public Collection options() { + return configAPI.getConfigProfiles(); + } + + @Override + public String getText(String option) { + return option == null || option.isEmpty() ? "(none)" : option; + } + } + + @Override + public void setConfig(ConfigSetting config) { + this.config = config.getValue(); + } + + @Override + public void onTickBehavior() { + process(); + } + + // Ship being destroyed routes ticks here instead of onTickBehavior - still need + // to keep tracking death edges & profile state while that's happening. + @Override + public void onStoppedBehavior() { + process(); + } + + private void process() { + if (config.FROM_PROFILE.isEmpty() || config.TO_PROFILE.isEmpty()) return; + + trackDeathEdge(); + pruneOldDeaths(); + + String current = configAPI.getCurrentProfile(); + + if (revertAt == null) { + // Currently expected to be on FROM_PROFILE, watching for a hunt starting. + if (config.FROM_PROFILE.equals(current) && recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { + log("Deaths/hour reached " + recentDeaths.size() + " (threshold " + config.DEATHS_PER_HOUR_THRESHOLD + + ") on '" + config.FROM_PROFILE + "'. Switching to '" + config.TO_PROFILE + "' for 60 minutes."); + configAPI.setConfigProfile(config.TO_PROFILE); + revertAt = Instant.now().plus(WINDOW); + recentDeaths.clear(); + } + } else { + // Currently on TO_PROFILE, watching for either an early re-spike or the timer running out. + if (recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { + log("Deaths/hour reached " + recentDeaths.size() + " again while on '" + config.TO_PROFILE + + "'. Being hunted there too - reverting to '" + config.FROM_PROFILE + "' immediately."); + configAPI.setConfigProfile(config.FROM_PROFILE); + revertAt = null; + recentDeaths.clear(); + } else if (Instant.now().isAfter(revertAt)) { + log("60 minutes passed on '" + config.TO_PROFILE + "' with no further death spike. " + + "Reverting to '" + config.FROM_PROFILE + "'."); + configAPI.setConfigProfile(config.FROM_PROFILE); + revertAt = null; + recentDeaths.clear(); + } + } + } + + private void trackDeathEdge() { + boolean destroyed = repair.isDestroyed(); + if (destroyed && !wasDestroyed) { + recentDeaths.addLast(Instant.now()); + } + wasDestroyed = destroyed; + } + + private void pruneOldDeaths() { + Instant cutoff = Instant.now().minus(WINDOW); + while (!recentDeaths.isEmpty() && recentDeaths.peekFirst().isBefore(cutoff)) { + recentDeaths.pollFirst(); + } + } + + private void log(String message) { + System.out.println("[" + TIME_FORMAT.format(Instant.now()) + " | DeathRateProfileSwitcher] " + message); + } +} From 769dfaf1f41650c7c7ab0b63da501eb29095810f Mon Sep 17 00:00:00 2001 From: Haluzer Date: Fri, 14 Aug 2026 21:38:24 +0100 Subject: [PATCH 03/11] Delete src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java --- .../DeathRateProfileSwitcher.java | 159 ------------------ 1 file changed, 159 deletions(-) delete mode 100644 src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java diff --git a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java deleted file mode 100644 index b7f8feed..00000000 --- a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java +++ /dev/null @@ -1,159 +0,0 @@ -package dev.shared.halizeur.death_rate_switcher; - -import eu.darkbot.api.config.ConfigSetting; -import eu.darkbot.api.config.annotations.Configuration; -import eu.darkbot.api.config.annotations.Dropdown; -import eu.darkbot.api.config.annotations.Number; -import eu.darkbot.api.extensions.Behavior; -import eu.darkbot.api.extensions.Configurable; -import eu.darkbot.api.extensions.Feature; -import eu.darkbot.api.managers.BotAPI; -import eu.darkbot.api.managers.ConfigAPI; -import eu.darkbot.api.managers.RepairAPI; - -import java.time.Instant; -import java.time.Duration; -import java.time.format.DateTimeFormatter; -import java.time.ZoneId; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Deque; - -/** - * Watches death rate on a configured "home" profile (Config A). If deaths spike - * past a threshold within a rolling 60 minute window - i.e. you're being PvP hunted - - * it switches to a configured "safe" profile (Config B) for 60 minutes. - * - * While on Config B: - * - if deaths spike again before the 60 minutes are up, it's assumed you're - * being hunted there too, so it reverts to Config A immediately. - * - if 60 minutes pass with no further spike, it reverts to Config A as scheduled. - * - * Either way it always ends back on Config A, ready to trip again later. - */ -@Feature(name = "Death Rate Profile Switcher", description = - "Switches to a safer config profile when deaths/hour spike (PvP hunting), and reverts after a quiet hour.", - enabledByDefault = true) -public class DeathRateProfileSwitcher implements Behavior, Configurable { - - private static final DateTimeFormatter TIME_FORMAT = - DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").withZone(ZoneId.systemDefault()); - private static final Duration WINDOW = Duration.ofMinutes(60); - - private final ConfigAPI configAPI; - private final RepairAPI repair; - - private Config config; - - private final Deque recentDeaths = new ArrayDeque<>(); - private boolean wasDestroyed = false; - - // Set once we've switched to TO_PROFILE, holds when the 60 min revert is due. - private Instant revertAt = null; - - public DeathRateProfileSwitcher(ConfigAPI configAPI, RepairAPI repair, BotAPI bot) { - this.configAPI = configAPI; - this.repair = repair; - } - - @Configuration("death_rate_profile_switcher.config") - public static class Config { - @Dropdown(options = ProfileConfigOptions.class) - public String FROM_PROFILE = ""; - - @Dropdown(options = ProfileConfigOptions.class) - public String TO_PROFILE = ""; - - @Number(min = 1, max = 100, step = 1) - public int DEATHS_PER_HOUR_THRESHOLD = 20; - } - - public static class ProfileConfigOptions implements Dropdown.Options { - private final ConfigAPI configAPI; - - public ProfileConfigOptions(ConfigAPI configAPI) { - this.configAPI = configAPI; - } - - @Override - public Collection options() { - return configAPI.getConfigProfiles(); - } - - @Override - public String getText(String option) { - return option == null || option.isEmpty() ? "(none)" : option; - } - } - - @Override - public void setConfig(ConfigSetting config) { - this.config = config.getValue(); - } - - @Override - public void onTickBehavior() { - process(); - } - - // Ship being destroyed routes ticks here instead of onTickBehavior - still need - // to keep tracking death edges & profile state while that's happening. - @Override - public void onStoppedBehavior() { - process(); - } - - private void process() { - if (config.FROM_PROFILE.isEmpty() || config.TO_PROFILE.isEmpty()) return; - - trackDeathEdge(); - pruneOldDeaths(); - - String current = configAPI.getCurrentProfile(); - - if (revertAt == null) { - // Currently expected to be on FROM_PROFILE, watching for a hunt starting. - if (config.FROM_PROFILE.equals(current) && recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { - log("Deaths/hour reached " + recentDeaths.size() + " (threshold " + config.DEATHS_PER_HOUR_THRESHOLD + - ") on '" + config.FROM_PROFILE + "'. Switching to '" + config.TO_PROFILE + "' for 60 minutes."); - configAPI.setConfigProfile(config.TO_PROFILE); - revertAt = Instant.now().plus(WINDOW); - recentDeaths.clear(); - } - } else { - // Currently on TO_PROFILE, watching for either an early re-spike or the timer running out. - if (recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { - log("Deaths/hour reached " + recentDeaths.size() + " again while on '" + config.TO_PROFILE + - "'. Being hunted there too - reverting to '" + config.FROM_PROFILE + "' immediately."); - configAPI.setConfigProfile(config.FROM_PROFILE); - revertAt = null; - recentDeaths.clear(); - } else if (Instant.now().isAfter(revertAt)) { - log("60 minutes passed on '" + config.TO_PROFILE + "' with no further death spike. " + - "Reverting to '" + config.FROM_PROFILE + "'."); - configAPI.setConfigProfile(config.FROM_PROFILE); - revertAt = null; - recentDeaths.clear(); - } - } - } - - private void trackDeathEdge() { - boolean destroyed = repair.isDestroyed(); - if (destroyed && !wasDestroyed) { - recentDeaths.addLast(Instant.now()); - } - wasDestroyed = destroyed; - } - - private void pruneOldDeaths() { - Instant cutoff = Instant.now().minus(WINDOW); - while (!recentDeaths.isEmpty() && recentDeaths.peekFirst().isBefore(cutoff)) { - recentDeaths.pollFirst(); - } - } - - private void log(String message) { - System.out.println("[" + TIME_FORMAT.format(Instant.now()) + " | DeathRateProfileSwitcher] " + message); - } -} From 0b82a1d141ab66304693aa0f787789264de04e76 Mon Sep 17 00:00:00 2001 From: Haluzer Date: Fri, 14 Aug 2026 21:43:50 +0100 Subject: [PATCH 04/11] Create DeathRateProfileSwitcher.java --- .../DeathRateProfileSwitcher.java | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java diff --git a/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java b/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java new file mode 100644 index 00000000..b7f8feed --- /dev/null +++ b/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java @@ -0,0 +1,159 @@ +package dev.shared.halizeur.death_rate_switcher; + +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.config.annotations.Configuration; +import eu.darkbot.api.config.annotations.Dropdown; +import eu.darkbot.api.config.annotations.Number; +import eu.darkbot.api.extensions.Behavior; +import eu.darkbot.api.extensions.Configurable; +import eu.darkbot.api.extensions.Feature; +import eu.darkbot.api.managers.BotAPI; +import eu.darkbot.api.managers.ConfigAPI; +import eu.darkbot.api.managers.RepairAPI; + +import java.time.Instant; +import java.time.Duration; +import java.time.format.DateTimeFormatter; +import java.time.ZoneId; +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; + +/** + * Watches death rate on a configured "home" profile (Config A). If deaths spike + * past a threshold within a rolling 60 minute window - i.e. you're being PvP hunted - + * it switches to a configured "safe" profile (Config B) for 60 minutes. + * + * While on Config B: + * - if deaths spike again before the 60 minutes are up, it's assumed you're + * being hunted there too, so it reverts to Config A immediately. + * - if 60 minutes pass with no further spike, it reverts to Config A as scheduled. + * + * Either way it always ends back on Config A, ready to trip again later. + */ +@Feature(name = "Death Rate Profile Switcher", description = + "Switches to a safer config profile when deaths/hour spike (PvP hunting), and reverts after a quiet hour.", + enabledByDefault = true) +public class DeathRateProfileSwitcher implements Behavior, Configurable { + + private static final DateTimeFormatter TIME_FORMAT = + DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").withZone(ZoneId.systemDefault()); + private static final Duration WINDOW = Duration.ofMinutes(60); + + private final ConfigAPI configAPI; + private final RepairAPI repair; + + private Config config; + + private final Deque recentDeaths = new ArrayDeque<>(); + private boolean wasDestroyed = false; + + // Set once we've switched to TO_PROFILE, holds when the 60 min revert is due. + private Instant revertAt = null; + + public DeathRateProfileSwitcher(ConfigAPI configAPI, RepairAPI repair, BotAPI bot) { + this.configAPI = configAPI; + this.repair = repair; + } + + @Configuration("death_rate_profile_switcher.config") + public static class Config { + @Dropdown(options = ProfileConfigOptions.class) + public String FROM_PROFILE = ""; + + @Dropdown(options = ProfileConfigOptions.class) + public String TO_PROFILE = ""; + + @Number(min = 1, max = 100, step = 1) + public int DEATHS_PER_HOUR_THRESHOLD = 20; + } + + public static class ProfileConfigOptions implements Dropdown.Options { + private final ConfigAPI configAPI; + + public ProfileConfigOptions(ConfigAPI configAPI) { + this.configAPI = configAPI; + } + + @Override + public Collection options() { + return configAPI.getConfigProfiles(); + } + + @Override + public String getText(String option) { + return option == null || option.isEmpty() ? "(none)" : option; + } + } + + @Override + public void setConfig(ConfigSetting config) { + this.config = config.getValue(); + } + + @Override + public void onTickBehavior() { + process(); + } + + // Ship being destroyed routes ticks here instead of onTickBehavior - still need + // to keep tracking death edges & profile state while that's happening. + @Override + public void onStoppedBehavior() { + process(); + } + + private void process() { + if (config.FROM_PROFILE.isEmpty() || config.TO_PROFILE.isEmpty()) return; + + trackDeathEdge(); + pruneOldDeaths(); + + String current = configAPI.getCurrentProfile(); + + if (revertAt == null) { + // Currently expected to be on FROM_PROFILE, watching for a hunt starting. + if (config.FROM_PROFILE.equals(current) && recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { + log("Deaths/hour reached " + recentDeaths.size() + " (threshold " + config.DEATHS_PER_HOUR_THRESHOLD + + ") on '" + config.FROM_PROFILE + "'. Switching to '" + config.TO_PROFILE + "' for 60 minutes."); + configAPI.setConfigProfile(config.TO_PROFILE); + revertAt = Instant.now().plus(WINDOW); + recentDeaths.clear(); + } + } else { + // Currently on TO_PROFILE, watching for either an early re-spike or the timer running out. + if (recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { + log("Deaths/hour reached " + recentDeaths.size() + " again while on '" + config.TO_PROFILE + + "'. Being hunted there too - reverting to '" + config.FROM_PROFILE + "' immediately."); + configAPI.setConfigProfile(config.FROM_PROFILE); + revertAt = null; + recentDeaths.clear(); + } else if (Instant.now().isAfter(revertAt)) { + log("60 minutes passed on '" + config.TO_PROFILE + "' with no further death spike. " + + "Reverting to '" + config.FROM_PROFILE + "'."); + configAPI.setConfigProfile(config.FROM_PROFILE); + revertAt = null; + recentDeaths.clear(); + } + } + } + + private void trackDeathEdge() { + boolean destroyed = repair.isDestroyed(); + if (destroyed && !wasDestroyed) { + recentDeaths.addLast(Instant.now()); + } + wasDestroyed = destroyed; + } + + private void pruneOldDeaths() { + Instant cutoff = Instant.now().minus(WINDOW); + while (!recentDeaths.isEmpty() && recentDeaths.peekFirst().isBefore(cutoff)) { + recentDeaths.pollFirst(); + } + } + + private void log(String message) { + System.out.println("[" + TIME_FORMAT.format(Instant.now()) + " | DeathRateProfileSwitcher] " + message); + } +} From 07160e5d86071fec70bd124956d0032f925cd5fd Mon Sep 17 00:00:00 2001 From: Haluzer Date: Fri, 14 Aug 2026 21:46:41 +0100 Subject: [PATCH 05/11] Update plugin.json --- src/main/resources/plugin.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/resources/plugin.json b/src/main/resources/plugin.json index f8d30a0e..49a007ee 100644 --- a/src/main/resources/plugin.json +++ b/src/main/resources/plugin.json @@ -19,8 +19,10 @@ "dev.shared.orbithelper.behaviours.fast_travel.FastTravel", "dev.shared.do_gamer.module.simple_galaxy_gate.SimpleGalaxyGate", "dev.shared.do_gamer.task.autobuy.Autobuy", - "dev.shared.halizeur.log_overlay.LogOverlay" + "dev.shared.halizeur.log_overlay.LogOverlay", + "dev.shared.halizeur.revive_loop_watchdog.ReviveLoopWatchdog", + "dev.shared.halizeur.death_rate_switcher.DeathRateProfileSwitcher" ], "update": "https://raw.githubusercontent.com/Darkbot-Plugins/SharedPlugin/main/src/main/resources/plugin.json", "download": "https://github.com/Darkbot-Plugins/SharedPlugin/releases/latest/download/SharedPlugin.jar" -} \ No newline at end of file +} From fd9adcc6c3d6ac53c424a668678b67eef45b18f4 Mon Sep 17 00:00:00 2001 From: Haluzer Date: Sat, 15 Aug 2026 15:25:59 +0100 Subject: [PATCH 06/11] Update ReviveLoopWatchdog.java --- .../ReviveLoopWatchdog.java | 23 +------------------ 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java index 067d12fb..f665296d 100644 --- a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java +++ b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java @@ -15,17 +15,6 @@ import java.time.format.DateTimeFormatter; import java.time.ZoneId; -/** - * Detects DarkBot's known "stuck on revive" refresh loop bug - * (see: github.com/darkbot-reloaded/DarkBot issue #391 / #455). - * - * If the ship stays destroyed for far longer than a real revive should ever take, - * this assumes the bot is stuck refreshing uselessly, pauses it (so it stops - * burning hours doing nothing), and keeps watching in the background. Once the - * game genuinely finishes loading again AND the ship is confirmed alive, it - * resumes the bot automatically. Every state change is logged to the console/log - * file so it's visible after the fact even if nobody was watching live. - */ @Feature(name = "Revive Loop Watchdog", description = "Detects the stuck-on-revive refresh loop bug, pauses the bot, and auto-resumes once it recovers.", enabledByDefault = true) @@ -38,12 +27,9 @@ public class ReviveLoopWatchdog implements Behavior, Configurable config) { this.config = config.getValue(); } - // Runs while the bot is actively working normally. @Override public void onTickBehavior() { - // Ship is alive & bot is ticking normally: nothing is wrong, clear any stale death timer. deadSince = null; } - // Runs whenever onTickBehavior wouldn't: bot stopped/paused, ship destroyed, refreshing, or still loading. - // This is the only place we can watch a stuck-revive situation, since the ship being - // "destroyed" is itself one of the conditions that routes ticks here instead of onTickBehavior. @Override public void onStoppedBehavior() { if (pausedByWatchdog) { @@ -90,7 +71,6 @@ public void onStoppedBehavior() { pauseForStuckLoop(stuckMinutes); } } else { - // Not destroyed, and we didn't pause it - just a normal stop/refresh/loading moment. deadSince = null; } } @@ -112,7 +92,6 @@ private void checkForRecovery() { deadSince = null; bot.setRunning(true); } - // else: keep waiting, still stuck or still loading - checked again next tick. } private void log(String message) { From 8b28ba7c5eb3c8d3439f05dda5deb2e5801c9e53 Mon Sep 17 00:00:00 2001 From: Haluzer Date: Sat, 15 Aug 2026 15:26:38 +0100 Subject: [PATCH 07/11] DeathRateProfileSwitcher.java --- .../DeathRateProfileSwitcher.java | 58 +------------------ 1 file changed, 2 insertions(+), 56 deletions(-) diff --git a/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java b/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java index b7f8feed..99bfeca9 100644 --- a/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java +++ b/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java @@ -7,7 +7,6 @@ import eu.darkbot.api.extensions.Behavior; import eu.darkbot.api.extensions.Configurable; import eu.darkbot.api.extensions.Feature; -import eu.darkbot.api.managers.BotAPI; import eu.darkbot.api.managers.ConfigAPI; import eu.darkbot.api.managers.RepairAPI; @@ -19,18 +18,6 @@ import java.util.Collection; import java.util.Deque; -/** - * Watches death rate on a configured "home" profile (Config A). If deaths spike - * past a threshold within a rolling 60 minute window - i.e. you're being PvP hunted - - * it switches to a configured "safe" profile (Config B) for 60 minutes. - * - * While on Config B: - * - if deaths spike again before the 60 minutes are up, it's assumed you're - * being hunted there too, so it reverts to Config A immediately. - * - if 60 minutes pass with no further spike, it reverts to Config A as scheduled. - * - * Either way it always ends back on Config A, ready to trip again later. - */ @Feature(name = "Death Rate Profile Switcher", description = "Switches to a safer config profile when deaths/hour spike (PvP hunting), and reverts after a quiet hour.", enabledByDefault = true) @@ -43,15 +30,14 @@ public class DeathRateProfileSwitcher implements Behavior, Configurable recentDeaths = new ArrayDeque<>(); private boolean wasDestroyed = false; - // Set once we've switched to TO_PROFILE, holds when the 60 min revert is due. private Instant revertAt = null; - public DeathRateProfileSwitcher(ConfigAPI configAPI, RepairAPI repair, BotAPI bot) { + public DeathRateProfileSwitcher(ConfigAPI configAPI, RepairAPI repair) { this.configAPI = configAPI; this.repair = repair; } @@ -96,8 +82,6 @@ public void onTickBehavior() { process(); } - // Ship being destroyed routes ticks here instead of onTickBehavior - still need - // to keep tracking death edges & profile state while that's happening. @Override public void onStoppedBehavior() { process(); @@ -112,7 +96,6 @@ private void process() { String current = configAPI.getCurrentProfile(); if (revertAt == null) { - // Currently expected to be on FROM_PROFILE, watching for a hunt starting. if (config.FROM_PROFILE.equals(current) && recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { log("Deaths/hour reached " + recentDeaths.size() + " (threshold " + config.DEATHS_PER_HOUR_THRESHOLD + ") on '" + config.FROM_PROFILE + "'. Switching to '" + config.TO_PROFILE + "' for 60 minutes."); @@ -120,40 +103,3 @@ private void process() { revertAt = Instant.now().plus(WINDOW); recentDeaths.clear(); } - } else { - // Currently on TO_PROFILE, watching for either an early re-spike or the timer running out. - if (recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { - log("Deaths/hour reached " + recentDeaths.size() + " again while on '" + config.TO_PROFILE + - "'. Being hunted there too - reverting to '" + config.FROM_PROFILE + "' immediately."); - configAPI.setConfigProfile(config.FROM_PROFILE); - revertAt = null; - recentDeaths.clear(); - } else if (Instant.now().isAfter(revertAt)) { - log("60 minutes passed on '" + config.TO_PROFILE + "' with no further death spike. " + - "Reverting to '" + config.FROM_PROFILE + "'."); - configAPI.setConfigProfile(config.FROM_PROFILE); - revertAt = null; - recentDeaths.clear(); - } - } - } - - private void trackDeathEdge() { - boolean destroyed = repair.isDestroyed(); - if (destroyed && !wasDestroyed) { - recentDeaths.addLast(Instant.now()); - } - wasDestroyed = destroyed; - } - - private void pruneOldDeaths() { - Instant cutoff = Instant.now().minus(WINDOW); - while (!recentDeaths.isEmpty() && recentDeaths.peekFirst().isBefore(cutoff)) { - recentDeaths.pollFirst(); - } - } - - private void log(String message) { - System.out.println("[" + TIME_FORMAT.format(Instant.now()) + " | DeathRateProfileSwitcher] " + message); - } -} From 2a37a41f6b43077af8ba3f5b5c3df8b35e41a576 Mon Sep 17 00:00:00 2001 From: Haluzer Date: Wed, 19 Aug 2026 19:46:20 +0100 Subject: [PATCH 08/11] Update ReviveLoopWatchdog.java --- .../halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java index f665296d..58a0a93b 100644 --- a/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java +++ b/src/main/java/dev/shared/halizeur/revive_loop_watchdog/ReviveLoopWatchdog.java @@ -76,6 +76,12 @@ public void onStoppedBehavior() { } private void pauseForStuckLoop(long stuckMinutes) { + if (!bot.isRunning()) { + // Bot is already stopped for some other reason (e.g. you paused it manually) - + // not our doing, so don't take ownership of it or auto-resume it later. + return; + } + log("Ship has been destroyed for " + stuckMinutes + "+ minute(s) with no successful revive. " + "Assuming the known DarkBot stuck-on-revive refresh loop bug. Pausing bot to stop wasted refreshing."); bot.setRunning(false); From f6915efa0412e2a6c1bc75987b0aedb5f3df916a Mon Sep 17 00:00:00 2001 From: Haluzer Date: Wed, 2 Sep 2026 20:47:28 +0100 Subject: [PATCH 09/11] Update DeathRateProfileSwitcher.java --- .../DeathRateProfileSwitcher.java | 104 ------------------ 1 file changed, 104 deletions(-) diff --git a/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java b/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java index 99bfeca9..8b137891 100644 --- a/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java +++ b/src/main/java/dev/shared/halizeur/death_rate_switcher/DeathRateProfileSwitcher.java @@ -1,105 +1 @@ -package dev.shared.halizeur.death_rate_switcher; -import eu.darkbot.api.config.ConfigSetting; -import eu.darkbot.api.config.annotations.Configuration; -import eu.darkbot.api.config.annotations.Dropdown; -import eu.darkbot.api.config.annotations.Number; -import eu.darkbot.api.extensions.Behavior; -import eu.darkbot.api.extensions.Configurable; -import eu.darkbot.api.extensions.Feature; -import eu.darkbot.api.managers.ConfigAPI; -import eu.darkbot.api.managers.RepairAPI; - -import java.time.Instant; -import java.time.Duration; -import java.time.format.DateTimeFormatter; -import java.time.ZoneId; -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Deque; - -@Feature(name = "Death Rate Profile Switcher", description = - "Switches to a safer config profile when deaths/hour spike (PvP hunting), and reverts after a quiet hour.", - enabledByDefault = true) -public class DeathRateProfileSwitcher implements Behavior, Configurable { - - private static final DateTimeFormatter TIME_FORMAT = - DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss").withZone(ZoneId.systemDefault()); - private static final Duration WINDOW = Duration.ofMinutes(60); - - private final ConfigAPI configAPI; - private final RepairAPI repair; - - private Config config = new Config(); - - private final Deque recentDeaths = new ArrayDeque<>(); - private boolean wasDestroyed = false; - - private Instant revertAt = null; - - public DeathRateProfileSwitcher(ConfigAPI configAPI, RepairAPI repair) { - this.configAPI = configAPI; - this.repair = repair; - } - - @Configuration("death_rate_profile_switcher.config") - public static class Config { - @Dropdown(options = ProfileConfigOptions.class) - public String FROM_PROFILE = ""; - - @Dropdown(options = ProfileConfigOptions.class) - public String TO_PROFILE = ""; - - @Number(min = 1, max = 100, step = 1) - public int DEATHS_PER_HOUR_THRESHOLD = 20; - } - - public static class ProfileConfigOptions implements Dropdown.Options { - private final ConfigAPI configAPI; - - public ProfileConfigOptions(ConfigAPI configAPI) { - this.configAPI = configAPI; - } - - @Override - public Collection options() { - return configAPI.getConfigProfiles(); - } - - @Override - public String getText(String option) { - return option == null || option.isEmpty() ? "(none)" : option; - } - } - - @Override - public void setConfig(ConfigSetting config) { - this.config = config.getValue(); - } - - @Override - public void onTickBehavior() { - process(); - } - - @Override - public void onStoppedBehavior() { - process(); - } - - private void process() { - if (config.FROM_PROFILE.isEmpty() || config.TO_PROFILE.isEmpty()) return; - - trackDeathEdge(); - pruneOldDeaths(); - - String current = configAPI.getCurrentProfile(); - - if (revertAt == null) { - if (config.FROM_PROFILE.equals(current) && recentDeaths.size() >= config.DEATHS_PER_HOUR_THRESHOLD) { - log("Deaths/hour reached " + recentDeaths.size() + " (threshold " + config.DEATHS_PER_HOUR_THRESHOLD + - ") on '" + config.FROM_PROFILE + "'. Switching to '" + config.TO_PROFILE + "' for 60 minutes."); - configAPI.setConfigProfile(config.TO_PROFILE); - revertAt = Instant.now().plus(WINDOW); - recentDeaths.clear(); - } From 86d14738bf08a96ab106c900e981e54d84126376 Mon Sep 17 00:00:00 2001 From: Haluzer Date: Wed, 2 Sep 2026 20:49:22 +0100 Subject: [PATCH 10/11] Update plugin.json --- src/main/resources/plugin.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/resources/plugin.json b/src/main/resources/plugin.json index 49a007ee..b1619dd3 100644 --- a/src/main/resources/plugin.json +++ b/src/main/resources/plugin.json @@ -21,7 +21,6 @@ "dev.shared.do_gamer.task.autobuy.Autobuy", "dev.shared.halizeur.log_overlay.LogOverlay", "dev.shared.halizeur.revive_loop_watchdog.ReviveLoopWatchdog", - "dev.shared.halizeur.death_rate_switcher.DeathRateProfileSwitcher" ], "update": "https://raw.githubusercontent.com/Darkbot-Plugins/SharedPlugin/main/src/main/resources/plugin.json", "download": "https://github.com/Darkbot-Plugins/SharedPlugin/releases/latest/download/SharedPlugin.jar" From 9f1d4dc19ce725483ed37d57469e84a924a40382 Mon Sep 17 00:00:00 2001 From: Haluzer Date: Wed, 2 Sep 2026 20:55:42 +0100 Subject: [PATCH 11/11] Update plugin.json --- src/main/resources/plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/plugin.json b/src/main/resources/plugin.json index b1619dd3..d41366fa 100644 --- a/src/main/resources/plugin.json +++ b/src/main/resources/plugin.json @@ -20,7 +20,7 @@ "dev.shared.do_gamer.module.simple_galaxy_gate.SimpleGalaxyGate", "dev.shared.do_gamer.task.autobuy.Autobuy", "dev.shared.halizeur.log_overlay.LogOverlay", - "dev.shared.halizeur.revive_loop_watchdog.ReviveLoopWatchdog", + "dev.shared.halizeur.revive_loop_watchdog.ReviveLoopWatchdog" ], "update": "https://raw.githubusercontent.com/Darkbot-Plugins/SharedPlugin/main/src/main/resources/plugin.json", "download": "https://github.com/Darkbot-Plugins/SharedPlugin/releases/latest/download/SharedPlugin.jar"