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 new file mode 100644 index 00000000000..0d7f4986897 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Config.java @@ -0,0 +1,262 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import net.runelite.client.config.*; +import net.runelite.client.plugins.microbot.util.world.RegionPreference; +import net.runelite.client.plugins.microbot.util.world.WorldSelectionMode; + +@ConfigGroup(BreakHandlerV2Config.configGroup) +public interface BreakHandlerV2Config extends Config { + String configGroup = "break-handler-v2"; + + // ========== BREAK TIMING SECTION ========== + @ConfigSection( + name = "Break Timing", + description = "Configure break timing and duration", + position = 0 + ) + String breakTimingSettings = "breakTimingSettings"; + + @ConfigItem( + keyName = "minPlaytime", + name = "Min Playtime (minutes)", + description = "Minimum time to play before taking a break", + position = 0, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minPlaytime() { + return 45; + } + + @ConfigItem( + keyName = "maxPlaytime", + name = "Max Playtime (minutes)", + description = "Maximum time to play before taking a break", + position = 1, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxPlaytime() { + return 90; + } + + @ConfigItem( + keyName = "minBreakDuration", + name = "Min Break Duration (minutes)", + description = "Minimum break duration", + position = 2, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int minBreakDuration() { + return 5; + } + + @ConfigItem( + keyName = "maxBreakDuration", + name = "Max Break Duration (minutes)", + description = "Maximum break duration", + position = 3, + section = breakTimingSettings + ) + @Range(min = 1, max = 600) + default int maxBreakDuration() { + return 15; + } + + // ========== BREAK BEHAVIOR SECTION ========== + @ConfigSection( + name = "Break Behavior", + description = "Configure how breaks work", + position = 1 + ) + String breakBehaviorOptions = "breakBehaviorOptions"; + + @ConfigItem( + keyName = "logoutOnBreak", + name = "Logout on Break", + description = "Logout when taking a break", + position = 0, + section = breakBehaviorOptions + ) + default boolean logoutOnBreak() { + return true; + } + + @ConfigItem( + keyName = "safetyCheck", + name = "Safety Check", + description = "Wait until not in combat/interaction before breaking (tries up to 12 times over ~60 seconds)", + position = 1, + section = breakBehaviorOptions + ) + default boolean safetyCheck() { + return true; + } + + // ========== LOGIN & WORLD SECTION ========== + @ConfigSection( + name = "Login & World Selection", + description = "Configure login and world selection behavior", + position = 2 + ) + String loginWorldSettings = "loginWorldSettings"; + + @ConfigItem( + keyName = "autoLogin", + name = "Auto Login", + description = "Automatically log back in after break using profile data", + position = 0, + section = loginWorldSettings + ) + default boolean autoLogin() { + return true; + } + + @ConfigItem( + keyName = "worldSelectionMode", + name = "World Selection Mode", + description = "How to select worlds when logging back in", + position = 1, + section = loginWorldSettings + ) + default WorldSelectionMode worldSelectionMode() { + return WorldSelectionMode.CURRENT_PREFERRED_WORLD; + } + + @ConfigItem( + keyName = "regionPreference", + name = "Region Preference", + description = "Preferred region for world selection", + position = 2, + section = loginWorldSettings + ) + default RegionPreference regionPreference() { + return RegionPreference.ANY_REGION; + } + + @ConfigItem( + keyName = "avoidEmptyWorlds", + name = "Avoid Empty Worlds", + description = "Avoid worlds with very few players", + position = 3, + section = loginWorldSettings + ) + default boolean avoidEmptyWorlds() { + return true; + } + + @ConfigItem( + keyName = "avoidOvercrowdedWorlds", + name = "Avoid Crowded Worlds", + description = "Avoid worlds with too many players", + position = 4, + section = loginWorldSettings + ) + default boolean avoidOvercrowdedWorlds() { + return true; + } + + // ========== PROFILE SETTINGS SECTION ========== + @ConfigSection( + name = "Profile Settings", + description = "Profile and account management", + position = 3 + ) + String profileSettings = "profileSettings"; + + @ConfigItem( + keyName = "useActiveProfile", + name = "Use Active Profile", + description = "Use the currently active profile for login credentials", + position = 0, + section = profileSettings + ) + default boolean useActiveProfile() { + return true; + } + + @ConfigItem( + keyName = "respectMemberStatus", + name = "Respect Member Status", + description = "Use profile's member status to select appropriate worlds", + position = 1, + section = profileSettings + ) + default boolean respectMemberStatus() { + return true; + } + + // ========== NOTIFICATIONS SECTION ========== + @ConfigSection( + name = "Notifications", + description = "Discord webhook and notification settings", + position = 4 + ) + String notificationSettings = "notificationSettings"; + + @ConfigItem( + keyName = "enableDiscordWebhook", + name = "Enable Discord Webhook", + description = "Send notifications via Discord webhook from profile", + position = 0, + section = notificationSettings + ) + default boolean enableDiscordWebhook() { + return false; + } + + @ConfigItem( + keyName = "notifyOnBreakStart", + name = "Notify on Break Start", + description = "Send notification when break starts", + position = 1, + section = notificationSettings + ) + default boolean notifyOnBreakStart() { + return true; + } + + @ConfigItem( + keyName = "notifyOnBreakEnd", + name = "Notify on Break End", + description = "Send notification when break ends", + position = 2, + section = notificationSettings + ) + default boolean notifyOnBreakEnd() { + return true; + } + + @ConfigItem( + keyName = "notifyOnLoginFail", + name = "Notify on Login Failure", + description = "Send notification when login fails", + position = 3, + section = notificationSettings + ) + default boolean notifyOnLoginFail() { + return true; + } + + // ========== OVERLAY SETTINGS ========== + @ConfigItem( + keyName = "hideOverlay", + name = "Hide Overlay", + description = "Hide the break handler overlay", + position = 0 + ) + default boolean hideOverlay() { + return false; + } + + @ConfigItem( + keyName = "showDetailedInfo", + name = "Show Detailed Info", + description = "Show detailed information in overlay", + position = 1 + ) + default boolean showDetailedInfo() { + return true; + } +} 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 new file mode 100644 index 00000000000..5b3e3840751 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Overlay.java @@ -0,0 +1,212 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import lombok.extern.slf4j.Slf4j; +import net.runelite.client.config.ConfigProfile; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.security.LoginManager; +import net.runelite.client.ui.overlay.OverlayPanel; +import net.runelite.client.ui.overlay.OverlayPosition; +import net.runelite.client.ui.overlay.components.LineComponent; +import net.runelite.client.ui.overlay.components.TitleComponent; + +import javax.inject.Inject; +import java.awt.*; +import java.time.Duration; + +/** + * Overlay for Break Handler V2 + * Displays break status, timers, and profile information + */ +@Slf4j +public class BreakHandlerV2Overlay extends OverlayPanel { + + private final BreakHandlerV2Config config; + private final BreakHandlerV2Script script; + + @Inject + public BreakHandlerV2Overlay(BreakHandlerV2Config config, BreakHandlerV2Script script) { + super(); + this.config = config; + this.script = script; + setPosition(OverlayPosition.TOP_LEFT); + panelComponent.setPreferredSize(new Dimension(300, 300)); + } + + @Override + public Dimension render(Graphics2D graphics) { + try { + // Check if overlay should be hidden + if (config.hideOverlay()) { + return null; + } + + + panelComponent.getChildren().clear(); + + // Title + panelComponent.getChildren().add(TitleComponent.builder() + .text("Break Handler V2") + .color(Color.CYAN) + .build()); + + // Current state + BreakHandlerV2State currentState = BreakHandlerV2State.getCurrentState(); + Color stateColor = getStateColor(currentState); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Status:") + .right(currentState.getDescription()) + .rightColor(stateColor) + .build()); + + // Version + panelComponent.getChildren().add(LineComponent.builder() + .left("Version:") + .right(BreakHandlerV2Script.version) + .rightColor(Color.GRAY) + .build()); + + // Time until break or break remaining + if (currentState == BreakHandlerV2State.WAITING_FOR_BREAK) { + long secondsUntilBreak = script.getTimeUntilBreak(); + //if (secondsUntilBreak >= 0) { + String timeStr = formatDuration(secondsUntilBreak); + panelComponent.getChildren().add(LineComponent.builder() + .left("Next break:") + .right(timeStr) + .rightColor(Color.GREEN) + .build()); + //} + } else if (BreakHandlerV2State.isBreakActive()) { + long secondsRemaining = script.getBreakTimeRemaining(); + if (secondsRemaining >= 0) { + String timeStr = formatDuration(secondsRemaining); + panelComponent.getChildren().add(LineComponent.builder() + .left("Break ends:") + .right(timeStr) + .rightColor(Color.ORANGE) + .build()); + } + } + + // Show detailed info if enabled + if (config.showDetailedInfo()) { + // Profile information + ConfigProfile profile = LoginManager.getActiveProfile(); + if (profile != null) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Profile:") + .right(profile.getName()) + .rightColor(Color.WHITE) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Member:") + .right(profile.isMember() ? "Yes" : "No") + .rightColor(profile.isMember() ? Color.YELLOW : Color.GRAY) + .build()); + } + + // World selection mode + panelComponent.getChildren().add(LineComponent.builder() + .left("World mode:") + .right(config.worldSelectionMode().name()) + .rightColor(Color.LIGHT_GRAY) + .build()); + + // Region preference + if (config.regionPreference() != null) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Region:") + .right(config.regionPreference().name()) + .rightColor(Color.LIGHT_GRAY) + .build()); + } + + // Current world if logged in + if (Microbot.isLoggedIn()) { + int currentWorld = Microbot.getClient().getWorld(); + panelComponent.getChildren().add(LineComponent.builder() + .left("Current world:") + .right(String.valueOf(currentWorld)) + .rightColor(Color.GREEN) + .build()); + } + + // Break configuration + panelComponent.getChildren().add(LineComponent.builder() + .left("Break type:") + .right(config.logoutOnBreak() ? "Logout" : "Stay logged in") + .rightColor(Color.LIGHT_GRAY) + .build()); + + // Auto-login status + panelComponent.getChildren().add(LineComponent.builder() + .left("Auto-login:") + .right(config.autoLogin() ? "Enabled" : "Disabled") + .rightColor(config.autoLogin() ? Color.GREEN : Color.RED) + .build()); + + // Discord notifications + if (config.enableDiscordWebhook()) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Discord:") + .right("Enabled") + .rightColor(Color.CYAN) + .build()); + } + } + + } catch (Exception ex) { + log.error("[BreakHandlerV2Overlay] Error rendering overlay", ex); + } + + return super.render(graphics); + } + + /** + * Get color for current state + */ + private Color getStateColor(BreakHandlerV2State state) { + switch (state) { + case WAITING_FOR_BREAK: + return Color.GREEN; + case BREAK_REQUESTED: + case INITIATING_BREAK: + return Color.YELLOW; + case LOGOUT_REQUESTED: + case LOGGED_OUT: + return Color.ORANGE; + case LOGIN_REQUESTED: + case LOGGING_IN: + return Color.CYAN; + case LOGIN_EXTENDED_SLEEP: + return Color.RED; + case BREAK_ENDING: + return Color.LIGHT_GRAY; + case PROFILE_SWITCHING: + return Color.MAGENTA; + default: + return Color.WHITE; + } + } + + /** + * Format duration in seconds to human-readable string + */ + private String formatDuration(long seconds) { + Duration duration = Duration.ofSeconds(seconds); + + long hours = duration.toHours(); + long minutes = duration.toMinutesPart(); + long secs = duration.toSecondsPart(); + + if (hours > 0) { + return String.format("%dh %dm %ds", hours, minutes, secs); + } else if (minutes > 0) { + return String.format("%dm %ds", minutes, secs); + } else { + return String.format("%ds", secs); + } + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Plugin.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Plugin.java new file mode 100644 index 00000000000..0d14ee15370 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Plugin.java @@ -0,0 +1,111 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import com.google.inject.Provides; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.events.GameStateChanged; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.ui.overlay.OverlayManager; + +import javax.inject.Inject; +import java.awt.*; + +/** + * Break Handler V2 Plugin + * Enhanced break handler with profile-based auto-login and intelligent world selection + * + * Features: + * - Automatic login using profile data (username, password, member status) + * - Intelligent world selection based on multiple modes: + * * Current/Preferred world + * * Random accessible world + * * Regional selection + * * Best population balance + * * Best ping performance + * - Configurable break timing (min/max playtime and break duration) + * - Optional in-game breaks (pause scripts without logout) + * - Safety checks (waits for combat/interaction to end) + * - Discord webhook notifications + * - Profile-aware member/F2P world selection + * - Retry mechanism with configurable attempts and delays + * - In-game overlay showing break status and timers + * + * @version 2.0.0 + */ +@PluginDescriptor( + name = PluginDescriptor.Default + "BreakHandler V2", + description = "Advanced break handler with profile-based login and world selection", + tags = {"break", "microbot", "breakhandler", "login", "world", "profile", "v2"}, + enabledByDefault = false +) +@Slf4j +public class BreakHandlerV2Plugin extends Plugin { + + @Inject + private BreakHandlerV2Config config; + + @Inject + private BreakHandlerV2Script script; + + @Inject + private OverlayManager overlayManager; + + @Inject + private BreakHandlerV2Overlay overlay; + + @Provides + BreakHandlerV2Config provideConfig(ConfigManager configManager) { + return configManager.getConfig(BreakHandlerV2Config.class); + } + + @Override + protected void startUp() throws AWTException { + log.info("[BreakHandlerV2] Plugin starting up"); + System.out.println("[DEBUG] BreakHandlerV2Plugin.startUp() with script instance hash: " + System.identityHashCode(script)); + + // Add in-game overlay + if (overlayManager != null && overlay != null) { + overlayManager.add(overlay); + log.info("[BreakHandlerV2] In-game overlay added"); + } + + // Start the script + if (script != null) { + script.run(config); + log.info("[BreakHandlerV2] Script started"); + } + + log.info("[BreakHandlerV2] Plugin started successfully (v{})", BreakHandlerV2Script.version); + } + + @Override + protected void shutDown() { + log.info("[BreakHandlerV2] Plugin shutting down"); + + // Shutdown script + if (script != null) { + script.shutdown(); + log.info("[BreakHandlerV2] Script stopped"); + } + + // Remove in-game overlay + if (overlayManager != null && overlay != null) { + overlayManager.remove(overlay); + log.info("[BreakHandlerV2] In-game overlay removed"); + } + + log.info("[BreakHandlerV2] Plugin shut down successfully"); + } + + /** + * Handle game state changes + * Can be used to detect unexpected logouts or other state changes + */ + @Subscribe + public void onGameStateChanged(GameStateChanged event) { + // Future implementation: detect unexpected logouts, bans, etc. + log.debug("[BreakHandlerV2] Game state changed: {}", event.getGameState()); + } +} 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 new file mode 100644 index 00000000000..613d3a618f4 --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2Script.java @@ -0,0 +1,703 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.GameState; +import net.runelite.client.config.ConfigProfile; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.discord.Rs2Discord; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.security.Login; +import net.runelite.client.plugins.microbot.util.security.LoginManager; +import net.runelite.client.plugins.microbot.util.world.Rs2WorldUtil; +import net.runelite.client.ui.ClientUI; +import net.runelite.http.api.worlds.WorldRegion; + +import javax.inject.Singleton; +import java.awt.Color; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.concurrent.TimeUnit; + +/** + * Break Handler V2 Script + * Enhanced break handler with profile-based login and intelligent world selection + * Version: 2.0.0 + */ +@Singleton +@Slf4j +public class BreakHandlerV2Script extends Script { + + // Instance tracking for debugging + private static int instanceCounter = 0; + private final int instanceId; + + public BreakHandlerV2Script() { + instanceId = ++instanceCounter; + System.out.println("[DEBUG] BreakHandlerV2Script instance #" + instanceId + " created. Hash: " + System.identityHashCode(this)); + } + + @Getter + private BreakHandlerV2Config config; + + // Timing variables (volatile for thread visibility from overlay/UI threads) + private volatile Instant nextBreakTime; + private volatile Instant breakEndTime; + private volatile Instant loginAttemptTime; + + // State tracking + private int loginRetryCount = 0; + private int safetyCheckAttempts = 0; + private int preBreakWorld = -1; + private ConfigProfile activeProfile; + private boolean unexpectedLogoutDetected = false; + private String originalWindowTitle = ""; + + // Break duration in milliseconds + private long currentBreakDuration = 0; + + // Login retry backoff constants + private static final int MAX_LOGIN_ATTEMPTS = 10; + private static final int INITIAL_FAST_RETRIES = 3; + private static final int BACKOFF_BASE_DELAY_MS = 30000; // 30 seconds + + // Safety check backoff constants + 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.0"; + + /** + * Run the break handler script + */ + public boolean run(BreakHandlerV2Config config) { + this.config = config; + BreakHandlerV2State.setState(BreakHandlerV2State.WAITING_FOR_BREAK); + + // Initialize next break time immediately to prevent null values in overlay + scheduleNextBreak(); + log.info("[BreakHandlerV2] Initial break scheduled for {}", nextBreakTime); + // Load active profile + loadActiveProfile(); + originalWindowTitle = ClientUI.getFrame().getTitle(); + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!super.run()) return; + + + + // Detect unexpected logout while waiting for break + detectUnexpectedLogout(); + updateWindowTitle(); + + // Main state machine + switch (BreakHandlerV2State.getCurrentState()) { + case WAITING_FOR_BREAK: + handleWaitingForBreak(); + break; + case BREAK_REQUESTED: + handleBreakRequested(); + break; + case INITIATING_BREAK: + handleInitiatingBreak(); + break; + case LOGOUT_REQUESTED: + handleLogoutRequested(); + break; + case LOGGED_OUT: + handleLoggedOut(); + break; + case LOGIN_REQUESTED: + handleLoginRequested(); + break; + case LOGGING_IN: + handleLoggingIn(); + break; + case LOGIN_EXTENDED_SLEEP: + handleLoginExtendedSleep(); + break; + case BREAK_ENDING: + handleBreakEnding(); + break; + case PROFILE_SWITCHING: + handleProfileSwitching(); + break; + } + + } catch (Exception ex) { + log.error("[BreakHandlerV2] Error in main loop", ex); + } + }, 0, 1000, TimeUnit.MILLISECONDS); + + return true; + } + + /** + * Load the active profile from config manager + */ + private void loadActiveProfile() { + if (config.useActiveProfile()) { + try { + activeProfile = Microbot.getConfigManager().getProfile(); + if (activeProfile != null) { + LoginManager.setActiveProfile(activeProfile); + } + } catch (Exception ex) { + log.error("[BreakHandlerV2] Failed to load active profile", ex); + } + } + } + + /** + * Handle WAITING_FOR_BREAK state + * Schedules next break and monitors for break time + */ + private void handleWaitingForBreak() { + // Check if it's time for a break + if (nextBreakTime != null && Instant.now().isAfter(nextBreakTime)) { + log.info("[BreakHandlerV2] Break time reached, requesting break"); + transitionToState(BreakHandlerV2State.BREAK_REQUESTED); + } + } + + /** + * Handle BREAK_REQUESTED state + * Initiates break based on configuration + */ + private void handleBreakRequested() { + // If breakEndTime is already set, we're in a no-logout break waiting for it to end + if (breakEndTime != null) { + // Check if break is over + if (Instant.now().isAfter(breakEndTime)) { + log.info("[BreakHandlerV2] No-logout break ended"); + Microbot.pauseAllScripts.set(false); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + } + return; + } + + // Store current world before break + if (Microbot.isLoggedIn()) { + preBreakWorld = Microbot.getClient().getWorld(); + } + + if (config.logoutOnBreak()) { + log.info("[BreakHandlerV2] Starting break (with logout)"); + transitionToState(BreakHandlerV2State.INITIATING_BREAK); + } else { + log.info("[BreakHandlerV2] Starting break (no logout - scripts paused)"); + currentBreakDuration = calculateBreakDuration(); + breakEndTime = Instant.now().plus(currentBreakDuration, ChronoUnit.MILLIS); + + sendDiscordNotification("Break Started", + "Duration: " + (currentBreakDuration / 60000) + " minutes (no logout)"); + + // Pause all scripts and stay in this state until break ends + Microbot.pauseAllScripts.set(true); + } + } + + /** + * Handle INITIATING_BREAK state + * Performs safety checks before logout with backoff retry + */ + private void handleInitiatingBreak() { + if (!Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Already logged out, transitioning to LOGGED_OUT"); + currentBreakDuration = calculateBreakDuration(); + breakEndTime = Instant.now().plus(currentBreakDuration, ChronoUnit.MILLIS); + safetyCheckAttempts = 0; // Reset counter + transitionToState(BreakHandlerV2State.LOGGED_OUT); + return; + } + + // Safety check if enabled + if (config.safetyCheck()) { + boolean isInCombat = Rs2Player.isInCombat(); + boolean isInteracting = Rs2Player.isInteracting(); + + if (isInCombat || isInteracting) { + safetyCheckAttempts++; + + if (safetyCheckAttempts >= MAX_SAFETY_CHECK_ATTEMPTS) { + log.warn("[BreakHandlerV2] Safety check max attempts ({}) reached, forcing break", + MAX_SAFETY_CHECK_ATTEMPTS); + + String unsafeReason = isInCombat && isInteracting ? "in combat and interacting" + : isInCombat ? "in combat" + : "interacting"; + + sendDiscordNotification("Safety Check Failed", + "Failed to achieve safe conditions after " + MAX_SAFETY_CHECK_ATTEMPTS + " attempts.\n" + + "Player still " + unsafeReason + ".\n" + + "Forcing break anyway."); + + safetyCheckAttempts = 0; // Reset counter + } else { + log.debug("[BreakHandlerV2] Waiting for safe conditions... (attempt {}/{})", + safetyCheckAttempts, MAX_SAFETY_CHECK_ATTEMPTS); + sleep(SAFETY_CHECK_DELAY_MS); + return; // Stay in this state and check again + } + } else { + // Safe conditions met + if (safetyCheckAttempts > 0) { + log.info("[BreakHandlerV2] Safe conditions achieved after {} attempts", safetyCheckAttempts); + } + safetyCheckAttempts = 0; // Reset counter + } + } + + // Proceed to logout + currentBreakDuration = calculateBreakDuration(); + breakEndTime = Instant.now().plus(currentBreakDuration, ChronoUnit.MILLIS); + + sendDiscordNotification("Break Started", + "Type: Logout break\nDuration: " + (currentBreakDuration / 60000) + " minutes"); + + transitionToState(BreakHandlerV2State.LOGOUT_REQUESTED); + } + + /** + * Handle LOGOUT_REQUESTED state + * Performs logout + */ + private void handleLogoutRequested() { + if (!Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Logout successful"); + transitionToState(BreakHandlerV2State.LOGGED_OUT); + return; + } + + try { + log.info("[BreakHandlerV2] Attempting logout..."); + Rs2Player.logout(); + sleep(2000, 3000); + } catch (Exception ex) { + log.error("[BreakHandlerV2] Error during logout", ex); + } + } + + /** + * Handle LOGGED_OUT state + * Waits for break duration to complete + */ + private void handleLoggedOut() { + if (breakEndTime == null) { + log.error("[BreakHandlerV2] Break end time not set, resetting"); + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + return; + } + + // Check if break is over + if (Instant.now().isAfter(breakEndTime)) { + if (config.autoLogin()) { + log.info("[BreakHandlerV2] Break ended, requesting login"); + loginRetryCount = 0; + transitionToState(BreakHandlerV2State.LOGIN_REQUESTED); + } else { + log.info("[BreakHandlerV2] Break ended, auto-login disabled"); + sendDiscordNotification("Break Ended", "Auto-login is disabled"); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + } + } + } + + /** + * Handle LOGIN_REQUESTED state + * Initiates login with profile data and world selection + * Uses exponential backoff: first 3 attempts are fast, then 30s incremental delays + */ + private void handleLoginRequested() { + // Check if already logged in + if (Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Already logged in"); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + return; + } + + // Check retry limit (max 10 attempts) + if (loginRetryCount >= MAX_LOGIN_ATTEMPTS) { + log.error("[BreakHandlerV2] Max login attempts ({}) reached", MAX_LOGIN_ATTEMPTS); + sendDiscordNotification("Login Failed", + "Max login attempts (" + MAX_LOGIN_ATTEMPTS + ") reached. Giving up."); + transitionToState(BreakHandlerV2State.LOGIN_EXTENDED_SLEEP); + return; + } + + // Validate profile + if (activeProfile == null) { + log.error("[BreakHandlerV2] No active profile available for login"); + sendDiscordNotification("Login Failed", "No active profile available"); + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + return; + } + + // Apply backoff delay if needed (after first 3 attempts) + if (loginRetryCount >= INITIAL_FAST_RETRIES) { + int backoffDelay = calculateLoginBackoffDelay(loginRetryCount); + log.info("[BreakHandlerV2] Applying backoff delay: {} seconds", backoffDelay / 1000); + sleep(backoffDelay); + } else if (loginRetryCount > 0) { + // Small delay between initial fast retries (5 seconds) + sleep(5000); + } + + // Select world based on configuration + int targetWorld = selectWorld(); + + if (targetWorld == -1) { + log.error("[BreakHandlerV2] Failed to select valid world"); + loginRetryCount++; + return; + } + + log.info("[BreakHandlerV2] Attempting login to world {} (attempt {}/{})", + targetWorld, loginRetryCount + 1, MAX_LOGIN_ATTEMPTS); + + // Perform login + boolean loginInitiated = LoginManager.login( + activeProfile.getName(), + activeProfile.getPassword(), + targetWorld + ); + + if (loginInitiated) { + loginRetryCount++; + loginAttemptTime = Instant.now(); + transitionToState(BreakHandlerV2State.LOGGING_IN); + } else { + log.error("[BreakHandlerV2] Failed to initiate login"); + loginRetryCount++; + } + } + + /** + * Calculate exponential backoff delay for login retries + * First 3 attempts: 5s delay + * After that: 30s, 60s, 90s, 120s, etc. + */ + private int calculateLoginBackoffDelay(int attemptCount) { + if (attemptCount < INITIAL_FAST_RETRIES) { + return 5000; // 5 seconds for initial retries + } + // Exponential backoff: 30s * (attempt - 3) + int backoffMultiplier = attemptCount - INITIAL_FAST_RETRIES + 1; + return BACKOFF_BASE_DELAY_MS * backoffMultiplier; + } + + /** + * Handle LOGGING_IN state + * Monitors login progress + */ + private void handleLoggingIn() { + // Check if logged in + if (Microbot.isLoggedIn()) { + log.info("[BreakHandlerV2] Login successful"); + sendDiscordNotification("Login Successful", + "Logged into world " + Microbot.getClient().getWorld()); + transitionToState(BreakHandlerV2State.BREAK_ENDING); + return; + } + + // Check for timeout (60 seconds) + if (loginAttemptTime != null && + Instant.now().isAfter(loginAttemptTime.plusSeconds(60))) { + log.warn("[BreakHandlerV2] Login timeout, retrying"); + transitionToState(BreakHandlerV2State.LOGIN_REQUESTED); + } + } + + /** + * Handle LOGIN_EXTENDED_SLEEP state + * Extended wait after multiple failed login attempts + */ + private void handleLoginExtendedSleep() { + log.info("[BreakHandlerV2] Entering extended sleep (5 minutes)"); + sleep(300000); // 5 minutes + loginRetryCount = 0; + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + } + + /** + * Handle BREAK_ENDING state + * Finalizes break and schedules next break + */ + private void handleBreakEnding() { + log.info("[BreakHandlerV2] Break cycle complete"); + + // Reset variables + breakEndTime = null; + loginAttemptTime = null; + loginRetryCount = 0; + safetyCheckAttempts = 0; + preBreakWorld = -1; + unexpectedLogoutDetected = false; + + // Unpause scripts + Microbot.pauseAllScripts.set(false); + + // Schedule next break + scheduleNextBreak(); + + sendDiscordNotification("Break Ended", + "Next break scheduled for " + nextBreakTime); + + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + } + + /** + * Handle PROFILE_SWITCHING state + * Future implementation for multi-account support + */ + private void handleProfileSwitching() { + // Placeholder for future profile switching functionality + log.info("[BreakHandlerV2] Profile switching not yet implemented"); + transitionToState(BreakHandlerV2State.WAITING_FOR_BREAK); + } + + /** + * Detect unexpected logout (kicked, disconnected, etc.) + * Handles case where player is logged out while waiting for a scheduled break + */ + private void detectUnexpectedLogout() { + // Only check if we're in WAITING_FOR_BREAK state and have a scheduled break + if (BreakHandlerV2State.getCurrentState() != BreakHandlerV2State.WAITING_FOR_BREAK) { + unexpectedLogoutDetected = false; // Reset flag when not in WAITING_FOR_BREAK + return; + } + + if (nextBreakTime == null) { + return; + } + + // Reset flag when player is logged in + if (Microbot.isLoggedIn()) { + unexpectedLogoutDetected = false; + return; + } + + // Check if player is logged out unexpectedly + if (!Microbot.isLoggedIn() && !unexpectedLogoutDetected) { + long secondsUntilBreak = Instant.now().until(nextBreakTime, ChronoUnit.SECONDS); + + if (secondsUntilBreak > 0) { + log.warn("[BreakHandlerV2] Unexpected logout detected with {}s until scheduled break", secondsUntilBreak); + unexpectedLogoutDetected = true; // Prevent repeated detections + + // Handle based on configuration + if (config.autoLogin()) { + log.info("[BreakHandlerV2] Auto-login enabled, attempting to log back in"); + loginRetryCount = 0; + transitionToState(BreakHandlerV2State.LOGIN_REQUESTED); + } else { + log.info("[BreakHandlerV2] Auto-login disabled, pausing break timer"); + // Keep the state as WAITING_FOR_BREAK but don't count time while logged out + // The timer will resume when player logs back in manually + sendDiscordNotification("Unexpected Logout", + "Player logged out with " + (secondsUntilBreak / 60) + " minutes until break.\nAuto-login is disabled."); + } + } + } + } + + /** + * Select world based on configuration and profile + */ + private int selectWorld() { + boolean membersOnly = config.respectMemberStatus() && + activeProfile != null && + activeProfile.isMember(); + + WorldRegion region = config.regionPreference().getWorldRegion(); + + int targetWorld = -1; + + switch (config.worldSelectionMode()) { + case CURRENT_PREFERRED_WORLD: + targetWorld = preBreakWorld != -1 ? preBreakWorld : + Rs2WorldUtil.getRandomAccessibleWorldFromRegion( + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case RANDOM_WORLD: + targetWorld = Rs2WorldUtil.getRandomAccessibleWorld( + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case REGIONAL_RANDOM: + targetWorld = Rs2WorldUtil.getRandomAccessibleWorldFromRegion( + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case BEST_POPULATION: + targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( + false, // by population, not ping + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + + case BEST_PING: + targetWorld = Rs2WorldUtil.getBestAccessibleWorldForLogin( + true, // by ping + region, + config.avoidEmptyWorlds(), + config.avoidOvercrowdedWorlds(), + membersOnly + ); + break; + } + + log.info("[BreakHandlerV2] Selected world: {} (mode: {}, members: {})", + targetWorld, config.worldSelectionMode(), membersOnly); + + return targetWorld; + } + + /** + * Schedule the next break + */ + private void scheduleNextBreak() { + int minMinutes = config.minPlaytime(); + int maxMinutes = config.maxPlaytime(); + + int playtimeMinutes = Rs2Random.between(minMinutes, maxMinutes); + nextBreakTime = Instant.now().plus(playtimeMinutes, ChronoUnit.MINUTES); + + log.info("[BreakHandlerV2] Next break in {} minutes", playtimeMinutes); + } + + /** + * Calculate break duration + */ + private long calculateBreakDuration() { + int minMinutes = config.minBreakDuration(); + int maxMinutes = config.maxBreakDuration(); + + int breakMinutes = Rs2Random.between(minMinutes, maxMinutes); + log.info("[BreakHandlerV2] Break duration: {} minutes", breakMinutes); + + return breakMinutes * 60000L; // Convert to milliseconds + } + + /** + * Transition to a new state + */ + private void transitionToState(BreakHandlerV2State newState) { + BreakHandlerV2State oldState = BreakHandlerV2State.getCurrentState(); + log.info("[BreakHandlerV2] State transition: {} -> {}", oldState, newState); + BreakHandlerV2State.setState(newState); + } + + /** + * Send Discord notification if enabled + */ + private void sendDiscordNotification(String title, String message) { + if (!config.enableDiscordWebhook()) { + return; + } + + if (activeProfile == null || activeProfile.getDiscordWebhookUrl() == null) { + return; + } + + try { + String playerName = activeProfile.getName(); + Rs2Discord.sendCustomNotification( + title, + message, + Rs2Discord.convertColorToInt(Color.CYAN), + playerName != null ? playerName : "Unknown", + "BreakHandler V2" + ); + } catch (Exception ex) { + log.error("[BreakHandlerV2] Failed to send Discord notification", ex); + } + } + + /** + * Get time until next break in seconds + */ + public long getTimeUntilBreak() { + if (nextBreakTime == null) { + return -1; + } + return Instant.now().until(nextBreakTime, ChronoUnit.SECONDS); + } + + /** + * Get time remaining in break in seconds + */ + public long getBreakTimeRemaining() { + if (breakEndTime == null) { + return -1; + } + return Instant.now().until(breakEndTime, ChronoUnit.SECONDS); + } + + @Override + public void shutdown() { + super.shutdown(); + log.info("[BreakHandlerV2] Shutting down"); + + // Reset state + BreakHandlerV2State.setState(BreakHandlerV2State.WAITING_FOR_BREAK); + Microbot.pauseAllScripts.set(false); + + // Clear timers + nextBreakTime = null; + breakEndTime = null; + loginAttemptTime = null; + + // Reset counters and flags + unexpectedLogoutDetected = false; + loginRetryCount = 0; + safetyCheckAttempts = 0; + } + + private void updateWindowTitle() { + BreakHandlerV2State state = BreakHandlerV2State.getCurrentState(); + + if (getBreakTimeRemaining() > 0) { + ClientUI.getFrame().setTitle(originalWindowTitle + " - " + state.toString() + ": " + + formatDuration(Duration.ofSeconds(Math.max(0, getBreakTimeRemaining())))); + } + } + + /** + * Formats a duration with header text. + */ + public static String formatDuration(Duration duration, String header) { + return String.format(header + " %s", formatDuration(duration)); + } + + /** + * Formats a duration into HH:MM:SS format. + */ + public static String formatDuration(Duration duration) { + if (duration == null || duration.isNegative() || duration.isZero()) { + return "00:00:00"; + } + long hours = duration.toHours(); + long minutes = duration.toMinutes() % 60; + long seconds = duration.getSeconds() % 60; + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2State.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2State.java new file mode 100644 index 00000000000..8fb6626d47c --- /dev/null +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/breakhandler/breakhandlerv2/BreakHandlerV2State.java @@ -0,0 +1,77 @@ +package net.runelite.client.plugins.microbot.breakhandler.breakhandlerv2; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.concurrent.atomic.AtomicReference; + +/** + * State machine for Break Handler V2 + * Manages all states and transitions for the break system + */ +@Getter +@RequiredArgsConstructor +public enum BreakHandlerV2State { + WAITING_FOR_BREAK("Waiting for break"), + BREAK_REQUESTED("Break requested"), + INITIATING_BREAK("Initiating break"), + LOGOUT_REQUESTED("Logout requested"), + LOGGED_OUT("Logged out"), + LOGIN_REQUESTED("Login requested"), + LOGGING_IN("Logging in"), + LOGIN_EXTENDED_SLEEP("Login extended sleep"), + BREAK_ENDING("Break ending"), + PROFILE_SWITCHING("Switching profile"); + + private final String description; + + private static final AtomicReference currentState = + new AtomicReference<>(WAITING_FOR_BREAK); + + /** + * Get the current state (thread-safe) + */ + public static BreakHandlerV2State getCurrentState() { + return currentState.get(); + } + + /** + * Set the current state (thread-safe) + */ + public static void setState(BreakHandlerV2State newState) { + currentState.set(newState); + } + + /** + * Check if break is currently active + */ + public static boolean isBreakActive() { + BreakHandlerV2State state = getCurrentState(); + return state == BREAK_REQUESTED || + state == INITIATING_BREAK || + state == LOGOUT_REQUESTED || + state == LOGGED_OUT || + state == LOGIN_REQUESTED || + state == LOGGING_IN || + state == LOGIN_EXTENDED_SLEEP || + state == BREAK_ENDING || + state == PROFILE_SWITCHING; + } + + /** + * Check if this is a lock state that prevents new breaks + */ + public boolean isLockState() { + return this == BREAK_REQUESTED || + this == INITIATING_BREAK || + this == LOGOUT_REQUESTED || + this == LOGGING_IN || + this == BREAK_ENDING || + this == PROFILE_SWITCHING; + } + + @Override + public String toString() { + return description; + } +} diff --git a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java index 105e4ccffaa..6a4b25eb926 100644 --- a/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java +++ b/runelite-client/src/main/java/net/runelite/client/plugins/microbot/util/tileobject/Rs2TileObjectModel.java @@ -140,19 +140,18 @@ public String getName() { return tileObject.getClickbox(); } - @Override - public @Nullable String getOpOverride(int index) - { - return tileObject.getOpOverride(index); - } - - @Override - public boolean isOpShown(int index) - { - return tileObject.isOpShown(index); - } - - public ObjectComposition getObjectComposition() { + + @Override + public @Nullable String getOpOverride(int index) { + return ""; + } + + @Override + public boolean isOpShown(int index) { + return false; + } + + public ObjectComposition getObjectComposition() { return Microbot.getClientThread().invoke(() -> { ObjectComposition composition = Microbot.getClient().getObjectDefinition(tileObject.getId()); if(composition.getImpostorIds() != null)