diff --git a/.github/agents/plugin-review.agent.md b/.github/agents/plugin-review.agent.md new file mode 100644 index 0000000000..fac000de93 --- /dev/null +++ b/.github/agents/plugin-review.agent.md @@ -0,0 +1,130 @@ +--- +description: "Audit a Microbot Hub plugin for best practices, completeness, and common issues. USE FOR: reviewing plugin code quality, checking descriptor fields, finding client thread violations, verifying version consistency, checking documentation, assessing antiban integration." +tools: [read, search] +--- + +You are a Plugin Review specialist for the Microbot Hub project. Your job is to audit plugin code and produce a structured report of findings. You are **read-only** — never edit files. + +## Skills Available + +Always load these skills for domain-specific review guidance: + +- **Walking & visibility**: Read `.github/skills/rs2walker/SKILL.md` to check for `walkFastCanvas` without visibility checks, inconsistent location APIs, missing `Rs2Camera.isTileOnScreen` before canvas clicks, and walker anti-patterns +- **Antiban patterns**: Read `.github/skills/antiban/SKILL.md` to assess antiban integration quality — Rs2Antiban setup, Gaussian delays, action cooldowns, mouse/camera randomization + +## What You Review + +For any plugin under `src/main/java/net/runelite/client/plugins/microbot//`, check the following categories in order. + +### 1. Plugin Descriptor Completeness (Critical) + +Read the `*Plugin.java` file and verify the `@PluginDescriptor` annotation has: + +**Required fields (fail if missing):** +- `name` — Should use a `PluginConstants` prefix (e.g., `PluginConstants.DEFAULT_PREFIX`, `PluginConstants.MOCROSOFT`) +- `version` — Must reference `PluginClassName.version`, NOT a hardcoded string +- `minClientVersion` — Must be present with a valid version string + +**Expected fields (warn if missing):** +- `authors` — Array of author names +- `description` — Brief plugin description +- `tags` — Array of search tags +- `iconUrl` — URL to icon image +- `cardUrl` — URL to card image +- `enabledByDefault` — Must be `PluginConstants.DEFAULT_ENABLED` (never `true`) +- `isExternal` — Must be `PluginConstants.IS_EXTERNAL` (never `false`) + +**Version field check:** +- Verify `static final String version = "X.Y.Z"` exists as a field in the plugin class +- Verify the descriptor's `version =` references it (e.g., `version = MyPlugin.version`) +- Verify the version follows semantic versioning (MAJOR.MINOR.PATCH) + +### 2. Client Thread Safety (High) + +Search the plugin's Script and other non-Plugin/non-Overlay files for these patterns that indicate client thread violations: + +**Unsafe outside `invoke()`:** +- `client.getWidget(` — Must use `Rs2Widget.getWidget()` or wrap in `Microbot.getClientThread().invoke()` +- `client.getVarbitValue(` — Must use `Microbot.getVarbitValue()` or wrap in `invoke()` +- `widget.isHidden()` — Must be on client thread +- `client.getLocalPlayer().getWorldView()` — Must be on client thread + +**Safe patterns (no issue):** +- Inside `@Subscribe` event handlers (these run on client thread) +- Inside `Microbot.getClientThread().invoke()` blocks +- Using utility wrappers: `Rs2Widget`, `Rs2Player`, `Rs2GameObject`, etc. + +### 3. Walking & Visibility (High) + +Search the script for walking and interaction patterns using the walker skill as reference: +- `walkFastCanvas` or `walkCanvas` called without a prior `Rs2Camera.isTileOnScreen` check — these fail silently if the tile is off-screen +- Inconsistent location APIs — mixing `Rs2Player.getWorldLocation()` with `Microbot.getClientThread().invoke(() -> client.getLocalPlayer().getWorldLocation())` in similar methods +- Missing camera turns before game object/NPC interactions when the target may be off-screen +- Randomized canvas-vs-walker distance thresholds (e.g., `Rs2Random.between(7,14)`) — should use a fixed threshold like `<= 12` + +### 4. Script Structure (Medium) + +Check the `*Script.java` file for: +- Extends `Script` class +- Has a proper main loop with `mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(...)` +- Uses `sleepUntilTrue()` or `sleep()` for waiting, not busy loops +- Handles exceptions in the main loop with try/catch + +### 5. Config & Injection (Medium) + +Check the `*Config.java` and `*Plugin.java` for: +- Config interface extends `Config` with `@ConfigGroup` annotation +- Plugin has a `@Provides` method for the config +- Uses `@Inject` for dependencies (config, overlays, scripts) +- Overlays registered in `startUp()` and unregistered in `shutDown()` + +### 6. Documentation (Low) + +Check for: +- `src/main/resources/net/runelite/client/plugins/microbot//docs/README.md` exists +- README has meaningful content (not just a title) +- Assets directory exists if `iconUrl`/`cardUrl` are specified + +### 7. Antiban Integration (Info) + +Check if the script has: +- `Rs2Antiban` setup (resetAntibanSettings, template, activity) +- Action cooldowns after interactions +- Gaussian delays instead of flat random +- Mouse/camera randomization +- Break handler integration + +Report whether antiban is integrated, partially integrated, or absent. + +## Output Format + +Produce a report with this structure: + +``` +## Plugin Review: +Version: | Authors: | Min Client: + +### 🔴 Critical Issues +- : + +### 🟡 Warnings +- : + +### 🟢 Good Practices +- + +### 📊 Antiban Status: +-
+ +### Summary + critical, warnings, info items +``` + +## Constraints + +- **DO NOT** edit any files — you are read-only +- **DO NOT** suggest code fixes inline — just report the issues +- **DO NOT** review build configuration or Gradle files +- **ONLY** review plugin source code and resources +- When checking client thread safety, account for `@Subscribe` handlers being safe +- Read the full Plugin, Script, Config, and Overlay files before making judgments diff --git a/.github/agents/script-updater.agent.md b/.github/agents/script-updater.agent.md new file mode 100644 index 0000000000..2183db2a7d --- /dev/null +++ b/.github/agents/script-updater.agent.md @@ -0,0 +1,82 @@ +--- +description: "Modify existing Microbot Hub plugin scripts — apply antiban patterns, fix bugs, add config options, update threading, improve behavior. USE FOR: updating script logic, adding Rs2Antiban integration, fixing client thread violations, adding config settings, refactoring script code." +tools: [read, edit, search, execute] +--- + +You are a Script Updater specialist for the Microbot Hub project. Your job is to modify existing plugin scripts based on user requests while following project conventions. + +## Before Making Changes + +1. **Read the full plugin** — Read all Java files in the plugin's directory before editing +2. **Understand the script's state machine** — Identify the states, transitions, and main loop structure +3. **Check existing antiban** — See if Rs2Antiban is already configured; don't duplicate +4. **Note the current version** — You MUST increment it after any code changes + +## Skills Available + +When performing specific tasks, load these skills for detailed guidance: + +- **Antiban patterns**: Read `.github/skills/antiban/SKILL.md` before adding human-like behavior, Rs2Antiban setup, mouse/camera randomization, Gaussian delays, or break systems +- **Walking & visibility**: Read `.github/skills/rs2walker/SKILL.md` before modifying any walking, navigation, or game object interaction code. Always verify `walkFastCanvas` has a visibility check, use consistent location APIs, and follow the canvas-vs-walker decision patterns +- **Building**: Read `.github/skills/build-plugin/SKILL.md` before compiling. Use `./gradlew build -PpluginList=` to verify changes compile + +## Common Tasks + +### Adding Antiban Integration + +1. Read the antiban skill file first +2. Add `configureAntibanSettings()` method using the appropriate setup template +3. Call it in the script's `run()` method before the main loop +4. Replace `sleep(random)` calls with `sleepGaussian(mean, stddev)` +5. Add `maybeNudgeMouse()` after game object interactions +6. Add `Rs2Antiban.actionCooldown()` gated behind `Rs2AntibanSettings.usePlayStyle` +7. Add camera movement scheduling in the main loop +8. Add overlay integration if the plugin has an overlay + +### Fixing Client Thread Violations + +Replace unsafe direct client calls with thread-safe alternatives: +- `client.getWidget(id)` → `Rs2Widget.getWidget(id)` or wrap in `Microbot.getClientThread().invoke()` +- `client.getVarbitValue(id)` → `Microbot.getVarbitValue(id)` or wrap in `invoke()` +- `client.getLocalPlayer().getWorldView()` → wrap in `invoke()` +- Location checks should use `Rs2Player.getWorldLocation()` consistently — don't mix it with `Microbot.getClientThread().invoke(() -> client.getLocalPlayer().getWorldLocation())` in similar methods + +Note: Code inside `@Subscribe` handlers is already on the client thread and does NOT need wrapping. + +### Fixing Walking & Visibility Issues + +Read the walker skill before modifying walking code. Common fixes: +- `walkFastCanvas` without a visibility check → add `Rs2Camera.isTileOnScreen()` guard or replace with `Rs2Walker.walkTo()` +- Missing camera turn before interacting with off-screen game objects/NPCs → add `Rs2Camera.turnTo()` +- Randomized canvas-vs-walker threshold → use a fixed threshold (`<= 12` tiles) + +### Adding Config Options + +1. Add the `@ConfigItem` to the Config interface with proper section, position, keyName +2. Add a `@ConfigSection` if a new logical group is needed +3. Reference the config value in the script via the injected config instance +4. Use descriptive HTML in `description` for complex settings + +### Updating Script Logic + +- Maintain the existing state machine pattern +- Use `changeState()` for state transitions (don't set `state` directly if a helper exists) +- Use `setLockState()` / unlock patterns if the script uses state locking +- Add proper logging with `Microbot.log()` + +## After Making Changes + +1. **Increment the version** — Update `static final String version` in the Plugin class (bump PATCH for fixes, MINOR for features, MAJOR for breaking changes) +2. **Build the plugin** — Run `./gradlew build -PpluginList=` to verify compilation +3. **Report changes** — Summarize what was modified and the new version number + +## Constraints + +- **DO NOT** create new plugins from scratch — only modify existing ones +- **DO NOT** change the plugin's `name` or `@PluginDescriptor` name field +- **DO NOT** remove existing functionality unless explicitly asked +- **DO NOT** add features, refactor code, or make improvements beyond what was requested +- **DO NOT** modify `PluginConstants.java` unless adding a new author prefix +- **ALWAYS** increment the version after any code change +- **ALWAYS** build after changes to verify compilation +- **ALWAYS** use Gaussian delays (`sleepGaussian`) instead of flat random delays when adding timing diff --git a/.github/skills/antiban/SKILL.md b/.github/skills/antiban/SKILL.md new file mode 100644 index 0000000000..177c1c57fe --- /dev/null +++ b/.github/skills/antiban/SKILL.md @@ -0,0 +1,404 @@ +--- +name: antiban +description: "Antiban integration patterns for Microbot Hub plugin scripts. USE FOR: adding human-like behavior to scripts, configuring Rs2Antiban and Rs2AntibanSettings, implementing mouse/camera randomization, break systems, action cooldowns, Gaussian delays, and natural timing. DO NOT USE FOR: creating new plugins from scratch, build issues, publishing." +--- + +# Antiban Skill + +Guidelines for implementing human-like antiban behavior in Microbot Hub scripts, inspired by the MKE Wintertodt AI plugin's approach. + +## Core Antiban System: Rs2Antiban + +The Microbot client provides `Rs2Antiban` and `Rs2AntibanSettings` as the foundation. Always configure these in your script's initialization method. + +### Setup Template + +```java +import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; +import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; +import net.runelite.client.plugins.microbot.util.antiban.enums.Activity; +import net.runelite.client.plugins.microbot.util.antiban.enums.ActivityIntensity; + +private void configureAntibanSettings() { + // 1. Reset to defaults first + Rs2Antiban.resetAntibanSettings(); + + // 2. Apply a setup template that matches your activity + // Available templates (pick the closest match): + // - applyFiremakingSetup() + // - applyMiningSetup() + // - applyFishingSetup() + // - applyCookingSetup() + // - applyWoodcuttingSetup() + // - applyGeneralBasicSetup() + Rs2Antiban.antibanSetupTemplates.applyFiremakingSetup(); + + // 3. Set the activity type for behavior profiling + Rs2Antiban.setActivity(Activity.GENERAL_FIREMAKING); + + // 4. Override specific settings for your script's needs + Rs2AntibanSettings.takeMicroBreaks = false; // Disable if using custom break system + Rs2AntibanSettings.microBreakChance = 0.0; + Rs2AntibanSettings.actionCooldownChance = 0.15; // 15% chance per action + Rs2AntibanSettings.moveMouseRandomly = true; + Rs2AntibanSettings.moveMouseRandomlyChance = 0.36; + Rs2AntibanSettings.moveMouseOffScreen = true; + Rs2AntibanSettings.moveMouseOffScreenChance = 0.38; + Rs2AntibanSettings.naturalMouse = true; + Rs2AntibanSettings.simulateMistakes = true; + Rs2AntibanSettings.simulateFatigue = true; + + // 5. Set activity intensity (LOW, MODERATE, HIGH) + Rs2Antiban.setActivityIntensity(ActivityIntensity.HIGH); +} +``` + +### Action Cooldown Integration + +After performing an action (clicking an object, using an item), conditionally trigger an action cooldown: + +```java +if (Rs2AntibanSettings.usePlayStyle) { + Rs2Antiban.actionCooldown(); +} +``` + +Check if a cooldown is active before proceeding with logic: + +```java +if (!Rs2AntibanSettings.actionCooldownActive) { + // Safe to perform next action +} +``` + +### Break Handler Integration + +Respect the break handler lock state in your main loop: + +```java +private boolean shouldPauseForBreaks() { + // Check Rs2Antiban cooldown + if (Rs2AntibanSettings.actionCooldownActive) { + return true; + } + // Check break handler + if (BreakHandlerScript.isLockState()) { + return true; + } + return false; +} +``` + +### Overlay Integration + +Show antiban status in your overlay: + +```java +if (config.showAntibanOverlay() + && Rs2AntibanSettings.antibanEnabled + && Rs2Antiban.getActivity() != null) { + Rs2Antiban.renderAntibanOverlayComponents(panelComponent); +} +``` + +--- + +## Human-Like Timing + +### Gaussian Delays (preferred over uniform random) + +Use `sleepGaussian(mean, stddev)` instead of flat `sleep(random)`. Gaussian distributions create a natural bell curve where most delays cluster near the mean with occasional outliers. + +```java +// Standard action delay: most delays around 1600ms, occasionally 1200-2000ms +sleepGaussian(1600, 400); + +// Quick reaction delay: most around 200ms +sleepGaussian(200, 150); + +// Post-action cooldown: centered on 1200ms +sleepGaussian(1200, 300); +``` + +### Variable Base Delays + +Add randomized delays after actions using both a base and a variation: + +```java +int baseDelay = 150; +int variation = 50 + random.nextInt(100); +sleepGaussian(baseDelay, variation); +``` + +--- + +## Mouse Behavior + +### Mouse Nudge After Actions + +After interacting with game objects, occasionally perform a small mouse wobble. This simulates natural hand jitter: + +```java +private void maybeNudgeMouse() { + // Probability: 18% base + Gaussian component (5-40% effective range) + int moveChance = (int) Math.round(18 + Math.abs(random.nextGaussian()) * 10); + moveChance = Math.max(5, Math.min(40, moveChance)); + if (random.nextInt(100) >= moveChance) return; + + Point start = Microbot.getMouse().getMousePosition(); + if (start == null) return; + + // First micro-movement: σ ≈ 20px, capped ±80px + int dx = (int) Math.max(-80, Math.min(80, random.nextGaussian() * 20)); + int dy = (int) Math.max(-80, Math.min(80, random.nextGaussian() * 20)); + Microbot.getMouse().move(start.x + dx, start.y + dy); + + // 50% chance of a follow-up wobble + if (random.nextBoolean()) { + sleepGaussian(30, 15); + int dx2 = (int) Math.max(-30, Math.min(30, random.nextGaussian() * 10)); + int dy2 = (int) Math.max(-30, Math.min(30, random.nextGaussian() * 10)); + Microbot.getMouse().move(start.x + dx + dx2, start.y + dy + dy2); + } +} +``` + +### Mouse Offscreen During Idle + +When the script is idle/waiting, move the mouse offscreen to simulate AFK: + +```java +// Move mouse offscreen with 85% probability of going to a natural edge position +Rs2Antiban.moveMouseOffScreen(85); +``` + +### Hover Before Action + +Before a game round or event starts, hover over the next interactive object slightly early (1-8 seconds before), like a real player anticipating: + +```java +// Calculate random hover time once per round +int hoverBeforeStartTime = Rs2Random.randomGaussian(1000, 8000); +hoverBeforeStartTime = Math.max(500, Math.min(8000, hoverBeforeStartTime)); + +// When time remaining reaches hover threshold +if (timeUntilStart > 0 && timeUntilStart <= hoverBeforeStartTime) { + Rs2GameObject.hoverOverObject(nextObject); +} +``` + +### Random Mouse Movements During Actions + +Periodically move the mouse to simulate checking the screen: + +```java +if (currentTime - lastMouseMovement > 15000 + random.nextInt(10000)) { + if (random.nextInt(100) < 15) { // 15% chance + if (currentBrazier != null && random.nextBoolean()) { + Rs2GameObject.hoverOverObject(currentBrazier); + } + lastMouseMovement = currentTime; + } +} +``` + +--- + +## Camera Behavior + +### Natural Camera Movements + +Simulate camera adjustments using keyboard holds (not instant teleports). Use Gaussian distributions for yaw/pitch deltas so small adjustments are common and large sweeps are rare: + +```java +private void performHumanLikeCameraMovement() { + // Yaw: μ=0°, σ=20° → 68% within ±20°, rarely >±60° + int yawDelta = (int) Math.round(random.nextGaussian() * 20); + yawDelta = Math.max(-75, Math.min(75, yawDelta)); + + int targetYaw = (Rs2Camera.getAngle() + yawDelta + 360) % 360; + + // Hold arrow key proportional to rotation needed + int degreesToMove = Math.abs(Rs2Camera.getAngleTo(targetYaw)); + int pressTime = 80 + (int) (degreesToMove * 2.2); + pressTime = (int) (pressTime * (0.8 + random.nextGaussian() * 0.1)); + + if (Rs2Camera.getAngleTo(targetYaw) > 0) { + Rs2Keyboard.keyHold(KeyEvent.VK_LEFT); + sleepGaussian(pressTime, (int) (pressTime * 0.15)); + Rs2Keyboard.keyRelease(KeyEvent.VK_LEFT); + } else { + Rs2Keyboard.keyHold(KeyEvent.VK_RIGHT); + sleepGaussian(pressTime, (int) (pressTime * 0.15)); + Rs2Keyboard.keyRelease(KeyEvent.VK_RIGHT); + } + + // 42% chance to also adjust pitch + if (random.nextInt(100) < 42) { + float deltaPct = (float) (random.nextGaussian() * 0.10); + float targetPitchPct = Math.max(0.30f, Math.min(0.90f, + Rs2Camera.cameraPitchPercentage() + deltaPct)); + Rs2Camera.adjustPitch(targetPitchPct); + } +} +``` + +### Camera Movement Scheduling + +Use a probability that increases over time since last movement, with a configurable minimum frequency: + +```java +private boolean shouldPerformCameraMovement(long currentTime) { + long cameraMovementMinDelay = config.cameraMovementFrequency() * 1000L; + if (cameraMovementMinDelay <= 0) return false; + + long timeSinceLastMove = currentTime - lastCameraMovement; + if (timeSinceLastMove < cameraMovementMinDelay) return false; + + // Probability increases over time + double baseChance = 0.001; + double timeMultiplier = Math.min(50.0, timeSinceLastMove / (double) cameraMovementMinDelay); + return random.nextDouble() <= baseChance * timeMultiplier; +} +``` + +--- + +## Custom Break System + +For scripts that need more control than the built-in break handler, implement a custom break manager with AFK and logout break types. + +### Break Types + +| Type | Duration | Behavior | +|------|----------|----------| +| AFK (Short) | 1-6 min | Mouse goes offscreen, player stays logged in | +| Logout (Long) | 5-40 min | Player logs out entirely | + +### Key Design Principles + +1. **Location-aware**: Only trigger breaks in safe locations (e.g., bank areas, safe spots) +2. **Configurable intervals**: Min/max break interval, min/max break duration +3. **Break type weighting**: Configurable chance for AFK vs logout (e.g., 60% AFK / 40% logout) +4. **Emergency override**: Always allow eating/healing even during breaks +5. **Reward-safe**: Never interrupt reward collection or banking flows +6. **Graceful timeout**: If a safe spot isn't reached within a time limit, walk to one + +### Config Pattern for Breaks + +```java +@ConfigSection(name = "Break System", position = 5) +String breakSection = "breaks"; + +@ConfigItem(section = breakSection) +default boolean enableCustomBreaks() { return true; } + +@ConfigItem(section = breakSection) +default int minBreakInterval() { return 20; } // minutes + +@ConfigItem(section = breakSection) +default int maxBreakInterval() { return 60; } // minutes + +@ConfigItem(section = breakSection) +default int logoutBreakChance() { return 40; } // % chance for logout vs AFK +``` + +### Config Pattern for Advanced Antiban + +```java +@ConfigSection(name = "Advanced Options", position = 6) +String advancedSection = "advanced"; + +@ConfigItem(section = advancedSection) +default boolean humanizedTiming() { return true; } + +@ConfigItem(section = advancedSection) +default boolean randomMouseMovements() { return true; } + +@ConfigItem(section = advancedSection, + description = "Higher = less frequent. 0 to disable.") +default int cameraMovementFrequency() { return 10; } // seconds + +@ConfigItem(section = advancedSection) +default boolean showAntibanOverlay() { return true; } +``` + +--- + +## Spam Clicking (Round Start Behavior) + +Simulate impatient clicking when a game event is about to begin — a common real-player behavior: + +```java +private boolean spamClickingActive = false; +private long spamClickEndTime = 0; +private long lastSpamClick = 0; + +// Activate spam clicking in a short window before event +private void startSpamClicking(GameObject target) { + spamClickingActive = true; + spamClickEndTime = System.currentTimeMillis() + Rs2Random.between(800, 2500); + spamClickTarget = target; +} + +// Execute during main loop — rapidly click target with variable intervals +private void executeSpamClicking() { + if (!spamClickingActive) return; + if (System.currentTimeMillis() > spamClickEndTime) { + spamClickingActive = false; + return; + } + long now = System.currentTimeMillis(); + if (now - lastSpamClick > 200 + random.nextInt(300)) { + Rs2GameObject.interact(spamClickTarget, "Use"); + lastSpamClick = now; + } +} +``` + +--- + +## Live Action Duration Tracking + +Track real action durations via XP drops and refine timing estimates using an exponential moving average (EMA). This makes the bot adapt to the player's actual performance: + +```java +// Track via @Subscribe or onStatChanged +public static void onStatChanged(StatChanged event) { + long now = System.currentTimeMillis(); + if (event.getSkill() == Skill.WOODCUTTING && lastWoodcuttingXpDropTime > 0) { + long duration = now - lastWoodcuttingXpDropTime; + if (duration > 600 && duration < 10000) { // sanity check + // EMA smoothing: 80% old, 20% new + avgChopMs = (avgChopMs * 0.8) + (duration * 0.2); + } + } + lastWoodcuttingXpDropTime = now; +} +``` + +--- + +## Checklist: Adding Antiban to a Script + +When updating or creating a script, apply these in order: + +1. **Initialize Rs2Antiban** — Call `configureAntibanSettings()` in your `run()` method before the main loop +2. **Gaussian delays** — Replace all `sleep(random)` calls with `sleepGaussian(mean, stddev)` +3. **Action cooldowns** — Add `Rs2Antiban.actionCooldown()` after object interactions (when `usePlayStyle` is enabled) +4. **Mouse nudge** — Call `maybeNudgeMouse()` after clicking game objects +5. **Camera movements** — Schedule `performHumanLikeCameraMovement()` in the main loop with time-based probability +6. **Mouse offscreen** — Move mouse offscreen during idle/waiting periods +7. **Hover pre-action** — Hover over the next target object before the action window opens +8. **Break system** — Implement or integrate breaks (AFK + logout) with safe-location checks +9. **Overlay** — Show antiban status via `Rs2Antiban.renderAntibanOverlayComponents()` +10. **Config options** — Expose humanized timing, mouse movements, camera frequency, and overlay toggle + +## Common Mistakes + +- **Using uniform `random.nextInt()` for delays** — Always prefer Gaussian distributions for natural bell-curve timing +- **Instant camera teleports** — Use keyboard holds (`keyHold`/`keyRelease`) to simulate real camera panning +- **Breaking during critical flows** — Never interrupt banking, reward collection, or combat healing +- **Forgetting to reset antiban on startup** — Always call `Rs2Antiban.resetAntibanSettings()` before applying templates +- **Hard-coded delay values** — Make timing configurable or at least add ±20% Gaussian variation +- **Not checking `actionCooldownActive`** — The script will double-act if you don't gate actions behind this check diff --git a/.github/skills/banking-equipment/SKILL.md b/.github/skills/banking-equipment/SKILL.md new file mode 100644 index 0000000000..56b8c7ee78 --- /dev/null +++ b/.github/skills/banking-equipment/SKILL.md @@ -0,0 +1,186 @@ +--- +name: banking-equipment +description: "Banking, equipment management, and inventory setup patterns for Microbot Hub plugin scripts. USE FOR: handling bank deposits/withdrawals, managing charged items (rings, staffs), preserving equipment during banking, using Rs2InventorySetup safely, handling gear swaps. DO NOT USE FOR: creating new plugins from scratch, build issues, publishing." +--- + +# Banking & Equipment Management Skill + +Guidelines for safe banking, equipment preservation, and charged item handling in Microbot Hub scripts. + +## Key Problems & Solutions + +### 1. Charged Items (Ring of Dueling, Amulet of Glory, etc.) + +Each charge level is a **different ItemID** in OSRS. `Ring of dueling(8)` ≠ `Ring of dueling(5)`. + +**NEVER** hardcode a single charge variant (e.g. `ItemID.RING_OF_DUELING8`). Use name-based matching: + +```java +// BAD - only finds fully charged rings +if (Rs2Bank.count(ItemID.RING_OF_DUELING8) > 0) { ... } + +// GOOD - finds any charge level +if (Rs2Bank.hasItem("Ring of dueling")) { ... } +``` + +**Withdrawing charged items:** +```java +// BAD +Rs2Bank.withdrawOne(ItemID.RING_OF_DUELING8); + +// GOOD - partial name match gets highest charge first (bank ordering) +Rs2Bank.withdrawOne("Ring of dueling"); +``` + +**Checking equipped charged items:** +```java +// BAD - misses other charge levels +if (Rs2Equipment.isWearing(ItemID.RING_OF_DUELING8)) { ... } + +// GOOD +Rs2ItemModel ring = Rs2Equipment.get(EquipmentInventorySlot.RING); +if (ring != null && ring.getName().contains("dueling")) { ... } +``` + +**Equipping from inventory:** +```java +Rs2ItemModel ring = Rs2Inventory.get(it -> it != null && it.getName().contains("Ring of dueling")); +if (ring != null) { + Rs2Inventory.interact(ring, "Wear"); + sleepUntil(() -> { + Rs2ItemModel equipped = Rs2Equipment.get(EquipmentInventorySlot.RING); + return equipped != null && equipped.getName().contains("dueling"); + }, Rs2Random.between(3000, 6000)); +} +``` + +### 2. Rs2InventorySetup and doesEquipmentMatch() + +`doesEquipmentMatch()` does exact ID comparison by default. A ring with fewer charges than the setup expects will cause it to return `false`, triggering `loadEquipment()` which calls `depositEquipment()` — stripping ALL worn gear. + +**Always add a retry limit and charged-item tolerance:** + +```java +if (!inventorySetup.doesEquipmentMatch()) { + // Check if the "mismatch" is just a charged item with different charges + boolean chargedItemOk = Rs2Equipment.get(EquipmentInventorySlot.RING) != null + && Rs2Equipment.get(EquipmentInventorySlot.RING).getName().contains("dueling"); + + int retries = 0; + while (!inventorySetup.doesEquipmentMatch() && retries < 3) { + if (!super.isRunning()) break; + inventorySetup.loadEquipment(); + retries++; + } + // Accept near-match if only charged items differ + if (!inventorySetup.doesEquipmentMatch() && chargedItemOk) { + Microbot.log("Equipment close enough (charged item charges differ) - continuing."); + } +} +``` + +### 3. Depositing Safely + +**`Rs2Bank.depositAll()`** deposits inventory only, NOT equipment. Equipment is deposited by `depositEquipment()` or indirectly by `loadEquipment()`. + +**Prefer `depositAllExcept()` when you have items to preserve:** + +```java +// Available overloads: +Rs2Bank.depositAllExcept(String... names); // partial name match +Rs2Bank.depositAllExcept(Integer... ids); // exact item IDs +Rs2Bank.depositAllExcept(Collection names); // collection of names +Rs2Bank.depositAllExcept(Predicate p); // custom logic +``` + +**Build a keep-list for items that should stay in inventory:** +```java +List keepItems = new ArrayList<>(Arrays.asList( + "Spade", "Prayer potion(4)", "Prayer potion(3)", + "Ring of dueling", foodName +)); +// Add conditional items +if (config.useBrews()) { + keepItems.add("Forgotten brew(4)"); + keepItems.add("Forgotten brew(3)"); +} +Rs2Bank.depositAllExcept(keepItems); +``` + +**MKE Wintertodt's per-slot approach (most precise but more complex):** +```java +// Deposit slot-by-slot, preserving specific items +for (int slot = 0; slot < 28; slot++) { + Rs2ItemModel item = Rs2Inventory.get(slot); + if (item != null && !shouldKeep(item)) { + Rs2Bank.depositOne(item.getId()); + } +} +``` + +### 4. Equipment Replacement During Banking + +**Check by name, not null, for charged equipment:** +```java +// BAD - only triggers when slot is completely empty +if (Rs2Equipment.get(EquipmentInventorySlot.RING) == null) { getNewRing(); } + +// GOOD - also catches wrong item type in slot +Rs2ItemModel ring = Rs2Equipment.get(EquipmentInventorySlot.RING); +if (ring == null || !ring.getName().contains("dueling")) { + getNewRing(); +} +``` + +**Pattern for items that degrade/break (e.g. barrows equipment, charged items):** +```java +// Check if equipped item still has charges/is functional +Rs2ItemModel staff = Rs2Equipment.get(EquipmentInventorySlot.WEAPON); +if (staff != null && staff.getName().contains("uncharged")) { + // Need to replace +} +``` + +### 5. Banking Flow Template + +```java +if (shouldBank) { + if (!Rs2Bank.isOpen()) { + Rs2Bank.walkToBankAndUseBank(bankLocation); + } else { + // 1. Save notable loot before depositing + saveLootToTracking(); + + // 2. Deposit inventory (keeping essentials) + Rs2Bank.depositAllExcept(keepItems); + + // 3. Withdraw supplies (check current count first) + if (Rs2Inventory.count(foodId) < targetFood) { + int needed = targetFood - Rs2Inventory.count(foodId); + Rs2Bank.withdrawX(foodId, needed); + } + + // 4. Check/replace equipment (name-based for charged items) + Rs2ItemModel ring = Rs2Equipment.get(EquipmentInventorySlot.RING); + if (ring == null || !ring.getName().contains("dueling")) { + Rs2Bank.withdrawOne("Ring of dueling"); + Rs2ItemModel newRing = Rs2Inventory.get( + it -> it != null && it.getName().contains("Ring of dueling")); + if (newRing != null) Rs2Inventory.interact(newRing, "Wear"); + } + + // 5. Verify supplies before leaving + suppliesCheck(config, true); + } +} +``` + +## Common Pitfalls + +| Pitfall | Fix | +|---------|-----| +| `loadEquipment()` infinite loop on charged items | Add retry limit + charged-item tolerance | +| Hardcoded `ItemID.RING_OF_DUELING8` | Use `"Ring of dueling"` name matching | +| `depositAll()` then re-withdraw everything | Use `depositAllExcept(keepItems)` | +| Building a keepItems list but never using it | Pass it to `depositAllExcept()` | +| Only checking `== null` for equipment slot | Also check item name matches expected type | diff --git a/.github/skills/build-plugin/SKILL.md b/.github/skills/build-plugin/SKILL.md new file mode 100644 index 0000000000..52767735f1 --- /dev/null +++ b/.github/skills/build-plugin/SKILL.md @@ -0,0 +1,82 @@ +--- +name: build-plugin +description: "Build one or more Microbot Hub plugins using Gradle. USE FOR: compiling plugin JARs, building specific plugins by name, building all plugins, troubleshooting build failures. DO NOT USE FOR: creating new plugins, editing plugin code, publishing releases." +--- + +# Build Plugin Skill + +## Overview + +Microbot Hub uses Gradle with dynamic plugin discovery. Each plugin under `src/main/java/net/runelite/client/plugins/microbot//` that contains a `*Plugin.java` file is automatically discovered and gets its own source set, compile task, and shadow JAR. + +## Build Commands + +### Build a single plugin (fastest for iteration) + +```bash +./gradlew build -PpluginList= +``` + +The `` is the Java class name **without** `.java`. Examples: + +```bash +./gradlew build -PpluginList=GotrPlugin +./gradlew build -PpluginList=PestControlPlugin +./gradlew build -PpluginList=AutoMiningPlugin +``` + +### Build multiple specific plugins + +Comma-separate the plugin names (no spaces): + +```bash +./gradlew build -PpluginList=GotrPlugin,PestControlPlugin,AutoMiningPlugin +``` + +### Build all plugins + +```bash +./gradlew clean build +``` + +This scans every directory under the plugins source path and builds all discovered plugins. Takes significantly longer than targeted builds. + +### Other useful tasks + +| Command | Purpose | +|---------|---------| +| `./gradlew generatePluginsJson` | Generate `plugins.json` metadata with SHA256 hashes (requires JDK 11) | +| `./gradlew copyPluginDocs` | Copy plugin `docs/` folders to `public/docs/` | +| `./gradlew test` | Run all tests (test classes can access all plugin source sets) | +| `./gradlew validateJdkVersion` | Verify the correct JDK version is active | + +## Finding the plugin name + +The plugin name used with `-PpluginList` is the Java class name of the main plugin file (the one with `@PluginDescriptor`), without the `.java` extension. To find it: + +```bash +# List all discoverable plugin names +find src/main/java/net/runelite/client/plugins/microbot -name "*Plugin.java" -printf "%f\n" | sed 's/.java$//' +``` + +## Build output + +- Plugin JARs are written to `build/libs/-.jar` +- The version comes from the `static final String version` field in the Plugin class +- JARs are shadow JARs that include any dependencies listed in the plugin's `dependencies.txt` + +## Troubleshooting + +- **JDK version errors**: This project requires JDK 11 (Adoptium). Run `./gradlew validateJdkVersion`. +- **Plugin not found**: Ensure the directory contains a file matching `*Plugin.java` and the class has `@PluginDescriptor`. +- **Dependency issues**: Check `src/main/resources/net/runelite/client/plugins/microbot//dependencies.txt` for correct Maven coordinates. +- **Client JAR missing**: The build auto-downloads the Microbot client. Override with `-PmicrobotClientVersion=` or `-PmicrobotClientPath=/path/to/jar` for offline use. + +## Procedure + +When asked to build a plugin: + +1. Identify the plugin class name (e.g., `GotrPlugin`, `PestControlPlugin`) +2. Run `./gradlew build -PpluginList=` in the terminal +3. Verify the build succeeds and report the result +4. If building multiple plugins, comma-separate them in a single `-PpluginList` argument \ No newline at end of file diff --git a/.github/skills/rs2walker/SKILL.md b/.github/skills/rs2walker/SKILL.md new file mode 100644 index 0000000000..d7a1a23242 --- /dev/null +++ b/.github/skills/rs2walker/SKILL.md @@ -0,0 +1,504 @@ +--- +name: rs2walker +description: "Walking, navigation, and tile visibility patterns for Microbot Hub scripts. USE FOR: choosing between walkTo/walkFastCanvas/walkCanvas/walkMiniMap, checking tile visibility with Rs2Camera.isTileOnScreen, ensuring NPCs and game objects are on-screen before interaction, camera adjustments, and canvas vs walker decision logic. DO NOT USE FOR: creating new plugins from scratch, build issues, publishing." +--- + +# Rs2Walker & Tile Visibility Skill + +Guidelines for walking, navigation, and on-screen tile validation in Microbot Hub scripts. + +## Walking Methods Overview + +Microbot provides several walking methods with different behaviors and use cases: + +| Method | How it works | When to use | Distance | +|--------|-------------|-------------|----------| +| `Rs2Walker.walkTo(WorldPoint)` | Full web walker with pathfinding | Long distances, cross-region travel | Any | +| `Rs2Walker.walkTo(WorldPoint, distance)` | Web walker, stops within `distance` tiles | Long distances with tolerance | Any | +| `Rs2Walker.walkFastCanvas(WorldPoint)` | Clicks directly on the game canvas tile | Short distances, tile must be visible | ≤ ~12 tiles | +| `Rs2Walker.walkCanvas(WorldPoint)` | Clicks on canvas, slower than walkFast | Short distances, tile must be visible | ≤ ~12 tiles | +| `Rs2Walker.walkMiniMap(WorldPoint)` | Clicks on minimap to walk | Medium distances, avoids pathing | ~5–15 tiles | +| `Rs2Walker.walkFastLocal(LocalPoint)` | Canvas click using a LocalPoint directly | When you already have a LocalPoint | ≤ ~12 tiles | + +### Key Differences + +- **`walkTo`**: Uses the full web walker with API pathfinding. Reliable over any distance but slower and more CPU-intensive. Best for traveling between areas. Always works regardless of camera angle. +- **`walkFastCanvas`**: Clicks directly on the game canvas at the target tile. Very fast and lightweight, but **the tile must be visible on-screen**. Fails silently if the tile is off-screen or behind camera. +- **`walkCanvas`**: Similar to `walkFastCanvas` but with additional movement handling. Same visibility requirement. +- **`walkMiniMap`**: Clicks on the minimap. Good middle ground—doesn't require the tile to be on-screen but has limited range (~15 tiles). Useful when the target is nearby but camera isn't facing it. + +## Tile Visibility Check: `Rs2Camera.isTileOnScreen` + +Before using canvas-based walking or before interacting with NPCs/game objects, always verify the target is actually visible on-screen. + +### Method Overloads + +`Rs2Camera.isTileOnScreen` accepts: + +- `LocalPoint` — most common, convert from WorldPoint if needed +- `TileObject` — for game objects directly (agility obstacles, furnaces, etc.) + +### Converting WorldPoint to LocalPoint + +```java +import net.runelite.api.coords.LocalPoint; + +LocalPoint localTile = LocalPoint.fromWorld( + Microbot.getClient().getTopLevelWorldView(), + worldPoint +); +// localTile will be null if the world point is not in the loaded scene +``` + +**Important**: Always null-check the result. `LocalPoint.fromWorld()` returns `null` if the world point is outside the currently loaded scene (too far away). + +### Basic Visibility Check Pattern + +```java +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.api.coords.LocalPoint; + +LocalPoint localTile = LocalPoint.fromWorld( + Microbot.getClient().getTopLevelWorldView(), targetWorldPoint +); +if (localTile != null && Rs2Camera.isTileOnScreen(localTile)) { + // Tile is visible — safe to use canvas walking or interact +} else { + // Tile is NOT visible — use walkTo, walkMiniMap, or turnTo first +} +``` + +## Pattern 1: Canvas Walk with Visibility Fallback + +Use this when walking short-to-medium distances. Prefer canvas walking when the tile is visible and close, fall back to the web walker otherwise. + +```java +WorldPoint target = someWorldPoint; +int distance = Rs2Player.getWorldLocation().distanceTo(target); + +if (distance <= 12) { + LocalPoint localTile = LocalPoint.fromWorld( + Microbot.getClient().getTopLevelWorldView(), target + ); + if (localTile != null && Rs2Camera.isTileOnScreen(localTile)) { + Rs2Walker.walkFastCanvas(target); + Rs2Player.waitForWalking(); + } else { + Rs2Walker.walkTo(target); + Rs2Player.waitForWalking(); + } +} else { + Rs2Walker.walkTo(target); + Rs2Player.waitForWalking(); +} +``` + +**Why 12 tiles?** The game canvas renders roughly 12–14 tiles in any direction depending on camera zoom. Using 12 guarantees the tile is within render range. Using a random threshold (e.g., `Rs2Random.between(7,14)`) causes the decision to flip-flop across iterations at boundary distances — always use a fixed threshold. + +### Two-Phase Approach (Long then Short) + +For precise tile targeting (e.g., safe spots), use the web walker for long-range approach, then canvas for exact placement: + +```java +// Phase 1: Long-range approach with web walker +if (Rs2Player.distanceTo(target) > 15) { + Rs2Walker.walkTo(target, 0); + sleepUntil(() -> Rs2Player.distanceTo(target) <= 5, 30000); +} + +// Phase 2: Precise canvas click for exact tile +if (Rs2Player.distanceTo(target) > 0) { + Rs2Walker.walkFastCanvas(target); + sleepUntil(() -> Rs2Player.getWorldLocation().equals(target), 15000); +} +``` + +## Pattern 2: Ensure NPC/Object is On-Screen Before Interaction + +**Always check visibility before interacting with NPCs or game objects.** If the target is not on-screen, turn the camera to face it first. This prevents failed interactions and looks more human. + +### NPCs + +```java +Rs2NpcModel npc = rs2NpcCache.query().withName("Goblin").nearest(); +if (npc != null) { + if (!Rs2Camera.isTileOnScreen(npc.getLocalLocation())) { + Rs2Camera.turnTo(npc); + } + Rs2Npc.interact(npc, "Attack"); +} +``` + +For NPCs, the `Rs2Npc.validateInteractable()` helper combines the visibility check and camera turn in one call: + +```java +import static net.runelite.client.plugins.microbot.util.npc.Rs2Npc.validateInteractable; + +if (!Rs2Camera.isTileOnScreen(fishingspot.getLocalLocation())) { + validateInteractable(fishingspot); +} +Rs2Npc.interact(fishingspot); +``` + +### Game Objects + +```java +TileObject furnace = Rs2GameObject.findObjectById(furnaceId); +if (furnace == null) { + Rs2Walker.walkTo(furnaceLocation); + return; +} + +if (!Rs2Camera.isTileOnScreen(furnace.getLocalLocation())) { + Rs2Camera.turnTo(furnace.getLocalLocation()); + return; // Wait for camera turn to complete before clicking +} + +Rs2GameObject.interact(furnace, "Smelt"); +``` + +### TileObjectModel (Cache API) + +When using `Rs2TileObjectCache`, convert the world location to a LocalPoint: + +```java +Rs2TileObjectModel tree = rs2TileObjectCache.query().withName("Oak").nearest(); +if (tree != null) { + LocalPoint local = LocalPoint.fromWorld( + Microbot.getClient().getTopLevelWorldView(), tree.getWorldLocation() + ); + if (local != null && !Rs2Camera.isTileOnScreen(local)) { + Rs2Camera.turnTo(local); + } + tree.click("Chop down"); +} +``` + +## Pattern 3: Camera Turn for Off-Screen Targets + +When a target is within interaction range but the camera isn't facing it, use `Rs2Camera.turnTo()` instead of walking: + +```java +Rs2Camera.turnTo(LocalPoint localPoint); // Turn to a tile +Rs2Camera.turnTo(Rs2NpcModel npc); // Turn to an NPC +Rs2Camera.turnTo(TileObject gameObject); // Turn to a game object +``` + +After turning the camera, either return from the loop iteration (let the next tick handle interaction) or add a brief sleep: + +```java +if (!Rs2Camera.isTileOnScreen(target.getLocalLocation())) { + Rs2Camera.turnTo(target.getLocalLocation()); + return; // Let the next iteration attempt the interaction +} +``` + +## Pattern 4: GPU Plugin Extended Range + +When the GPU plugin is enabled, the visible range on canvas is extended. Some scripts check for this: + +```java +LocalPoint localPoint = LocalPoint.fromWorld( + Microbot.getClient().getTopLevelWorldView(), targetPoint +); +if (Rs2Camera.isTileOnScreen(localPoint) && Microbot.isPluginEnabled(GpuPlugin.class)) { + Rs2Walker.walkFastLocal(localPoint); +} else { + Rs2Walker.walkTo(targetPoint); +} +``` + +Only use this pattern if your plugin explicitly wants to take advantage of GPU draw distance. For general use, stick to the standard 12-tile threshold which works with or without GPU. + +## Common Mistakes + +### 1. Using canvas walking without visibility check + +**Bad:** +```java +Rs2Walker.walkFastCanvas(target); // May fail silently if off-screen +``` + +**Good:** +```java +LocalPoint local = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), target); +if (local != null && Rs2Camera.isTileOnScreen(local)) { + Rs2Walker.walkFastCanvas(target); +} else { + Rs2Walker.walkTo(target); +} +``` + +### 2. Randomizing the canvas vs walker distance threshold + +**Bad:** +```java +// Threshold flips between canvas and walker randomly each iteration +if (distance < Rs2Random.between(7, 14)) { + Rs2Walker.walkFastCanvas(target); +} else { + Rs2Walker.walkTo(target); +} +``` + +**Good:** +```java +// Deterministic threshold — no oscillation +if (distance <= 12) { + // ... visibility check, then canvas or fallback +} else { + Rs2Walker.walkTo(target); +} +``` + +### 3. Forgetting the null check on LocalPoint.fromWorld() + +**Bad:** +```java +LocalPoint local = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), target); +Rs2Camera.isTileOnScreen(local); // NPE if target is outside loaded scene +``` + +**Good:** +```java +LocalPoint local = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), target); +if (local != null && Rs2Camera.isTileOnScreen(local)) { + // safe +} +``` + +### 4. Interacting with an NPC/object without checking visibility + +**Bad:** +```java +Rs2Npc.interact(npc, "Attack"); // Might fail if NPC is behind camera +``` + +**Good:** +```java +if (!Rs2Camera.isTileOnScreen(npc.getLocalLocation())) { + Rs2Camera.turnTo(npc); +} +Rs2Npc.interact(npc, "Attack"); +``` + +### 5. Using walkTo for very short distances + +**Bad:** +```java +// Target is 3 tiles away — web walker is overkill +Rs2Walker.walkTo(target); +``` + +**Good:** +```java +// Target is 3 tiles away — use canvas for speed +Rs2Walker.walkFastCanvas(target); +``` + +### 6. Using `invoke()` for player location instead of `Rs2Player` + +`Rs2Player.getWorldLocation()` is already thread-safe. Wrapping `client.getLocalPlayer().getWorldLocation()` in `invoke()` is redundant and inconsistent with other location checks that use the utility wrapper. + +**Bad:** +```java +// Redundant invoke — Rs2Player already handles thread safety +int x = Microbot.getClientThread().invoke(() -> + Microbot.getClient().getLocalPlayer().getWorldLocation().getX()); +``` + +**Good:** +```java +// Use the thread-safe utility consistently +int x = Rs2Player.getWorldLocation().getX(); +``` + +## Required Imports + +```java +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +``` + +## Pattern 5: Interact with Game Object if Within Range, Otherwise Walk + +When a game object (chest, sarcophagus, bank booth, etc.) is within 12–14 tiles AND on-screen, interact directly instead of walking next to it first. The game client handles pathing to the object when you click it. If it's within range but off-screen, turn the camera first. If it's farther away, walk closer before interacting. + +```java +Rs2TileObjectModel chest = rs2TileObjectCache.query().withId(CHEST_ID).nearest(); +if (chest != null) { + int distance = chest.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()); + if (distance <= 14) { + // Within interaction range — ensure it's on-screen, then click + LocalPoint chestLocal = LocalPoint.fromWorld( + Microbot.getClient().getTopLevelWorldView(), chest.getWorldLocation() + ); + if (chestLocal != null && !Rs2Camera.isTileOnScreen(chestLocal)) { + Rs2Camera.turnTo(chestLocal); + sleep(300, 600); + } + chest.click("Open"); + sleepUntil(() -> Rs2Player.isMoving(), Rs2Random.between(1000, 3000)); + sleepUntil(() -> !Rs2Player.isMoving(), Rs2Random.between(3000, 6000)); + } else { + // Too far — walk closer first + Rs2Walker.walkTo(chest.getWorldLocation()); + Rs2Player.waitForWalking(); + } +} +``` + +**Why 14 tiles?** The game client allows clicking objects up to ~14 tiles away if they are visible on the canvas. Using `<= 14` as the upper threshold catches cases where the object is renderable but you'd otherwise waste time walking right next to it. Use `<= 12` for more conservative scripts. + +## Quick Reference: Decision Tree + +``` +Need to walk to a target? +├── Distance > 15 tiles? +│ └── Use Rs2Walker.walkTo() +├── Distance 5-12 tiles? +│ ├── LocalPoint.fromWorld() returns non-null AND isTileOnScreen()? +│ │ └── Use Rs2Walker.walkFastCanvas() +│ └── Otherwise? +│ └── Use Rs2Walker.walkTo() or Rs2Walker.walkMiniMap() +└── Distance < 5 tiles? + └── Use Rs2Walker.walkFastCanvas() (almost always visible) + +Need to interact with NPC/object? +├── Distance ≤ 14 tiles? +│ ├── Is it on-screen? (Rs2Camera.isTileOnScreen) +│ │ └── Yes → interact directly (game handles pathing) +│ └── No → Rs2Camera.turnTo() first, then interact +└── Distance > 14 tiles? + └── Rs2Walker.walkTo() first, then interact when close +``` + +## Pattern 6: Boss Arena Navigation (Moons of Peril Patterns) + +Boss arenas are instanced areas where all movement is short-range (typically <15 tiles). Use `walkFastCanvas` exclusively and never the web walker, since instanced regions have no web graph. + +### Two-Step Approach to Boss Lobby + +Use `walkWithState` for long-range travel to the lobby, then `walkFastCanvas` for precise tile placement: + +```java +// Phase 1: Walk to boss lobby (non-instanced, uses web walker) +Rs2Walker.walkWithState(bossLobbyLocation, 0); +sleep(600); + +// Phase 2: Precise placement on the statue tile (short range, canvas) +if (!Rs2Player.getWorldLocation().equals(bossLobbyLocation)) { + Rs2Walker.walkFastCanvas(bossLobbyLocation); + sleepUntil(() -> Rs2Player.getWorldLocation().equals(bossLobbyLocation)); +} +``` + +### walkFastCanvas with `shiftClick` Parameter + +`Rs2Walker.walkFastCanvas(WorldPoint, boolean shiftClick)` — the second parameter controls whether to shift-click (which forces a walk without queuing actions). Use `true` when you need to interrupt combat or cancel the current action: + +```java +// Normal walk — click without shift (good for non-combat scenarios) +Rs2Walker.walkFastCanvas(safeTile, false); + +// Shift-click walk — interrupts current action (good for dodging during combat) +Rs2Walker.walkFastCanvas(evadeTile, true); +``` + +**When to use shiftClick=true:** +- Dodging boss special attacks mid-combat +- Cancelling an attack animation to move immediately +- Any movement that must override the current action queue + +### Exact Tile Placement in Boss Arenas + +Boss mechanics often require standing on an exact tile. Always use `sleepUntil` with an exact WorldPoint match: + +```java +WorldPoint attackTile = Locations.ATTACK_TILE.getWorldPoint(); +Rs2Walker.walkFastCanvas(attackTile, true); +sleepUntil(() -> Rs2Player.getWorldLocation().equals(attackTile), 3_000); +``` + +### Running a Lap (Sequential Anchor Tiles) + +For mechanics that require running a path (e.g., Eclipse Moon shield dodge), walk through a sequence of anchor tiles: + +```java +WorldPoint[] lap = { + new WorldPoint(1483, 9627, 0), // SW + new WorldPoint(1483, 9637, 0), // NW + new WorldPoint(1493, 9637, 0), // NE + new WorldPoint(1493, 9627, 0) // SE +}; + +for (WorldPoint p : lap) { + Rs2Walker.walkFastCanvas(p, false); + // Eat/drink between tiles if needed + boss.eatIfNeeded(); + boss.drinkIfNeeded(); + // Break early if mechanic ends + if (!isSpecialAttackActive()) return; + sleepUntil(() -> Rs2Player.getWorldLocation().equals(p)); +} +``` + +### Avoiding Dangerous Tiles (Rs2Tile.getDangerousGraphicsObjectTiles) + +Check if the player's current tile is dangerous and move to a safe adjacent tile: + +```java +WorldPoint playerTile = Rs2Player.getWorldLocation(); +if (Rs2Tile.getDangerousGraphicsObjectTiles().containsKey(playerTile)) { + WorldPoint safeTile = Rs2Tile.getSafeTiles(1).get(0); + if (safeTile != null) { + Rs2Walker.walkFastCanvas(safeTile, true); + sleepUntil(() -> Rs2Player.getWorldLocation().equals(safeTile), 600); + } +} +``` + +For custom dangerous objects (not in the standard graphics objects), query the tile object cache: + +```java +Set dangerTiles = Microbot.getRs2TileObjectCache().query() + .withId(DANGEROUS_OBJECT_ID) + .within(distance) + .toList().stream() + .map(o -> o.getWorldLocation()) + .collect(Collectors.toSet()); + +// Build safe candidates within radius +List candidates = new ArrayList<>(); +for (int dx = -1; dx <= 1; dx++) { + for (int dy = -1; dy <= 1; dy++) { + WorldPoint wp = new WorldPoint(centre.getX() + dx, centre.getY() + dy, centre.getPlane()); + if (!dangerTiles.contains(wp)) candidates.add(wp); + } +} +WorldPoint safeTile = candidates.get(ThreadLocalRandom.current().nextInt(candidates.size())); +Rs2Walker.walkFastCanvas(safeTile, true); +``` + +### walkCanvas for NPC Tile Targeting (Clone Parrying) + +When you need to click an NPC's tile directly (e.g., parrying clones), use `Rs2Walker.walkCanvas()` with the NPC's local-to-world location: + +```java +Rs2NpcModel clone = /* find the clone NPC */; +WorldPoint cloneLocalLocation = WorldPoint.fromLocal(Microbot.getClient(), clone.getLocalLocation()); +Rs2Walker.walkCanvas(cloneLocalLocation); +``` + +### Key Boss Arena Rules + +1. **Never use `Rs2Walker.walkTo()` inside instanced boss arenas** — no web graph exists +2. **Always use `walkFastCanvas`** — all boss tiles are within canvas range +3. **Use shiftClick=true for dodge mechanics** — ensures immediate movement +4. **Use `walkWithState` for non-instanced lobby travel** — handles long distances with state tracking +5. **Camera reset before boss fights** — `Rs2Camera.resetPitch()` + `Rs2Camera.resetZoom()` ensures all arena tiles are visible on canvas +6. **sleepUntil with exact tile match** — boss mechanics require precision, use `.equals()` not distance checks diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/Axe.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/Axe.java new file mode 100644 index 0000000000..c4b4a18ef7 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/Axe.java @@ -0,0 +1,40 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.runelite.api.Skill; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +@Getter +@AllArgsConstructor +public enum Axe { + + BRONZE_AXE("bronze axe", ItemID.BRONZE_AXE, 1, 1), + BRONZE_FELLING_AXE("bronze felling axe", ItemID.BRONZE_AXE_2H, 1, 1), + IRON_AXE("iron axe", ItemID.IRON_AXE, 1, 1), + IRON_FELLING_AXE("iron felling axe", ItemID.IRON_AXE_2H, 1, 1), + STEEL_AXE("steel felling axe", ItemID.STEEL_AXE_2H, 6, 5), + STEEL_FELLING_AXE("steel axe", ItemID.STEEL_AXE, 6, 5), + BLACK_FELLING_AXE("black felling axe", ItemID.BLACK_AXE_2H, 11, 10), + BLACK_AXE("black axe", ItemID.BLACK_AXE, 11, 10), + MITHRIL_AXE("mithril axe", ItemID.MITHRIL_AXE, 21, 20), + MITHRIL_FELLING_AXE("mithril felling axe", ItemID.MITHRIL_AXE_2H, 21, 20), + ADAMANT_AXE("adamant axe", ItemID.ADAMANT_AXE, 31, 30), + ADAMANT_FELLING_AXE("adamant felling axe", ItemID.ADAMANT_AXE_2H, 31, 30), + RUNE_AXE("rune axe", ItemID.RUNE_AXE, 41, 40), + RUNE_FELLING_AXE("rune felling axe", ItemID.RUNE_AXE_2H, 41, 40), + DRAGON_AXE("dragon axe", ItemID.DRAGON_AXE, 61, 60), + DRAGON_FELLING_AXE("dragon felling axe", ItemID.DRAGON_AXE_2H, 61, 60), + CRYSTAL_AXE("crystal axe", ItemID.CRYSTAL_AXE, 71, 70), + CRYSTAL_FELLING_AXE("crystal felling axe", ItemID.CRYSTAL_AXE_2H, 71, 70); + + private final String itemName; + private final int itemID; + private final int woodcuttingLevel; + private final int attackLevel; + + public boolean hasRequirements(boolean axeInInventory) { + return Rs2Player.getSkillRequirement(Skill.WOODCUTTING, this.woodcuttingLevel) && (Rs2Player.getSkillRequirement(Skill.ATTACK, this.attackLevel) || axeInInventory); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/FurHandling.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/FurHandling.java new file mode 100644 index 0000000000..fc94531c53 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/FurHandling.java @@ -0,0 +1,18 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum FurHandling { + DROP("Drop"), + BANK("Bank"); + + private final String name; + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/MeatPouch.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/MeatPouch.java new file mode 100644 index 0000000000..22785cad06 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/MeatPouch.java @@ -0,0 +1,20 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.Getter; +import net.runelite.api.gameval.ItemID; + +public enum MeatPouch { + SMALL_MEAT_POUCH(ItemID.HG_MEATPOUCH_SMALL, ItemID.HG_MEATPOUCH_SMALL_OPEN), + LARGE_MEAT_POUCH(ItemID.HG_MEATPOUCH_LARGE, ItemID.HG_MEATPOUCH_LARGE_OPEN); + + @Getter + private final int closedItemID; + + @Getter + private final int openItemID; + + MeatPouch(int closedItemID, int openItemID) { + this.openItemID = openItemID; + this.closedItemID = closedItemID; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallState.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallState.java new file mode 100644 index 0000000000..701eb97969 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallState.java @@ -0,0 +1,12 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +public enum PitFallState { + IDLE, + WOODCUTTING, + SETTING_TRAP, + TEASING, + JUMPING, + CHECKING, + BANKING, + FLETCHING +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterConfig.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterConfig.java new file mode 100644 index 0000000000..f8fdab6d54 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterConfig.java @@ -0,0 +1,234 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import net.runelite.client.config.Config; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigInformation; +import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.plugins.microbot.util.misc.Rs2Food; + +@ConfigInformation("" + + "Pitfall Trap Hunter by TaF" + + "

This plugin automates hunting creatures with Pitfall traps.

\n" + + "

Requirements:

\n" + + "
    \n" + + "
  1. Appropriate Hunter level for your chosen creature
  2. \n" + + "
  3. Teasing stick or Hunter's spear (equipped or in inventory)
  4. \n" + + "
  5. Axe in bank or inventory (for chopping logs)
  6. \n" + + "
  7. Knife in bank or inventory
  8. \n" + + "
\n" + + "

Cycle: Set trap on pit → Tease NPC → Jump over spiked pit → Check pit for loot

\n" + + "

Other valuable items:

\n" + + "
    \n" + + "
  1. Kandarin headgear for a chance at extra logs being cut
  2. \n" + + "
  3. Graceful for banking/travel
  4. \n" + + "
  5. Guild hunter outfit for increased catch rates
  6. \n" + + "
  7. Chisel for fletching sunlight antler bolts
  8. \n" + + "
\n" + + "") +@ConfigGroup("PitFallTrapHunter") +public interface PitFallTrapHunterConfig extends Config { + + // ---- Sections ---- + + @ConfigSection( + name = "General", + description = "General hunting settings", + position = 0 + ) + String generalSection = "General"; + + @ConfigSection( + name = "Supplies", + description = "Axe, meat pouch, and food settings", + position = 1 + ) + String suppliesSection = "Supplies"; + + @ConfigSection( + name = "Loot & Fletching", + description = "How to handle antelope fur and antler bolt fletching", + position = 2 + ) + String lootSection = "Loot & Fletching"; + + @ConfigSection( + name = "Timings", + description = "Sleep timings after actions", + position = 3 + ) + String timingsSection = "Timings"; + + // ---- General ---- + + @ConfigItem( + section = generalSection, + position = 0, + keyName = "pitFallTrapHunting", + name = "Creature to hunt", + description = "Select which creature to hunt with pitfall traps" + ) + default PitFallTrapHunting pitFallTrapHunting() { + return PitFallTrapHunting.SUNLIGHT_ANTELOPE; + } + + @ConfigItem( + section = generalSection, + position = 1, + keyName = "xpMode", + name = "XP Mode", + description = "Disables looting and banking to maximize XP per hour" + ) + default boolean xpMode() { + return false; + } + + @ConfigItem( + section = generalSection, + position = 2, + keyName = "showOverlay", + name = "Show Overlay", + description = "Displays overlay with traps and status" + ) + default boolean showOverlay() { + return true; + } + + // ---- Supplies ---- + + @ConfigItem( + section = suppliesSection, + position = 0, + keyName = "axeInInventory", + name = "Use axe in inventory?", + description = "Keep axe in inventory instead of equipping it" + ) + default boolean axeInInventory() { + return true; + } + + @ConfigItem( + section = suppliesSection, + position = 1, + keyName = "UseMeatPouch", + name = "Use meat pouch?", + description = "Do you have a meat pouch?" + ) + default boolean UseMeatPouch() { + return true; + } + + @ConfigItem( + section = suppliesSection, + position = 2, + keyName = "MeatPouch", + name = "Meat pouch", + description = "Which meat pouch should the script use?" + ) + default MeatPouch MeatPouch() { + return MeatPouch.LARGE_MEAT_POUCH; + } + + @ConfigItem( + section = suppliesSection, + position = 3, + keyName = "EatAtBank", + name = "Eat at bank", + description = "Auto eats food at the bank to fill up your hitpoints." + ) + default boolean AutoEat() { + return true; + } + + @ConfigItem( + section = suppliesSection, + position = 4, + keyName = "FoodToEatAtBank", + name = "Food to eat at bank", + description = "What food should we eat at the bank?" + ) + default Rs2Food FoodToEatAtBank() { + return Rs2Food.LOBSTER; + } + + @ConfigItem( + section = suppliesSection, + position = 5, + keyName = "runToBankHP", + name = "Run to bank at HP", + description = "Run to the bank to eat when HP drops to this level or below" + ) + default int runToBankHP() { + return 25; + } + + // ---- Loot & Fletching ---- + + @ConfigItem( + section = lootSection, + position = 0, + keyName = "furHandling", + name = "Antelope fur", + description = "Drop antelope fur or bank it" + ) + default FurHandling furHandling() { + return FurHandling.DROP; + } + + @ConfigItem( + section = lootSection, + position = 1, + keyName = "fletchAntlerBolts", + name = "Fletch sunlight antler bolts", + description = "Use chisel on sunlight antlers to fletch bolts when inventory is nearly full. Requires chisel in inventory." + ) + default boolean fletchAntlerBolts() { + return false; + } + + // ---- Timings ---- + + @ConfigItem( + section = timingsSection, + position = 0, + keyName = "MinSleepAfterCatch", + name = "Min. Sleep After Catch (ms)", + description = "Minimum sleep in ms after checking a caught creature" + ) + default int minSleepAfterCatch() { + return 4500; + } + + @ConfigItem( + section = timingsSection, + position = 1, + keyName = "MaxSleepAfterCatch", + name = "Max. Sleep After Catch (ms)", + description = "Maximum sleep in ms after checking a caught creature" + ) + default int maxSleepAfterCatch() { + return 7000; + } + + @ConfigItem( + section = timingsSection, + position = 2, + keyName = "MinSleepAfterLay", + name = "Min. Sleep After Trap Set (ms)", + description = "Minimum sleep in ms after setting a trap" + ) + default int minSleepAfterLay() { + return 2000; + } + + @ConfigItem( + section = timingsSection, + position = 3, + keyName = "MaxSleepAfterLay", + name = "Max. Sleep After Trap Set (ms)", + description = "Maximum sleep in ms after setting a trap" + ) + default int maxSleepAfterLay() { + return 5000; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterOverlay.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterOverlay.java new file mode 100644 index 0000000000..227c352369 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterOverlay.java @@ -0,0 +1,119 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import net.runelite.api.Client; +import net.runelite.api.Skill; +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.*; + +public class PitFallTrapHunterOverlay extends OverlayPanel { + private final Client client; + private final PitFallTrapHunterConfig config; + private final PitFallTrapHunterPlugin plugin; + private final PitFallTrapHunterScript script; + private int startingLevel = 0; + + @Inject + public PitFallTrapHunterOverlay(Client client, PitFallTrapHunterConfig config, PitFallTrapHunterPlugin plugin, PitFallTrapHunterScript script) { + super(plugin); + this.client = client; + this.config = config; + this.plugin = plugin; + this.script = script; + setPosition(OverlayPosition.TOP_LEFT); + setNaughty(); + } + + @Override + public Dimension render(Graphics2D graphics) { + if (!config.showOverlay()) { + return null; + } + + if (startingLevel == 0) { + startingLevel = client.getRealSkillLevel(Skill.HUNTER); + } + + panelComponent.getChildren().clear(); + panelComponent.setPreferredSize(new Dimension(200, 300)); + + panelComponent.getChildren().add(TitleComponent.builder() + .text("Pitfall Trap Hunter by TaF") + .color(Color.GREEN) + .build()); + + panelComponent.getChildren().add(LineComponent.builder().build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Running: ") + .right(plugin.getTimeRunning()) + .leftColor(Color.WHITE) + .rightColor(Color.WHITE) + .build()); + + panelComponent.getChildren().add(LineComponent.builder() + .left("Hunter Level:") + .right(startingLevel + "/" + client.getRealSkillLevel(Skill.HUNTER)) + .leftColor(Color.WHITE) + .rightColor(Color.ORANGE) + .build()); + + if (config.pitFallTrapHunting() != null) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Hunting:") + .right(config.pitFallTrapHunting().getName()) + .leftColor(Color.WHITE) + .rightColor(Color.YELLOW) + .build()); + } + + // Current state + panelComponent.getChildren().add(LineComponent.builder() + .left("State:") + .right(script.getState().name()) + .leftColor(Color.WHITE) + .rightColor(Color.CYAN) + .build()); + + // Traps information + int currentTraps = plugin.getTraps().size(); + panelComponent.getChildren().add(LineComponent.builder() + .left("Traps:") + .right(String.valueOf(currentTraps)) + .leftColor(Color.WHITE) + .rightColor(currentTraps > 0 ? Color.GREEN : Color.CYAN) + .build()); + + // Statistics + panelComponent.getChildren().add(LineComponent.builder() + .left("Creatures Caught:") + .right(String.valueOf(PitFallTrapHunterScript.creaturesCaught)) + .leftColor(Color.WHITE) + .rightColor(Color.GREEN) + .build()); + + // Fletching status + if (config.fletchAntlerBolts()) { + panelComponent.getChildren().add(LineComponent.builder() + .left("Bolt Fletching:") + .right("Enabled") + .leftColor(Color.WHITE) + .rightColor(Color.MAGENTA) + .build()); + } + + // Fur handling + panelComponent.getChildren().add(LineComponent.builder() + .left("Fur:") + .right(config.furHandling().getName()) + .leftColor(Color.WHITE) + .rightColor(config.furHandling() == FurHandling.BANK ? Color.YELLOW : Color.GRAY) + .build()); + + return super.render(graphics); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterPlugin.java new file mode 100644 index 0000000000..7d70df12ea --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterPlugin.java @@ -0,0 +1,189 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import com.google.inject.Provides; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.events.ChatMessage; +import net.runelite.api.events.GameObjectSpawned; +import net.runelite.api.events.GameTick; +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.plugins.hunter.HunterTrap; +import net.runelite.client.plugins.microbot.PluginConstants; +import net.runelite.client.plugins.microbot.util.misc.TimeUtils; +import net.runelite.client.ui.overlay.OverlayManager; + +import javax.inject.Inject; +import java.time.Instant; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +@Slf4j +@PluginDescriptor( + name = PluginDescriptor.TaFCat + "Pitfall Trap Hunter", + description = "Automates hunting creatures with Pitfall traps (Sunlight Antelope).", + tags = {"hunter", "pitfall", "antelope", "skilling", "xp", "loot", "TaF"}, + version = PitFallTrapHunterPlugin.version, + minClientVersion = "2.1.0", + cardUrl = "", + iconUrl = "", + enabledByDefault = PluginConstants.DEFAULT_ENABLED, + isExternal = PluginConstants.IS_EXTERNAL +) +public class PitFallTrapHunterPlugin extends Plugin { + + public static final String version = "1.0.0"; + + // TODO: Replace these placeholder object IDs with actual values from in-game dev tools + // These are used in onGameObjectSpawned to track trap states + private static final int PIT_UNSET_ID = -1; // Unset pit (collapsed/empty) + private static final int PIT_SET_ID = -1; // Pit after "Trap" interaction (set, waiting) + private static final int PIT_SPIKED_ID = -1; // Spiked pit ready for "Jump" + private static final int PIT_FULL_ANTELOPE_ID = -1; // Pit with caught Sunlight Antelope + private static final int PIT_TRAPPING_ID = -1; // Transition state while creature falls in + + @Getter + private final Map traps = new HashMap<>(); + @Inject + private Client client; + @Inject + private PitFallTrapHunterConfig config; + @Inject + private OverlayManager overlayManager; + @Inject + private PitFallTrapHunterOverlay overlay; + private PitFallTrapHunterScript script; + private PitFallTrapInventoryHandlerScript inventoryHandler; + private Instant scriptStartTime; + + @Provides + PitFallTrapHunterConfig provideConfig(ConfigManager configManager) { + return configManager.getConfig(PitFallTrapHunterConfig.class); + } + + @Override + protected void startUp() throws Exception { + log.info("Pitfall Trap Hunter plugin started!"); + scriptStartTime = Instant.now(); + overlayManager.add(overlay); + script = new PitFallTrapHunterScript(); + script.run(config, this); + inventoryHandler = new PitFallTrapInventoryHandlerScript(); + inventoryHandler.run(config, script); + } + + @Override + protected void shutDown() throws Exception { + log.info("Pitfall Trap Hunter plugin stopped!"); + scriptStartTime = null; + overlayManager.remove(overlay); + if (script != null) { + script.shutdown(); + inventoryHandler.shutdown(); + } + } + + protected String getTimeRunning() { + return scriptStartTime != null ? TimeUtils.getFormattedDurationBetween(scriptStartTime, Instant.now()) : ""; + } + + @Subscribe + public void onGameObjectSpawned(GameObjectSpawned event) { + final GameObject gameObject = event.getGameObject(); + final WorldPoint trapLocation = gameObject.getWorldLocation(); + final HunterTrap myTrap = traps.get(trapLocation); + final Player localPlayer = client.getLocalPlayer(); + final int objectId = gameObject.getId(); + + // Using if-else instead of switch because placeholder IDs are all -1 during development. + // Replace with a proper switch once real object IDs are filled in. + if (objectId == PIT_SET_ID || objectId == PIT_SPIKED_ID) { + // Trap placed — player interacted with "Trap" on pit + if (localPlayer.getWorldLocation().distanceTo(trapLocation) <= 3) { + log.debug("Pitfall trap set by \"{}\" at {}", localPlayer.getName(), trapLocation); + traps.put(trapLocation, new HunterTrap(gameObject)); + } + } else if (objectId == PIT_FULL_ANTELOPE_ID) { + // Creature caught — pit is full + if (myTrap != null) { + myTrap.setState(HunterTrap.State.FULL); + myTrap.resetTimer(); + } + } else if (objectId == PIT_UNSET_ID) { + // Pit collapsed/reset — empty again + if (myTrap != null) { + myTrap.setState(HunterTrap.State.EMPTY); + myTrap.resetTimer(); + } + } else if (objectId == PIT_TRAPPING_ID) { + // Transition — creature falling in + if (myTrap != null) { + myTrap.setState(HunterTrap.State.TRANSITION); + } + } + } + + @Subscribe + public void onChatMessage(ChatMessage event) { + if (event.getType() == ChatMessageType.GAMEMESSAGE) { + String msg = event.getMessage(); + if (msg.equalsIgnoreCase("oh dear, you are dead!")) { + script.hasDied = true; + } + if (msg.contains("You don't have enough inventory space. You need")) { + script.forceBank = true; + } + } + } + + @Subscribe + public void onGameTick(GameTick event) { + Iterator> it = traps.entrySet().iterator(); + Tile[][][] tiles = client.getScene().getTiles(); + Instant expire = Instant.now().minus(HunterTrap.TRAP_TIME.multipliedBy(2)); + + while (it.hasNext()) { + Map.Entry entry = it.next(); + HunterTrap trap = entry.getValue(); + WorldPoint world = entry.getKey(); + LocalPoint local = LocalPoint.fromWorld(client, world); + + if (local == null) { + if (trap.getPlacedOn().isBefore(expire)) { + log.debug("Pitfall trap removed due to timeout, {} left", traps.size()); + it.remove(); + } + continue; + } + + Tile tile = tiles[world.getPlane()][local.getSceneX()][local.getSceneY()]; + GameObject[] objects = tile.getGameObjects(); + + boolean containsAnything = false; + boolean containsUnsetPit = false; + for (GameObject object : objects) { + if (object != null) { + containsAnything = true; + if (object.getId() == PIT_UNSET_ID) { + containsUnsetPit = true; + break; + } + } + } + + if (!containsAnything) { + it.remove(); + log.debug("Pitfall trap removed (no objects), {} left", traps.size()); + } else if (containsUnsetPit) { + it.remove(); + log.debug("Pitfall trap removed (pit collapsed), {} left", traps.size()); + } + } + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterScript.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterScript.java new file mode 100644 index 0000000000..362f96c6c4 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunterScript.java @@ -0,0 +1,606 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.Getter; +import lombok.Setter; +import net.runelite.api.GameObject; +import net.runelite.api.Skill; +import net.runelite.api.coords.WorldPoint; +import net.runelite.api.gameval.ItemID; +import net.runelite.client.plugins.hunter.HunterTrap; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; +import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; +import net.runelite.client.plugins.microbot.util.antiban.enums.Activity; +import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; +import net.runelite.client.plugins.microbot.api.npc.models.Rs2NpcModel; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class PitFallTrapHunterScript extends Script { + public static int creaturesCaught = 0; + public boolean hasDied = false; + public boolean forceBank = false; + public boolean forceDrop = false; + + @Getter + @Setter + private PitFallState state = PitFallState.IDLE; + + // Track which pit we're currently working with + private PitLocation activePit = null; + + public boolean run(PitFallTrapHunterConfig config, PitFallTrapHunterPlugin plugin) { + Rs2Antiban.resetAntibanSettings(); + applyAntiBanSettings(); + Rs2Antiban.setActivity(Activity.GENERAL_HUNTER); + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!super.run()) return; + if (!Microbot.isLoggedIn()) return; + if (!this.isRunning()) return; + + PitFallTrapHunting creature = config.pitFallTrapHunting(); + + // --- Priority checks --- + if (hasDied) { + Microbot.log("We died - Shutting down"); + shutdown(); + return; + } + if (forceBank) { + Microbot.log("Inventory too full - Banking"); + state = PitFallState.BANKING; + } + if (Rs2Player.getBoostedSkillLevel(Skill.HITPOINTS) <= config.runToBankHP()) { + state = PitFallState.BANKING; + } + + // --- Fletching check: when inventory nearly full and antler bolts enabled --- + if (config.fletchAntlerBolts() && Rs2Inventory.count() >= 24 + && Rs2Inventory.hasItem(creature.getAntlerItemId()) + && Rs2Inventory.hasItem(ItemID.CHISEL)) { + state = PitFallState.FLETCHING; + } + + // --- Supply checks --- + if (state != PitFallState.BANKING) { + if ((Rs2Inventory.count("axe") < 1 && !Rs2Equipment.isWearing("axe")) && plugin.getTraps().isEmpty()) { + Microbot.log("No axe found - Banking"); + state = PitFallState.BANKING; + } + if (!Rs2Inventory.hasItem("Knife")) { + Microbot.log("No knife found - Banking"); + state = PitFallState.BANKING; + } + if (!hasTeasingTool()) { + Microbot.log("No teasing stick or hunter's spear found - Banking"); + state = PitFallState.BANKING; + } + if (Rs2Inventory.isFull()) { + Microbot.log("Inventory full - Banking"); + state = PitFallState.BANKING; + } + } + + // --- Walk to area if far away --- + if (!isNearArea(creature) && state != PitFallState.BANKING) { + Microbot.log("Walking to " + creature.getName() + " hunting area"); + Rs2Walker.walkTo(creature.getHuntingPoint()); + return; + } + + // --- State machine --- + switch (state) { + case BANKING: + handleBanking(config); + state = PitFallState.IDLE; + break; + + case FLETCHING: + handleFletching(creature); + state = PitFallState.IDLE; + break; + + case WOODCUTTING: + handleWoodCutting(config, plugin); + // Transition to IDLE once we have logs + if (Rs2Inventory.hasItem("logs")) { + state = PitFallState.IDLE; + } + break; + + case SETTING_TRAP: + handleSettingTrap(creature, config); + break; + + case TEASING: + handleTeasing(creature, plugin); + break; + + case JUMPING: + handleJumping(creature, config); + break; + + case CHECKING: + handleChecking(plugin, config); + break; + + case IDLE: + default: + handleIdle(creature, plugin, config); + break; + } + + } catch (Exception ex) { + System.out.println("PitFallTrapHunter Script Error: " + ex.getMessage()); + ex.printStackTrace(); + } + }, 0, 1000, TimeUnit.MILLISECONDS); + return true; + } + + // ==================== STATE HANDLERS ==================== + + /** + * IDLE: Decide what to do next based on current trap state. + */ + private void handleIdle(PitFallTrapHunting creature, PitFallTrapHunterPlugin plugin, PitFallTrapHunterConfig config) { + // Check for full traps first (highest priority: collect loot) + var fullTrap = plugin.getTraps().entrySet().stream() + .filter(entry -> entry.getValue().getState() == HunterTrap.State.FULL) + .findFirst().orElse(null); + if (fullTrap != null) { + state = PitFallState.CHECKING; + return; + } + + // Check for set/spiked traps that need teasing + var setTrap = plugin.getTraps().entrySet().stream() + .filter(entry -> entry.getValue().getState() == HunterTrap.State.EMPTY) + .findFirst().orElse(null); + if (setTrap != null) { + // Trap is set, find NPC to tease toward it + activePit = findPitForWorldPoint(creature, setTrap.getKey()); + if (activePit != null) { + state = PitFallState.TEASING; + return; + } + } + + // No active traps — need to set one + if (!Rs2Inventory.hasItem("logs")) { + state = PitFallState.WOODCUTTING; + } else { + state = PitFallState.SETTING_TRAP; + } + } + + /** + * SETTING_TRAP: Walk to a pit and interact with "Trap" to set it (uses a log). + */ + private void handleSettingTrap(PitFallTrapHunting creature, PitFallTrapHunterConfig config) { + if (Rs2Player.isAnimating() || Rs2Player.isMoving()) return; + + if (!Rs2Inventory.hasItem("logs")) { + state = PitFallState.WOODCUTTING; + return; + } + + // Pick the nearest unset pit + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + activePit = creature.findNearestPit(playerLoc); + if (activePit == null) { + Microbot.log("No pit locations configured"); + return; + } + + // Walk toward the pit's ground object if not close + if (playerLoc.distanceTo(activePit.getGroundObjectPoint()) > 5) { + Rs2Walker.walkTo(activePit.getGroundObjectPoint()); + return; + } + + // Interact with the pit ground object to set trap + if (Rs2GameObject.interact(creature.getPitObjectId(), "Trap")) { + Microbot.log("Setting pitfall trap"); + sleep(config.minSleepAfterLay(), config.maxSleepAfterLay()); + // After setting, transition to teasing + state = PitFallState.TEASING; + } + } + + /** + * TEASING: Find the nearest 2x2 NPC, determine which side of the pit it's on, + * walk to the NPC's side of the pit, then tease it. + */ + private void handleTeasing(PitFallTrapHunting creature, PitFallTrapHunterPlugin plugin) { + if (Rs2Player.isAnimating() || Rs2Player.isMoving()) return; + + // Find nearest NPC using the queryable API + Rs2NpcModel npc = findNearestNpc(creature.getNpcName()); + if (npc == null) { + Microbot.log("No " + creature.getNpcName() + " found nearby - waiting"); + return; + } + + // Find the best pit for this NPC (closest set pit) + if (activePit == null) { + activePit = creature.findNearestPit(npc.getWorldLocation()); + } + if (activePit == null) { + Microbot.log("No pit location available"); + state = PitFallState.IDLE; + return; + } + + // Determine which side of the pit the NPC is on + WorldPoint npcLocation = npc.getWorldLocation(); + PitLocation.Side npcSide = activePit.getNpcSide(npcLocation); + WorldPoint teasingTile = activePit.getTeasingTile(npcSide); + + Microbot.log("NPC is on " + npcSide + " side of " + activePit.getOrientation() + " pit"); + + // Walk to the teasing tile on the NPC's side if not already there + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc.distanceTo(teasingTile) > 2) { + Rs2Walker.walkTo(teasingTile); + return; + } + + // Tease the NPC + if (npc.click("Tease")) { + Microbot.log("Teasing " + creature.getNpcName()); + Rs2Player.waitForAnimation(); + state = PitFallState.JUMPING; + } + } + + /** + * JUMPING: Run to the spiked pit ground object and interact with "Jump". + * The player crosses the pit, and the NPC follows and falls in. + */ + private void handleJumping(PitFallTrapHunting creature, PitFallTrapHunterConfig config) { + if (Rs2Player.isAnimating() || Rs2Player.isMoving()) return; + + if (activePit == null) { + state = PitFallState.IDLE; + return; + } + + // Walk toward the pit ground object if not close enough + WorldPoint playerLoc = Rs2Player.getWorldLocation(); + if (playerLoc.distanceTo(activePit.getGroundObjectPoint()) > 3) { + Rs2Walker.walkTo(activePit.getGroundObjectPoint()); + return; + } + + // Jump over the spiked pit + if (Rs2GameObject.interact(creature.getSpikedPitObjectId(), "Jump")) { + Microbot.log("Jumping over spiked pit"); + Rs2Player.waitForAnimation(); + sleep(1500, 2500); + // After jump, NPC should fall in — transition to checking + state = PitFallState.CHECKING; + } + } + + /** + * CHECKING: Check a full pit to collect loot. + */ + private void handleChecking(PitFallTrapHunterPlugin plugin, PitFallTrapHunterConfig config) { + if (Rs2Player.isAnimating() || Rs2Player.isMoving()) return; + + var fullTrap = plugin.getTraps().entrySet().stream() + .filter(entry -> entry.getValue().getState() == HunterTrap.State.FULL) + .findFirst().orElse(null); + + if (fullTrap != null) { + WorldPoint location = fullTrap.getKey(); + var gameObject = Rs2GameObject.getGameObject(location); + if (gameObject != null) { + if (Rs2Inventory.count() > 24) { + forceDrop = true; + Rs2Inventory.waitForInventoryChanges(8000); + } + Rs2GameObject.interact(gameObject, "Check"); + creaturesCaught++; + Microbot.log("Checking pit - total caught: " + creaturesCaught); + sleep(config.minSleepAfterCatch(), config.maxSleepAfterCatch()); + } + } + + // Reset for next cycle + activePit = null; + state = PitFallState.IDLE; + } + + // ==================== SUPPORT METHODS ==================== + + /** + * Find the nearest NPC matching the given name. Sunlight antelopes are 2x2 NPCs. + */ + private Rs2NpcModel findNearestNpc(String npcName) { + return Microbot.getRs2NpcCache().query() + .withName(npcName) + .where(npc -> !npc.isDead()) + .nearestOnClientThread(); + } + + /** + * Find the PitLocation corresponding to a given world point (for trap tracking). + */ + private PitLocation findPitForWorldPoint(PitFallTrapHunting creature, WorldPoint trapPoint) { + return creature.getPitLocations().stream() + .min(Comparator.comparingInt(pit -> pit.distanceTo(trapPoint))) + .orElse(null); + } + + /** + * Check if player has a teasing stick or hunter's spear in inventory or equipment. + */ + private boolean hasTeasingTool() { + for (TeasingTool tool : TeasingTool.values()) { + if (Rs2Inventory.hasItem(tool.getItemId()) || Rs2Equipment.isWearing(tool.getItemName())) { + return true; + } + } + return false; + } + + /** + * Find the first available teasing tool from equipment or inventory. + */ + private TeasingTool findTeasingTool() { + for (TeasingTool tool : TeasingTool.values()) { + if (Rs2Equipment.isWearing(tool.getItemName()) || Rs2Inventory.hasItem(tool.getItemId())) { + return tool; + } + } + return null; + } + + private boolean isNearArea(PitFallTrapHunting creature) { + WorldPoint currentLocation = Rs2Player.getWorldLocation(); + return currentLocation.distanceTo(creature.getHuntingPoint()) <= 50; + } + + // ==================== FLETCHING ==================== + + private void handleFletching(PitFallTrapHunting creature) { + if (!Rs2Inventory.hasItem(creature.getAntlerItemId()) || !Rs2Inventory.hasItem(ItemID.CHISEL)) { + state = PitFallState.IDLE; + return; + } + Microbot.log("Fletching sunlight antler bolts"); + Rs2Inventory.combineClosest(creature.getAntlerItemId(), ItemID.CHISEL); + Rs2Player.waitForAnimation(); + sleep(1200, 2400); + } + + // ==================== WOODCUTTING ==================== + + private void handleWoodCutting(PitFallTrapHunterConfig config, PitFallTrapHunterPlugin plugin) { + if (Rs2Player.isInCombat()) { + Rs2Walker.walkTo(config.pitFallTrapHunting().getHuntingPoint()); + return; + } + + if (Rs2Inventory.count("logs") > 5) { + walkBackToHunterArea(config, plugin); + return; + } + + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) { + return; + } + + GameObject tree = Rs2GameObject.findReachableObject("tree", true, 20, config.pitFallTrapHunting().getHuntingPoint()); + if (tree == null) { + tree = Rs2GameObject.findReachableObject("dead tree", true, 20, config.pitFallTrapHunting().getHuntingPoint()); + } + if (tree != null) { + if (Rs2GameObject.interact(tree, "Chop down")) { + Rs2Player.waitForAnimation(); + Rs2Antiban.actionCooldown(); + } + } else { + Microbot.log("No nearby tree found - Walking back to origin"); + walkBackToHunterArea(config, plugin); + } + } + + private void walkBackToHunterArea(PitFallTrapHunterConfig config, PitFallTrapHunterPlugin plugin) { + if (!plugin.getTraps().isEmpty()) { + var trap = plugin.getTraps().values().stream().findFirst().orElse(null); + if (trap != null) { + Rs2Walker.walkTo(trap.getWorldLocation()); + } else { + Rs2Walker.walkTo(config.pitFallTrapHunting().getHuntingPoint()); + } + } else { + Rs2Walker.walkTo(config.pitFallTrapHunting().getHuntingPoint()); + } + } + + // ==================== BANKING ==================== + + private void handleBanking(PitFallTrapHunterConfig config) { + Rs2Bank.walkToBank(); + Rs2Bank.openBank(); + + PitFallTrapHunting creature = config.pitFallTrapHunting(); + + // Handle meat pouch + if (config.UseMeatPouch()) { + if (!Rs2Inventory.hasItem(config.MeatPouch().getClosedItemID()) && !Rs2Inventory.hasItem(config.MeatPouch().getOpenItemID())) { + Rs2Bank.withdrawOne(config.MeatPouch().getOpenItemID()); + sleep(600, 1200); + Rs2Bank.withdrawOne(config.MeatPouch().getClosedItemID()); + sleep(600, 1200); + } + if (Rs2Inventory.hasItem(config.MeatPouch().getClosedItemID())) { + Rs2Inventory.interact(config.MeatPouch().getClosedItemID(), "Open"); + sleep(400, 600); + } + if (Rs2Inventory.hasItem(config.MeatPouch().getOpenItemID())) { + Rs2Inventory.interact(config.MeatPouch().getOpenItemID(), "Empty"); + sleep(400, 600); + } + } + + // Withdraw knife + if (!Rs2Inventory.hasItem(ItemID.KNIFE)) { + Rs2Bank.withdrawOne(ItemID.KNIFE); + } + + // Withdraw chisel if fletching enabled + if (config.fletchAntlerBolts() && !Rs2Inventory.hasItem(ItemID.CHISEL)) { + Rs2Bank.withdrawOne(ItemID.CHISEL); + sleep(300, 600); + } + + // Ensure we have a teasing tool + if (!hasTeasingTool()) { + for (TeasingTool tool : TeasingTool.values()) { + if (tool.hasRequirements()) { + Rs2Bank.withdrawOne(tool.getItemId()); + sleep(600, 800); + if (Rs2Inventory.hasItem(tool.getItemId())) break; + } + } + } + + // Handle axe + getBestAxe(config); + + // Deposit loot + var loot = creature.getLootId(); + if (loot != null && loot > 0) { + Rs2Bank.depositAll(loot); + } + + // Deposit fur if banking + if (config.furHandling() == FurHandling.BANK && creature.getFurItemId() > 0) { + Rs2Bank.depositAll(creature.getFurItemId()); + } + + // Deposit antler bolts (they're the product, always bank them) + // TODO: deposit antler bolt item ID when known + + sleep(50, 1200); + + // Auto-eat until full + if (config.AutoEat()) { + while (!Rs2Player.isFullHealth()) { + if (!isRunning()) break; + Rs2Bank.withdrawOne(config.FoodToEatAtBank().getId()); + sleep(200, 400); + Rs2Inventory.interact(config.FoodToEatAtBank().getId(), "Eat"); + sleep(200, 400); + } + } + + if (Rs2Inventory.contains(config.FoodToEatAtBank().getId())) { + Rs2Bank.depositAll(config.FoodToEatAtBank().getId()); + } + + if (Rs2Bank.isOpen()) { + Rs2Bank.closeBank(); + sleep(600, 900); + } + hasDied = false; + forceBank = false; + } + + // ==================== AXE MANAGEMENT ==================== + + private void getBestAxe(PitFallTrapHunterConfig config) { + Axe bestAxeInInventory = getBestAxe(Rs2Inventory.items().collect(Collectors.toList()), config); + Axe bestAxeEquipped = getBestAxe(Rs2Equipment.items(), config); + Axe bestAxeInBank = getBestAxe(Rs2Bank.bankItems(), config); + + Axe bestAxeOverall = null; + if (bestAxeInInventory != null && (bestAxeOverall == null || bestAxeInInventory.getWoodcuttingLevel() > bestAxeOverall.getWoodcuttingLevel())) { + bestAxeOverall = bestAxeInInventory; + } + if (bestAxeEquipped != null && (bestAxeOverall == null || bestAxeEquipped.getWoodcuttingLevel() > bestAxeOverall.getWoodcuttingLevel())) { + bestAxeOverall = bestAxeEquipped; + } + if (bestAxeInBank != null && (bestAxeOverall == null || bestAxeInBank.getWoodcuttingLevel() > bestAxeOverall.getWoodcuttingLevel())) { + bestAxeOverall = bestAxeInBank; + } + + if (bestAxeOverall != null) { + if (config.axeInInventory()) { + if (bestAxeOverall == bestAxeEquipped && bestAxeInInventory == null) { + Rs2Equipment.unEquip(bestAxeOverall.getItemID()); + sleep(600, 800); + } else if (bestAxeOverall == bestAxeInBank && bestAxeInInventory == null) { + Rs2Bank.withdrawItem(bestAxeOverall.getItemID()); + sleep(600, 800); + } + } else { + if (bestAxeOverall == bestAxeInBank && bestAxeEquipped == null) { + Rs2Bank.withdrawItem(bestAxeOverall.getItemID()); + sleep(600, 800); + Rs2Inventory.interact(bestAxeOverall.getItemID(), "Wield"); + sleep(600, 800); + } else if (bestAxeOverall == bestAxeInInventory && bestAxeEquipped == null) { + Rs2Inventory.interact(bestAxeOverall.getItemID(), "Wield"); + } + } + } else { + Microbot.log("No suitable axe found in bank, inventory, or equipment"); + shutdown(); + } + } + + private Axe getBestAxe(List items, PitFallTrapHunterConfig config) { + Axe best = null; + for (Axe axe : Axe.values()) { + if (items.stream().noneMatch(i -> i.getName().toLowerCase().contains(axe.getItemName()))) continue; + if (axe.hasRequirements(config.axeInInventory())) { + if (best == null || axe.getWoodcuttingLevel() > best.getWoodcuttingLevel()) { + best = axe; + } + } + } + return best; + } + + // ==================== ANTIBAN ==================== + + private void applyAntiBanSettings() { + Rs2AntibanSettings.antibanEnabled = true; + Rs2AntibanSettings.usePlayStyle = true; + Rs2AntibanSettings.simulateFatigue = true; + Rs2AntibanSettings.simulateAttentionSpan = true; + Rs2AntibanSettings.behavioralVariability = true; + Rs2AntibanSettings.nonLinearIntervals = true; + Rs2AntibanSettings.naturalMouse = true; + Rs2AntibanSettings.simulateMistakes = true; + Rs2AntibanSettings.moveMouseOffScreen = true; + Rs2AntibanSettings.contextualVariability = true; + Rs2AntibanSettings.devDebug = false; + Rs2AntibanSettings.playSchedule = true; + Rs2AntibanSettings.actionCooldownChance = 0.1; + } + + @Override + public void shutdown() { + super.shutdown(); + creaturesCaught = 0; + state = PitFallState.IDLE; + activePit = null; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunting.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunting.java new file mode 100644 index 0000000000..50f7943cbc --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapHunting.java @@ -0,0 +1,83 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +import java.util.List; + +@Getter +@RequiredArgsConstructor +public enum PitFallTrapHunting { + + // TODO: Replace placeholder IDs with actual ObjectIDs, NpcIDs, and ItemIDs from in-game dev tools + // Pit locations: center, orientation, and ground object point per pit (up to 3 per creature) + SUNLIGHT_ANTELOPE( + "Sunlight Antelope", + "Sunlight antelope", + -1, // npcId — TODO: fill from game + -1, // pitObjectId — unset pit ground object (interact "Trap") + -1, // spikedPitObjectId — set pit (interact "Jump") + -1, // fullPitObjectId — pit with caught creature (interact "Check") + -1, // emptyPitObjectId — pit after loot collected (collapsed/reset state) + 72, // requiredHunterLevel + new WorldPoint(1600, 3000, 0), // huntingPoint — TODO: fill from game + List.of( + new PitLocation( + new WorldPoint(1600, 3002, 0), // pit 1 center — TODO + PitOrientation.EAST_WEST, + new WorldPoint(1600, 3002, 0) // pit 1 ground object — TODO + ), + new PitLocation( + new WorldPoint(1604, 3002, 0), // pit 2 center — TODO + PitOrientation.EAST_WEST, + new WorldPoint(1604, 3002, 0) // pit 2 ground object — TODO + ), + new PitLocation( + new WorldPoint(1608, 3002, 0), // pit 3 center — TODO + PitOrientation.NORTH_SOUTH, + new WorldPoint(1608, 3002, 0) // pit 3 ground object — TODO + ) + ), + List.of(-1, -1), // itemsToDrop — TODO: fill with big bones ID, fur ID etc. + -1, // furItemId — TODO: antelope fur item ID + -1, // antlerItemId — TODO: sunlight antler item ID + -1 // lootId — raw meat for banking (ItemID.HUNTING_ANTELOPESUN_MEAT) + ); + + private final String name; + private final String npcName; + private final int npcId; + private final int pitObjectId; + private final int spikedPitObjectId; + private final int fullPitObjectId; + private final int emptyPitObjectId; + private final int requiredHunterLevel; + private final WorldPoint huntingPoint; + private final List pitLocations; + private final List itemsToDrop; + private final int furItemId; + private final int antlerItemId; + private final Integer lootId; + + /** + * Find the pit location closest to the given world point. + */ + public PitLocation findNearestPit(WorldPoint point) { + PitLocation nearest = null; + int nearestDist = Integer.MAX_VALUE; + for (PitLocation pit : pitLocations) { + int dist = pit.distanceTo(point); + if (dist < nearestDist) { + nearestDist = dist; + nearest = pit; + } + } + return nearest; + } + + @Override + public String toString() { + return name; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapInventoryHandlerScript.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapInventoryHandlerScript.java new file mode 100644 index 0000000000..f48d134436 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitFallTrapInventoryHandlerScript.java @@ -0,0 +1,63 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.concurrent.TimeUnit; + +public class PitFallTrapInventoryHandlerScript extends Script { + private PitFallTrapHunterConfig config; + private PitFallTrapHunterScript script; + + public boolean run(PitFallTrapHunterConfig config, PitFallTrapHunterScript script) { + this.config = config; + this.script = script; + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!super.run()) return; + if (!Microbot.isLoggedIn()) return; + if (!this.isRunning()) return; + if (!script.isRunning()) return; + if (!script.forceDrop) { + if (Rs2Player.isMoving()) return; + if (Rs2Player.isAnimating()) return; + if (Rs2Inventory.count() > Rs2Random.between(8, 28)) { + cleanInventory(); + } + return; + } + cleanInventory(); + } catch (Exception ex) { + System.out.println("PitFallTrapInventoryHandlerScript: " + ex.getMessage()); + ex.printStackTrace(); + } + }, 0, 5000, TimeUnit.MILLISECONDS); + return true; + } + + private void cleanInventory() { + PitFallTrapHunting creature = config.pitFallTrapHunting(); + boolean keepFur = config.furHandling() == FurHandling.BANK; + boolean keepAntlers = config.fletchAntlerBolts(); + + Rs2Inventory.items().forEachOrdered(item -> { + int itemId = item.getId(); + + // Keep fur if banking, otherwise drop + if (keepFur && itemId == creature.getFurItemId()) return; + + // Keep antlers if fletching bolts is enabled + if (keepAntlers && itemId == creature.getAntlerItemId()) return; + + // Drop items in the creature's drop list + if (creature.getItemsToDrop().contains(itemId)) { + Rs2Inventory.interact(item, "Drop"); + sleep(600, 1200); + } + }); + script.forceDrop = false; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitLocation.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitLocation.java new file mode 100644 index 0000000000..c08a0df97f --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitLocation.java @@ -0,0 +1,70 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.coords.WorldPoint; + +/** + * Represents a single pitfall trap location with orientation-aware positioning. + *

+ * Pits are 8x2 (EAST_WEST) or 2x8 (NORTH_SOUTH). The clickable ground objects + * ("Trap"/"Jump") occupy the middle 4 tiles. + *

+ * For EAST_WEST pits: the NPC is north or south → player jumps across Y axis. + * For NORTH_SOUTH pits: the NPC is east or west → player jumps across X axis. + */ +@Getter +@RequiredArgsConstructor +public class PitLocation { + + public enum Side { + NORTH, SOUTH, EAST, WEST + } + + private final WorldPoint center; + private final PitOrientation orientation; + private final WorldPoint groundObjectPoint; + + /** + * Determines which side of this pit an NPC is on, based on the pit's orientation. + * For EAST_WEST pits (8x2): compares Y coords → NORTH or SOUTH. + * For NORTH_SOUTH pits (2x8): compares X coords → EAST or WEST. + */ + public Side getNpcSide(WorldPoint npcLocation) { + if (orientation == PitOrientation.EAST_WEST) { + // Pit is wide along X, narrow along Y → NPC is north or south + return npcLocation.getY() >= center.getY() ? Side.NORTH : Side.SOUTH; + } else { + // Pit is tall along Y, narrow along X → NPC is east or west + return npcLocation.getX() >= center.getX() ? Side.EAST : Side.WEST; + } + } + + /** + * Returns a WorldPoint on the given side of the pit where the player should + * stand to tease the NPC. The tile is adjacent to the pit edge on the NPC's + * side, roughly centered along the pit's long axis. + */ + public WorldPoint getTeasingTile(Side npcSide) { + int offsetDistance = 2; // tiles away from pit center toward the NPC's side + switch (npcSide) { + case NORTH: + return new WorldPoint(center.getX(), center.getY() + offsetDistance, center.getPlane()); + case SOUTH: + return new WorldPoint(center.getX(), center.getY() - offsetDistance, center.getPlane()); + case EAST: + return new WorldPoint(center.getX() + offsetDistance, center.getY(), center.getPlane()); + case WEST: + return new WorldPoint(center.getX() - offsetDistance, center.getY(), center.getPlane()); + default: + return center; + } + } + + /** + * Returns the distance from this pit's center to the given point. + */ + public int distanceTo(WorldPoint point) { + return center.distanceTo(point); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitOrientation.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitOrientation.java new file mode 100644 index 0000000000..5d4b26eb6f --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/PitOrientation.java @@ -0,0 +1,26 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * Orientation of a pitfall trap pit. + *

+ * EAST_WEST = pit is 8 tiles wide along X, 2 tiles tall along Y → player jumps north↔south. + * NORTH_SOUTH = pit is 2 tiles wide along X, 8 tiles tall along Y → player jumps east↔west. + */ +@Getter +@RequiredArgsConstructor +public enum PitOrientation { + EAST_WEST("East-West (8x2)", 8, 2), + NORTH_SOUTH("North-South (2x8)", 2, 8); + + private final String description; + private final int xSize; + private final int ySize; + + @Override + public String toString() { + return description; + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/TeasingTool.java b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/TeasingTool.java new file mode 100644 index 0000000000..4db52f4dd0 --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/PitFallTrapHunter/TeasingTool.java @@ -0,0 +1,23 @@ +package net.runelite.client.plugins.microbot.PitFallTrapHunter; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.runelite.api.Skill; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +@Getter +@RequiredArgsConstructor +public enum TeasingTool { + // TODO: Replace placeholder item IDs with actual values from game data + TEASING_STICK("teasing stick", 10029, 0), // ItemID for teasing stick + HUNTERS_SPEAR("hunter's spear", 29796, 0); // ItemID for hunter's spear — TODO: confirm attack level + + private final String itemName; + private final int itemId; + private final int attackLevel; + + public boolean hasRequirements() { + if (attackLevel <= 0) return true; + return Rs2Player.getSkillRequirement(Skill.ATTACK, attackLevel); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/aiofighter/cannon/CannonScript.java b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/cannon/CannonScript.java new file mode 100644 index 0000000000..f84177ec4e --- /dev/null +++ b/src/main/java/net/runelite/client/plugins/microbot/aiofighter/cannon/CannonScript.java @@ -0,0 +1,84 @@ +package net.runelite.client.plugins.microbot.aiofighter.cannon; + +import net.runelite.api.TileObject; +import net.runelite.api.coords.WorldArea; +import net.runelite.client.plugins.cannon.CannonPlugin; +import net.runelite.client.plugins.microbot.Microbot; +import net.runelite.client.plugins.microbot.Script; +import net.runelite.client.plugins.microbot.aiofighter.AIOFighterConfig; +import net.runelite.client.plugins.microbot.aiofighter.enums.State; +import net.runelite.client.plugins.microbot.util.Global; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2Cannon; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; +import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.player.Rs2Player; + +import java.util.concurrent.TimeUnit; + +public class CannonScript extends Script { + + private static final int CANNON_REFILL_AMOUNT = 5; + + public boolean run(AIOFighterConfig config) { + mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + if (!Microbot.isLoggedIn()) return; + if (!super.run() || !config.toggleCannon()) return; + + if (config.state().equals(State.BANKING) || config.state().equals(State.WALKING)) + return; + + if (Rs2Cannon.repair()) + return; + + refill(CANNON_REFILL_AMOUNT); + } catch (Exception ex) { + Microbot.logStackTrace(this.getClass().getSimpleName(), ex); + } + }, 0, 2000, TimeUnit.MILLISECONDS); + return true; + } + + private boolean refill(int cannonRefillAmount) { + if (!Rs2Inventory.hasItemAmount("cannonball", 15, true)) { + return false; + } + + int cannonBallsLeft = Microbot.getClientThread().runOnClientThreadOptional( + () -> Microbot.getClient().getVarpValue(3)).orElse(0); + + if (cannonBallsLeft > cannonRefillAmount) { + return false; + } + + Microbot.status = "Refilling Cannon"; + TileObject cannon = Rs2GameObject.findObject(new Integer[]{6, 43027}); + if (cannon == null) { + return false; + } + + WorldArea cannonLocation = new WorldArea( + cannon.getWorldLocation().getX() - 1, + cannon.getWorldLocation().getY() - 1, + 3, 3, + cannon.getWorldLocation().getPlane()); + + if (!cannonLocation.toWorldPoint().equals(CannonPlugin.getCannonPosition().toWorldPoint())) { + return false; + } + + Microbot.pauseAllScripts.compareAndSet(false, true); + Rs2Inventory.useItemOnObject(2, cannon.getId()); + Rs2Player.waitForWalking(5000); + Global.sleepUntil(() -> Microbot.getClientThread().runOnClientThreadOptional( + () -> Microbot.getClient().getVarpValue(3)).orElse(0) > Rs2Random.between(10, 15)); + Microbot.pauseAllScripts.compareAndSet(true, false); + return true; + } + + @Override + public void shutdown() { + super.shutdown(); + } +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderPlugin.java index 612585110d..4566f6b33a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderPlugin.java @@ -33,7 +33,7 @@ ) @Slf4j public class AutoBankStanderPlugin extends Plugin { - static final String version = "1.0.2"; + static final String version = "1.0.3"; @Inject private AutoBankStanderConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderScript.java b/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderScript.java index 3651c596d4..cc914355b6 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/autobankstander/AutoBankStanderScript.java @@ -124,6 +124,7 @@ private void handleBanking() { if (processor.hasRequiredItems()) { log.info("Have all required items - switching to processing"); Rs2Bank.closeBank(); // close the bank interface + sleepUntil(() -> !Rs2Bank.isOpen(), 2000); // wait for bank to fully close changeState(AutoBankStanderState.PROCESSING); // switch to processing mode return; } @@ -133,6 +134,7 @@ private void handleBanking() { if (bankingSuccess) { log.info("Banking complete - switching to processing"); Rs2Bank.closeBank(); // close the bank interface + sleepUntil(() -> !Rs2Bank.isOpen(), 2000); // wait for bank to fully close changeState(AutoBankStanderState.PROCESSING); // switch to processing mode } else { log.info("Banking failed - no required items available, shutting down"); diff --git a/src/main/java/net/runelite/client/plugins/microbot/autobankstander/skills/herblore/HerbloreProcessor.java b/src/main/java/net/runelite/client/plugins/microbot/autobankstander/skills/herblore/HerbloreProcessor.java index fe1d64f2a3..3cc76e4244 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/autobankstander/skills/herblore/HerbloreProcessor.java +++ b/src/main/java/net/runelite/client/plugins/microbot/autobankstander/skills/herblore/HerbloreProcessor.java @@ -15,6 +15,7 @@ import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; import net.runelite.client.plugins.microbot.util.inventory.InteractOrder; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; @@ -134,6 +135,9 @@ public boolean performBanking() { return false; } + // Reset processing flag from previous cycle + currentlyMakingPotions = false; + log.info("Depositing all items"); Rs2Bank.depositAll(); sleepUntil(() -> Rs2Inventory.isEmpty(), 3000); @@ -346,8 +350,9 @@ private boolean processUnfinishedPotions() { if (Rs2Inventory.hasItem(currentHerbForUnfinished.clean) && Rs2Inventory.hasItem(ItemID.VIAL_WATER)) { log.info("Combining {} with vial of water", currentHerbForUnfinished.name()); + sleep(400, 550); if (Rs2Inventory.combine(currentHerbForUnfinished.clean, ItemID.VIAL_WATER)) { - sleep(600, 800); + sleep(250, 350); if (withdrawnAmount > 1) { sleepUntil(() -> Rs2Dialogue.hasCombinationDialogue(), 3000); Rs2Keyboard.keyPress('1'); @@ -377,8 +382,9 @@ private boolean processSuperCombat() { if (Rs2Inventory.hasItem(ItemID.TORSTOL) && Rs2Inventory.hasItem(ItemID._4DOSE2ATTACK)) { log.info("Combining torstol with super attack for super combat"); + sleep(400, 550); if (Rs2Inventory.combine(ItemID.TORSTOL, ItemID._4DOSE2ATTACK)) { - sleep(600, 800); + sleep(250, 350); if (withdrawnAmount > 1) { sleepUntil(() -> Rs2Dialogue.hasQuestion("How many do you wish to make?"), 3000); Rs2Keyboard.keyPress('1'); @@ -395,8 +401,9 @@ private boolean processRegularPotion() { if (Rs2Inventory.hasItem(currentPotion.unfinished) && Rs2Inventory.hasItem(currentPotion.secondary)) { log.info("Combining {} unfinished with secondary ingredient", currentPotion.name()); + sleep(400, 550); if (Rs2Inventory.combine(currentPotion.unfinished, currentPotion.secondary)) { - sleep(600, 800); + sleep(250, 350); if (withdrawnAmount > 1) { sleepUntil(() -> Rs2Dialogue.hasQuestion("How many do you wish to make?"), 3000); Rs2Keyboard.keyPress('1'); diff --git a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperConfig.java b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperConfig.java index d5093f98bc..77427f1628 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperConfig.java @@ -13,6 +13,9 @@ "

  • Hop to the next world for better stock.
  • " + "
  • Specify items to trade and maintain minimum stock.
  • " + "
  • Trade with specific NPCs using exact naming if needed.
  • " + + "
  • Unlimited Stock mode: loops buying until full, then banks (no hopping).
  • " + + "
  • Fast Mode: skips closing bank/shop interfaces for faster cycles.
  • " + + "
  • Set shop coordinates for precise walk-back after banking.
  • " + "" ) public interface BanksShopperConfig extends Config { @@ -26,6 +29,16 @@ public interface BanksShopperConfig extends Config { String useBank = "useBank"; String logout = "logout"; String useNextWorld = "useNextWorld"; + String blastFurnaceOptimization = "blastFurnaceOptimization"; + String useKeyboardWorldHop = "useKeyboardWorldHop"; + String unlimitedStock = "unlimitedStock"; + String fastMode = "fastMode"; + String bankName = "bankName"; + String shopX = "shopX"; + String shopY = "shopY"; + String shopZ = "shopZ"; + String useGameObject = "useGameObject"; + String shopAction = "shopAction"; @ConfigSection( name = "Action Settings", @@ -106,6 +119,50 @@ default boolean useNextWorld() { return false; } + @ConfigItem( + position = 5, + keyName = blastFurnaceOptimization, + name = "Blast Furnace optimization", + description = "Optimize route at Blast Furnace by banking/interacting directly", + section = actionSection + ) + default boolean blastFurnaceOptimization() { + return false; + } + + @ConfigItem( + position = 6, + keyName = useKeyboardWorldHop, + name = "Use Ctrl+Shift+Right to hop", + description = "Use keyboard shortcut to hop to the next world", + section = actionSection + ) + default boolean useKeyboardWorldHop() { + return false; + } + + @ConfigItem( + position = 7, + keyName = unlimitedStock, + name = "Unlimited Stock", + description = "Shop has unlimited stock - keep buying until inventory is full, then bank (no world hopping)", + section = actionSection + ) + default boolean unlimitedStock() { + return false; + } + + @ConfigItem( + position = 8, + keyName = fastMode, + name = "Fast Mode", + description = "Don't close bank/shop interfaces - interact directly with bank object and shop NPC by name", + section = actionSection + ) + default boolean fastMode() { + return false; + } + @ConfigItem( keyName = itemNames, name = "Item Name(s)/ID(s)", @@ -132,8 +189,8 @@ default int minimumStock() { @ConfigItem( keyName = npcName, - name = "NPC Name", - description = "Name of the NPC to trade with", + name = "NPC/Object Name", + description = "Name of the NPC or Game Object to trade with (e.g., Shop keeper, Culinaromancer's chest)", position = 0, section = shopSection ) @@ -152,4 +209,70 @@ default String npcName() { default boolean useExactNaming() { return true; } + + @ConfigItem( + position = 2, + keyName = useGameObject, + name = "Shop is a Game Object", + description = "Enable if the shop is a Game Object instead of an NPC (e.g., Culinaromancer's chest)", + section = shopSection + ) + default boolean useGameObject() { + return false; + } + + @ConfigItem( + keyName = shopAction, + name = "Shop Action", + description = "Action to open the shop (e.g., Trade, Buy-food, Buy-items)", + position = 3, + section = shopSection + ) + default String shopAction() { + return "Trade"; + } + + @ConfigItem( + keyName = bankName, + name = "Bank Object/NPC Name", + description = "Name of the bank booth, chest, or NPC to interact with in Fast Mode (e.g., Bank chest, Bank booth)", + position = 2, + section = shopSection + ) + default String bankName() { + return "Bank chest"; + } + + @ConfigItem( + keyName = shopX, + name = "Shop X", + description = "X coordinate of the shop location (0 to use current position)", + position = 3, + section = shopSection + ) + default int shopX() { + return 0; + } + + @ConfigItem( + keyName = shopY, + name = "Shop Y", + description = "Y coordinate of the shop location (0 to use current position)", + position = 4, + section = shopSection + ) + default int shopY() { + return 0; + } + + @ConfigItem( + keyName = shopZ, + name = "Shop Plane", + description = "Plane/floor of the shop location (usually 0)", + position = 5, + section = shopSection + ) + default int shopZ() { + return 0; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java index 9dee6d3554..7e85025d2a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperPlugin.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import net.runelite.api.ChatMessageType; +import net.runelite.api.coords.WorldPoint; import net.runelite.api.events.ChatMessage; import net.runelite.client.config.ConfigManager; import net.runelite.client.eventbus.Subscribe; @@ -36,7 +37,7 @@ ) @Slf4j public class BanksShopperPlugin extends Plugin { - public final static String version = "1.4.0"; + public final static String version = "1.4.2"; @Inject private BanksShopperConfig config; @@ -70,6 +71,22 @@ BanksShopperConfig provideConfig(ConfigManager configManager) { private boolean useLogout; @Getter private boolean useExactNaming; + @Getter + private boolean blastFurnaceOptimization; + @Getter + private boolean useKeyboardWorldHop; + @Getter + private boolean unlimitedStock; + @Getter + private boolean fastMode; + @Getter + private String bankName; + @Getter + private boolean useGameObject; + @Getter + private String shopAction; + @Getter + private WorldPoint shopLocation; @Override protected void startUp() throws AWTException { @@ -81,6 +98,14 @@ protected void startUp() throws AWTException { useLogout = config.logout(); useExactNaming = config.useExactNaming(); useNextWorld = config.useNextWorld(); + blastFurnaceOptimization = config.blastFurnaceOptimization(); + useKeyboardWorldHop = config.useKeyboardWorldHop(); + unlimitedStock = config.unlimitedStock(); + fastMode = config.fastMode(); + bankName = config.bankName(); + useGameObject = config.useGameObject(); + shopAction = config.shopAction(); + updateShopLocation(); updateItemList(config.itemNames()); if (overlayManager != null) { @@ -140,6 +165,40 @@ public void onConfigChanged(ConfigChanged event) { useNextWorld = config.useNextWorld(); } + if (event.getKey().equals(BanksShopperConfig.blastFurnaceOptimization)) { + blastFurnaceOptimization = config.blastFurnaceOptimization(); + } + + if (event.getKey().equals(BanksShopperConfig.useKeyboardWorldHop)) { + useKeyboardWorldHop = config.useKeyboardWorldHop(); + } + + if (event.getKey().equals(BanksShopperConfig.unlimitedStock)) { + unlimitedStock = config.unlimitedStock(); + } + + if (event.getKey().equals(BanksShopperConfig.fastMode)) { + fastMode = config.fastMode(); + } + + if (event.getKey().equals(BanksShopperConfig.bankName)) { + bankName = config.bankName(); + } + + if (event.getKey().equals(BanksShopperConfig.useGameObject)) { + useGameObject = config.useGameObject(); + } + + if (event.getKey().equals(BanksShopperConfig.shopAction)) { + shopAction = config.shopAction(); + } + + if (event.getKey().equals(BanksShopperConfig.shopX) + || event.getKey().equals(BanksShopperConfig.shopY) + || event.getKey().equals(BanksShopperConfig.shopZ)) { + updateShopLocation(); + } + if (event.getKey().equals(BanksShopperConfig.itemNames)) { updateItemList(config.itemNames()); } @@ -157,4 +216,11 @@ private void updateItemList(String items) { itemNames = Collections.singletonList(items.trim().toLowerCase()); } } + + private void updateShopLocation() { + int x = config.shopX(); + int y = config.shopY(); + int z = config.shopZ(); + shopLocation = (x > 0 && y > 0) ? new WorldPoint(x, y, z) : null; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java index e5600c8e46..3e2c0f5e57 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/banksshopper/BanksShopperScript.java @@ -1,16 +1,25 @@ package net.runelite.client.plugins.microbot.banksshopper; +import net.runelite.api.GameState; +import net.runelite.api.coords.LocalPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.util.antiban.Rs2Antiban; import net.runelite.client.plugins.microbot.util.antiban.Rs2AntibanSettings; import net.runelite.client.plugins.microbot.util.bank.Rs2Bank; +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; import net.runelite.client.plugins.microbot.util.math.Rs2Random; +import net.runelite.client.plugins.microbot.util.npc.Rs2Npc; 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.shop.Rs2Shop; +import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import java.awt.event.KeyEvent; import java.util.concurrent.TimeUnit; enum ShopperState { @@ -28,6 +37,18 @@ public BanksShopperScript(final BanksShopperPlugin plugin) { this.plugin = plugin; } + /** + * Opens the shop - handles both NPC shops and game object shops (e.g., Culinaromancer's chest). + */ + private boolean openShopInterface() { + if (Rs2Shop.isOpen()) return true; + if (plugin.isUseGameObject()) { + return Rs2GameObject.interact(plugin.getNpcName(), plugin.getShopAction()); + } else { + return Rs2Shop.openShop(plugin.getNpcName(), plugin.isUseExactNaming()); + } + } + public boolean run(BanksShopperConfig config) { Microbot.pauseAllScripts.compareAndSet(true, false); Microbot.enableAutoRunOn = false; @@ -37,7 +58,8 @@ public boolean run(BanksShopperConfig config) { mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { - if (!super.run() || !Microbot.isLoggedIn() || Rs2AntibanSettings.actionCooldownActive) return; + if (!super.run() || !Microbot.isLoggedIn() || Rs2AntibanSettings.actionCooldownActive) + return; if (initialPlayerLocation == null) { initialPlayerLocation = Rs2Player.getWorldLocation(); @@ -46,7 +68,8 @@ public boolean run(BanksShopperConfig config) { switch (state) { case SHOPPING: boolean missingAllRequiredItems = plugin.getItemNames().stream().noneMatch((itemName) -> { - if (itemName == null || itemName.isEmpty()) return false; + if (itemName == null || itemName.isEmpty()) + return false; if (itemName.matches("\\d+")) { return Rs2Inventory.hasItem(Integer.parseInt(itemName)); } else { @@ -61,51 +84,86 @@ public boolean run(BanksShopperConfig config) { return; } - sleepUntil(() -> Rs2Shop.openShop(plugin.getNpcName(), plugin.isUseExactNaming()), 5000); + sleepUntil(this::openShopInterface, 5000); boolean successfullAction = false; boolean outOfStock = false; if (Rs2Shop.isOpen()) { for (String itemName : plugin.getItemNames()) { - if (!isRunning() || Microbot.pauseAllScripts.get()) break; - if (itemName.length() <= 1) continue; + if (!isRunning() || Microbot.pauseAllScripts.get()) + break; + if (itemName.length() <= 1) + continue; switch (plugin.getSelectedAction()) { case BUY: // Check if name is purely numeric or alphanumeric if (itemName.matches("\\d+")) { - outOfStock = !Rs2Shop.hasMinimumStock(Integer.parseInt(itemName), plugin.getMinStock()); - if (outOfStock) continue; - successfullAction = processBuyAction(Integer.parseInt(itemName), plugin.getSelectedQuantity().toString()); + outOfStock = !Rs2Shop.hasMinimumStock(Integer.parseInt(itemName), + plugin.getMinStock()); + if (outOfStock) + continue; + if (plugin.isUnlimitedStock()) { + while (isRunning() && !Rs2Inventory.isFull()) { + if (!processBuyAction(Integer.parseInt(itemName), + plugin.getSelectedQuantity().toString())) + break; + sleepGaussian(200, 40); + } + successfullAction = true; + } else { + successfullAction = processBuyAction(Integer.parseInt(itemName), + plugin.getSelectedQuantity().toString()); + } } else { outOfStock = !Rs2Shop.hasMinimumStock(itemName, plugin.getMinStock()); - if (outOfStock) continue; - successfullAction = processBuyAction(itemName, plugin.getSelectedQuantity().toString()); + if (outOfStock) + continue; + if (plugin.isUnlimitedStock()) { + while (isRunning() && !Rs2Inventory.isFull()) { + if (!processBuyAction(itemName, + plugin.getSelectedQuantity().toString())) + break; + sleepGaussian(200, 40); + } + successfullAction = true; + } else { + successfullAction = processBuyAction(itemName, + plugin.getSelectedQuantity().toString()); + } } - if (Rs2Inventory.isFull()){ + if (Rs2Inventory.isFull()) { System.out.println("Inventory is full, stopping buy action to bank."); - Rs2Shop.closeShop(); + if (!plugin.isBlastFurnaceOptimization() && !plugin.isFastMode()) { + Rs2Shop.closeShop(); + } state = ShopperState.BANKING; return; } break; case SELL: - if (Rs2Shop.isFull()) continue; + if (Rs2Shop.isFull()) + continue; // Check if name is purely numeric or alphanumeric if (itemName.matches("\\d+")) { - while(isRunning() && processSellAction(Integer.parseInt(itemName), plugin.getSelectedQuantity().toString())){ + while (isRunning() && processSellAction(Integer.parseInt(itemName), + plugin.getSelectedQuantity().toString())) { sleepGaussian(200, 40); - if (Rs2Shop.hasMinimumStock(Integer.parseInt(itemName), plugin.getMinStock())){ - System.out.println("Stop selling over the minimum stock for item: " + itemName); + if (Rs2Shop.hasMinimumStock(Integer.parseInt(itemName), + plugin.getMinStock())) { + System.out.println("Stop selling over the minimum stock for item: " + + itemName); successfullAction = true; break; } } } else { - while(isRunning() && processSellAction(itemName, plugin.getSelectedQuantity().toString())){ + while (isRunning() && processSellAction(itemName, + plugin.getSelectedQuantity().toString())) { sleepGaussian(200, 40); - if (Rs2Shop.hasMinimumStock(itemName, plugin.getMinStock())){ - System.out.println("Stop selling over the minimum stock for item: " + itemName); + if (Rs2Shop.hasMinimumStock(itemName, plugin.getMinStock())) { + System.out.println("Stop selling over the minimum stock for item: " + + itemName); successfullAction = true; break; } @@ -116,11 +174,17 @@ public boolean run(BanksShopperConfig config) { System.out.println("Invalid action specified in config."); } } - Rs2Shop.closeShop(); + if (!plugin.isFastMode()) { + Rs2Shop.closeShop(); + } if (successfullAction) { - state = ShopperState.HOPPING; + if (plugin.isUnlimitedStock()) { + state = ShopperState.SHOPPING; + } else { + state = ShopperState.HOPPING; + } return; - }else if (outOfStock){ + } else if (outOfStock) { System.out.println("Out of stock for all items, hopping worlds..."); state = ShopperState.HOPPING; return; @@ -128,8 +192,22 @@ public boolean run(BanksShopperConfig config) { } break; case BANKING: - if (!Rs2Bank.bankItemsAndWalkBackToOriginalPosition(plugin.getItemNames(), initialPlayerLocation)) - return; + if (plugin.isFastMode()) { + if (!bankItemsFastMode()) { + return; + } + } else if (plugin.isBlastFurnaceOptimization()) { + if (!bankItemsWithoutWalkBack()) { + return; + } + } else { + var walkBackLocation = plugin.getShopLocation() != null + ? plugin.getShopLocation() : initialPlayerLocation; + if (!Rs2Bank.bankItemsAndWalkBackToOriginalPosition(plugin.getItemNames(), + walkBackLocation)) { + return; + } + } state = ShopperState.SHOPPING; break; case HOPPING: @@ -165,17 +243,45 @@ public void shutdown() { * Hops to a new world */ private void hopWorld() { - System.out.println("Hopping worlds..."); - Rs2Random.waitEx(3200, 800); // this sleep is required to avoid the message: please finish what you're doing before using the world switcher. - - int world = plugin.isUseNextWorld() ? Login.getNextWorld(Rs2Player.isMember()) : Login.getRandomWorld(Rs2Player.isMember()); - sleepUntil(() -> Microbot.hopToWorld(world), 15000); - System.out.println("Successfully hopped to world: " + world); + System.out.println("[BanksShopper] Starting world hop sequence..."); + Rs2Shop.closeShop(); + + int currentWorld = Microbot.getClient().getWorld(); + int targetWorld = currentWorld + 1; + System.out.println("[BanksShopper] Current world: " + currentWorld + ", target world: " + targetWorld); + Microbot.hopToWorld(targetWorld); + System.out.println("[BanksShopper] Hop command sent."); + + // Wait for hop to complete + boolean enteredHoppingState = sleepUntil(() -> Microbot.getClient().getGameState() == GameState.HOPPING, 5000); + System.out.println(enteredHoppingState + ? "[BanksShopper] Client entered HOPPING state." + : "[BanksShopper] Client did not enter HOPPING state within timeout."); + + boolean returnedToLoggedIn = sleepUntil(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN, 10000); + System.out.println(returnedToLoggedIn + ? "[BanksShopper] Hop completed and client returned to LOGGED_IN state." + : "[BanksShopper] Client did not return to LOGGED_IN state within timeout."); + + // Brief pause after hopping + sleep(1500, 2500); + System.out.println("[BanksShopper] World hop sequence finished."); } + private void hopWorldWithKeyboardShortcut() { + Rs2Keyboard.keyHold(KeyEvent.VK_CONTROL); + Rs2Keyboard.keyHold(KeyEvent.VK_SHIFT); + Rs2Keyboard.keyHold(KeyEvent.VK_RIGHT); + sleepGaussian(300, 150); + Rs2Keyboard.keyRelease(KeyEvent.VK_SHIFT); + Rs2Keyboard.keyRelease(KeyEvent.VK_CONTROL); + Rs2Keyboard.keyRelease(KeyEvent.VK_RIGHT); + System.out.println("Sent world hop shortcut: Ctrl+Shift+Right"); + } /** * Processes the buy action for the specified item. + * * @param itemName The name of the item to buy. * @param quantity The quantity of the item to buy. * @return true if bought successfully, false otherwise. @@ -188,18 +294,19 @@ private boolean processBuyAction(String itemName, String quantity) { boolean boughtItem = Rs2Shop.buyItem(itemName, quantity); - if (boughtItem){ + if (boughtItem) { Rs2Inventory.waitForInventoryChanges(3000); } - System.out.println(boughtItem ? "Successfully bought " + quantity + " item: " + itemName : "Failed to buy " + quantity + " item ID: " + itemName); + System.out.println(boughtItem ? "Successfully bought " + quantity + " item: " + itemName + : "Failed to buy " + quantity + " item ID: " + itemName); return boughtItem; } - /** * Processes the buy action for the specified item. - * @param itemID The ID of the item to buy. + * + * @param itemID The ID of the item to buy. * @param quantity The quantity of the item to buy. * @return true if bought successfully, false otherwise. */ @@ -211,16 +318,18 @@ private boolean processBuyAction(int itemID, String quantity) { boolean boughtItem = Rs2Shop.buyItem(itemID, quantity); - if (boughtItem){ + if (boughtItem) { Rs2Inventory.waitForInventoryChanges(3000); } - System.out.println(boughtItem ? "Successfully bought " + quantity + " item ID: " + itemID : "Failed to buy " + quantity + " item ID: " + itemID); + System.out.println(boughtItem ? "Successfully bought " + quantity + " item ID: " + itemID + : "Failed to buy " + quantity + " item ID: " + itemID); return boughtItem; } /** * Processes the sell action for the specified item. + * * @param itemName The name of the item to sell. * @param quantity The quantity of the item to sell. * @return true if sold successfully, false otherwise. @@ -228,7 +337,8 @@ private boolean processBuyAction(int itemID, String quantity) { private boolean processSellAction(String itemName, String quantity) { if (Rs2Inventory.hasItem(itemName)) { boolean soldItem = Rs2Inventory.sellItem(itemName, quantity); - System.out.println(soldItem ? "Successfully sold " + quantity + " " + itemName : "Failed to sell " + quantity + " " + itemName); + System.out.println(soldItem ? "Successfully sold " + quantity + " " + itemName + : "Failed to sell " + quantity + " " + itemName); return soldItem; } System.out.println("Item " + itemName + " not found in inventory."); @@ -237,17 +347,124 @@ private boolean processSellAction(String itemName, String quantity) { /** * Processes the sell action for the specified item. - * @param itemID The name of the item to sell. + * + * @param itemID The name of the item to sell. * @param quantity The quantity of the item to sell. * @return true if sold successfully, false otherwise. */ private boolean processSellAction(int itemID, String quantity) { if (Rs2Inventory.hasItem(itemID)) { boolean soldItem = Rs2Inventory.sellItem(itemID, quantity); - System.out.println(soldItem ? "Successfully sold " + quantity + ", item ID:" + itemID : "Failed to sell " + quantity + ", item ID: " + itemID); + System.out.println(soldItem ? "Successfully sold " + quantity + ", item ID:" + itemID + : "Failed to sell " + quantity + ", item ID: " + itemID); return soldItem; } System.out.println("Item ID" + itemID + " not found in inventory."); return false; } + + private boolean bankItemsWithoutWalkBack() { + boolean openedBank = Rs2Bank.isOpen() || Rs2Bank.openBank(); + + if (!openedBank) { + openedBank = Rs2Bank.walkToBankAndUseBank(); + } + + if (!openedBank || !Rs2Bank.isOpen()) { + return false; + } + + for (String itemName : plugin.getItemNames()) { + if (itemName == null || itemName.length() <= 1) { + continue; + } + + if (itemName.matches("\\d+")) { + Rs2Bank.depositAll(Integer.parseInt(itemName)); + } else { + Rs2Bank.depositAll(itemName); + } + sleepGaussian(120, 40); + } + + if (Rs2Bank.isOpen()) { + if (plugin.isBlastFurnaceOptimization()) { + sleepUntil(this::openShopInterface, 5000); + return true; + } + + Rs2Bank.closeBank(); + sleepUntil(() -> !Rs2Bank.isOpen(), 2000); + } + + return true; + } + + /** + * Fast mode banking: don't close shop/bank interfaces, interact directly by name. + * Uses walkFastCanvas for short distances, checks if NPC is on screen before walking. + */ + private boolean bankItemsFastMode() { + String bankObjectName = plugin.getBankName(); + + // Open bank - interact directly by name (Rs2GameObject.interact walks if needed) + if (!Rs2Bank.isOpen()) { + boolean interacted = Rs2GameObject.interact(bankObjectName, "Bank") || + Rs2GameObject.interact(bankObjectName, "Use"); + if (!interacted) { + if (!Rs2Bank.walkToBankAndUseBank()) { + System.out.println("[FastMode] Failed to interact with bank: " + bankObjectName); + return false; + } + } + if (!sleepUntil(Rs2Bank::isOpen, 5000)) { + System.out.println("[FastMode] Bank did not open in time."); + return false; + } + } + + // Deposit all items + Rs2Bank.depositAll(); + sleepGaussian(200, 40); + + // Check if the shop is already interactable from here + if (plugin.isUseGameObject()) { + // For game object shops (e.g., Culinaromancer's chest) - try interacting directly + if (openShopInterface()) { + sleepUntil(Rs2Shop::isOpen, 5000); + return true; + } + } else { + // Check if the shop NPC is already on screen - if so, just click it directly + var shopNpc = Rs2Npc.getNpc(plugin.getNpcName(), plugin.isUseExactNaming()); + if (shopNpc != null && Rs2Camera.isTileOnScreen(shopNpc.getLocalLocation())) { + sleepUntil(this::openShopInterface, 5000); + return true; + } + } + + // NPC not on screen - walk back to shop location using fast canvas if close enough + var walkBackLocation = plugin.getShopLocation() != null + ? plugin.getShopLocation() : initialPlayerLocation; + if (walkBackLocation != null && Rs2Player.getWorldLocation().distanceTo(walkBackLocation) > 1) { + int distance = Rs2Player.getWorldLocation().distanceTo(walkBackLocation); + if (distance <= 12) { + LocalPoint local = LocalPoint.fromWorld( + Microbot.getClient().getTopLevelWorldView(), walkBackLocation); + if (local != null && Rs2Camera.isTileOnScreen(local)) { + Rs2Walker.walkFastCanvas(walkBackLocation); + } else { + Rs2Walker.walkTo(walkBackLocation, 1); + } + } else { + Rs2Walker.walkTo(walkBackLocation, 1); + } + sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(walkBackLocation) <= 2, 10000); + } + + // Re-open shop + sleepUntil(this::openShopInterface, 5000); + + return true; + } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsConfig.java b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsConfig.java index e3a01dad74..050d4595b9 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsConfig.java @@ -137,7 +137,8 @@ default selectedToBarrowsTPMethod selectedToBarrowsTPMethod() { enum selectedToBarrowsTPMethod { Tablet(ItemID.TELETAB_BARROWS, "Barrows teleport"), - POH(ItemID.POH_TABLET_TELEPORTTOHOUSE, "Teleport to house"); + POH(ItemID.POH_TABLET_TELEPORTTOHOUSE, "Teleport to house"), + Walk(-1, "Walk to Barrows"); private final int id; private final String name; @@ -180,11 +181,37 @@ default int minBarrowsTeleports() { return 1; } + @ConfigItem( + keyName = "combatSpell", + name = "Combat Spell", + description = "Which wind spell to use (determines which runes to bring)", + position = 12 + ) + default CombatSpell combatSpell() { + return CombatSpell.WIND_SURGE; + } + + enum CombatSpell { + WIND_BLAST("Death rune"), + WIND_WAVE("Blood rune"), + WIND_SURGE("Wrath rune"); + + private final String runeName; + + CombatSpell(String runeName) { + this.runeName = runeName; + } + + public String getRuneName() { + return runeName; + } + } + @ConfigItem( keyName = "minRuneAmount", name = "Min Runes", description = "Minimum amount of runes before banking", - position = 12 + position = 13 ) @Range(min = 50, max = 1000) default int minRuneAmount() { @@ -195,7 +222,7 @@ default int minRuneAmount() { keyName = "shouldGainRP", name = "Aim for 86+% rewards potential", description = "Should we gain additional RP other than the barrows brothers?", - position = 13 + position = 14 ) default boolean shouldGainRP() { return false; @@ -205,10 +232,61 @@ default boolean shouldGainRP() { keyName = "shouldPrayAgainstWeakerBrothers", name = "Pray against Torag, Verac, and Guthans?", description = "Should we Pray against Torag, Verac, and Guthans?", - position = 14 + position = 15 ) default boolean shouldPrayAgainstWeakerBrothers() { return true; } + @ConfigItem( + keyName = "rangeAhrim", + name = "Range Ahrim", + description = "Switch to ranged gear when fighting Ahrim", + position = 16 + ) + default boolean rangeAhrim() { + return false; + } + + @ConfigItem( + keyName = "rangeAhrimGear", + name = "Range Gear (Ahrim)", + description = "Comma-separated item names to equip for Ahrim (e.g. Magic shortbow,Leather body)", + position = 17 + ) + default String rangeAhrimGear() { + return ""; + } + + @ConfigItem( + keyName = "mageGearSwapBack", + name = "Mage Gear (swap back)", + description = "Comma-separated item names to re-equip after Ahrim (e.g. Staff of fire,Book of darkness)", + position = 18 + ) + default String mageGearSwapBack() { + return ""; + } + + @ConfigItem( + keyName = "eatAtHealthPercent", + name = "Eat at HP %", + description = "Eat food when health drops below this percentage", + position = 19 + ) + @Range(min = 10, max = 90) + default int eatAtHealthPercent() { + return 70; + } + + @ConfigItem( + keyName = "eatForSpace", + name = "Eat for inventory space", + description = "Eat food before looting chest to free up inventory slots", + position = 20 + ) + default boolean eatForSpace() { + return false; + } + } diff --git a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java index 9827169efb..b9e92b8ce7 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsPlugin.java @@ -32,7 +32,7 @@ ) @Slf4j public class BarrowsPlugin extends Plugin { - public static final String version = "2.0.9"; + public static final String version = "2.1.5"; @Inject private BarrowsConfig config; diff --git a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java index 71a3c9d55a..b0ad1ed676 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/barrows/BarrowsScript.java @@ -2,6 +2,7 @@ import com.google.inject.Inject; import net.runelite.api.*; +import net.runelite.api.coords.LocalPoint; import net.runelite.api.coords.WorldPoint; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; @@ -32,6 +33,7 @@ import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.prayer.Rs2Prayer; import net.runelite.client.plugins.microbot.util.prayer.Rs2PrayerEnum; +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; @@ -72,6 +74,8 @@ public class BarrowsScript extends Script { public static List barrowsPieces = new ArrayList<>(); private ScheduledFuture WalkToTheChestFuture; + private BarrowsConfig config; + @Inject Rs2NpcCache rs2NpcCache; @Inject Rs2TileItemCache rs2TileItemCache; @@ -81,6 +85,7 @@ public class BarrowsScript extends Script { public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { + this.config = config; Microbot.enableAutoRunOn = false; mainScheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { try { @@ -94,10 +99,10 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { if (!inventorySetup.doesEquipmentMatch()) { while(!inventorySetup.doesEquipmentMatch()) { if(!super.isRunning()){ break; } - if (Rs2Bank.getNearestBank().getWorldPoint().distanceTo(Rs2Player.getWorldLocation()) > 6) { + if (Rs2Bank.getNearestBank().getWorldPoint().distanceTo(Rs2Player.getWorldLocation()) > 10) { Rs2Bank.walkToBank(); } - if (Rs2Bank.getNearestBank().getWorldPoint().distanceTo(Rs2Player.getWorldLocation()) <= 6) { + if (Rs2Bank.getNearestBank().getWorldPoint().distanceTo(Rs2Player.getWorldLocation()) <= 10) { inventorySetup.loadEquipment(); } } @@ -120,15 +125,17 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } //powered staffs - if(Rs2Equipment.get(EquipmentInventorySlot.WEAPON).getName().contains("Trident of the") || - Rs2Equipment.get(EquipmentInventorySlot.WEAPON).getName().contains("Tumeken's") || - Rs2Equipment.get(EquipmentInventorySlot.WEAPON).getName().contains("sceptre") || - Rs2Equipment.get(EquipmentInventorySlot.WEAPON).getName().contains("Sanguinesti") || - Rs2Equipment.get(EquipmentInventorySlot.WEAPON).getName().contains("Crystal staff")) { + var equippedWeapon = Rs2Equipment.get(EquipmentInventorySlot.WEAPON); + String weaponName = equippedWeapon != null ? equippedWeapon.getName() : ""; + if(weaponName.contains("Trident of the") || + weaponName.contains("Tumeken's") || + weaponName.contains("sceptre") || + weaponName.contains("Sanguinesti") || + weaponName.contains("Crystal staff")) { usingPoweredStaffs = true; } else { usingPoweredStaffs = false; - gettheRune(); + neededRune = config.combatSpell().getRuneName(); minRuneAmt = config.minRuneAmount(); if(!Rs2Magic.getSpellbook().equals(Rs2Spellbook.MODERN)){ swapTheSpellbook(); @@ -146,7 +153,9 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } - outOfSupplies(config); + if(!shouldBank) { + outOfSupplies(config); + } if(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID() == ItemID.TELEPORT_TO_HOUSE) { if (!inTunnels && !shouldBank && Rs2Player.getWorldLocation().distanceTo(new WorldPoint(3573, 3296, 0)) > 60) { @@ -166,6 +175,19 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } + if(config.selectedToBarrowsTPMethod() == BarrowsConfig.selectedToBarrowsTPMethod.Walk) { + if (!inTunnels && !shouldBank && Rs2Player.getWorldLocation().distanceTo(new WorldPoint(3550, 3285, 0)) > 30) { + walkToBarrows(); + // Drop rotten food received from the swamp boat + while(Rs2Inventory.contains("Rotten food")) { + if(!super.isRunning()) break; + Rs2Inventory.drop("Rotten food"); + sleep(150, 350); + } + return; + } + } + if(!inTunnels && !shouldBank) { if(!BreakHandlerScript.lockState.get()){ @@ -264,15 +286,6 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { if (Rs2Player.getWorldLocation().getPlane() == 3) { Microbot.log("We're in the mound"); - if(config.shouldPrayAgainstWeakerBrothers()){ - activatePrayer(brother.getWhatToPray()); - } else { - if(!brother.getName().contains("Torag") && !brother.getName().contains("Guthan") && !brother.getName().contains("Verac")){ - activatePrayer(brother.getWhatToPray()); - } - } - - // we're in the mound, prayer is active Rs2TileObjectModel sarc = rs2TileObjectCache.query().withIds(20770,20720,20722,20771,20721,20772).nearest(); Rs2NpcModel currentBrother = null; Microbot.log("Found the Sarcophagus"); @@ -280,12 +293,29 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { Microbot.log("Searching the Sarcophagus"); if (!super.isRunning()) break; + // Ensure sarcophagus is on-screen before clicking + LocalPoint sarcLocal = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), sarc.getWorldLocation()); + if (sarcLocal != null && !Rs2Camera.isTileOnScreen(sarcLocal)) { + Rs2Camera.turnTo(sarcLocal); + sleep(300, 600); + } if (sarc.click("Search")) { sleepUntil(() -> Rs2Player.isMoving(), Rs2Random.between(1000, 3000)); sleepUntil(() -> !Rs2Player.isMoving() || Rs2Player.isInCombat(), Rs2Random.between(3000, 6000)); // the brother could take a second to spawn in. - sleepUntil(() -> hintNpcModel() != null || Rs2Dialogue.isInDialogue(), Rs2Random.between(750, 1500)); + sleepUntil(() -> hintNpcModel() != null || Rs2Dialogue.isInDialogue(), Rs2Random.between(1000, 1500)); + + // Activate prayer only after brother has spawned + // if (hintNpcModel() != null) { + if(config.shouldPrayAgainstWeakerBrothers()){ + activatePrayer(brother.getWhatToPray()); + } else { + if(!brother.getName().contains("Torag") && !brother.getName().contains("Guthan") && !brother.getName().contains("Verac")){ + activatePrayer(brother.getWhatToPray()); + } + } + // } } if(Rs2Dialogue.isInDialogue() && Rs2Dialogue.hasDialogueText("You've found a hidden")){ @@ -390,8 +420,8 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { stuckInTunsCheck(); solvePuzzle(); checkForAndFightBrother(config); - eatFood(); - outOfSupplies(config); + eatFood(config); + if(outOfSupplies(config)) return; gainRP(config); lootChampionScroll(); @@ -403,11 +433,24 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { Rs2TileObjectModel barrowsChest = rs2TileObjectCache.query().withId(20973).nearest(); if(barrowsChest != null && - (barrowsChest.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()) < 5)){ + (barrowsChest.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()) < 5) ){ //chest ID: 20973 stopFutureWalker(); + // Eat food to free inventory space before looting + if(config.eatForSpace()) { + while(Rs2Inventory.getEmptySlots() < 4 && Rs2Inventory.contains(it -> it != null && it.isFood())) { + if(!super.isRunning()) break; + Rs2ItemModel food = Rs2Inventory.get(it -> it != null && it.isFood()); + if(food != null) { + Rs2Inventory.interact(food, "Eat"); + sleep(300, 600); + } + } + } + + if(barrowsChest.click("Open")){ sleepUntil(()-> hintNpcModel()!=null && hintNpcModel().getWorldLocation().distanceTo(Rs2Player.getWorldLocation()) <= 5, Rs2Random.between(4000,6000)); @@ -437,7 +480,7 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } //we looted the chest time to reset - suppliesCheck(config); + suppliesCheck(config, true); if(shouldBank){ Microbot.log("We should bank."); @@ -451,6 +494,18 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { ChestsOpened++; WhoisTun = "Unknown"; inTunnels = false; + } else if(config.selectedToBarrowsTPMethod() == BarrowsConfig.selectedToBarrowsTPMethod.Walk) { + // Walk method - teleport to Ferox Enclave and bank every run + Microbot.log("Teleporting to Ferox Enclave via Ring of Dueling..."); + if(Rs2Equipment.interact(EquipmentInventorySlot.RING, "Ferox Enclave")){ + sleepUntil(() -> Rs2Player.isAnimating(), Rs2Random.between(2000, 4000)); + sleepUntil(() -> !Rs2Player.isAnimating(), Rs2Random.between(6000, 10000)); + sleepUntil(() -> Rs2Player.getWorldLocation().getY() < 9600 || Rs2Player.getWorldLocation().getY() > 9730, Rs2Random.between(6000, 10000)); + } + ChestsOpened++; + WhoisTun = "Unknown"; + inTunnels = false; + shouldBank = true; } else { if(Rs2Bank.isOpen()){ closeBank(); @@ -474,8 +529,9 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { if(!Rs2Bank.isOpen()){ //stop the walker stopFutureWalker(); - //tele out - outOfSupplies(config); + //use rejuv pool before banking + reJfount(); + //walk to and open the bank Rs2Bank.walkToBankAndUseBank(BankLocation.FEROX_ENCLAVE); BreakHandlerScript.lockState.set(false); } else { @@ -483,43 +539,50 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { int ourFoodsID = ourfood.getId(); String ourfoodsname = ourfood.getName(); - if(Rs2Inventory.isFull() || Rs2Inventory.contains(it->it!=null&&it.getName().contains("'s") || it.getName().contains("Coins"))){ - if(Rs2Inventory.contains(it->it!=null&&it.getName().contains("'s"))){ - Rs2ItemModel piece = Rs2Inventory.get(it->it!=null&&it.getName().contains("'s")); + if(Rs2Inventory.contains(it->it!=null&&it.getName().contains("'s") && !it.getName().contains("Ava"))){ + Rs2ItemModel piece = Rs2Inventory.get(it->it!=null&&it.getName().contains("'s") && !it.getName().contains("Ava")); - if(piece!=null){ - barrowsPieces.add(piece.getName()); - if(barrowsPieces.contains("Nothing yet.")){ - barrowsPieces.remove("Nothing yet."); - } + if(piece != null){ + barrowsPieces.add(piece.getName()); + if(barrowsPieces.contains("Nothing yet.")){ + barrowsPieces.remove("Nothing yet."); } + } + } + java.util.List keepItems = new java.util.ArrayList<>(java.util.Arrays.asList( + neededRune, "Moonlight moth", "Moonlight moth mix (2)", "Teleport to house", "Spade", "Prayer potion(4)", "Prayer potion(3)", "Forgotten brew(4)", "Forgotten brew(3)", "Dramen staff", "Barrows teleport", + "Ring of dueling", ourfoodsname)); + if(config.rangeAhrim() && config.rangeAhrimGear() != null && !config.rangeAhrimGear().trim().isEmpty()) { + for(String item : config.rangeAhrimGear().split("\\s*,\\s*")) { + if(!item.trim().isEmpty()) keepItems.add(item.trim()); } - Rs2Bank.depositAllExcept(neededRune, "Moonlight moth", "Moonlight moth mix (2)", "Teleport to house", "Spade", "Prayer potion(4)", "Prayer potion(3)", "Forgotten brew(4)", "Forgotten brew(3)", "Barrows teleport", - ourfoodsname); } + if(config.rangeAhrim() && config.mageGearSwapBack() != null && !config.mageGearSwapBack().trim().isEmpty()) { + for(String item : config.mageGearSwapBack().split("\\s*,\\s*")) { + if(!item.trim().isEmpty()) keepItems.add(item.trim()); + } + } + Rs2Bank.depositAllExcept(keepItems); - int howtoBank = Rs2Random.between(0,100); if(!usingPoweredStaffs) { - if (howtoBank <= 40) { - if (Rs2Inventory.get(neededRune) == null || Rs2Inventory.get(neededRune).getQuantity() <= config.minRuneAmount()) { - if (Rs2Bank.getBankItem(neededRune) != null) { - if (Rs2Bank.getBankItem(neededRune).getQuantity() > config.minRuneAmount()) { - if (Rs2Bank.withdrawX(neededRune, Rs2Random.between(config.minRuneAmount(), Rs2Bank.getBankItem(neededRune).getQuantity()))) { - String therune = neededRune; - sleepUntil(() -> Rs2Inventory.get(therune).getQuantity() > config.minRuneAmount(), Rs2Random.between(2000, 4000)); - } + if (Rs2Inventory.get(neededRune) == null || Rs2Inventory.get(neededRune).getQuantity() <= config.minRuneAmount()) { + if (Rs2Bank.getBankItem(neededRune) != null) { + if (Rs2Bank.getBankItem(neededRune).getQuantity() > config.minRuneAmount()) { + if (Rs2Bank.withdrawX(neededRune, Rs2Random.between(config.minRuneAmount(), Rs2Bank.getBankItem(neededRune).getQuantity()))) { + String therune = neededRune; + sleepUntil(() -> Rs2Inventory.get(therune).getQuantity() > config.minRuneAmount(), Rs2Random.between(2000, 4000)); } - } else { - if(neededRune.equals("Wrath rune")){ - if(Rs2Bank.hasItem("Blood rune") && Rs2Bank.count("Blood rune") > config.minRuneAmount()){ - neededRune = "Blood rune"; - return; - } + } + } else { + if(neededRune.equals("Wrath rune")){ + if(Rs2Bank.hasItem("Blood rune") && Rs2Bank.count("Blood rune") > config.minRuneAmount()){ + neededRune = "Blood rune"; + return; } - Microbot.log("We're out of " + neededRune + "s. stopping..."); - super.shutdown(); } + Microbot.log("We're out of " + neededRune + "s. stopping..."); + super.shutdown(); } } } else { @@ -529,18 +592,16 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } - howtoBank = Rs2Random.between(0,100); - if(howtoBank<= 60){ - if(Rs2Inventory.count(config.prayerRestoreType().getPrayerRestoreTypeID()) < Rs2Random.between(config.minPrayerPots(),config.targetPrayerPots())){ + if(Rs2Inventory.count(config.prayerRestoreType().getPrayerRestoreTypeID()) < config.targetPrayerPots()){ if(Rs2Bank.getBankItem(config.prayerRestoreType().getPrayerRestoreTypeID())!=null){ if(Rs2Bank.getBankItem(config.prayerRestoreType().getPrayerRestoreTypeID()).getQuantity()>=config.targetPrayerPots()){ - int amt = ((Rs2Random.between(config.minPrayerPots(),config.targetPrayerPots())) - (Rs2Inventory.count(config.prayerRestoreType().getPrayerRestoreTypeID()))); + int amt = config.targetPrayerPots() - Rs2Inventory.count(config.prayerRestoreType().getPrayerRestoreTypeID()); if(amt <= 0){ amt = 1; } - Microbot.log("Withdrawing "+amt); + Microbot.log("Withdrawing "+amt+" prayer pots"); if(Rs2Bank.withdrawX(config.prayerRestoreType().getPrayerRestoreTypeID(), amt)){ - sleepUntil(()-> Rs2Inventory.count(config.prayerRestoreType().getPrayerRestoreTypeID()) > Rs2Random.between(4,8), Rs2Random.between(2000,4000)); + sleepUntil(()-> Rs2Inventory.count(config.prayerRestoreType().getPrayerRestoreTypeID()) >= config.targetPrayerPots(), Rs2Random.between(2000,4000)); } } else { Microbot.log("We're out of "+config.prayerRestoreType().getPrayerRestoreTypeID()+" need at least "+config.targetPrayerPots()+" stopping..."); @@ -548,10 +609,7 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } } - } - howtoBank = Rs2Random.between(0,100); - if(howtoBank<= 40){ if(config.minForgottenBrew() > 0) { if (Rs2Inventory.count("Forgotten brew(4)") + Rs2Inventory.count("Forgotten brew(3)") < Rs2Random.between(config.minForgottenBrew(), config.targetForgottenBrew())) { if (Rs2Bank.getBankItem("Forgotten brew(4)") != null) { @@ -571,10 +629,8 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } } - } - howtoBank = Rs2Random.between(0,100); - if(howtoBank<= 40){ - if(Rs2Inventory.get(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID())==null || Rs2Inventory.get(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID()).getQuantity() < Rs2Random.between(config.minBarrowsTeleports(),config.targetBarrowsTeleports())){ + if(config.selectedToBarrowsTPMethod() != BarrowsConfig.selectedToBarrowsTPMethod.Walk) { + if(Rs2Inventory.get(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID())==null || Rs2Inventory.get(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID()).getQuantity() < config.minBarrowsTeleports()){ if(Rs2Bank.getBankItem(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID())!=null){ if(Rs2Bank.getBankItem(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID()).getQuantity()>=config.targetBarrowsTeleports()){ if(Rs2Bank.withdrawX(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID(), Rs2Random.between(config.minBarrowsTeleports(),config.targetBarrowsTeleports()))){ @@ -590,19 +646,17 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } } - howtoBank = Rs2Random.between(0,100); - if(howtoBank<= 40){ if(Rs2Inventory.count(ourFoodsID) < config.targetFoodAmount()){ if(Rs2Bank.getBankItem(ourFoodsID)!=null){ if(Rs2Bank.getBankItem(ourFoodsID).getQuantity()>=config.targetFoodAmount()){ - int amt = (Rs2Random.between(config.minFood(),config.targetFoodAmount()) - (Rs2Inventory.count(ourFoodsID))); + int amt = config.targetFoodAmount() - Rs2Inventory.count(ourFoodsID); if(amt <= 0){ amt = 1; } - Microbot.log("Withdrawing "+amt); + Microbot.log("Withdrawing "+amt+" food"); if(Rs2Bank.withdrawX(ourFoodsID, amt)){ - sleepUntil(()-> Rs2Inventory.count(ourFoodsID) >= 10, Rs2Random.between(2000,4000)); + sleepUntil(()-> Rs2Inventory.count(ourFoodsID) >= config.targetFoodAmount(), Rs2Random.between(2000,4000)); } } else { Microbot.log("We're out of "+ourfoodsname+" need at least "+config.targetFoodAmount()+" stopping..."); @@ -610,10 +664,6 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } } - } - - howtoBank = Rs2Random.between(0,100); - if(howtoBank<= 40){ if(!Rs2Inventory.contains("Spade")){ if(Rs2Bank.getBankItem("Spade")!=null){ if(Rs2Bank.getBankItem("Spade").getQuantity()>=1){ @@ -625,33 +675,27 @@ public boolean run(BarrowsConfig config, BarrowsPlugin plugin) { } } } - } - howtoBank = Rs2Random.between(0,100); - if(howtoBank <= 40){ - if(Rs2Equipment.get(EquipmentInventorySlot.RING)!=null){ - // we have our ring do nothing - } else { - Microbot.log("Getting the ring of dueling"); - if(Rs2Bank.count(ItemID.RING_OF_DUELING8)>0){ - if(!Rs2Inventory.contains(ItemID.RING_OF_DUELING8)){ - if(Rs2Bank.withdrawX(ItemID.RING_OF_DUELING8, 1)){ - sleepUntil(()-> Rs2Inventory.contains(ItemID.RING_OF_DUELING8), Rs2Random.between(5000,15000)); - } + if(Rs2Equipment.get(EquipmentInventorySlot.RING)==null){ + Microbot.log("Getting the ring of dueling"); + if(Rs2Bank.count(ItemID.RING_OF_DUELING8)>0){ + if(!Rs2Inventory.contains(ItemID.RING_OF_DUELING8)){ + if(Rs2Bank.withdrawX(ItemID.RING_OF_DUELING8, 1)){ + sleepUntil(()-> Rs2Inventory.contains(ItemID.RING_OF_DUELING8), Rs2Random.between(5000,15000)); } - } else { - Microbot.log("Out of rings of dueling"); - super.shutdown(); } - if(Rs2Inventory.contains(ItemID.RING_OF_DUELING8)){ - if(Rs2Inventory.interact(ItemID.RING_OF_DUELING8, "Wear")){ - sleepUntil(()-> Rs2Equipment.get(EquipmentInventorySlot.RING).getName().contains("dueling"), Rs2Random.between(5000,15000)); - } + } else { + Microbot.log("Out of rings of dueling"); + super.shutdown(); + } + if(Rs2Inventory.contains(ItemID.RING_OF_DUELING8)){ + if(Rs2Inventory.interact(ItemID.RING_OF_DUELING8, "Wear")){ + sleepUntil(()-> Rs2Equipment.get(EquipmentInventorySlot.RING) != null && Rs2Equipment.get(EquipmentInventorySlot.RING).getName().contains("dueling"), Rs2Random.between(5000,15000)); } } } - suppliesCheck(config); + suppliesCheck(config, true); if(!shouldBank){ closeBank(); @@ -701,61 +745,36 @@ public void closeBank(){ } public void handlePOH(BarrowsConfig config){ - if(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID() != ItemID.TELEPORT_TO_HOUSE){ - return; - } - Client client = Microbot.getClient(); - if(client == null){ - return; - } - WorldView worldView = client.getTopLevelWorldView(); - if(worldView == null){ - return; - } - if(!worldView.isInstance()){ - return; - } - Rs2TileObjectModel pohThing = rs2TileObjectCache.query().withId(4525).nearestOnClientThread(); - if(pohThing == null){ - return; - } - Microbot.log("We're in our POH"); - Rs2TileObjectModel rejPool = rs2TileObjectCache.query().withIds(29238,29239,29241,29240).nearestOnClientThread(); - if(rejPool != null){ - if(rejPool.click("Drink")){ - sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); - sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); - } - } - Rs2TileObjectModel regularPortal = rs2TileObjectCache.query().withIds(37603,37615,37591).nearestOnClientThread(); - if(regularPortal != null){ - for(int pohPortalAttempts = 0; pohPortalAttempts < 40; pohPortalAttempts++){ - if(!super.isRunning()){ - break; - } - pohThing = rs2TileObjectCache.query().withId(4525).nearestOnClientThread(); - if(pohThing == null){ - break; - } - regularPortal = rs2TileObjectCache.query().withIds(37603,37615,37591).nearestOnClientThread(); - if(regularPortal == null){ - break; - } - if(Rs2Player.isMoving()){ - sleep(Rs2Random.between(200, 600)); - continue; + if(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID() == ItemID.TELEPORT_TO_HOUSE){ + Rs2TileObjectModel pohThing = rs2TileObjectCache.query().withId(4525).nearest(); + if(pohThing != null){ + Microbot.log("We're in our POH"); + Rs2TileObjectModel rejPool = rs2TileObjectCache.query().withIds(29238,29239,29241,29240).nearest(); + if(rejPool != null){ + if(rejPool.click("Drink")){ + sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); + sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); + } } - if(regularPortal.click("Enter")){ - sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); - sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); - sleepUntil(()-> rs2TileObjectCache.query().withIds(37603,37615,37591).nearestOnClientThread() == null, Rs2Random.between(10000,15000)); + Rs2TileObjectModel regularPortal = rs2TileObjectCache.query().withIds(37603,37615,37591).nearest(); + if(regularPortal != null){ + while(pohThing != null){ + if(!super.isRunning()){break;} + if(!Rs2Player.isMoving()){ + if(regularPortal.click("Enter")){ + sleepUntil(()-> Rs2Player.isMoving(), Rs2Random.between(2000,4000)); + sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(10000,15000)); + sleepUntil(()-> rs2TileObjectCache.query().withIds(37603,37615,37591).nearest() == null, Rs2Random.between(10000,15000)); + } + } + } + } else { - break; + // we have a nexus 33410 + Microbot.log("No nexus support yet, shutting down"); + super.shutdown(); } } - } else { - Microbot.log("No nexus support yet, shutting down"); - super.shutdown(); } } @@ -801,12 +820,6 @@ public void digIntoTheMound(Rs2WorldArea moundArea){ if (!super.isRunning()) break; - //antipattern turn on prayer early - antiPatternEnableWrongPrayer(); - - antiPatternActivatePrayer(); - //antipattern - if (Rs2Inventory.contains("Spade")) { if (Rs2Inventory.interact("Spade", "Dig")) { sleepUntil(() -> Rs2Player.getWorldLocation().getPlane() == 3, Rs2Random.between(3000, 5000)); @@ -818,50 +831,72 @@ public void digIntoTheMound(Rs2WorldArea moundArea){ } public void goToTheMound(Rs2WorldArea moundArea){ + int moundAttempts = 0; while (!moundArea.contains(Rs2Player.getWorldLocation())) { checkForWorldMap(); int totalTiles = moundArea.toWorldPointList().size(); WorldPoint randomMoundTile; if (!super.isRunning()) break; - //antipattern turn on prayer early - antiPatternEnableWrongPrayer(); - - antiPatternActivatePrayer(); + // Don't issue new walk commands while already moving or animating + if (Rs2Player.isMoving() || Rs2Player.isAnimating()) { + Rs2Player.waitForWalking(); + continue; + } antiPatternDropVials(); - //antipattern // We're not in the mound yet. randomMoundTile = moundArea.toWorldPointList().get(Rs2Random.between(0,(totalTiles-1))); - if(Rs2Walker.walkTo(randomMoundTile))sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(2000,4000)); + if(Rs2Player.getWorldLocation().distanceTo(randomMoundTile) <= Rs2Random.between(10,15)){ + LocalPoint localTile = LocalPoint.fromWorld(Microbot.getClient().getTopLevelWorldView(), randomMoundTile); + if(localTile != null && Rs2Camera.isTileOnScreen(localTile)){ + Rs2Walker.walkFastCanvas(randomMoundTile); + Rs2Player.waitForWalking(); + } else { + Rs2Walker.walkTo(randomMoundTile); + Rs2Player.waitForWalking(); + } + } else { + Rs2Walker.walkTo(randomMoundTile); + Rs2Player.waitForWalking(); + } if (moundArea.contains(Rs2Player.getWorldLocation())) { if(!Rs2Player.isMoving()) break; } else { - Microbot.log("At the mound, but we can't dig yet."); - randomMoundTile = moundArea.toWorldPointList().get(Rs2Random.between(0,(totalTiles-1))); + moundAttempts++; + Microbot.log("At the mound, but we can't dig yet. Attempt: " + moundAttempts); //strange old man body blocking us Rs2NpcModel strangeOldMan = rs2NpcCache.query().withName("Strange Old Man").nearestOnClientThread(); - if(strangeOldMan !=null){ - if(strangeOldMan.getWorldLocation() != null){ - if(strangeOldMan.getWorldLocation() == randomMoundTile){ - while(strangeOldMan.getWorldLocation() == randomMoundTile){ - if(!super.isRunning()){break;} - randomMoundTile = moundArea.toWorldPointList().get(Rs2Random.between(0,(totalTiles-1))); - sleep(250,500); - } - } + // Pick a new random tile, avoiding the Strange Old Man's tile + randomMoundTile = moundArea.toWorldPointList().get(Rs2Random.between(0,(totalTiles-1))); + if(strangeOldMan != null && strangeOldMan.getWorldLocation() != null){ + int tileRetries = 0; + while(strangeOldMan.getWorldLocation().equals(randomMoundTile) && tileRetries < 10){ + if(!super.isRunning()){break;} + randomMoundTile = moundArea.toWorldPointList().get(Rs2Random.between(0,(totalTiles-1))); + tileRetries++; + sleep(250,500); } } - Rs2Walker.walkCanvas(randomMoundTile); - sleepUntil(()-> !Rs2Player.isMoving(), Rs2Random.between(2000,4000)); + if (!Rs2Player.isMoving() && !Rs2Player.isAnimating()) { + Rs2Walker.walkCanvas(randomMoundTile); + Rs2Player.waitForWalking(); + } + + if(moundAttempts >= 10){ + Microbot.log("Stuck trying to reach mound, using webwalker."); + Rs2Walker.walkTo(moundArea.toWorldPointList().get(Rs2Random.between(0,(totalTiles-1)))); + Rs2Player.waitForWalking(); + moundAttempts = 0; + } } } } @@ -932,7 +967,7 @@ public void gainRP(BarrowsConfig config){ stopFutureWalker(); sleep(750,1500); - eatFood(); + eatFood(config); outOfSupplies(config); antiPatternDropVials(); @@ -972,6 +1007,10 @@ public void gainRP(BarrowsConfig config){ } public void suppliesCheck(BarrowsConfig config){ + suppliesCheck(config, false); + } + + public void suppliesCheck(BarrowsConfig config, boolean postLoot){ if(!usingPoweredStaffs) { if (Rs2Inventory.get(neededRune) == null || Rs2Inventory.get(neededRune).getQuantity() <= minRuneAmt) { Microbot.log("We have less than 180 " + neededRune); @@ -1003,10 +1042,12 @@ public void suppliesCheck(BarrowsConfig config){ shouldBank = true; return; } - if ((Rs2Inventory.get(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID()) == null)) { - Microbot.log("We don't have a "+config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemName()); - shouldBank = true; - return; + if (config.selectedToBarrowsTPMethod() != BarrowsConfig.selectedToBarrowsTPMethod.Walk) { + if ((Rs2Inventory.get(config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemID()) == null)) { + Microbot.log("We don't have a "+config.selectedToBarrowsTPMethod().getToBarrowsTPMethodItemName()); + shouldBank = true; + return; + } } if (Rs2Inventory.count(it->it!=null&&it.getName().contains("Forgotten brew(")) < minForgottenBrews) { Microbot.log("We forgot our Forgotten brew."); @@ -1014,19 +1055,22 @@ public void suppliesCheck(BarrowsConfig config){ return; } - String name = config.prayerRestoreType().getPrayerRestoreTypeName(); - if(name.contains("(")) name = config.prayerRestoreType().getPrayerRestoreTypeName().split("\\(")[0]; - String splitName = name; - if (Rs2Inventory.count(it->it!=null&&it.getName().toLowerCase().contains(splitName.toLowerCase())) < 1) { - Microbot.log("We don't have enough "+splitName); - shouldBank = true; - return; - } + // Only check prayer restores and run energy after looting the chest or during banking + if(postLoot) { + String name = config.prayerRestoreType().getPrayerRestoreTypeName(); + if(name.contains("(")) name = name.split("\\(")[0]; + String splitName = name; + if (Rs2Inventory.count(it->it!=null&&it.getName().toLowerCase().contains(splitName.toLowerCase())) < 1) { + Microbot.log("We don't have enough "+splitName); + shouldBank = true; + return; + } - if(Rs2Player.getRunEnergy() <= 5){ - Microbot.log("We need more run energy "); - shouldBank = true; - return; + if(Rs2Player.getRunEnergy() <= 5){ + Microbot.log("We need more run energy"); + shouldBank = true; + return; + } } shouldBank = false; @@ -1060,36 +1104,23 @@ public void swapTheSpellbook(){ } } - public void gettheRune(){ - if(!neededRune.equals("unknown")) return; - - neededRune = "unknown"; - int magicLvl = Rs2Player.getRealSkillLevel(Skill.MAGIC); - - if(magicLvl >= 41 && magicLvl < 62) neededRune = "Death rune"; - - if(magicLvl >= 62 && magicLvl < 81) neededRune = "Blood rune"; - - if(magicLvl >= 81) neededRune = "Wrath rune"; - } - public void setAutoCast(){ - if(neededRune == "Wrath rune"){ - if (Rs2Magic.getCurrentAutoCastSpell() != Rs2CombatSpells.WIND_SURGE) { - Rs2Combat.setAutoCastSpell(Rs2CombatSpells.WIND_SURGE, false); - } - } - - if(neededRune == "Blood rune"){ - if (Rs2Magic.getCurrentAutoCastSpell() != Rs2CombatSpells.WIND_WAVE) { - Rs2Combat.setAutoCastSpell(Rs2CombatSpells.WIND_WAVE, false); - } - } - - if(neededRune == "Death rune"){ - if (Rs2Magic.getCurrentAutoCastSpell() != Rs2CombatSpells.WIND_BLAST) { - Rs2Combat.setAutoCastSpell(Rs2CombatSpells.WIND_BLAST, false); - } + switch (config.combatSpell()) { + case WIND_SURGE: + if (Rs2Magic.getCurrentAutoCastSpell() != Rs2CombatSpells.WIND_SURGE) { + Rs2Combat.setAutoCastSpell(Rs2CombatSpells.WIND_SURGE, false); + } + break; + case WIND_WAVE: + if (Rs2Magic.getCurrentAutoCastSpell() != Rs2CombatSpells.WIND_WAVE) { + Rs2Combat.setAutoCastSpell(Rs2CombatSpells.WIND_WAVE, false); + } + break; + case WIND_BLAST: + if (Rs2Magic.getCurrentAutoCastSpell() != Rs2CombatSpells.WIND_BLAST) { + Rs2Combat.setAutoCastSpell(Rs2CombatSpells.WIND_BLAST, false); + } + break; } } @@ -1097,6 +1128,7 @@ public void activatePrayer(Rs2PrayerEnum prayer){ if(!Rs2Prayer.isPrayerActive(prayer)){ Microbot.log("Turning on Prayer."); drinkPrayerPot(); + sleepGaussian(150, 350); Rs2Prayer.toggle(prayer); } } @@ -1128,7 +1160,7 @@ public void antiPatternActivatePrayer(){ } public void antiPatternDropVials(){ if(Rs2Random.between(0,100) <= Rs2Random.between(1,25)) { - Rs2ItemModel whatToDrop = Rs2Inventory.get(it->it!=null&&it.getName().contains("Vial")||it.getName().contains("Butterfly jar")); + Rs2ItemModel whatToDrop = Rs2Inventory.get(it->it!=null&&(it.getName().contains("Vial")||it.getName().contains("Butterfly jar"))); if(whatToDrop!=null) { if (Rs2Inventory.contains(whatToDrop.getName())) { if (Rs2Inventory.drop(whatToDrop.getName())) sleep(0, 750); @@ -1136,110 +1168,40 @@ public void antiPatternDropVials(){ } } } - public void outOfSupplies(BarrowsConfig config){ - suppliesCheck(config); - if(!shouldBank){ - return; - } - boolean needFeroxRingTeleport = false; - if(inTunnels){ - needFeroxRingTeleport = true; - } - if(Rs2Player.getWorldLocation().getPlane() == 3){ - needFeroxRingTeleport = true; - } - if(isInPlayerOwnedHouse()){ - needFeroxRingTeleport = true; - } - if(!needFeroxRingTeleport){ - return; - } - if(tryFeroxTeleportViaRingOfDueling()){ - Microbot.log("We're out of supplies. Teleporting to Ferox Enclave."); - if(inTunnels){ - inTunnels = false; - } - sleepUntil(() -> Rs2Player.isAnimating(), Rs2Random.between(2000, 4000)); - sleepUntil(() -> !Rs2Player.isAnimating(), Rs2Random.between(6000, 10000)); - } - } - - private boolean isInPlayerOwnedHouse(){ - Client c = Microbot.getClient(); - if(c == null){ - return false; - } - WorldView wv = c.getTopLevelWorldView(); - if(wv == null){ - return false; - } - if(!wv.isInstance()){ - return false; - } - if(inTunnels){ - return false; - } - Rs2TileObjectModel portal = rs2TileObjectCache.query().withId(4525).nearestOnClientThread(); - return portal != null; - } - - private boolean tryFeroxTeleportViaRingOfDueling(){ - Rs2ItemModel equippedRing = Rs2Equipment.get(EquipmentInventorySlot.RING); - if(equippedRing != null){ - String equippedName = equippedRing.getName(); - if(equippedName != null){ - if(equippedName.contains("Ring of dueling")){ - if(Rs2Equipment.interact(EquipmentInventorySlot.RING, "Ferox Enclave")){ - return true; - } - } - } - } - int[] duelingRingIds = new int[]{ - ItemID.RING_OF_DUELING1, - ItemID.RING_OF_DUELING2, - ItemID.RING_OF_DUELING3, - ItemID.RING_OF_DUELING4, - ItemID.RING_OF_DUELING5, - ItemID.RING_OF_DUELING6, - ItemID.RING_OF_DUELING7, - ItemID.RING_OF_DUELING8 - }; - for(int idx = duelingRingIds.length - 1; idx >= 0; idx--){ - int ringId = duelingRingIds[idx]; - if(!Rs2Inventory.hasItem(ringId)){ - continue; - } - if(tryRubInventoryRingToFerox(ringId)){ - return true; + private void equipGearFromInventory(String gearConfig) { + if (gearConfig == null || gearConfig.trim().isEmpty()) return; + String[] items = gearConfig.split("\\s*,\\s*"); + for (String item : items) { + item = item.trim(); + if (!item.isEmpty() && Rs2Inventory.contains(item)) { + Rs2Inventory.wield(item); + sleep(50, 150); } } - return false; } - private boolean tryRubInventoryRingToFerox(int ringId){ - String feroxLabel = JewelleryLocationEnum.FEROX_ENCLAVE.getDestination(); - if(Rs2Inventory.interact(ringId, feroxLabel)){ - return true; - } - if(Rs2Inventory.interact(ringId, "Rub")){ - sleepUntil(() -> Rs2Dialogue.hasDialogueOption(feroxLabel), Rs2Random.between(1500, 3500)); - if(Rs2Dialogue.clickOption(feroxLabel)){ - return true; + public boolean outOfSupplies(BarrowsConfig config){ + suppliesCheck(config); + // Needed because the walker won't teleport to the enclave while in the tunnels or in a barrow + if(shouldBank && (inTunnels || Rs2Player.getWorldLocation().getPlane() == 3)){ + if(Rs2Equipment.interact(EquipmentInventorySlot.RING, "Ferox Enclave")){ + Microbot.log("We're out of supplies. Teleporting."); + if(inTunnels) inTunnels=false; + sleepUntil(() -> Rs2Player.isAnimating(), Rs2Random.between(2000, 4000)); + sleepUntil(() -> !Rs2Player.isAnimating(), Rs2Random.between(6000, 10000)); } - return Rs2Dialogue.clickOption(feroxLabel, false); } return false; } public void disablePrayer(){ - if(Rs2Random.between(0,100) >= Rs2Random.between(0,5)) { + if(Rs2Random.between(0,100) >= Rs2Random.between(0,2)) { Rs2Prayer.disableAllPrayers(); sleep(0,750); } } public void reJfount(){ - int rejat = Rs2Random.between(10,30); - int runener = Rs2Random.between(50,65); + int rejat = 45; + int runener = 80; while(Rs2Player.getBoostedSkillLevel(Skill.PRAYER) < rejat || Rs2Player.getRunEnergy() <= runener){ if (!super.isRunning()) break; @@ -1267,12 +1229,12 @@ public void drinkPrayerPot(){ if(!skipThePot) { if (Rs2Player.getBoostedSkillLevel(Skill.PRAYER) <= Rs2Random.between(8, 15)) { - if (Rs2Inventory.contains(it -> it != null && it.getName().contains("Prayer potion") || it.getName().contains("Moonlight moth"))) { - Rs2ItemModel prayerpotion = Rs2Inventory.get(it -> it != null && it.getName().contains("Prayer potion") || it.getName().contains("Moonlight moth")); + if (Rs2Inventory.contains(it -> it != null && (it.getName().contains("Prayer potion") || it.getName().contains("Moonlight moth")))) { + Rs2ItemModel prayerpotion = Rs2Inventory.get(it -> it != null && (it.getName().contains("Prayer potion") || it.getName().contains("Moonlight moth"))); String action = "Drink"; if (prayerpotion.getName().equals("Moonlight moth")) action = "Release"; - if (Rs2Inventory.interact(prayerpotion, action)) sleep(0, 750); + if (Rs2Inventory.interact(prayerpotion, action)) sleep(300, 750); } } } @@ -1295,10 +1257,17 @@ public void checkForAndFightBrother(BarrowsConfig config){ if (hintNpcModel() != null && currentBrother !=null) { stopFutureWalker(); - if(hintNpcModel().getName().contains("Ahrim")) neededprayer = Rs2PrayerEnum.PROTECT_MAGIC; + boolean isAhrim = hintNpcModel().getName().contains("Ahrim"); + if(isAhrim) neededprayer = Rs2PrayerEnum.PROTECT_MAGIC; if(hintNpcModel().getName().contains("Karil")) neededprayer = Rs2PrayerEnum.PROTECT_RANGE; + if(isAhrim && config.rangeAhrim()) { + Microbot.log("Switching to range gear for Ahrim..."); + equipGearFromInventory(config.rangeAhrimGear()); + sleepGaussian(600, 850); + } + while(hintNpcModel() != null){ Microbot.log("Fighting the brother."); @@ -1337,20 +1306,28 @@ public void checkForAndFightBrother(BarrowsConfig config){ sleep(750,1500); drinkPrayerPot(); - eatFood(); - outOfSupplies(config); + eatFood(config); + if(outOfSupplies(config)) break; antiPatternDropVials(); drinkforgottonbrew(); if(hintNpcModel() == null) { Microbot.log("Breaking out the brother is null."); disablePrayer(); + if(isAhrim && config.rangeAhrim()) { + Microbot.log("Switching back to mage gear..."); + equipGearFromInventory(config.mageGearSwapBack()); + } break; } if(hintNpcModel().isDead()){ Microbot.log("Breaking out the brother is dead."); disablePrayer(); + if(isAhrim && config.rangeAhrim()) { + Microbot.log("Switching back to mage gear..."); + equipGearFromInventory(config.mageGearSwapBack()); + } sleepUntil(()-> hintNpcModel() == null, Rs2Random.between(3000,6000)); break; } @@ -1359,6 +1336,16 @@ public void checkForAndFightBrother(BarrowsConfig config){ } } + public void walkToBarrows() { + WorldPoint barrowsLocation = new WorldPoint(3550, 3283, 0); + if (Rs2Player.getWorldLocation().distanceTo(barrowsLocation) > 10) { + Microbot.log("Walking to Barrows..."); + Rs2Walker.walkTo(barrowsLocation); + sleepUntil(() -> Rs2Player.getWorldLocation().distanceTo(barrowsLocation) <= 15, 120000); + sleepUntil(() -> !Rs2Player.isMoving(), Rs2Random.between(3000, 6000)); + } + } + public void stopFutureWalker(){ if(WalkToTheChestFuture!=null) { Rs2Walker.setTarget(null); @@ -1375,6 +1362,7 @@ private void walkToChest(){ } Rs2Walker.walkTo(Chest, 2); + sleepGaussian(1200, 2200); } catch (Exception e) { Microbot.log("walkToChest failed: " + e.getMessage()); } @@ -1413,8 +1401,8 @@ public void drinkforgottonbrew() { } } - public void eatFood(){ - if(Rs2Player.getHealthPercentage() <= 60){ + public void eatFood(BarrowsConfig config){ + if(Rs2Player.getHealthPercentage() <= config.eatAtHealthPercent()){ if(Rs2Inventory.contains(it->it!=null&&it.isFood())){ Rs2ItemModel food = Rs2Inventory.get(it->it!=null&&it.isFood()); if(Rs2Inventory.interact(food, "Eat")){ diff --git a/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceConfig.java b/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceConfig.java index 2241899088..ce33197df5 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceConfig.java @@ -33,6 +33,21 @@ default boolean useStamina() { default Bars getBars() { return Bars.STEEL_BAR; } + + @Range( + min = 0, + max = 10000 + ) + @ConfigItem( + keyName = "runEnergyThreshold", + name = "Run energy threshold", + description = "Drink stamina/energy potions below this run energy value (0-10000)", + position = 2, + section = "bFSettings" + ) + default int runEnergyThreshold() { + return 8100; + } @ConfigSection( name = "Credits", description = "Credits", @@ -50,4 +65,4 @@ default Bars getBars() { default String Credits() { return "Created by: Fishy \n\nUpdated by: Acun, Wassuppzzz"; } -} \ No newline at end of file +} diff --git a/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceScript.java b/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceScript.java index a36472d803..837c762bad 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/blastoisefurnace/BlastoiseFurnaceScript.java @@ -40,6 +40,11 @@ public class BlastoiseFurnaceScript extends Script { static final int coalBag = 12019; private static final int MAX_ORE_PER_INTERACTION = 27; private static final int MAX_ORE_PER_HYBRID_INTERACTION = 26; + private static final int TARGET_RUN_ENERGY_LEVEL = 9001; + private static final int ENERGY_RESTORE_PER_DOSE = 1000; + private static final int SUPER_ENERGY_RESTORE_PER_DOSE = 2000; + private static final int COAL_BAG_FILL_DELAY_MIN_MS = 650; + private static final int COAL_BAG_FILL_DELAY_MAX_MS = 1450; public static State state = State.BANKING; static boolean coalBagEmpty; static boolean primaryOreEmpty; @@ -157,7 +162,7 @@ public boolean run() { Microbot.stopPlugin(plugin); } - if (Microbot.getClient().getEnergy() < 8100) { + if (Microbot.getClient().getEnergy() < config.runEnergyThreshold()) { useStaminaPotions(); } @@ -276,6 +281,7 @@ private void retrieveDoubleCoal() { } if (!Rs2Inventory.interact(coalBag, "Fill")) return; + sleepAfterCoalBagFill(); depositOre(); } @@ -288,8 +294,7 @@ private void retrieveCoalAndPrimary() { } if (!Rs2Inventory.interact(coalBag, "Fill")) return; - - sleep(500, 1200); + sleepAfterCoalBagFill(); Rs2Bank.closeBank(); sleepUntil(() -> !Rs2Bank.isOpen()); depositOre(); @@ -303,8 +308,7 @@ private void retrieveCoalAndGold() { } if (!Rs2Inventory.interact(coalBag, "Fill")) return; - - sleep(500, 1200); + sleepAfterCoalBagFill(); Rs2Bank.closeBank(); sleepUntil(() -> !Rs2Bank.isOpen()); depositOre(); @@ -427,11 +431,39 @@ private void useStaminaPotions() { if (hasEnergyPotion) { String potionName = getLowestDosePotionName(Rs2Potion.getRestoreEnergyPotionsVariants()); if (potionName != null) { - withdrawAndDrink(potionName); + int targetEnergy = TARGET_RUN_ENERGY_LEVEL; + int currentEnergy = Microbot.getClient().getEnergy(); + if (currentEnergy >= targetEnergy) { + return; + } + int perDoseRestore = getEstimatedEnergyRestorePerDose(potionName); + int dosesNeeded = Math.max(1, (int) Math.ceil((targetEnergy - currentEnergy) / (double) perDoseRestore)); + int maxDoses = getAvailableDosesForVariants(Rs2Potion.getRestoreEnergyPotionsVariants()); + int dosesToDrink = Math.min(dosesNeeded, maxDoses); + String currentPotionName = null; + for (int i = 0; i < dosesToDrink && Microbot.getClient().getEnergy() < targetEnergy; i++) { + if (currentPotionName == null || !Rs2Inventory.hasItem(currentPotionName)) { + String dosePotionName = getLowestDosePotionName(Rs2Potion.getRestoreEnergyPotionsVariants()); + if (dosePotionName == null || !withdrawPotion(dosePotionName)) { + break; + } + Rs2Inventory.waitForInventoryChanges(1800); + currentPotionName = dosePotionName; + } + if (!drinkPotionDose(currentPotionName)) { + break; + } + currentPotionName = getNextPotionDoseName(currentPotionName); + } + bankPotionRemnants(currentPotionName == null ? null : getBaseName(currentPotionName)); } } } + private void sleepAfterCoalBagFill() { + sleep(Rs2Random.between(COAL_BAG_FILL_DELAY_MIN_MS, COAL_BAG_FILL_DELAY_MAX_MS)); + } + private String getLowestDosePotionName(List variants) { return Rs2Bank.getAll(item -> variants.stream().anyMatch(variant -> item.getName().toLowerCase().contains(variant.toLowerCase()))) .min(Comparator.comparingInt(item -> getDoseFromName(item.getName()))) @@ -455,27 +487,67 @@ private String getBaseName(String itemName) { return itemName; } + private int getEstimatedEnergyRestorePerDose(String potionItemName) { + String lowerName = potionItemName.toLowerCase(); + return lowerName.contains("super energy") ? SUPER_ENERGY_RESTORE_PER_DOSE : ENERGY_RESTORE_PER_DOSE; + } + + private int getAvailableDosesForVariants(List variants) { + return Rs2Bank.getAll(item -> variants.stream().anyMatch(variant -> item.getName().toLowerCase().contains(variant.toLowerCase()))) + .mapToInt(item -> item.getQuantity() * getDoseFromName(item.getName())) + .sum(); + } + + private String getNextPotionDoseName(String potionItemName) { + Matcher matcher = ITEM_NAME_SUFFIX_PATTERN.matcher(potionItemName); + if (matcher.find()) { + String baseName = matcher.group(1).trim(); + int currentDose = Integer.parseInt(matcher.group(2)); + if (currentDose > 1) { + return baseName + "(" + (currentDose - 1) + ")"; + } + return baseName; + } + return potionItemName; + } + private void withdrawAndDrink(String potionItemName) { String baseName = getBaseName(potionItemName); + if (!withdrawPotion(potionItemName)) { + return; + } + Rs2Inventory.waitForInventoryChanges(1800); + if (drinkPotionDose(potionItemName)) { + bankPotionRemnants(baseName); + } + } + + private boolean withdrawPotion(String potionItemName) { boolean withdrewPotion = Rs2Bank.withdrawOne(potionItemName); - if (!withdrewPotion) { - if (Rs2Inventory.isFull()) { - // Wait a full tick for safety - if (!Rs2Inventory.waitForInventoryChanges(600) && Rs2Inventory.isFull()) { - log.debug("Inventory remained full while attempting to withdraw {}", potionItemName); - return; - } - withdrewPotion = Rs2Bank.withdrawOne(potionItemName); - } - if (!withdrewPotion) { - log.debug("Failed to withdraw potion {} from the bank", potionItemName); - return; + if (!withdrewPotion && Rs2Inventory.isFull()) { + // Wait a full tick for safety + if (!Rs2Inventory.waitForInventoryChanges(600) && Rs2Inventory.isFull()) { + log.debug("Inventory remained full while attempting to withdraw {}", potionItemName); + return false; } + withdrewPotion = Rs2Bank.withdrawOne(potionItemName); } - Rs2Inventory.waitForInventoryChanges(1800); - Rs2Inventory.interact(potionItemName, "drink"); - Rs2Inventory.waitForInventoryChanges(1800); - if (Rs2Inventory.hasItem(baseName)) { + if (!withdrewPotion) { + log.debug("Failed to withdraw potion {} from the bank", potionItemName); + return false; + } + return true; + } + + private boolean drinkPotionDose(String potionName) { + if (!Rs2Inventory.interact(potionName, "drink")) { + return false; + } + return Rs2Inventory.waitForInventoryChanges(1800); + } + + private void bankPotionRemnants(String baseName) { + if (baseName != null && Rs2Inventory.hasItem(baseName)) { Rs2Bank.depositOne(baseName); Rs2Inventory.waitForInventoryChanges(1800); } @@ -699,4 +771,3 @@ public boolean fullCoffer() { return coffer == 1; } } - diff --git a/src/main/java/net/runelite/client/plugins/microbot/cooking/scripts/AutoCookingScript.java b/src/main/java/net/runelite/client/plugins/microbot/cooking/scripts/AutoCookingScript.java index bbb5ebcfcd..de47a111d4 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/cooking/scripts/AutoCookingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/cooking/scripts/AutoCookingScript.java @@ -106,7 +106,11 @@ public boolean run(AutoCookingConfig config) { Rs2Camera.turnTo(cookingObject.getLocalLocation()); return; } - Rs2Inventory.useItemOnObject(cookingItem.getRawItemID(), cookingObject.getId()); + if (location == CookingLocation.ROUGES_DEN) { + Rs2GameObject.interact(cookingObject, "Cook"); + } else { + Rs2Inventory.useItemOnObject(cookingItem.getRawItemID(), cookingObject.getId()); + } boolean productionWidgetOpen = Rs2Widget.isProductionWidgetOpen(); if (!productionWidgetOpen) { diff --git a/src/main/java/net/runelite/client/plugins/microbot/crafting/jewelry/JewelryScript.java b/src/main/java/net/runelite/client/plugins/microbot/crafting/jewelry/JewelryScript.java index 338402bf14..a5d0eec865 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/crafting/jewelry/JewelryScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/crafting/jewelry/JewelryScript.java @@ -75,7 +75,7 @@ public boolean run() { Rs2Bank.preHover(); break; case BANKING: - boolean isBankOpen = Rs2Bank.isNearBank(15) ? Rs2Bank.openBank() : Rs2Bank.walkToBankAndUseBank(); + boolean isBankOpen = Rs2Bank.isNearBank(21) ? Rs2Bank.openBank() : Rs2Bank.walkToBankAndUseBank(); if (!isBankOpen || !Rs2Bank.isOpen()) return; diff --git a/src/main/java/net/runelite/client/plugins/microbot/glassmake/GabulhasGlassMakeScript.java b/src/main/java/net/runelite/client/plugins/microbot/glassmake/GabulhasGlassMakeScript.java index 76d1b11f65..a1d4bf2db8 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/glassmake/GabulhasGlassMakeScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/glassmake/GabulhasGlassMakeScript.java @@ -73,7 +73,7 @@ public void shutdown() { } private void takeBreak() { - if (Rs2Random.nextInt(0, 20, 1, true) == 30) { + if (Rs2Random.nextInt(0, 20, 1, true) == 20) { sleep(1000, 20000); } diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrConfig.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrConfig.java index 97dbb93599..b4c361220a 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrConfig.java @@ -4,6 +4,7 @@ import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigInformation; import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.Range; import net.runelite.client.plugins.microbot.gotr.data.Mode; @ConfigGroup("gotr") @@ -40,11 +41,25 @@ default int maxAmountEssence() { return 20; } + @Range( + min = 0, + max = 5 + ) + @ConfigItem( + keyName = "emptySlotsToKeep", + name = "Empty slots to keep", + description = "How many inventory spaces to keep empty before considering inventory effectively full", + position = 3 + ) + default int emptySlotsToKeep() { + return 0; + } + @ConfigItem( keyName = "shouldDepositRunes", name = "Deposit runes?", description = "Should you deposit runes into the deposit pool?", - position = 3 + position = 4 ) default boolean shouldDepositRunes() { return true; @@ -54,7 +69,7 @@ default boolean shouldDepositRunes() { keyName = "useLunarSpellbook", name = "Use lunar spellbook", description = "Switch to lunar spellbook for NPC Contact spell to repair pouches. Disable if using Cordelia repair with pearls.", - position = 4 + position = 5 ) default boolean useLunarSpellbook() { return true; @@ -64,7 +79,7 @@ default boolean useLunarSpellbook() { keyName = "useInventorySetup", name = "Use inventory setup", description = "Use a specific inventory setup instead of progressive equipment management", - position = 5 + position = 6 ) default boolean useInventorySetup() { return false; @@ -74,7 +89,7 @@ default boolean useInventorySetup() { keyName = "inventorySetupName", name = "Inventory setup name", description = "Name of the inventory setup to use (only if 'Use inventory setup' is enabled)", - position = 6 + position = 7 ) default String inventorySetupName() { return ""; diff --git a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java index c36c592c6c..cefee3e9b7 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/gotr/GotrScript.java @@ -187,6 +187,10 @@ public boolean run(GotrConfig config) { if (craftGuardianEssences()) return; + // No fragments left but have essence – enter altar instead of getting stuck + if (!Rs2Inventory.hasItem(GUARDIAN_FRAGMENTS) && Rs2Inventory.hasItem(GUARDIAN_ESSENCE)) { + if (enterAltar()) return; + } } else if (Rs2Inventory.hasItem(GUARDIAN_ESSENCE)) { if (leaveLargeMine()) return; if (enterAltar()) return; @@ -218,7 +222,7 @@ public boolean run(GotrConfig config) { long endTime = System.currentTimeMillis(); totalTime = endTime - startTime; - System.out.println("Total time for loop " + totalTime); + log("Total time for loop " + totalTime + "ms"); } catch (Exception ex) { Microbot.log("Something went wrong in the GOTR Script: " + ex.getMessage() + ". If the script is stuck, please contact us on discord with this log."); @@ -231,7 +235,7 @@ public boolean run(GotrConfig config) { private boolean waitingForGameToStart(int timeToStart) { if (isInHugeMine()) return false; - if (getStartTimer() > Rs2Random.randomGaussian(35, Rs2Random.between(1, 5)) || getStartTimer() == -1 || timeToStart > 10) { + if (getStartTimer() > Rs2Random.randomGaussian(25, Rs2Random.between(1, 3)) || getStartTimer() == -1 || timeToStart > 13) { // Only take cells if we don't already have them if (!Rs2Inventory.hasItem("Uncharged cell")) { @@ -323,7 +327,8 @@ private void takeUnchargedCells() { private boolean usePortal() { if (!isInHugeMine() && Microbot.getClient().hasHintArrow() && Rs2Inventory.count() < config.maxAmountEssence()) { if (leaveLargeMine()) return true; - Rs2Walker.walkFastCanvas(Microbot.getClient().getHintArrowPoint()); + WorldPoint portalPoint = Microbot.getClient().getHintArrowPoint(); + Rs2Walker.walkTo(portalPoint); sleepUntil(Rs2Player::isMoving); Microbot.getRs2TileObjectCache().query().within(Microbot.getClient().getHintArrowPoint(), 0).interact(); log("Found a portal spawn...interacting with it..."); @@ -362,6 +367,9 @@ private boolean enterAltar() { } private boolean craftGuardianEssences() { + if (!Rs2Inventory.hasItem(GUARDIAN_FRAGMENTS)) { + return false; + } if (Microbot.getRs2TileObjectCache().query().interact(ObjectID.WORKBENCH_43754)) { state = GotrState.CRAFT_GUARDIAN_ESSENCE; sleep(Rs2Random.randomGaussian(Rs2Random.between(600, 900), Rs2Random.between(150, 300))); @@ -391,8 +399,14 @@ private boolean fillPouches() { return false; } + private boolean hasConfiguredEmptySlots() { + return Rs2Inventory.emptySlotCount() > config.emptySlotsToKeep(); + } + private boolean isOutOfFragments() { - if ((!Rs2Inventory.hasItem(GUARDIAN_FRAGMENTS) && !Rs2Inventory.isFull()) || (getTimeSincePortal() > 85 && !Rs2Inventory.hasItem(GUARDIAN_ESSENCE))) { + boolean noFragments = !Rs2Inventory.hasItem(GUARDIAN_FRAGMENTS); + boolean noEssence = !Rs2Inventory.hasItem(GUARDIAN_ESSENCE) && !Rs2Inventory.anyPouchFull(); + if ((noFragments && (hasConfiguredEmptySlots() || noEssence)) || (getTimeSincePortal() > 85 && !Rs2Inventory.hasItem(GUARDIAN_ESSENCE))) { shouldMineGuardianRemains = true; if(!Rs2Inventory.hasItem(GUARDIAN_FRAGMENTS)) log("Memorize that we no longer have guardian fragments..."); @@ -408,7 +422,7 @@ private boolean craftRunes() { Rs2TileObjectModel rcAltar = findRcAltar(); if (rcAltar != null) { if (Rs2Player.isMoving()) return true; - if (Rs2Inventory.anyPouchFull() && !Rs2Inventory.isFull()) { + if (Rs2Inventory.anyPouchFull() && hasConfiguredEmptySlots()) { //test when back from gym Rs2Inventory.emptyPouches(); Rs2Inventory.waitForInventoryChanges(5000); sleep(Rs2Random.randomGaussian(350, 150)); @@ -612,16 +626,16 @@ public static boolean isOutsideBarrier() { && Rs2Player.getWorldLocation().getRegionID() == 14484; } - public static boolean isInLargeMine() { + public static boolean isInLargeMine() { int largeMineX = 3637; return Rs2Player.getWorldLocation().getRegionID() == 14484 - && Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation().getX()) >= largeMineX; + && Rs2Player.getWorldLocation().getX() >= largeMineX; } - public boolean isInHugeMine() { + public boolean isInHugeMine() { int hugeMineX = 3594; return Rs2Player.getWorldLocation().getRegionID() == 14484 - && Microbot.getClientThread().invoke(() -> Microbot.getClient().getLocalPlayer().getWorldLocation().getX()) <= hugeMineX; + && Rs2Player.getWorldLocation().getX() <= hugeMineX; } public static boolean isGuardianPortal(GameObject gameObject) { @@ -634,7 +648,7 @@ public ItemManager getItemManager() { public boolean isInMiniGame() { int parentWidgetId = 48889857; - Widget elementalRuneWidget = Microbot.getClient().getWidget(parentWidgetId); + Widget elementalRuneWidget = Rs2Widget.getWidget(parentWidgetId); return elementalRuneWidget != null; } @@ -727,17 +741,20 @@ public static List getAvailableAltars() { .collect(Collectors.toList()); } - if ((config.Mode() == Mode.BALANCED && elementalPoints < catalyticPoints) || config.Mode() == Mode.ELEMENTAL) { - Microbot.log(elementalPoints < catalyticPoints + if (config.Mode() == Mode.ELEMENTAL || config.Mode() == Mode.CATALYTIC || config.Mode() == Mode.BALANCED) { + boolean preferElemental = config.Mode() == Mode.ELEMENTAL + || (config.Mode() == Mode.BALANCED && elementalPoints <= catalyticPoints); + + Microbot.log(preferElemental ? "We have " + elementalPoints + " elemental points, looking for elemental altar..." - : "We have " + catalyticPoints +" catalytic points, looking for catalytic altar..."); + : "We have " + catalyticPoints + " catalytic points, looking for catalytic altar..."); - Microbot.log("Sorting for BALANCED/ELEMENTAL mode (" + - (elementalPoints < catalyticPoints ? "Elemental priority" : "Catalytic priority") + ")"); + Microbot.log("Sorting for " + config.Mode() + " mode (" + + (preferElemental ? "Elemental priority" : "Catalytic priority") + ")"); return availableAltars.stream() .sorted( - (elementalPoints < catalyticPoints) + preferElemental ? Comparator.comparingInt(TileObject::getId) : Comparator.comparingInt(TileObject::getId).reversed() ) @@ -799,4 +816,4 @@ public static boolean leaveMinigame() { GotrScript.isInMiniGame = !isOutsideBarrier() && isInMainRegion(); return !GotrScript.isInMiniGame;// Successfully left the minigame } -} +} \ No newline at end of file diff --git a/src/main/java/net/runelite/client/plugins/microbot/karambwans/GabulhasKarambwansScript.java b/src/main/java/net/runelite/client/plugins/microbot/karambwans/GabulhasKarambwansScript.java index ab99202a8b..e2fda84ec2 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/karambwans/GabulhasKarambwansScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/karambwans/GabulhasKarambwansScript.java @@ -144,7 +144,6 @@ private void useBank() { } - Rs2Bank.closeBank(); sleepUntil(() -> !Rs2Bank.isOpen(), 3000); } @@ -189,7 +188,7 @@ private void walkToFish() { if (interacted) { waitTillPlayerNextToFishingSpot(); } else { - Rs2Player.waitForWalking(); + sleepUntil(() -> Rs2Player.isMoving(), 5000); } } } diff --git a/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java b/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java index d10598a152..4721eb2e29 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java +++ b/src/main/java/net/runelite/client/plugins/microbot/moonsofperil/handlers/ResupplyHandler.java @@ -102,6 +102,9 @@ private void makeMoonlightPotions(int moonlightPotionsQuantum) { sleepUntil(() -> !Rs2Player.isAnimating(), 10_000); } + /* Decant partial potions to 4-dose */ + decantMoonlightPotions(); + /* Drop leftovers */ while (Rs2Inventory.contains(ItemID.PESTLE_AND_MORTAR)) Rs2Inventory.drop(ItemID.PESTLE_AND_MORTAR); sleep(600); @@ -199,16 +202,70 @@ private void cookBream() { Rs2Walker.walkFastCanvas(new WorldPoint(1512, 9693, 0)); while (Rs2Inventory.contains(ItemID.BREAM_FISH_RAW)) { - if (!Rs2Player.isAnimating()) { + if (!Rs2Player.isAnimating(1200) || !Rs2Player.isInteracting()) { if (Microbot.getRs2TileObjectCache().query().interact(ObjectID.PMOON_RANGE, "Cook")) { - sleep(600, 900); + sleepUntil(() -> !Rs2Player.isAnimating() || !Rs2Inventory.contains(ItemID.BREAM_FISH_RAW), 30_000); } } - sleep(900, 1200); + sleep(600); } if (debugLogging) {Microbot.log("Finished cooking bream.");} } + private void decantMoonlightPotions() { + if (debugLogging) { Microbot.log("Decanting moonlight potions to 4-dose"); } + + int[] partialDoseIds = { + ItemID._3DOSEMOONLIGHTPOTION, + ItemID._2DOSEMOONLIGHTPOTION, + ItemID._1DOSEMOONLIGHTPOTION + }; + + boolean combined = true; + while (combined) { + combined = false; + + int partialCount = 0; + for (int id : partialDoseIds) { + partialCount += Rs2Inventory.count(id); + } + if (partialCount < 2) break; + + int firstId = -1; + int secondId = -1; + for (int id : partialDoseIds) { + int count = Rs2Inventory.count(id); + if (count > 0) { + if (firstId == -1) { + firstId = id; + if (count >= 2) { + secondId = id; + break; + } + } else { + secondId = id; + break; + } + } + } + + if (firstId != -1 && secondId != -1) { + if (Rs2Inventory.combine(firstId, secondId)) { + Rs2Inventory.waitForInventoryChanges(2_000); + combined = true; + } + } + sleep(300, 600); + } + + while (Rs2Inventory.contains(ItemID.VIAL_EMPTY)) { + Rs2Inventory.drop(ItemID.VIAL_EMPTY); + sleep(300, 600); + } + + if (debugLogging) { Microbot.log("Decanting complete. 4-dose potions: " + Rs2Inventory.count(ItemID._4DOSEMOONLIGHTPOTION)); } + } + /** * Return int: the total number of Moonlight Potions currently in inventory */ diff --git a/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java b/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java index 119de97c91..42e9d6522f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/motherloadmine/MotherloadMineScript.java @@ -348,16 +348,17 @@ private void depositHopper() return; } } - - WorldPoint hopperDeposit = (isUpperFloor() && config.upstairsHopperUnlocked()) ? HOPPER_DEPOSIT_UP : HOPPER_DEPOSIT_DOWN; - Rs2TileObjectModel hopper = rs2TileObjectCache.query().where(x -> x.getWorldLocation().equals(hopperDeposit)).withId(ObjectID.MOTHERLODE_HOPPER).first(); - if(isUpperFloor() && !config.upstairsHopperUnlocked()) { ensureLowerFloor(); } - + final int paydirtToDeposit = payDirtCount(); + sleep(800, 1200); + + WorldPoint hopperDeposit = (isUpperFloor() && config.upstairsHopperUnlocked()) ? HOPPER_DEPOSIT_UP : HOPPER_DEPOSIT_DOWN; + Rs2TileObjectModel hopper = rs2TileObjectCache.query().where(x -> x.getWorldLocation().equals(hopperDeposit)).withId(ObjectID.MOTHERLODE_HOPPER).first(); + if (hopper != null && hopper.click()) { log.debug("Depositing pay-dirt into hopper"); @@ -548,6 +549,11 @@ private boolean walkToMiningSpot() return false; // Wait until we've gone up } + if (miningSpot.isUpstairs() && isUpperFloor()) + { + return true; // if on upper floor lets mine... instead of walking + } + if (miningSpot.isDownstairs() && isUpperFloor()) { goDown(); return false; // Wait until we've gone down diff --git a/src/main/java/net/runelite/client/plugins/microbot/plankrunner/PlankRunnerScript.java b/src/main/java/net/runelite/client/plugins/microbot/plankrunner/PlankRunnerScript.java index 5793a99b4c..d46d40046e 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/plankrunner/PlankRunnerScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/plankrunner/PlankRunnerScript.java @@ -13,6 +13,7 @@ import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.inventory.Rs2ItemModel; import net.runelite.client.plugins.microbot.util.misc.Rs2Potion; +import net.runelite.client.plugins.microbot.util.camera.Rs2Camera; import net.runelite.client.plugins.microbot.util.player.Rs2Player; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; @@ -74,41 +75,35 @@ public boolean run() { Rs2Inventory.waitForInventoryChanges(1800); } - if (plugin.isUseEnergyRestorePotions() && Rs2Player.getRunEnergy() <= plugin.getDrinkAtPercent()) { + while (plugin.isUseEnergyRestorePotions() && Rs2Player.getRunEnergy() <= plugin.getDrinkAtPercent()) { boolean hasStaminaPotion = Rs2Bank.hasItem(Rs2Potion.getStaminaPotion()); boolean hasEnergyRestorePotion = Rs2Bank.hasItem(Rs2Potion.getRestoreEnergyPotionsVariants()); - + + if (!hasStaminaPotion && !hasEnergyRestorePotion) { + Microbot.showMessage("Unable to find Stamina Potion OR Energy Restore Potions"); + shutdown(); + return; + } + if ((Rs2Player.hasStaminaBuffActive() && hasEnergyRestorePotion) || (!hasStaminaPotion && hasEnergyRestorePotion)) { Rs2ItemModel energyRestoreItem = Rs2Bank.bankItems().stream() .filter(rs2Item -> Rs2Potion.getRestoreEnergyPotionsVariants().stream() .anyMatch(variant -> rs2Item.getName().toLowerCase().contains(variant.toLowerCase()))) - .min(Comparator.comparingInt(rs2Item -> getDoseFromName(rs2Item.getName()))) + .max(Comparator.comparingInt(rs2Item -> getDoseFromName(rs2Item.getName()))) .orElse(null); - - if (energyRestoreItem == null) { - Microbot.showMessage("Unable to find Restore Energy Potion but hasItem?"); - shutdown(); - return; - } - + + if (energyRestoreItem == null) break; + withdrawAndDrink(energyRestoreItem.getName()); } else if (hasStaminaPotion) { Rs2ItemModel staminaPotionItem = Rs2Bank.bankItems().stream() .filter(rs2Item -> rs2Item.getName().toLowerCase().contains(Rs2Potion.getStaminaPotion().toLowerCase())) - .min(Comparator.comparingInt(rs2Item -> getDoseFromName(rs2Item.getName()))) + .max(Comparator.comparingInt(rs2Item -> getDoseFromName(rs2Item.getName()))) .orElse(null); - - if (staminaPotionItem == null) { - Microbot.showMessage("Unable to find Stamina Potion but hasItem?"); - shutdown(); - return; - } - + + if (staminaPotionItem == null) break; + withdrawAndDrink(staminaPotionItem.getName()); - } else { - Microbot.showMessage("Unable to find Stamina Potion OR Energy Restore Potions"); - shutdown(); - return; } } @@ -124,25 +119,22 @@ public boolean run() { sleepUntil(() -> !Rs2Bank.isOpen()); break; case RUNNING_TO_SAWMILL: - boolean isNearSawmill = Rs2Walker.getTotalTiles(plugin.getSawmillLocation().getWorldPoint()) < 15; - if (!isNearSawmill) { - Microbot.status = "Running to Sawmill"; - Rs2Walker.walkTo(plugin.getSawmillLocation().getWorldPoint()); - return; - } - Set sawmillNpcs = Set.of(NpcID.POH_SAWMILL_OPP, NpcID.AUBURN_SAWMILL_OPERATOR); var sawmillOperator = Microbot.getRs2NpcCache().query() .where(n -> sawmillNpcs.contains(n.getId())) .nearest(); - if (sawmillOperator == null) { - Microbot.showMessage("Unable to find Sawmill Operator!"); - shutdown(); + if (sawmillOperator != null && Rs2Camera.isTileOnScreen(sawmillOperator.getLocalLocation())) { + Rs2Walker.setTarget(null); + sleepUntil(() -> !Rs2Player.isMoving()); + sawmillOperator.click("Buy-plank"); + } else { + Microbot.status = "Running to Sawmill"; + if (!Rs2Player.isMoving()) { + Rs2Walker.walkFastCanvas(plugin.getSawmillLocation().getWorldPoint()); + } return; } - - sawmillOperator.click("Buy-plank"); Microbot.status = "Buying Planks"; Rs2Dialogue.sleepUntilHasCombinationDialogue(); Rs2Dialogue.clickCombinationOption(plugin.getPlank().getDialogueOption()); @@ -193,8 +185,10 @@ private void withdrawAndDrink(String potionItemName) { String simplifiedPotionName = potionItemName.replaceAll("\\s*\\(\\d+\\)", "").trim(); Rs2Bank.withdrawOne(potionItemName); Rs2Inventory.waitForInventoryChanges(1800); - Rs2Inventory.interact(potionItemName, "drink"); - Rs2Inventory.waitForInventoryChanges(1800); + while (Rs2Player.getRunEnergy() <= plugin.getDrinkAtPercent() && Rs2Inventory.hasItem(simplifiedPotionName)) { + Rs2Inventory.interact(simplifiedPotionName, "drink"); + Rs2Inventory.waitForInventoryChanges(1800); + } if (Rs2Inventory.hasItem(simplifiedPotionName)) { Rs2Bank.depositOne(simplifiedPotionName); Rs2Inventory.waitForInventoryChanges(1800); diff --git a/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingPlugin.java index 48180a70e9..c26b3e216f 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingPlugin.java @@ -29,7 +29,7 @@ ) @Slf4j public class AutoSmeltingPlugin extends Plugin { - public static final String version = "1.0.3"; + public static final String version = "1.0.4"; @Inject private AutoSmeltingConfig config; @Provides diff --git a/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingScript.java b/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingScript.java index 9599fc8865..31b82301da 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/smelting/AutoSmeltingScript.java @@ -11,10 +11,12 @@ import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.player.Rs2Player; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; +import java.awt.event.KeyEvent; import java.text.MessageFormat; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -58,7 +60,7 @@ public boolean run(AutoSmeltingConfig config) { } Rs2Player.waitForWalking(); sleep(600,1200); - if (!Rs2Player.isInMemberWorld()) { + if (!Rs2Player.isInMemberWorld() || config.SELECTED_BAR_TYPE().getId() == ItemID.MOLTEN_GLASS) { Rs2Bank.depositAll(); } else if (Rs2Player.isMember()) Rs2Bank.depositAllExcept(coalBag); if (config.SELECTED_BAR_TYPE().getId() == ItemID.IRON_BAR && Rs2Bank.hasItem(ItemID.RING_OF_FORGING) && !Rs2Equipment.isWearing(ItemID.RING_OF_FORGING)) { @@ -112,16 +114,14 @@ public boolean run(AutoSmeltingConfig config) { .within(initialPlayerLocation, 20) .nearest(); if (oneClickFurnace != null) { - if (Rs2Bank.isOpen()){ - Rs2Bank.closeBank(); - sleepUntil(() -> !Rs2Bank.isOpen(), 1000); - } oneClickFurnace.click("smelt"); sleepUntil(Rs2Player::isMoving, 1000); sleepUntil(() -> !Rs2Player.isMoving(), 4000); - Rs2Widget.sleepUntilHasWidgetText("What would you like to smelt?", 270, 5, false, 4000); - Rs2Widget.clickWidget(config.SELECTED_BAR_TYPE().getName()); - Rs2Widget.sleepUntilHasNotWidgetText("What would you like to smelt?", 270, 5, false, 4000); + String widgetText1 = config.SELECTED_BAR_TYPE().getId() == ItemID.MOLTEN_GLASS ? "How many do you wish to make?" : "What would you like to smelt?"; + Rs2Widget.sleepUntilHasWidgetText(widgetText1, 270, 5, false, 5000); + sleep(300, 950); + Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); + Rs2Widget.sleepUntilHasNotWidgetText(widgetText1, 270, 5, false, 5000); Rs2Antiban.actionCooldown(); Rs2Antiban.takeMicroBreakByChance(); return; @@ -142,9 +142,10 @@ public boolean run(AutoSmeltingConfig config) { .nearest(); if (furnace != null) { furnace.click("smelt"); - Rs2Widget.sleepUntilHasWidgetText("What would you like to smelt?", 270, 5, false, 4000); - Rs2Widget.clickWidget(config.SELECTED_BAR_TYPE().getName()); - Rs2Widget.sleepUntilHasNotWidgetText("What would you like to smelt?", 270, 5, false, 4000); + String widgetText2 = config.SELECTED_BAR_TYPE().getId() == ItemID.MOLTEN_GLASS ? "How many do you wish to make?" : "What would you like to smelt?"; + Rs2Widget.sleepUntilHasWidgetText(widgetText2, 270, 5, false, 5000); + Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); + Rs2Widget.sleepUntilHasNotWidgetText(widgetText2, 270, 5, false, 5000); Rs2Antiban.actionCooldown(); Rs2Antiban.takeMicroBreakByChance(); } diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/State.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/State.java index 636e46cc85..4b57d61356 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/State.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/State.java @@ -11,6 +11,7 @@ public enum State { LOOT, EAT(false), DROP(false), + SEED_VAULT(false), SHADOW_VEIL(false), PICKPOCKET, HOP, diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingConfig.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingConfig.java index a1e4eaee6c..9c3e76bbfb 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingConfig.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingConfig.java @@ -50,6 +50,17 @@ default boolean ardougneAreaCheck() { return false; } + @ConfigItem( + keyName = "farmingGuildMasterFarmer", + name = "Farming Guild Master Farmer", + description = "When thieving Master Farmers, target only those inside the Farming Guild. If your Farming level is below 65, focuses on the master farmer in the south (around y=3728).", + position = 2, + section = generalSection + ) + default boolean farmingGuildMasterFarmer() { + return false; + } + @ConfigSection( name = "Buffs", description = "Buffs and spell-casting options.", @@ -213,4 +224,15 @@ default int dodgyNecklaceAmount() { default String DoNotDropItemList() { return ""; } + + @ConfigItem( + keyName = "useSeedVault", + name = "Use Seed Vault", + description = "Walk to the Farming Guild seed vault to store seeds instead of dropping items when inventory is full.", + position = 4, + section = coinPouchSection + ) + default boolean useSeedVault() { + return false; + } } \ No newline at end of file diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingData.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingData.java index 9861537b5c..aaf99b1633 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingData.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingData.java @@ -11,8 +11,19 @@ public final class ThievingData { public static final WorldPoint NULL_WORLD_POINT = new WorldPoint(-1,-1,-1); public static final WorldPoint OUTSIDE_HALLOWED_BANK = new WorldPoint(3654,3384,0); + /** Farming Guild seed vault object ID */ + public static final int SEED_VAULT_OBJECT_ID = 26206; + /** Approximate tile in front of the Farming Guild seed vault */ + public static final WorldPoint SEED_VAULT_LOCATION = new WorldPoint(1243, 3740, 0); public static final WorldArea ARDOUGNE_AREA = new WorldArea(2649, 3280, 7, 8, 0); + /** Farming Guild area (all tiers) */ + public static final WorldArea FARMING_GUILD_AREA = new WorldArea(1215, 3715, 55, 60, 0); + /** NPC IDs for the Master Farmers in the southern (low-level accessible) section of the Farming Guild */ + public static final Set FARMING_GUILD_SOUTH_NPC_IDS = Set.of(11940, 5731); + /** Farming level required to access the full (upper) section of the Farming Guild */ + public static final int FARMING_GUILD_MID_FARMING_LEVEL = 85; + public static final Set VYRE_SET = Set.of( "vyre noble shoes", "vyre noble legs", diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java index 95dca2bc69..b641975055 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingPlugin.java @@ -34,7 +34,7 @@ ) @Slf4j public class ThievingPlugin extends Plugin { - public static final String version = "2.1.0"; + public static final String version = "2.1.1"; @Inject @Getter diff --git a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java index d0e99a53bf..6f38fc0a28 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/thieving/ThievingScript.java @@ -12,6 +12,7 @@ import net.runelite.api.GameState; import net.runelite.api.Skill; import net.runelite.api.events.ChatMessage; +import net.runelite.api.gameval.InterfaceID; import net.runelite.client.plugins.microbot.Microbot; import net.runelite.client.plugins.microbot.Script; import net.runelite.client.plugins.microbot.api.tileitem.Rs2TileItemCache; @@ -25,8 +26,8 @@ import net.runelite.client.plugins.microbot.util.bank.enums.BankLocation; import net.runelite.client.plugins.microbot.util.coords.Rs2WorldPoint; import net.runelite.client.plugins.microbot.util.equipment.Rs2Equipment; +import net.runelite.client.plugins.microbot.util.gameobject.Rs2GameObject; import net.runelite.client.plugins.microbot.util.grounditem.Rs2GroundItem; -import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel; import net.runelite.client.plugins.microbot.util.inventory.Rs2Inventory; import net.runelite.client.plugins.microbot.util.magic.Rs2Magic; import net.runelite.client.plugins.microbot.util.models.RS2Item; @@ -35,9 +36,13 @@ import net.runelite.client.plugins.microbot.util.security.Login; import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.walker.WalkerState; +import net.runelite.client.plugins.microbot.util.keyboard.Rs2Keyboard; import net.runelite.client.plugins.microbot.util.prayer.Rs2Prayer; import net.runelite.client.plugins.microbot.util.prayer.Rs2PrayerEnum; +import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; +import net.runelite.api.widgets.WidgetID; import net.runelite.client.plugins.skillcalculator.skills.MagicAction; +import java.awt.event.KeyEvent; import java.time.Duration; import java.time.Instant; @@ -74,12 +79,11 @@ public class ThievingScript extends Script { @Getter(AccessLevel.PROTECTED) private volatile boolean underAttack; - protected volatile long forceShadowVeilActive = System.currentTimeMillis()-1_000; + protected volatile long forceShadowVeilActive = System.currentTimeMillis() - 1_000; private long nextShadowVeil = 0; private long startupGraceUntil = 0; private final Map npcStrategies = Map.of( - ThievingNpc.WEALTHY_CITIZEN, new WealthyCitizenStrategy() - ); + ThievingNpc.WEALTHY_CITIZEN, new WealthyCitizenStrategy()); private static final int DOOR_CHECK_RADIUS = 10; private static final ActionTimer DOOR_TIMER = new ActionTimer(); @@ -101,8 +105,10 @@ protected static int getCloseDoorTime() { * @return the total runtime of the script */ public Instant startTime; + public Duration getRunTime() { - if (startTime == null) return Duration.ofSeconds(0); + if (startTime == null) + return Duration.ofSeconds(0); return Duration.between(startTime, Instant.now()); } @@ -112,12 +118,13 @@ public ThievingScript(final ThievingConfig config, final ThievingPlugin plugin) this.plugin = plugin; } -ThievingNpcStrategy getActiveStrategy() { - return npcStrategies.get(config.THIEVING_NPC()); -} + ThievingNpcStrategy getActiveStrategy() { + return npcStrategies.get(config.THIEVING_NPC()); + } private String getNpcName(Rs2NpcModel npc) { - if (npc == null) return null; + if (npc == null) + return null; return Microbot.getClientThread().runOnClientThreadOptional(npc::getName).orElse(null); } @@ -127,28 +134,34 @@ private boolean shouldHop() { private State applyOverride(State state) { final ThievingNpcStrategy strategy = getActiveStrategy(); - if (strategy == null) return state; + if (strategy == null) + return state; final State overridden = strategy.overrideState(this, state); return overridden == null ? state : overridden; } private Predicate validateName(Predicate stringPredicate) { return npc -> { - if (npc == null) return false; + if (npc == null) + return false; final String name = getNpcName(npc); - if (name == null) return false; + if (name == null) + return false; return stringPredicate.test(name); }; } protected String getThievingNpcName() { final Rs2NpcModel npc = thievingNpc; - if (npc == null) return "null"; - else return getNpcName(thievingNpc); + if (npc == null) + return "null"; + else + return getNpcName(thievingNpc); } /** - * Ensure we have a current thieving NPC reference; refreshes the cache if needed. + * Ensure we have a current thieving NPC reference; refreshes the cache if + * needed. */ public Rs2NpcModel ensureThievingNpc() { if (isNpcNull(thievingNpc)) { @@ -168,7 +181,8 @@ private Predicate getThievingNpcFilter() { final ThievingNpcStrategy strategy = getActiveStrategy(); if (strategy != null) { final Predicate customFilter = strategy.npcFilter(this); - if (customFilter != null) return customFilter; + if (customFilter != null) + return customFilter; } switch (finalNpc) { case VYRES: @@ -176,11 +190,24 @@ private Predicate getThievingNpcFilter() { break; case ARDOUGNE_KNIGHT: filter = validateName("knight of ardougne"::equalsIgnoreCase); - if (config.ardougneAreaCheck()) filter = filter.and(npc -> ThievingData.ARDOUGNE_AREA.contains(npc.getWorldLocation())); + if (config.ardougneAreaCheck()) + filter = filter.and(npc -> ThievingData.ARDOUGNE_AREA.contains(npc.getWorldLocation())); break; case ELVES: filter = validateName(ThievingData.ELVES::contains); break; + case MASTER_FARMER: + filter = validateName(name -> name.toLowerCase().contains(finalNpc.getName())); + if (config.farmingGuildMasterFarmer()) { + int farmingLevel = Microbot.getClient().getRealSkillLevel(Skill.FARMING); + if (farmingLevel < ThievingData.FARMING_GUILD_MID_FARMING_LEVEL) { + filter = filter.and(npc -> ThievingData.FARMING_GUILD_SOUTH_NPC_IDS.contains(npc.getId())); + } else { + filter = filter + .and(npc -> ThievingData.FARMING_GUILD_AREA.contains(npc.getWorldLocation())); + } + } + break; default: filter = validateName(name -> name.toLowerCase().contains(finalNpc.getName())); break; @@ -210,37 +237,37 @@ private Rs2NpcModel getThievingNpcCache() { .filter(n -> !isNpcNull(n)) .min(comparator); - if (npcOptional.isEmpty()) return null; + if (npcOptional.isEmpty()) + return null; Rs2NpcModel npc = npcOptional.get(); if (startingNpc == null && config.THIEVING_NPC() == ThievingNpc.VYRES) { startingNpc = getNpcName(npc); log.debug("Set starting npc to {}", startingNpc); } - + log.debug("Found new NPC={} to thieve @ {}", getNpcName(npc), toString(npc.getWorldLocation())); return npc; } private T getAttackingNpcs(Function, T> consumer, T defaultValue) { final Player me = Microbot.getClient().getLocalPlayer(); - if (me == null) return defaultValue; + if (me == null) + return defaultValue; final Rs2NpcModel[] npcs = Microbot.getRs2NpcCache().query().toList().toArray(Rs2NpcModel[]::new); - if (npcs.length == 0) return defaultValue; - - final Predicate customFilter = config.THIEVING_NPC() == ThievingNpc.VYRES ? - Rs2NpcModel.matches(true, "vyrewatch sentinel") : - npc -> true; - - return Microbot.getClientThread().runOnClientThreadOptional(() -> - consumer.apply(Arrays.stream(npcs) - .filter(npc -> npc.getCombatLevel() > 0) - .filter(getThievingNpcFilter().negate()) - .filter(customFilter) - .filter(npc -> !isNpcNull(npc)) - .filter(n -> me.equals(n.getInteracting())) - ) - ).orElse(defaultValue); + if (npcs.length == 0) + return defaultValue; + + final Predicate customFilter = config.THIEVING_NPC() == ThievingNpc.VYRES + ? Rs2NpcModel.matches(true, "vyrewatch sentinel") + : npc -> true; + + return Microbot.getClientThread().runOnClientThreadOptional(() -> consumer.apply(Arrays.stream(npcs) + .filter(npc -> npc.getCombatLevel() > 0) + .filter(getThievingNpcFilter().negate()) + .filter(customFilter) + .filter(npc -> !isNpcNull(npc)) + .filter(n -> me.equals(n.getInteracting())))).orElse(defaultValue); } private boolean isBeingAttackByNpc() { @@ -249,11 +276,12 @@ private boolean isBeingAttackByNpc() { private Rs2NpcModel getAttackingNpc() { final WorldPoint myLoc = Rs2Player.getWorldLocation(); - if (myLoc == null) return null; - return getAttackingNpcs(npcs -> - npcs.min(Comparator.comparingInt(npc -> myLoc.distanceTo(npc.getWorldLocation()))) - .orElse(null), null - ); + if (myLoc == null) + return null; + return getAttackingNpcs( + npcs -> npcs.min(Comparator.comparingInt(npc -> myLoc.distanceTo(npc.getWorldLocation()))) + .orElse(null), + null); } private int getMostExpensiveGroundItemId() { @@ -275,16 +303,20 @@ private int getMostExpensiveGroundItemId() { private State getCurrentState() { // Grace period right after startup/login to allow inventory/bank to populate. - if (System.currentTimeMillis() < startupGraceUntil) return State.WALK_TO_START; + if (System.currentTimeMillis() < startupGraceUntil) + return State.WALK_TO_START; - if (getMostExpensiveGroundItemId() != -1) return applyOverride(State.LOOT); + if (getMostExpensiveGroundItemId() != -1) + return applyOverride(State.LOOT); if (config.escapeAttacking() && (underAttack || isBeingAttackByNpc())) { - if (!underAttack) underAttack = true; + if (!underAttack) + underAttack = true; return applyOverride(State.ESCAPE); } - if (!hasReqs()) return applyOverride(State.BANK); + if (!hasReqs()) + return applyOverride(State.BANK); if (config.useFood()) { boolean needToEat = Rs2Player.getHealthPercentage() <= config.hitpoints(); @@ -294,16 +326,18 @@ private State getCurrentState() { return applyOverride(State.EAT); } - if (config.food() == ThievingFood.ANCIENT_BREW && - Rs2Player.getHealthPercentage() <= 15 && - !Rs2Prayer.isPrayerActive(Rs2PrayerEnum.REDEMPTION)) { + if (config.food() == ThievingFood.ANCIENT_BREW && + Rs2Player.getHealthPercentage() <= 15 && + !Rs2Prayer.isPrayerActive(Rs2PrayerEnum.REDEMPTION)) { Rs2Prayer.toggle(Rs2PrayerEnum.REDEMPTION, true); } } - if (Rs2Inventory.isFull()) return applyOverride(State.DROP); + if (Rs2Inventory.isFull()) + return applyOverride(config.useSeedVault() ? State.SEED_VAULT : State.DROP); - if (isNpcNull(thievingNpc) && (thievingNpc = getThievingNpcCache()) == null && shouldHop()) return applyOverride(State.HOP); + if (isNpcNull(thievingNpc) && (thievingNpc = getThievingNpcCache()) == null && shouldHop()) + return applyOverride(State.HOP); if (config.THIEVING_NPC() == ThievingNpc.VYRES) { final WorldPoint[] housePolygon = ThievingData.getVyreHouse(getNpcName(thievingNpc)); @@ -318,8 +352,10 @@ private State getCurrentState() { } if (!isPointInPolygon(housePolygon, thievingNpc.getWorldLocation())) { - if (!sleepUntil(() -> isPointInPolygon(housePolygon, thievingNpc.getWorldLocation()), 1_200 + (int)(Math.random() * 1_800))) { - log.debug("Vyre='{}' outside house @ {}", getNpcName(thievingNpc), toString(thievingNpc.getWorldLocation())); + if (!sleepUntil(() -> isPointInPolygon(housePolygon, thievingNpc.getWorldLocation()), + 1_200 + (int) (Math.random() * 1_800))) { + log.debug("Vyre='{}' outside house @ {}", getNpcName(thievingNpc), + toString(thievingNpc.getWorldLocation())); return applyOverride(State.HOP); } } @@ -329,36 +365,40 @@ private State getCurrentState() { } // delayed door closing logic - List doors = getDoors(Rs2Player.getWorldLocation(), DOOR_CHECK_RADIUS); + List doors = getDoors(Rs2Player.getWorldLocation(), DOOR_CHECK_RADIUS); if (doors.isEmpty()) { DOOR_TIMER.unset(); } else if (DOOR_TIMER.isSet()) { if (DOOR_TIMER.isTime()) { final long current = System.currentTimeMillis(); - // did we close the door 3 times in the last 2min? (probably someone troll opening door) + // did we close the door 3 times in the last 2min? (probably someone troll + // opening door) if (Arrays.stream(doorCloseTime).allMatch(time -> time != 0 && current - time < 120_000)) { Arrays.fill(doorCloseTime, 0); return applyOverride(State.HOP); } doorCloseTime[doorCloseIndex] = current; - doorCloseIndex = (doorCloseIndex+1) % doorCloseTime.length; + doorCloseIndex = (doorCloseIndex + 1) % doorCloseTime.length; return applyOverride(State.CLOSE_DOOR); } } else { // delayed door closing log.debug("Found {} open door(s).", doors.size()); - DOOR_TIMER.set(System.currentTimeMillis()+3_000+(int) (Math.random()*4_000)); + DOOR_TIMER.set(System.currentTimeMillis() + 3_000 + (int) (Math.random() * 4_000)); } } - if (shouldOpenCoinPouches()) return applyOverride(State.COIN_POUCHES); + if (shouldOpenCoinPouches()) + return applyOverride(State.COIN_POUCHES); - if (shouldCastShadowVeil()) return applyOverride(State.SHADOW_VEIL); + if (shouldCastShadowVeil()) + return applyOverride(State.SHADOW_VEIL); return applyOverride(State.PICKPOCKET); } protected boolean shouldRun() { - if (!Microbot.isLoggedIn()) return false; + if (!Microbot.isLoggedIn()) + return false; return true; } @@ -370,7 +410,8 @@ public void sleepBriefly(int min, int max) { sleep(min, max); } - private boolean sleepUntilWithInterrupt(BooleanSupplier awaitedCondition, BooleanSupplier interruptCondition, int time) { + private boolean sleepUntilWithInterrupt(BooleanSupplier awaitedCondition, BooleanSupplier interruptCondition, + int time) { final AtomicBoolean interrupted = new AtomicBoolean(false); final boolean result = sleepUntil(() -> { if (interruptCondition.getAsBoolean()) { @@ -379,7 +420,8 @@ private boolean sleepUntilWithInterrupt(BooleanSupplier awaitedCondition, Boolea } return awaitedCondition.getAsBoolean(); }, time); - if (interrupted.get()) throw new SelfInterruptException("Should not be running"); + if (interrupted.get()) + throw new SelfInterruptException("Should not be running"); return result; } @@ -399,7 +441,8 @@ private boolean walkTo(String info, WorldPoint dst, int distance) { } private void loop() { - if (!shouldRun()) return; + if (!shouldRun()) + return; if (cleanNpc) { cleanNpc = false; thievingNpc = null; @@ -415,7 +458,8 @@ private void loop() { currentState = getCurrentState(); if (currentState.isAwaitStuns()) { // some actions like eating/dropping can be done while stunned // await stun from most recent pickpocket action - if (lastAction+600 > System.currentTimeMillis() && (currentState != State.PICKPOCKET || !config.ignoreStuns())) { + if (lastAction + 600 > System.currentTimeMillis() + && (currentState != State.PICKPOCKET || !config.ignoreStuns())) { currentState = State.STUNNED; sleepUntilWithInterrupt(Rs2Player::isStunned, 600); sleepUntilWithInterrupt(() -> !Rs2Player.isStunned(), 10_000); @@ -423,10 +467,11 @@ private void loop() { } } - if (currentState != State.PICKPOCKET) log.debug("State {}", currentState); + if (currentState != State.PICKPOCKET) + log.debug("State {}", currentState); final WorldPoint myLoc; - switch(currentState) { + switch (currentState) { case LOOT: final int id = getMostExpensiveGroundItemId(); if (id == -1) return; @@ -443,19 +488,22 @@ private void loop() { WorldPoint escape = null; final String escapeLocationString = config.customEscapeLocation(); if (!escapeLocationString.isBlank()) { - final String[] split = Arrays.stream(escapeLocationString.split(",")).map(String::strip).toArray(String[]::new); + final String[] split = Arrays.stream(escapeLocationString.split(",")).map(String::strip) + .toArray(String[]::new); if (split.length == 3) { try { - escape = new WorldPoint(Integer.parseInt(split[0]),Integer.parseInt(split[1]),Integer.parseInt(split[2])); + escape = new WorldPoint(Integer.parseInt(split[0]), Integer.parseInt(split[1]), + Integer.parseInt(split[2])); } catch (NumberFormatException e) { log.error("Invalid custom escape location={}", escapeLocationString); } } } if (escape == null) { - if (thievingNpc == null) thievingNpc = getThievingNpcCache(); - final String name = thievingNpc == null ? null : getNpcName(thievingNpc); - escape = name == null ? ThievingData.NULL_WORLD_POINT : ThievingData.getVyreEscape(name); + if (thievingNpc == null) + thievingNpc = getThievingNpcCache(); + final String name = thievingNpc == null ? null : getNpcName(thievingNpc); + escape = name == null ? ThievingData.NULL_WORLD_POINT : ThievingData.getVyreEscape(name); } if (escape != ThievingData.NULL_WORLD_POINT) { walkTo("Escaping", escape, 3); @@ -463,7 +511,8 @@ private void loop() { if (myLoc != null && myLoc.distanceTo(escape) < 10) { if (underAttack) { underAttack = false; - if (!isRunning()) return; + if (!isRunning()) + return; if (shouldHop()) { hopWorld(); } else { @@ -497,7 +546,11 @@ private void loop() { return; case DROP: dropAllExceptImportant(); - if (Rs2Inventory.isFull()) Rs2Player.eatAt(99); + if (Rs2Inventory.isFull()) + Rs2Player.eatAt(99); + return; + case SEED_VAULT: + useSeedVault(); return; case HOP: if (shouldHop()) { @@ -507,7 +560,8 @@ private void loop() { } return; case COIN_POUCHES: - repeatedAction(() -> Rs2Inventory.interact("coin pouch", "Open-all"), () -> !Rs2Inventory.hasItem("coin pouch"), 3); + repeatedAction(() -> Rs2Inventory.interact("coin pouch", "Open-all"), + () -> !Rs2Inventory.hasItem("coin pouch"), 3); return; case WALK_TO_START: myLoc = Rs2Player.getWorldLocation(); @@ -531,13 +585,15 @@ private void loop() { castShadowVeil(); return; case CLOSE_DOOR: - if (isNpcNull(thievingNpc)) return; + if (isNpcNull(thievingNpc)) + return; final String name = getNpcName(thievingNpc); final WorldPoint[] house = ThievingData.getVyreHouse(name); final WorldPoint myLoc2 = Rs2Player.getWorldLocation(); if (isPointInPolygon(house, myLoc2)) { log.debug("Closing door {} in {} house", toString(myLoc2), name); - if (closeNearbyDoor(DOOR_CHECK_RADIUS)) DOOR_TIMER.unset(); + if (closeNearbyDoor(DOOR_CHECK_RADIUS)) + DOOR_TIMER.unset(); } else if (isPointInPolygon(house, thievingNpc.getWorldLocation())) { walkTo("Walk to npc", thievingNpc.getWorldLocation(), 1); } else { @@ -550,7 +606,8 @@ private void loop() { } return; case PICKPOCKET: - if (Rs2Inventory.hasItem(ThievingData.ROGUE_SET.toArray(String[]::new)) && !isWearing(ThievingData.ROGUE_SET)) { + if (Rs2Inventory.hasItem(ThievingData.ROGUE_SET.toArray(String[]::new)) + && !isWearing(ThievingData.ROGUE_SET)) { // only equip if we are safely in the house w/ the npc myLoc = Rs2Player.getWorldLocation(); final Rs2NpcModel npc = thievingNpc; @@ -581,18 +638,21 @@ private void loop() { } // limit is so breaks etc. don't cause a high last action time - long timeSince = Math.min(System.currentTimeMillis()-lastAction, 1_000); + long timeSince = Math.min(System.currentTimeMillis() - lastAction, 1_000); if (timeSince < 250) { - sleep((int) (250-timeSince) + 50, (int) (250-timeSince) + 250); + sleep((int) (250 - timeSince) + 50, (int) (250 - timeSince) + 250); timeSince = 350; } double rand = Math.random(); - if ((timeSince / 500_000d) > rand) sleep(5_000, 10_000); // around every 500s - if ((timeSince / 30_000d) > rand) sleep(300, 700); // around every 30s + if ((timeSince / 500_000d) > rand) + sleep(5_000, 10_000); // around every 500s + if ((timeSince / 30_000d) > rand) + sleep(300, 700); // around every 30s var highlighted = net.runelite.client.plugins.npchighlight.NpcIndicatorsPlugin.getHighlightedNpcs(); if (highlighted.isEmpty()) { - if (isNpcNull(thievingNpc)) return; + if (isNpcNull(thievingNpc)) + return; Rs2Npc.pickpocket(thievingNpc.getNpc()); } else { Rs2Npc.pickpocket(highlighted); @@ -608,7 +668,7 @@ private void loop() { public boolean run() { Microbot.isCantReachTargetDetectionEnabled = true; lastAction = System.currentTimeMillis(); - nextShadowVeil = System.currentTimeMillis()+60_000; + nextShadowVeil = System.currentTimeMillis() + 60_000; underAttack = false; startupGraceUntil = System.currentTimeMillis() + 4_000; startTime = Instant.now(); @@ -627,7 +687,8 @@ public boolean run() { } private boolean hasReqs() { - if (System.currentTimeMillis() < startupGraceUntil) return true; + if (System.currentTimeMillis() < startupGraceUntil) + return true; boolean hasReqs = true; if (config.useFood()) { if (config.food() == ThievingFood.ANCIENT_BREW) { @@ -654,7 +715,8 @@ private boolean hasReqs() { log.debug("Missing cosmic runes"); hasReqs = false; } - boolean hasRunes = Rs2Equipment.isWearing("Lava battlestaff") || Rs2Inventory.hasItem("Earth rune", "Fire rune"); + boolean hasRunes = Rs2Equipment.isWearing("Lava battlestaff") + || Rs2Inventory.hasItem("Earth rune", "Fire rune"); if (!hasRunes) { log.debug("Missing lava battle staff or earth & fire runes"); hasReqs = false; @@ -664,12 +726,15 @@ private boolean hasReqs() { } protected static boolean isPointInPolygon(WorldPoint[] polygon, WorldPoint point) { - if (polygon == null || point == null) return false; + if (polygon == null || point == null) + return false; int n = polygon.length; - if (n < 3) return false; + if (n < 3) + return false; int plane = polygon[0].getPlane(); - if (point.getPlane() != plane) return false; + if (point.getPlane() != plane) + return false; int px = point.getX(); int py = point.getY(); @@ -694,22 +759,26 @@ protected static boolean isPointInPolygon(WorldPoint[] polygon, WorldPoint point // apply the ray-casting algorithm boolean intersect = ((yi > py) != (yj > py)) && - (px < (double)(xj - xi) * (py - yi) / (double)(yj - yi) + xi); - if (intersect) inside = !inside; + (px < (double) (xj - xi) * (py - yi) / (double) (yj - yi) + xi); + if (intersect) + inside = !inside; } return inside; } private boolean shouldCastShadowVeil() { - if (!config.shadowVeil()) return false; + if (!config.shadowVeil()) + return false; final long current = System.currentTimeMillis(); - if (current <= forceShadowVeilActive) return false; + if (current <= forceShadowVeilActive) + return false; return current > nextShadowVeil || !Rs2Magic.isShadowVeilActive(); } private void castShadowVeil() { - if (!shouldCastShadowVeil()) return; + if (!shouldCastShadowVeil()) + return; if (!Rs2Magic.canCast(MagicAction.SHADOW_VEIL)) { log.error("Cannot cast shadow veil"); return; @@ -718,58 +787,68 @@ private void castShadowVeil() { log.error("Failed to cast shadow veil"); return; } - if (!sleepUntilWithInterrupt(() -> forceShadowVeilActive > System.currentTimeMillis() || Rs2Magic.isShadowVeilActive(), 2_400)) { + if (!sleepUntilWithInterrupt( + () -> forceShadowVeilActive > System.currentTimeMillis() || Rs2Magic.isShadowVeilActive(), 2_400)) { log.error("Failed to await shadow veil active"); return; } - nextShadowVeil = System.currentTimeMillis()+60_000; + nextShadowVeil = System.currentTimeMillis() + 60_000; } private int maxCoinPouches() { - if (config.coinPouchThreshold() >= 0) return config.coinPouchThreshold(); + if (config.coinPouchThreshold() >= 0) + return config.coinPouchThreshold(); return plugin.getMaxCoinPouch(); } private boolean shouldOpenCoinPouches() { - int threshold = Math.max(1, Math.min(plugin.getMaxCoinPouch(), maxCoinPouches() + (int)(Math.random() * (-7)))); + int threshold = Math.max(1, + Math.min(plugin.getMaxCoinPouch(), maxCoinPouches() + (int) (Math.random() * (-7)))); return Rs2Inventory.hasItemAmount("coin pouch", threshold, true); } private boolean isNpcNull(Rs2NpcModel npc) { - if (npc == null) return true; + if (npc == null) + return true; final String name = getNpcName(npc); - if (name == null) return true; - if (name.isBlank() || name.equalsIgnoreCase("null")) return true; - if (npc.getId() == -1) return true; + if (name == null) + return true; + if (name.isBlank() || name.equalsIgnoreCase("null")) + return true; + if (npc.getId() == -1) + return true; final WorldPoint worldPoint = npc.getWorldLocation(); - if (worldPoint == null) return true; + if (worldPoint == null) + return true; final WorldPoint myLoc = Rs2Player.getWorldLocation(); - if (myLoc == null || myLoc.distanceTo(worldPoint) >= 20) return true; + if (myLoc == null || myLoc.distanceTo(worldPoint) >= 20) + return true; return npc.getLocalLocation() == null; } private String toString(WorldPoint point) { - if (point == null) return "(-1,-1,-1)"; + if (point == null) + return "(-1,-1,-1)"; return "(" + point.getX() + "," + point.getY() + "," + point.getPlane() + ")"; } - private List getDoors(WorldPoint wp, int radius) { + private List getDoors(WorldPoint wp, int radius) { if (wp == null) return Collections.emptyList(); final Rs2WorldPoint rs2Wp = new Rs2WorldPoint(wp); - return Microbot.getClientThread().runOnClientThreadOptional(() -> - Microbot.getRs2TileObjectCache().query() - .within(wp, radius) - .where(o -> { - var comp = o.getObjectComposition(); + // this take 1.5s off client thread + return Microbot.getClientThread().>runOnClientThreadOptional(() -> Rs2GameObject.getAll( + o -> { + ObjectComposition comp = Rs2GameObject.convertToObjectComposition(o); if (comp == null || !Arrays.asList(comp.getActions()).contains("Close")) return false; - return rs2Wp.distanceToPath(o.getWorldLocation()) < Integer.MAX_VALUE; - }) - .toList() - ).orElse(Collections.emptyList()); + + final WorldPoint objWp = o.getWorldLocation(); + return rs2Wp.distanceToPath(objWp) < Integer.MAX_VALUE; + }, wp, radius + )).orElse(Collections.emptyList()); }; private boolean closeNearbyDoor(int radius) { - List doors; + List doors; int doorCount = 0; while (!(doors = getDoors(Rs2Player.getWorldLocation(), radius)).isEmpty()) { if (doorCount >= 3) { @@ -778,31 +857,36 @@ private boolean closeNearbyDoor(int radius) { } final WorldPoint myLoc = Rs2Player.getWorldLocation(); if (myLoc == null) return false; - final Rs2TileObjectModel door = doors.stream() + final TileObject door = doors.stream() .min(Comparator.comparingInt(d -> d.getWorldLocation().distanceTo(myLoc))) .orElseThrow(); final WorldPoint doorWp = door.getWorldLocation(); - if (!door.click("Close")) return false; + if (!Rs2GameObject.interact(door, "Close")) return false; if (door.getWorldLocation().distanceTo(Rs2Player.getWorldLocation()) > 1) { - if (!sleepUntilWithInterrupt(() -> Rs2Player.isMoving() || Rs2Player.isStunned(), 1_200)) return false; - if (Rs2Player.isStunned()) return false; + if (!sleepUntilWithInterrupt(() -> Rs2Player.isMoving() || Rs2Player.isStunned(), 1_200)) + return false; + if (Rs2Player.isStunned()) + return false; sleepUntilWithInterrupt(() -> !Rs2Player.isMoving(), 5_000); } if (!sleepUntilWithInterrupt(() -> getDoors(doorWp, 1).isEmpty() || Rs2Player.isStunned(), 1_200)) { log.warn("Failed to wait closing door @ {}", toString(doorWp)); return false; } - if (Rs2Player.isStunned()) return false; + if (Rs2Player.isStunned()) + return false; log.debug("Closed door @ {}", toString(doorWp)); doorCount++; } return true; } - private boolean repeatedAction(Runnable action, BooleanSupplier awaitedCondition, BooleanSupplier interruptCondition, int maxTries) { + private boolean repeatedAction(Runnable action, BooleanSupplier awaitedCondition, + BooleanSupplier interruptCondition, int maxTries) { for (int i = 0; i < maxTries; i++) { - if (awaitedCondition.getAsBoolean()) return true; + if (awaitedCondition.getAsBoolean()) + return true; action.run(); sleepUntilWithInterrupt(awaitedCondition, interruptCondition, 2_000); } @@ -814,20 +898,26 @@ private boolean repeatedAction(Runnable action, BooleanSupplier check, int maxTr } private boolean equip(String item, boolean shouldLog) { - if (Rs2Equipment.isWearing(item)) return true; + if (Rs2Equipment.isWearing(item)) + return true; final boolean success; if (Rs2Inventory.contains(item)) { - if (config.THIEVING_NPC() == ThievingNpc.VYRES && Rs2Bank.isOpen() && isWearing(ThievingData.VYRE_SET)) return true; + if (config.THIEVING_NPC() == ThievingNpc.VYRES && Rs2Bank.isOpen() && isWearing(ThievingData.VYRE_SET)) + return true; success = repeatedAction(() -> Rs2Inventory.wear(item), () -> Rs2Equipment.isWearing(item), 3); - if (shouldLog && !success) log.error("Failed to equip {}", item); + if (shouldLog && !success) + log.error("Failed to equip {}", item); } else if (Rs2Bank.isOpen() && Rs2Bank.hasBankItem(item)) { success = repeatedAction(() -> Rs2Bank.withdrawItem(item), () -> Rs2Inventory.contains(item), 3); - if (shouldLog && !success) log.error("Could not withdraw item to equip {}", item); - else return equip(item); + if (shouldLog && !success) + log.error("Could not withdraw item to equip {}", item); + else + return equip(item); } else { success = false; - if (shouldLog) log.error("Could not find item to equip {}", item); + if (shouldLog) + log.error("Could not find item to equip {}", item); } return success; } @@ -838,21 +928,22 @@ private boolean equip(String item) { private boolean isWearing(Set set) { for (String item : set) { - if (!Rs2Equipment.isWearing(item)) return false; + if (!Rs2Equipment.isWearing(item)) + return false; } return true; } private boolean equip(Set set, boolean shouldLog) { boolean success = repeatedAction( - () -> { - for (String item : set) { - if (!equip(item, shouldLog)) return; - } - }, - () -> isWearing(set), - 3 - ); + () -> { + for (String item : set) { + if (!equip(item, shouldLog)) + return; + } + }, + () -> isWearing(set), + 3); return success; } @@ -862,7 +953,8 @@ private boolean equip(Set set) { private Set getExclusions() { final Set exclusions = new HashSet<>(ThievingData.ROGUE_SET); - if (config.THIEVING_NPC() == ThievingNpc.VYRES) exclusions.addAll(ThievingData.VYRE_SET); + if (config.THIEVING_NPC() == ThievingNpc.VYRES) + exclusions.addAll(ThievingData.VYRE_SET); exclusions.add("coin pouch"); if (config.shadowVeil()) { exclusions.add("cosmic rune"); @@ -871,7 +963,8 @@ private Set getExclusions() { exclusions.add("fire rune"); } } - if (config.dodgyNecklaceAmount() > 0) exclusions.add("dodgy necklace"); + if (config.dodgyNecklaceAmount() > 0) + exclusions.add("dodgy necklace"); if (config.useFood()) { if (config.food() == ThievingFood.ANCIENT_BREW) { exclusions.addAll(ThievingData.ANCIENT_BREW_DOSES); @@ -885,12 +978,14 @@ private Set getExclusions() { private boolean getInventoryAmount(String name, int amount, boolean exact) { return repeatedAction( () -> { - final int deficit = amount-Rs2Inventory.itemQuantity(name, exact); - if (deficit == 0) return; + final int deficit = amount - Rs2Inventory.itemQuantity(name, exact); + if (deficit == 0) + return; if (deficit > 0) { - if (Rs2Bank.hasBankItem(name, deficit, exact)) Rs2Bank.withdrawX(name, deficit, exact); - } - else Rs2Bank.depositX(name, Math.abs(deficit)); + if (Rs2Bank.hasBankItem(name, deficit, exact)) + Rs2Bank.withdrawX(name, deficit, exact); + } else + Rs2Bank.depositX(name, Math.abs(deficit)); }, () -> Rs2Inventory.itemQuantity(name, exact) == amount, () -> { @@ -900,8 +995,7 @@ private boolean getInventoryAmount(String name, int amount, boolean exact) { } return !shouldRun(); }, - 3 - ); + 3); } private void showMessage(String message) { @@ -910,9 +1004,17 @@ private void showMessage(String message) { } private void bankAndEquip() { + if (config.useSeedVault() && !Rs2Bank.isOpen() && hasSeedsInInventory()) { + final WorldPoint myLoc = Rs2Player.getWorldLocation(); + if (myLoc != null && myLoc.distanceTo(ThievingData.SEED_VAULT_LOCATION) <= 20) { + log.debug("Depositing seeds to vault before banking"); + depositToSeedVault(); + } + } if (!Rs2Bank.isOpen()) { BankLocation bank; - if (config.THIEVING_NPC() == ThievingNpc.VYRES && ThievingData.OUTSIDE_HALLOWED_BANK.distanceTo(Rs2Player.getWorldLocation()) < 20) { + if (config.THIEVING_NPC() == ThievingNpc.VYRES + && ThievingData.OUTSIDE_HALLOWED_BANK.distanceTo(Rs2Player.getWorldLocation()) < 20) { log.debug("Near Hallowed"); bank = BankLocation.HALLOWED_SEPULCHRE; // it's not needed for banking, but if we have it in inv we should equip it @@ -928,12 +1030,15 @@ private void bankAndEquip() { } } - if (!isRunning()) return; + if (!isRunning()) + return; boolean opened = Rs2Bank.isNearBank(bank, 8) ? Rs2Bank.openBank() : Rs2Bank.walkToBankAndUseBank(bank); - if (!opened || !Rs2Bank.isOpen()) return; + if (!opened || !Rs2Bank.isOpen()) + return; } - if (!Rs2Bank.isOpen() || !isRunning()) return; + if (!Rs2Bank.isOpen() || !isRunning()) + return; Rs2Bank.depositAllExcept(getExclusions()); if (config.useFood()) { @@ -942,9 +1047,10 @@ private void bankAndEquip() { if (config.food() == ThievingFood.ANCIENT_BREW) { itemName = "Ancient brew(4)"; } - + if (!getInventoryAmount(itemName, config.foodAmount(), true)) { - if (!Rs2Bank.isOpen()) return; + if (!Rs2Bank.isOpen()) + return; showMessage("No " + config.food().getName() + " found in bank."); shutdown(); return; @@ -967,7 +1073,8 @@ private void bankAndEquip() { } if (!getInventoryAmount("Dodgy necklace", config.dodgyNecklaceAmount(), true)) { - if (!Rs2Bank.isOpen()) return; + if (!Rs2Bank.isOpen()) + return; showMessage("No Dodgy necklace found in bank."); shutdown(); return; @@ -986,7 +1093,8 @@ private void bankAndEquip() { Rs2Bank.withdrawAll(true, "Earth rune", true); Rs2Inventory.waitForInventoryChanges(1_500); } else { - if (!shouldRun() || !Rs2Bank.isOpen()) return; + if (!shouldRun() || !Rs2Bank.isOpen()) + return; showMessage("No Lava battlestaff and runes (Earth, Fire) found in bank."); shutdown(); return; @@ -1001,7 +1109,8 @@ private void bankAndEquip() { Rs2Bank.withdrawAll(true, "Cosmic rune", true); Rs2Inventory.waitForInventoryChanges(1_500); if (!Rs2Inventory.hasItem("Cosmic rune")) { - if (!shouldRun() || !Rs2Bank.isOpen()) return; + if (!shouldRun() || !Rs2Bank.isOpen()) + return; showMessage("No Cosmic runes found."); shutdown(); return; @@ -1026,6 +1135,47 @@ private void bankAndEquip() { DOOR_TIMER.set(); } + private boolean hasSeedsInInventory() { + return Rs2Inventory.all().stream().anyMatch(item -> { + if (item == null || item.getName() == null) + return false; + final String name = item.getName().toLowerCase(); + return name.endsWith(" seed") || name.endsWith(" seeds"); + }); + } + + /** + * Opens the seed vault and deposits all seeds. Does not walk to the vault or + * return to starting location. + */ + private void depositToSeedVault() { + if (!Rs2Widget.isWidgetVisible(InterfaceID.SEED_VAULT)) { + if (!Rs2GameObject.interact(ThievingData.SEED_VAULT_OBJECT_ID, "Use")) + return; + + Rs2Player.waitForWalking(); + if (!sleepUntil(() -> Rs2Widget.isWidgetVisible(InterfaceID.SEED_VAULT), 7_000)) { + log.warn("Seed vault did not open"); + return; + } + } + Rs2Widget.clickWidget(InterfaceID.SEED_VAULT, InterfaceID.SEED_VAULT_DEPOSIT); + Rs2Inventory.waitForInventoryChanges(3_500); + Rs2Keyboard.keyPress(KeyEvent.VK_ESCAPE); + sleepUntil(() -> !Rs2Widget.isWidgetVisible(InterfaceID.SEED_VAULT), 1_500); + } + + private void useSeedVault() { + final WorldPoint myLoc = Rs2Player.getWorldLocation(); + if (myLoc == null || myLoc.distanceTo(ThievingData.SEED_VAULT_LOCATION) > 20) { + if (!walkTo("Walk to seed vault", ThievingData.SEED_VAULT_LOCATION, 20)) + return; + } + depositToSeedVault(); + if (startingLocation != null) + walkTo("Return to thieving spot", startingLocation, 3); + } + private void dropAllExceptImportant() { final Set keep = getExclusions(); if (config.DoNotDropItemList() != null && !config.DoNotDropItemList().isEmpty()) @@ -1033,14 +1183,13 @@ private void dropAllExceptImportant() { Arrays.stream(config.DoNotDropItemList().split(",")) .map(String::strip) .map(String::toLowerCase) - .collect(Collectors.toSet()) - ); + .collect(Collectors.toSet())); Collections.addAll(keep, "coins", "book of the dead"); - if (config.THIEVING_NPC() == ThievingNpc.VYRES) Collections.addAll(keep,"drakan's medallion", "blood shard"); + if (config.THIEVING_NPC() == ThievingNpc.VYRES) + Collections.addAll(keep, "drakan's medallion", "blood shard"); Rs2Inventory.dropAllExcept(config.keepItemsAboveValue(), keep.toArray(String[]::new)); } - private void hopWorld() { final long now = System.currentTimeMillis(); if (now - lastHopAt < 15000) { @@ -1058,7 +1207,8 @@ private void hopWorld() { BooleanSupplier attackedInterrupt = () -> { final Rs2NpcModel attacking = getAttackingNpc(); - if (attacking == null) return false; + if (attacking == null) + return false; final WorldPoint me = Rs2Player.getWorldLocation(); final WorldPoint npc = attacking.getWorldLocation(); if (me != null && npc != null && me.distanceTo(npc) <= 2) { @@ -1067,10 +1217,13 @@ private void hopWorld() { } return false; }; - - sleepUntilWithInterrupt(() -> Microbot.getClient().getGameState() == GameState.HOPPING, attackedInterrupt, 15_000); - sleepUntilWithInterrupt(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN, attackedInterrupt, 15_000); - boolean changed = sleepUntilWithInterrupt(() -> Microbot.getClient().getWorld() != currentWorld, attackedInterrupt, 15_000); + + sleepUntilWithInterrupt(() -> Microbot.getClient().getGameState() == GameState.HOPPING, attackedInterrupt, + 15_000); + sleepUntilWithInterrupt(() -> Microbot.getClient().getGameState() == GameState.LOGGED_IN, attackedInterrupt, + 15_000); + boolean changed = sleepUntilWithInterrupt(() -> Microbot.getClient().getWorld() != currentWorld, + attackedInterrupt, 15_000); if (changed) { log.debug("Successful hop world"); @@ -1095,7 +1248,8 @@ private void drinkAncientBrew() { private boolean hasAncientBrew() { for (String dose : ThievingData.ANCIENT_BREW_DOSES) { - if (Rs2Inventory.contains(dose)) return true; + if (Rs2Inventory.contains(dose)) + return true; } return false; } diff --git a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java index 33d5a78c5e..fb622fe73d 100644 --- a/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java +++ b/src/main/java/net/runelite/client/plugins/microbot/woodcutting/AutoWoodcuttingScript.java @@ -29,7 +29,6 @@ import net.runelite.client.plugins.microbot.util.walker.Rs2Walker; import net.runelite.client.plugins.microbot.util.widget.Rs2Widget; import net.runelite.client.plugins.microbot.woodcutting.enums.*; -import net.runelite.client.plugins.microbot.woodcutting.enums.WoodcuttingTreeLocations; import javax.inject.Inject; import java.awt.event.KeyEvent; @@ -425,7 +424,7 @@ private void burnLog(AutoWoodcuttingConfig config) { Rs2Inventory.useLast(treeType.getLogID()); }, 300, 100); } else if (!isFiremake() && useCampfire) { - Rs2Inventory.useItemOnObject(treeType.getLogID(), fire.getId()); + fire.click("Tend-to"); sleepUntil(() -> (!Rs2Player.isMoving() && Rs2Widget.findWidget("How many would you like to burn?", null, false) != null), 5000); Rs2Random.waitEx(400, 200); Rs2Keyboard.keyPress(KeyEvent.VK_SPACE); diff --git a/src/test/java/net/runelite/client/Microbot.java b/src/test/java/net/runelite/client/Microbot.java index afcf25983c..8933287b08 100644 --- a/src/test/java/net/runelite/client/Microbot.java +++ b/src/test/java/net/runelite/client/Microbot.java @@ -9,9 +9,7 @@ import net.runelite.client.plugins.microbot.aiofighter.AIOFighterPlugin; import net.runelite.client.plugins.microbot.astralrc.AstralRunesPlugin; import net.runelite.client.plugins.microbot.autofishing.AutoFishingPlugin; -import net.runelite.client.plugins.microbot.crafting.jewelry.JewelryPlugin; import net.runelite.client.plugins.microbot.example.ExamplePlugin; -import net.runelite.client.plugins.microbot.kraken.KrakenPlugin; import net.runelite.client.plugins.microbot.leftclickcast.LeftClickCastPlugin; import net.runelite.client.plugins.microbot.sailing.MSailingPlugin; import net.runelite.client.plugins.microbot.thieving.ThievingPlugin; @@ -23,7 +21,8 @@ public class Microbot private static final Class[] debugPlugins = { AIOFighterPlugin.class, - AgentServerPlugin.class + AgentServerPlugin.class, + LeftClickCastPlugin.class }; public static void main(String[] args) throws Exception