From e54045592a6a3c6a00707b0df74779f038a97af1 Mon Sep 17 00:00:00 2001 From: MuffinKid23 <143044121+MuffinKid23@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:10:36 -0600 Subject: [PATCH 1/2] Add optional long breaks to Break Handler V2 --- .../breakhandlerv2/BreakHandlerV2Config.java | 59 +++++++ .../breakhandlerv2/BreakHandlerV2Overlay.java | 24 ++- .../breakhandlerv2/BreakHandlerV2Script.java | 163 ++++++++++++++++-- 3 files changed, 232 insertions(+), 14 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java index f32cfd33134..01888445868 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java @@ -66,6 +66,65 @@ default int maxBreakDuration() { return 15; } + @ConfigItem( + keyName = "enableLongBreaks", + name = "Enable Long Breaks", + description = "Enable a second independent long-break timer", + position = 4, + section = breakTimingSettings + ) + default boolean enableLongBreaks() { + return false; + } + + @ConfigItem( + keyName = "minLongBreakInterval", + name = "Min Long Break Interval (minutes)", + description = "Minimum time to play before a long break can trigger", + position = 5, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minLongBreakInterval() { + return 20; + } + + @ConfigItem( + keyName = "maxLongBreakInterval", + name = "Max Long Break Interval (minutes)", + description = "Maximum time to play before a long break can trigger", + position = 6, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxLongBreakInterval() { + return 30; + } + + @ConfigItem( + keyName = "minLongBreakDuration", + name = "Min Long Break Duration (minutes)", + description = "Minimum long-break duration", + position = 7, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minLongBreakDuration() { + return 8; + } + + @ConfigItem( + keyName = "maxLongBreakDuration", + name = "Max Long Break Duration (minutes)", + description = "Maximum long-break duration", + position = 8, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxLongBreakDuration() { + return 10; + } + // ========== BREAK BEHAVIOR SECTION ========== @ConfigSection( name = "Break Behavior", diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java index 579afdaeb8d..8b6e5cee760 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java @@ -67,6 +67,18 @@ public Dimension render(Graphics2D graphics) { .rightColor(Color.GRAY) .build()); + panelComponent.getChildren().add(LineComponent.builder() + .left("Runtime:") + .right(formatDuration(script.getScriptActiveSeconds())) + .rightColor(Color.WHITE) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Breaks:") + .right(String.valueOf(script.getBreaksActivatedCount())) + .rightColor(Color.WHITE) + .build()); + // Show play schedule info if enabled if (config.usePlaySchedule()) { panelComponent.getChildren().add(LineComponent.builder() @@ -88,12 +100,20 @@ public Dimension render(Graphics2D graphics) { .rightColor(Color.GREEN) .build()); } + long secondsUntilLongBreak = script.getTimeUntilLongBreak(); + if (config.enableLongBreaks() && secondsUntilLongBreak >= 0) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Long break:") + .right(formatDuration(secondsUntilLongBreak)) + .rightColor(Color.ORANGE) + .build()); + } } else if (BreakHandlerV2State.isBreakActive()) { long secondsRemaining = script.getBreakTimeRemaining(); if (secondsRemaining >= 0) { String timeStr = formatDuration(secondsRemaining); panelComponent.getChildren().add(LineComponent.builder() - .left("Break ends:") + .left(script.isCurrentBreakLong() ? "Long break ends:" : "Break ends:") .right(timeStr) .rightColor(Color.ORANGE) .build()); @@ -163,7 +183,7 @@ public Dimension render(Graphics2D graphics) { // Break configuration panelComponent.getChildren().add(LineComponent.builder() .left("Break type:") - .right(config.logoutOnBreak() ? "Logout" : "Stay logged in") + .right((config.logoutOnBreak() ? "Logout" : "Stay logged in") + (config.enableLongBreaks() ? " + long" : "")) .rightColor(Color.LIGHT_GRAY) .build()); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java index 697b3a24769..e22b9df2914 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java @@ -45,6 +45,7 @@ public BreakHandlerV2Script() { // Timing variables (volatile for thread visibility from overlay/UI threads) private volatile Instant nextBreakTime; + private volatile Instant nextLongBreakTime; private volatile Instant breakEndTime; private volatile Instant loginAttemptTime; @@ -60,6 +61,8 @@ public BreakHandlerV2Script() { private String stoppedPluginClassName = PluginStopOption.NONE_VALUE; private Instant pluginStopEarliestTime = Instant.MIN; private Instant pluginRestartAllowedAt = Instant.MIN; + private volatile Instant scriptStartedAt; + private volatile int breaksActivatedCount = 0; // Persisted break keys private static final String PERSISTED_BREAK_END_KEY = "persistedBreakEnd"; @@ -68,6 +71,8 @@ public BreakHandlerV2Script() { // Break duration in milliseconds private long currentBreakDuration = 0; private boolean logoutBreakActive = false; + private boolean longBreakDue = false; + private boolean currentBreakIsLong = false; // Login retry backoff constants private static final int MAX_LOGIN_ATTEMPTS = 10; @@ -78,17 +83,19 @@ public BreakHandlerV2Script() { private static final int MAX_SAFETY_CHECK_ATTEMPTS = 60; private static final int SAFETY_CHECK_DELAY_MS = 5000; // 5 seconds between checks - public static String version = "2.0.1"; + public static String version = "2.0.5"; /** * Run the break handler script */ public boolean run(BreakHandlerV2Config config) { this.config = config; + scriptStartedAt = Instant.now(); + breaksActivatedCount = 0; BreakHandlerV2State.setState(BreakHandlerV2State.WAITING_FOR_BREAK); // Initialize next break time immediately to prevent null values in overlay - scheduleNextBreak(); + scheduleNextBreak(true); log.info("[BreakHandlerV2] Initial break scheduled for {}", nextBreakTime); // Load active profile loadActiveProfile(); @@ -107,6 +114,7 @@ public boolean run(BreakHandlerV2Config config) { // Detect unexpected logout while waiting for break detectUnexpectedLogout(); enforceLogoutDuringActiveBreak(); + mergeLongBreakIntoActiveBreakIfDue(); updateWindowTitle(); // Main state machine @@ -191,7 +199,15 @@ private void handleWaitingForBreak() { } // Check if it's time for a break (only when play schedule is disabled) + if (config.enableLongBreaks() && nextLongBreakTime != null && !Instant.now().isBefore(nextLongBreakTime)) { + longBreakDue = true; + log.info("[BreakHandlerV2] Long break time reached, requesting long break"); + transitionToState(BreakHandlerV2State.BREAK_REQUESTED); + return; + } + if (nextBreakTime != null && Instant.now().isAfter(nextBreakTime)) { + longBreakDue = false; log.info("[BreakHandlerV2] Break time reached, requesting break"); transitionToState(BreakHandlerV2State.BREAK_REQUESTED); } @@ -243,6 +259,7 @@ private void handleInitiatingBreak() { if (!Microbot.isLoggedIn()) { log.info("[BreakHandlerV2] Already logged out, transitioning to LOGGED_OUT"); setBreakTimer(true); + recordBreakActivated(); sendBreakStartedNotification(true); safetyCheckAttempts = 0; // Reset counter transitionToState(BreakHandlerV2State.LOGGED_OUT); @@ -468,6 +485,8 @@ private void handleBreakEnding() { startConfiguredPluginIfNeeded(); + boolean completedLongBreak = currentBreakIsLong || longBreakDue; + // Reset variables breakEndTime = null; loginAttemptTime = null; @@ -482,8 +501,8 @@ private void handleBreakEnding() { // Unpause scripts Microbot.pauseAllScripts.set(false); - // Schedule next break - scheduleNextBreak(); + // Schedule next break. A normal break must not reset the pending long-break timer. + scheduleNextBreak(completedLongBreak); String breakMessage = nextBreakTime != null ? "Next break scheduled for " + nextBreakTime @@ -577,6 +596,36 @@ private void enforceLogoutDuringActiveBreak() { } } + /** + * If the long-break timer expires while a normal break is already active, fold it into the + * current break instead of dropping it or starting a second break cycle. + */ + private void mergeLongBreakIntoActiveBreakIfDue() { + if (config == null || !config.enableLongBreaks() || nextLongBreakTime == null || breakEndTime == null) { + return; + } + + if (!BreakHandlerV2State.isBreakActive() || currentBreakIsLong || Instant.now().isBefore(nextLongBreakTime)) { + return; + } + + longBreakDue = true; + currentBreakIsLong = true; + long longBreakDuration = calculateLongBreakDuration(); + Instant mergedBreakEndTime = Instant.now().plus(longBreakDuration, ChronoUnit.MILLIS); + if (mergedBreakEndTime.isAfter(breakEndTime)) { + breakEndTime = mergedBreakEndTime; + currentBreakDuration = Math.max(0, Instant.now().until(breakEndTime, ChronoUnit.MILLIS)); + persistBreakState(logoutBreakActive); + } + nextLongBreakTime = null; + + log.info("[BreakHandlerV2] Long break merged into active break; break now ends at {}", breakEndTime); + sendDiscordNotification("Long Break Merged", + "A long break became due during an active break.\nNew remaining duration: " + + Math.max(0, Instant.now().until(breakEndTime, ChronoUnit.MINUTES)) + " minutes"); + } + /** * Select world based on configuration and profile */ @@ -702,15 +751,19 @@ private Integer resolveProfilePreferredWorld(WorldRegion region) { /** * Schedule the next break */ - private void scheduleNextBreak() { + private void scheduleNextBreak(boolean rescheduleLongBreak) { + longBreakDue = false; + currentBreakIsLong = false; if (config.usePlaySchedule()) { if (!config.playSchedule().isOutsideSchedule()) { Duration timeUntilEnd = config.playSchedule().timeUntilScheduleEnds(); nextBreakTime = Instant.now().plus(timeUntilEnd); + nextLongBreakTime = null; log.info("[BreakHandlerV2] Play schedule active ({}), break when schedule ends in {} minutes", config.playSchedule().name(), timeUntilEnd.toMinutes()); } else { nextBreakTime = null; + nextLongBreakTime = null; log.info("[BreakHandlerV2] Outside play schedule ({}), currently on break", config.playSchedule().name()); } @@ -726,6 +779,21 @@ private void scheduleNextBreak() { log.info("[BreakHandlerV2] Next break in {} minutes", playtimeMinutes); + if (config.enableLongBreaks()) { + if (!rescheduleLongBreak && nextLongBreakTime != null) { + log.info("[BreakHandlerV2] Keeping pending long break for {}", nextLongBreakTime); + updatePluginStopLeadTime(); + return; + } + int minLongMinutes = Math.min(config.minLongBreakInterval(), config.maxLongBreakInterval()); + int maxLongMinutes = Math.max(config.minLongBreakInterval(), config.maxLongBreakInterval()); + int longBreakMinutes = Rs2Random.between(minLongMinutes, maxLongMinutes); + nextLongBreakTime = Instant.now().plus(longBreakMinutes, ChronoUnit.MINUTES); + log.info("[BreakHandlerV2] Next long break in {} minutes", longBreakMinutes); + } else { + nextLongBreakTime = null; + } + updatePluginStopLeadTime(); } @@ -739,8 +807,9 @@ private void updatePluginStopLeadTime() { } int leadSeconds = Math.max(0, config.stopPluginLeadSeconds()); - if (nextBreakTime != null && leadSeconds > 0) { - pluginStopEarliestTime = nextBreakTime.minusSeconds(leadSeconds); + Instant nextStopRelevantBreakTime = getNextScheduledBreakTime(); + if (nextStopRelevantBreakTime != null && leadSeconds > 0) { + pluginStopEarliestTime = nextStopRelevantBreakTime.minusSeconds(leadSeconds); } else { pluginStopEarliestTime = Instant.MIN; } @@ -771,6 +840,7 @@ private void applyPreBreakPluginStopLead() { private long calculateBreakDuration() { // If outside play schedule, break until next play time if (isOutsidePlaySchedule()) { + currentBreakIsLong = false; Duration timeUntilPlaySchedule = config.playSchedule().timeUntilNextSchedule(); long durationMs = timeUntilPlaySchedule.toMillis(); log.info("[BreakHandlerV2] Play schedule break duration: {} minutes (until next scheduled play time)", @@ -778,15 +848,32 @@ private long calculateBreakDuration() { return durationMs; } - int minMinutes = config.minBreakDuration(); - int maxMinutes = config.maxBreakDuration(); + int minMinutes; + int maxMinutes; + if (longBreakDue && config.enableLongBreaks()) { + currentBreakIsLong = true; + minMinutes = Math.min(config.minLongBreakDuration(), config.maxLongBreakDuration()); + maxMinutes = Math.max(config.minLongBreakDuration(), config.maxLongBreakDuration()); + } else { + currentBreakIsLong = false; + minMinutes = Math.min(config.minBreakDuration(), config.maxBreakDuration()); + maxMinutes = Math.max(config.minBreakDuration(), config.maxBreakDuration()); + } int breakMinutes = Rs2Random.between(minMinutes, maxMinutes); - log.info("[BreakHandlerV2] Break duration: {} minutes", breakMinutes); + log.info("[BreakHandlerV2] {} duration: {} minutes", currentBreakIsLong ? "Long break" : "Break", breakMinutes); return breakMinutes * 60000L; // Convert to milliseconds } + private long calculateLongBreakDuration() { + int minMinutes = Math.min(config.minLongBreakDuration(), config.maxLongBreakDuration()); + int maxMinutes = Math.max(config.minLongBreakDuration(), config.maxLongBreakDuration()); + int breakMinutes = Rs2Random.between(minMinutes, maxMinutes); + log.info("[BreakHandlerV2] Merged long break duration: {} minutes", breakMinutes); + return breakMinutes * 60000L; + } + /** * Stops a configured Microbot plugin once per break cycle. */ @@ -945,6 +1032,35 @@ public long getTimeUntilBreak() { return Instant.now().until(nextBreakTime, ChronoUnit.SECONDS); } + public long getTimeUntilLongBreak() { + if (nextLongBreakTime == null) { + return -1; + } + return Instant.now().until(nextLongBreakTime, ChronoUnit.SECONDS); + } + + public long getTimeUntilNextScheduledBreak() { + Instant nextScheduledBreak = getNextScheduledBreakTime(); + if (nextScheduledBreak == null) { + return -1; + } + return Instant.now().until(nextScheduledBreak, ChronoUnit.SECONDS); + } + + public boolean isCurrentBreakLong() { + return currentBreakIsLong; + } + + private Instant getNextScheduledBreakTime() { + if (nextBreakTime == null) { + return nextLongBreakTime; + } + if (nextLongBreakTime == null) { + return nextBreakTime; + } + return nextLongBreakTime.isBefore(nextBreakTime) ? nextLongBreakTime : nextBreakTime; + } + /** * Get time remaining in break in seconds */ @@ -973,6 +1089,7 @@ public void shutdown() { // Clear timers nextBreakTime = null; + nextLongBreakTime = null; breakEndTime = null; loginAttemptTime = null; @@ -981,6 +1098,8 @@ public void shutdown() { loginRetryCount = 0; safetyCheckAttempts = 0; logoutBreakActive = false; + longBreakDue = false; + currentBreakIsLong = false; pluginStopTriggered = false; pluginRestartPending = false; stoppedPluginClassName = PluginStopOption.NONE_VALUE; @@ -1043,6 +1162,7 @@ private void initializeBreakIfOutsideSchedule() { } setBreakTimer(true); + recordBreakActivated(); stopConfiguredPluginIfNeeded(); sendBreakStartedNotification(true); log.info("[BreakHandlerV2] Outside play schedule on startup, enforcing break until {}", breakEndTime); @@ -1079,12 +1199,14 @@ private void extendBreakUntilSchedule() { private void beginPauseBreak() { setBreakTimer(false); + recordBreakActivated(); sendBreakStartedNotification(false); Microbot.pauseAllScripts.set(true); } private void beginLogoutBreak() { setBreakTimer(true); + recordBreakActivated(); sendBreakStartedNotification(true); transitionToState(BreakHandlerV2State.LOGOUT_REQUESTED); } @@ -1095,10 +1217,27 @@ private void setBreakTimer(boolean logoutBreak) { persistBreakState(logoutBreak); } + private void recordBreakActivated() { + breaksActivatedCount++; + log.info("[BreakHandlerV2] Breaks activated this run: {}", breaksActivatedCount); + } + + public long getScriptActiveSeconds() { + if (scriptStartedAt == null) { + return 0; + } + return Math.max(0, scriptStartedAt.until(Instant.now(), ChronoUnit.SECONDS)); + } + + public int getBreaksActivatedCount() { + return breaksActivatedCount; + } + private void sendBreakStartedNotification(boolean logoutBreak) { + String breakType = currentBreakIsLong ? "Long break" : "Break"; String message = logoutBreak - ? "Type: Logout break\nDuration: " + (currentBreakDuration / 60000) + " minutes" - : "Duration: " + (currentBreakDuration / 60000) + " minutes (no logout)"; + ? "Type: " + breakType + " (logout)\nDuration: " + (currentBreakDuration / 60000) + " minutes" + : "Type: " + breakType + "\nDuration: " + (currentBreakDuration / 60000) + " minutes (no logout)"; sendDiscordNotification("Break Started", message); } From 468c655bc07083936ba9368602f1ced2b46c6ca1 Mon Sep 17 00:00:00 2001 From: MuffinKid23 <143044121+MuffinKid23@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:06:45 -0600 Subject: [PATCH 2/2] Add optional mega breaks to Break Handler V2 --- .../breakhandlerv2/BreakHandlerV2Config.java | 59 ++++++ .../breakhandlerv2/BreakHandlerV2Overlay.java | 16 +- .../breakhandlerv2/BreakHandlerV2Script.java | 184 ++++++++++++++---- 3 files changed, 220 insertions(+), 39 deletions(-) diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java index 01888445868..0e75f3bebd2 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java @@ -125,6 +125,65 @@ default int maxLongBreakDuration() { return 10; } + @ConfigItem( + keyName = "enableMegaBreaks", + name = "Enable Mega Breaks", + description = "Enable a third independent mega-break timer", + position = 9, + section = breakTimingSettings + ) + default boolean enableMegaBreaks() { + return false; + } + + @ConfigItem( + keyName = "minMegaBreakInterval", + name = "Min Mega Break Interval (minutes)", + description = "Minimum time to play before a mega break can trigger", + position = 10, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minMegaBreakInterval() { + return 120; + } + + @ConfigItem( + keyName = "maxMegaBreakInterval", + name = "Max Mega Break Interval (minutes)", + description = "Maximum time to play before a mega break can trigger", + position = 11, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxMegaBreakInterval() { + return 180; + } + + @ConfigItem( + keyName = "minMegaBreakDuration", + name = "Min Mega Break Duration (minutes)", + description = "Minimum mega-break duration", + position = 12, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minMegaBreakDuration() { + return 20; + } + + @ConfigItem( + keyName = "maxMegaBreakDuration", + name = "Max Mega Break Duration (minutes)", + description = "Maximum mega-break duration", + position = 13, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxMegaBreakDuration() { + return 30; + } + // ========== BREAK BEHAVIOR SECTION ========== @ConfigSection( name = "Break Behavior", diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java index 8b6e5cee760..0a2777c40d6 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java @@ -108,14 +108,22 @@ public Dimension render(Graphics2D graphics) { .rightColor(Color.ORANGE) .build()); } + long secondsUntilMegaBreak = script.getTimeUntilMegaBreak(); + if (config.enableMegaBreaks() && secondsUntilMegaBreak >= 0) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Mega break:") + .right(formatDuration(secondsUntilMegaBreak)) + .rightColor(Color.MAGENTA) + .build()); + } } else if (BreakHandlerV2State.isBreakActive()) { long secondsRemaining = script.getBreakTimeRemaining(); if (secondsRemaining >= 0) { String timeStr = formatDuration(secondsRemaining); panelComponent.getChildren().add(LineComponent.builder() - .left(script.isCurrentBreakLong() ? "Long break ends:" : "Break ends:") + .left(script.isCurrentBreakMega() ? "Mega break ends:" : script.isCurrentBreakLong() ? "Long break ends:" : "Break ends:") .right(timeStr) - .rightColor(Color.ORANGE) + .rightColor(script.isCurrentBreakMega() ? Color.MAGENTA : Color.ORANGE) .build()); } } @@ -183,7 +191,9 @@ public Dimension render(Graphics2D graphics) { // Break configuration panelComponent.getChildren().add(LineComponent.builder() .left("Break type:") - .right((config.logoutOnBreak() ? "Logout" : "Stay logged in") + (config.enableLongBreaks() ? " + long" : "")) + .right((config.logoutOnBreak() ? "Logout" : "Stay logged in") + + (config.enableLongBreaks() ? " + long" : "") + + (config.enableMegaBreaks() ? " + mega" : "")) .rightColor(Color.LIGHT_GRAY) .build()); diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java index e22b9df2914..c60d30d3d7e 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java @@ -46,6 +46,7 @@ public BreakHandlerV2Script() { // Timing variables (volatile for thread visibility from overlay/UI threads) private volatile Instant nextBreakTime; private volatile Instant nextLongBreakTime; + private volatile Instant nextMegaBreakTime; private volatile Instant breakEndTime; private volatile Instant loginAttemptTime; @@ -72,7 +73,9 @@ public BreakHandlerV2Script() { private long currentBreakDuration = 0; private boolean logoutBreakActive = false; private boolean longBreakDue = false; + private boolean megaBreakDue = false; private boolean currentBreakIsLong = false; + private boolean currentBreakIsMega = false; // Login retry backoff constants private static final int MAX_LOGIN_ATTEMPTS = 10; @@ -83,7 +86,7 @@ public BreakHandlerV2Script() { private static final int MAX_SAFETY_CHECK_ATTEMPTS = 60; private static final int SAFETY_CHECK_DELAY_MS = 5000; // 5 seconds between checks - public static String version = "2.0.5"; + public static String version = "2.0.6"; /** * Run the break handler script @@ -95,7 +98,7 @@ public boolean run(BreakHandlerV2Config config) { BreakHandlerV2State.setState(BreakHandlerV2State.WAITING_FOR_BREAK); // Initialize next break time immediately to prevent null values in overlay - scheduleNextBreak(true); + scheduleNextBreak(true, true); log.info("[BreakHandlerV2] Initial break scheduled for {}", nextBreakTime); // Load active profile loadActiveProfile(); @@ -114,7 +117,7 @@ public boolean run(BreakHandlerV2Config config) { // Detect unexpected logout while waiting for break detectUnexpectedLogout(); enforceLogoutDuringActiveBreak(); - mergeLongBreakIntoActiveBreakIfDue(); + mergeSecondaryBreaksIntoActiveBreakIfDue(); updateWindowTitle(); // Main state machine @@ -199,8 +202,22 @@ private void handleWaitingForBreak() { } // Check if it's time for a break (only when play schedule is disabled) + if (config.enableMegaBreaks() && nextMegaBreakTime != null && !Instant.now().isBefore(nextMegaBreakTime)) { + megaBreakDue = true; + nextMegaBreakTime = null; + longBreakDue = config.enableLongBreaks() && nextLongBreakTime != null && !Instant.now().isBefore(nextLongBreakTime); + if (longBreakDue) { + nextLongBreakTime = null; + } + log.info("[BreakHandlerV2] Mega break time reached, requesting mega break"); + transitionToState(BreakHandlerV2State.BREAK_REQUESTED); + return; + } + if (config.enableLongBreaks() && nextLongBreakTime != null && !Instant.now().isBefore(nextLongBreakTime)) { longBreakDue = true; + nextLongBreakTime = null; + megaBreakDue = false; log.info("[BreakHandlerV2] Long break time reached, requesting long break"); transitionToState(BreakHandlerV2State.BREAK_REQUESTED); return; @@ -208,6 +225,7 @@ private void handleWaitingForBreak() { if (nextBreakTime != null && Instant.now().isAfter(nextBreakTime)) { longBreakDue = false; + megaBreakDue = false; log.info("[BreakHandlerV2] Break time reached, requesting break"); transitionToState(BreakHandlerV2State.BREAK_REQUESTED); } @@ -486,6 +504,7 @@ private void handleBreakEnding() { startConfiguredPluginIfNeeded(); boolean completedLongBreak = currentBreakIsLong || longBreakDue; + boolean completedMegaBreak = currentBreakIsMega || megaBreakDue; // Reset variables breakEndTime = null; @@ -502,7 +521,7 @@ private void handleBreakEnding() { Microbot.pauseAllScripts.set(false); // Schedule next break. A normal break must not reset the pending long-break timer. - scheduleNextBreak(completedLongBreak); + scheduleNextBreak(completedLongBreak, completedMegaBreak); String breakMessage = nextBreakTime != null ? "Next break scheduled for " + nextBreakTime @@ -597,32 +616,67 @@ private void enforceLogoutDuringActiveBreak() { } /** - * If the long-break timer expires while a normal break is already active, fold it into the - * current break instead of dropping it or starting a second break cycle. + * If a secondary break timer expires while another break is active, fold it into the + * current break instead of dropping it or starting a competing break cycle. */ - private void mergeLongBreakIntoActiveBreakIfDue() { - if (config == null || !config.enableLongBreaks() || nextLongBreakTime == null || breakEndTime == null) { + private void mergeSecondaryBreaksIntoActiveBreakIfDue() { + if (config == null || breakEndTime == null || !BreakHandlerV2State.isBreakActive()) { return; } - if (!BreakHandlerV2State.isBreakActive() || currentBreakIsLong || Instant.now().isBefore(nextLongBreakTime)) { + Instant now = Instant.now(); + boolean merged = false; + StringBuilder mergedTypes = new StringBuilder(); + + if (config.enableLongBreaks() && nextLongBreakTime != null && !now.isBefore(nextLongBreakTime)) { + longBreakDue = true; + nextLongBreakTime = null; + mergedTypes.append("long"); + + if (!currentBreakIsLong && !currentBreakIsMega) { + currentBreakIsLong = true; + long longBreakDuration = calculateLongBreakDuration(); + Instant mergedBreakEndTime = now.plus(longBreakDuration, ChronoUnit.MILLIS); + if (mergedBreakEndTime.isAfter(breakEndTime)) { + breakEndTime = mergedBreakEndTime; + currentBreakDuration = Math.max(0, now.until(breakEndTime, ChronoUnit.MILLIS)); + merged = true; + } + } + } + + if (config.enableMegaBreaks() && nextMegaBreakTime != null && !now.isBefore(nextMegaBreakTime)) { + megaBreakDue = true; + nextMegaBreakTime = null; + if (mergedTypes.length() > 0) { + mergedTypes.append(" + "); + } + mergedTypes.append("mega"); + + if (!currentBreakIsMega) { + currentBreakIsMega = true; + currentBreakIsLong = false; + long megaBreakDuration = calculateMegaBreakDuration(); + Instant mergedBreakEndTime = now.plus(megaBreakDuration, ChronoUnit.MILLIS); + if (mergedBreakEndTime.isAfter(breakEndTime)) { + breakEndTime = mergedBreakEndTime; + currentBreakDuration = Math.max(0, now.until(breakEndTime, ChronoUnit.MILLIS)); + merged = true; + } + } + } + + if (mergedTypes.length() == 0) { return; } - longBreakDue = true; - currentBreakIsLong = true; - long longBreakDuration = calculateLongBreakDuration(); - Instant mergedBreakEndTime = Instant.now().plus(longBreakDuration, ChronoUnit.MILLIS); - if (mergedBreakEndTime.isAfter(breakEndTime)) { - breakEndTime = mergedBreakEndTime; - currentBreakDuration = Math.max(0, Instant.now().until(breakEndTime, ChronoUnit.MILLIS)); + if (merged) { persistBreakState(logoutBreakActive); } - nextLongBreakTime = null; - log.info("[BreakHandlerV2] Long break merged into active break; break now ends at {}", breakEndTime); - sendDiscordNotification("Long Break Merged", - "A long break became due during an active break.\nNew remaining duration: " + + log.info("[BreakHandlerV2] {} break timer(s) merged into active break; break now ends at {}", mergedTypes, breakEndTime); + sendDiscordNotification("Break Timer Merged", + "A " + mergedTypes + " break became due during an active break.\nNew remaining duration: " + Math.max(0, Instant.now().until(breakEndTime, ChronoUnit.MINUTES)) + " minutes"); } @@ -751,19 +805,23 @@ private Integer resolveProfilePreferredWorld(WorldRegion region) { /** * Schedule the next break */ - private void scheduleNextBreak(boolean rescheduleLongBreak) { + private void scheduleNextBreak(boolean rescheduleLongBreak, boolean rescheduleMegaBreak) { longBreakDue = false; + megaBreakDue = false; currentBreakIsLong = false; + currentBreakIsMega = false; if (config.usePlaySchedule()) { if (!config.playSchedule().isOutsideSchedule()) { Duration timeUntilEnd = config.playSchedule().timeUntilScheduleEnds(); nextBreakTime = Instant.now().plus(timeUntilEnd); nextLongBreakTime = null; + nextMegaBreakTime = null; log.info("[BreakHandlerV2] Play schedule active ({}), break when schedule ends in {} minutes", config.playSchedule().name(), timeUntilEnd.toMinutes()); } else { nextBreakTime = null; nextLongBreakTime = null; + nextMegaBreakTime = null; log.info("[BreakHandlerV2] Outside play schedule ({}), currently on break", config.playSchedule().name()); } @@ -782,18 +840,31 @@ private void scheduleNextBreak(boolean rescheduleLongBreak) { if (config.enableLongBreaks()) { if (!rescheduleLongBreak && nextLongBreakTime != null) { log.info("[BreakHandlerV2] Keeping pending long break for {}", nextLongBreakTime); - updatePluginStopLeadTime(); - return; + } else { + int minLongMinutes = Math.min(config.minLongBreakInterval(), config.maxLongBreakInterval()); + int maxLongMinutes = Math.max(config.minLongBreakInterval(), config.maxLongBreakInterval()); + int longBreakMinutes = Rs2Random.between(minLongMinutes, maxLongMinutes); + nextLongBreakTime = Instant.now().plus(longBreakMinutes, ChronoUnit.MINUTES); + log.info("[BreakHandlerV2] Next long break in {} minutes", longBreakMinutes); } - int minLongMinutes = Math.min(config.minLongBreakInterval(), config.maxLongBreakInterval()); - int maxLongMinutes = Math.max(config.minLongBreakInterval(), config.maxLongBreakInterval()); - int longBreakMinutes = Rs2Random.between(minLongMinutes, maxLongMinutes); - nextLongBreakTime = Instant.now().plus(longBreakMinutes, ChronoUnit.MINUTES); - log.info("[BreakHandlerV2] Next long break in {} minutes", longBreakMinutes); } else { nextLongBreakTime = null; } + if (config.enableMegaBreaks()) { + if (!rescheduleMegaBreak && nextMegaBreakTime != null) { + log.info("[BreakHandlerV2] Keeping pending mega break for {}", nextMegaBreakTime); + } else { + int minMegaMinutes = Math.min(config.minMegaBreakInterval(), config.maxMegaBreakInterval()); + int maxMegaMinutes = Math.max(config.minMegaBreakInterval(), config.maxMegaBreakInterval()); + int megaBreakMinutes = Rs2Random.between(minMegaMinutes, maxMegaMinutes); + nextMegaBreakTime = Instant.now().plus(megaBreakMinutes, ChronoUnit.MINUTES); + log.info("[BreakHandlerV2] Next mega break in {} minutes", megaBreakMinutes); + } + } else { + nextMegaBreakTime = null; + } + updatePluginStopLeadTime(); } @@ -841,6 +912,7 @@ private long calculateBreakDuration() { // If outside play schedule, break until next play time if (isOutsidePlaySchedule()) { currentBreakIsLong = false; + currentBreakIsMega = false; Duration timeUntilPlaySchedule = config.playSchedule().timeUntilNextSchedule(); long durationMs = timeUntilPlaySchedule.toMillis(); log.info("[BreakHandlerV2] Play schedule break duration: {} minutes (until next scheduled play time)", @@ -850,18 +922,25 @@ private long calculateBreakDuration() { int minMinutes; int maxMinutes; - if (longBreakDue && config.enableLongBreaks()) { + if (megaBreakDue && config.enableMegaBreaks()) { + currentBreakIsMega = true; + currentBreakIsLong = false; + minMinutes = Math.min(config.minMegaBreakDuration(), config.maxMegaBreakDuration()); + maxMinutes = Math.max(config.minMegaBreakDuration(), config.maxMegaBreakDuration()); + } else if (longBreakDue && config.enableLongBreaks()) { currentBreakIsLong = true; + currentBreakIsMega = false; minMinutes = Math.min(config.minLongBreakDuration(), config.maxLongBreakDuration()); maxMinutes = Math.max(config.minLongBreakDuration(), config.maxLongBreakDuration()); } else { currentBreakIsLong = false; + currentBreakIsMega = false; minMinutes = Math.min(config.minBreakDuration(), config.maxBreakDuration()); maxMinutes = Math.max(config.minBreakDuration(), config.maxBreakDuration()); } int breakMinutes = Rs2Random.between(minMinutes, maxMinutes); - log.info("[BreakHandlerV2] {} duration: {} minutes", currentBreakIsLong ? "Long break" : "Break", breakMinutes); + log.info("[BreakHandlerV2] {} duration: {} minutes", getCurrentBreakTypeName(), breakMinutes); return breakMinutes * 60000L; // Convert to milliseconds } @@ -874,6 +953,14 @@ private long calculateLongBreakDuration() { return breakMinutes * 60000L; } + private long calculateMegaBreakDuration() { + int minMinutes = Math.min(config.minMegaBreakDuration(), config.maxMegaBreakDuration()); + int maxMinutes = Math.max(config.minMegaBreakDuration(), config.maxMegaBreakDuration()); + int breakMinutes = Rs2Random.between(minMinutes, maxMinutes); + log.info("[BreakHandlerV2] Merged mega break duration: {} minutes", breakMinutes); + return breakMinutes * 60000L; + } + /** * Stops a configured Microbot plugin once per break cycle. */ @@ -1039,6 +1126,13 @@ public long getTimeUntilLongBreak() { return Instant.now().until(nextLongBreakTime, ChronoUnit.SECONDS); } + public long getTimeUntilMegaBreak() { + if (nextMegaBreakTime == null) { + return -1; + } + return Instant.now().until(nextMegaBreakTime, ChronoUnit.SECONDS); + } + public long getTimeUntilNextScheduledBreak() { Instant nextScheduledBreak = getNextScheduledBreakTime(); if (nextScheduledBreak == null) { @@ -1051,14 +1145,29 @@ public boolean isCurrentBreakLong() { return currentBreakIsLong; } + public boolean isCurrentBreakMega() { + return currentBreakIsMega; + } + + public String getCurrentBreakTypeName() { + if (currentBreakIsMega) { + return "Mega break"; + } + if (currentBreakIsLong) { + return "Long break"; + } + return "Break"; + } + private Instant getNextScheduledBreakTime() { - if (nextBreakTime == null) { - return nextLongBreakTime; + Instant nextScheduledBreak = nextBreakTime; + if (nextLongBreakTime != null && (nextScheduledBreak == null || nextLongBreakTime.isBefore(nextScheduledBreak))) { + nextScheduledBreak = nextLongBreakTime; } - if (nextLongBreakTime == null) { - return nextBreakTime; + if (nextMegaBreakTime != null && (nextScheduledBreak == null || nextMegaBreakTime.isBefore(nextScheduledBreak))) { + nextScheduledBreak = nextMegaBreakTime; } - return nextLongBreakTime.isBefore(nextBreakTime) ? nextLongBreakTime : nextBreakTime; + return nextScheduledBreak; } /** @@ -1090,6 +1199,7 @@ public void shutdown() { // Clear timers nextBreakTime = null; nextLongBreakTime = null; + nextMegaBreakTime = null; breakEndTime = null; loginAttemptTime = null; @@ -1099,7 +1209,9 @@ public void shutdown() { safetyCheckAttempts = 0; logoutBreakActive = false; longBreakDue = false; + megaBreakDue = false; currentBreakIsLong = false; + currentBreakIsMega = false; pluginStopTriggered = false; pluginRestartPending = false; stoppedPluginClassName = PluginStopOption.NONE_VALUE; @@ -1234,7 +1346,7 @@ public int getBreaksActivatedCount() { } private void sendBreakStartedNotification(boolean logoutBreak) { - String breakType = currentBreakIsLong ? "Long break" : "Break"; + String breakType = getCurrentBreakTypeName(); String message = logoutBreak ? "Type: " + breakType + " (logout)\nDuration: " + (currentBreakDuration / 60000) + " minutes" : "Type: " + breakType + "\nDuration: " + (currentBreakDuration / 60000) + " minutes (no logout)";