From 4600b5db6c95f18e03d7ca17a4b3f0a1aca4aee7 Mon Sep 17 00:00:00 2001 From: CrisisSheep <41985051+CrisisSheep@users.noreply.github.com> Date: Thu, 23 Jul 2026 06:42:42 +1200 Subject: [PATCH 1/6] fix: verify pearl pull interact instead of always reporting success fireClick previously sent the interact packet and unconditionally declared the pull successful, with no rotation toward the trapdoor and no check that the click actually landed - it could silently miss and still whisper 'Pulled'. Splits into two paths depending on whether the owner was already online when positioning finished: - Already online: uses Baritone's rightClickBlock, which reaches the block correctly regardless of intervening blocks, rotates and clicks atomically at a priority that beats Spook, and verifies the interact resolved against the right block via ClickResult before declaring success. Bounded by a 3s confirmation timeout since Baritone's own retry limit only covers path calculation failures, not stuck interact attempts. On a confirmed miss or timeout, reports failure and tells the player to re-request rather than retrying automatically. - Owner comes online after waiting: kept as the raw, zero-latency packet send (unverified) since routing this through rightClickBlock's InputManager round-trip would cost at least one extra tick right when responsiveness matters most. Correctness instead comes from continuously re-facing the trapdoor every tick for the whole wait, so the bot is already aimed correctly the instant the owner logs in instead of wherever Spook or idle behavior last left it pointed. --- .../java/org/pearlbot/PearlBotMessages.java | 2 + .../org/pearlbot/module/AutoPearlModule.java | 99 ++++++++++++++++--- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/pearlbot/PearlBotMessages.java b/src/main/java/org/pearlbot/PearlBotMessages.java index e9814a9..8cb38a0 100644 --- a/src/main/java/org/pearlbot/PearlBotMessages.java +++ b/src/main/java/org/pearlbot/PearlBotMessages.java @@ -33,6 +33,8 @@ public class PearlBotMessages { public String ownerTimedOut = "Expired - you did not log on within {timeout}s."; + public String pullFailed = "Failed to pull your pearl - please try again."; + public String authUsage = "Usage: !auth - get a code by typing !auth in Discord first."; public String authInvalidCode = "Invalid or expired code."; diff --git a/src/main/java/org/pearlbot/module/AutoPearlModule.java b/src/main/java/org/pearlbot/module/AutoPearlModule.java index f011198..782af6f 100644 --- a/src/main/java/org/pearlbot/module/AutoPearlModule.java +++ b/src/main/java/org/pearlbot/module/AutoPearlModule.java @@ -72,6 +72,8 @@ public class AutoPearlModule extends Module { private static final long PULL_RETRY_INTERVAL_MS = 1_000L; private static final long IDLE_RETURN_DELAY_MS = 1500L; private static final long GHOST_PRUNE_GRACE_MS = 5_000L; + private static final long CLICK_CONFIRM_TIMEOUT_MS = 3_000L; + private static final int FACE_TRAPDOOR_PRIORITY = 3000; private static final String DISCORD_AUTH_CMD = "!auth"; private static final String INGAME_AUTH_CMD = "!auth"; private static final String INGAME_LIST_CMD = "list"; @@ -94,6 +96,9 @@ public class AutoPearlModule extends Module { private long reopenAtMs = 0L; private int reopenTx, reopenTy, reopenTz; + private volatile boolean awaitingClickConfirmation = false; + private volatile long clickAttemptStartMs = 0L; + private final Map chamberEmptySinceMs = new HashMap<>(); private final Map pendingAuthCodes = new ConcurrentHashMap<>(); @@ -567,21 +572,21 @@ private void tickPending() { abortActivePull(PLUGIN_MESSAGES.format(PLUGIN_MESSAGES.pullTimedOut, "timeout", PLUGIN_CONFIG.pullTimeoutSeconds)); return; } + } else if (awaitingClickConfirmation) { + if (now - clickAttemptStartMs > CLICK_CONFIRM_TIMEOUT_MS) { + warn("Interact confirmation for {} timed out after {}ms; reporting failure", + labelOf(activePull), CLICK_CONFIRM_TIMEOUT_MS); + if (BARITONE.isActive()) BARITONE.stop(); + abortActivePull(PLUGIN_MESSAGES.pullFailed); + return; + } } else if (!pearlPresentNear(activePull.blockX, activePull.blockY, activePull.blockZ)) { - warn("Chamber for {} at ({}, {}, {}) is empty at the trapdoor; pruning and cancelling instead of clicking", - labelOf(activePull), activePull.blockX, activePull.blockY, activePull.blockZ); - int cx = activePull.blockX, cy = activePull.blockY, cz = activePull.blockZ; - removeChamberAt(cx, cy, cz); - abortActivePull(PLUGIN_MESSAGES.chamberEmpty); - PLUGIN_CONFIG.pendingPulls.removeIf(p -> { - if (p.blockX != cx || p.blockY != cy || p.blockZ != cz) return false; - if (p.ownerName != null) sendWhisper(p.ownerName, PLUGIN_MESSAGES.chamberEmpty); - return true; - }); + abortForEmptyChamber(activePull); return; } else if (isOwnerOnline(activePull.ownerUuid)) { fireClick(); } else { + submitTrapdoorFacingRotation(activePull); long waitMs = (long) PLUGIN_CONFIG.waitForOwnerSeconds * 1000L; if (waitMs > 0 && now - readyAtMs > waitMs) { warn("{} did not come online within {}s; expiring pull", @@ -658,6 +663,8 @@ private void clearActivePullState() { activePullStartMs = 0L; readyAtTrapdoor = false; readyAtMs = 0L; + awaitingClickConfirmation = false; + clickAttemptStartMs = 0L; } private boolean isChamberInRange(int x, int y, int z) { @@ -739,6 +746,19 @@ private void removeChamberAt(int x, int y, int z) { }); } + private void abortForEmptyChamber(PearlBotConfig.PendingPull pull) { + warn("Chamber for {} at ({}, {}, {}) is empty at the trapdoor; pruning and cancelling instead of clicking", + labelOf(pull), pull.blockX, pull.blockY, pull.blockZ); + int cx = pull.blockX, cy = pull.blockY, cz = pull.blockZ; + removeChamberAt(cx, cy, cz); + abortActivePull(PLUGIN_MESSAGES.chamberEmpty); + PLUGIN_CONFIG.pendingPulls.removeIf(p -> { + if (p.blockX != cx || p.blockY != cy || p.blockZ != cz) return false; + if (p.ownerName != null) sendWhisper(p.ownerName, PLUGIN_MESSAGES.chamberEmpty); + return true; + }); + } + private void cancelPullsForChamber(PearlBotConfig.StasisChamber c) { if (activePull != null && activePull.blockX == c.x && activePull.blockY == c.y && activePull.blockZ == c.z) { @@ -786,20 +806,73 @@ private void executePull(PearlBotConfig.PendingPull pull) { } readyAtTrapdoor = true; readyAtMs = System.currentTimeMillis(); - info("Ready at trapdoor for {} - {}", - label, isOwnerOnline(pull.ownerUuid) ? "owner online, clicking" : "waiting for owner online"); + + if (isOwnerOnline(pull.ownerUuid)) { + info("Ready at trapdoor for {} - owner online, interacting", label); + if (!pearlPresentNear(pull.blockX, pull.blockY, pull.blockZ)) { + abortForEmptyChamber(pull); + } else { + beginVerifiedClickAttempt(pull); + } + } else { + info("Ready at trapdoor for {} - waiting for owner online", label); + } }); } + /** + * Fast path: fires the moment an owner who was offline logs back in. Must not add + * latency here, so this skips InputManager/rightClickBlock's verification and just + * fires the packet directly - correctness instead comes from continuously re-facing + * the trapdoor throughout the wait via {@link #submitTrapdoorFacingRotation}. + */ private void fireClick() { PearlBotConfig.PendingPull pull = activePull; if (pull == null) return; + sendUseItemOn(pull.blockX, pull.blockY, pull.blockZ); + completePullSuccess(pull); + } + + /** + * Used when the owner was already online by the time positioning finished, so there's + * no live login event to react to and the extra tick or two rightClickBlock costs (it + * routes rotation+click through InputManager, applied on the next tick) doesn't matter. + * Unlike {@link #fireClick()}, this verifies the interact actually landed before + * declaring success. + */ + private void beginVerifiedClickAttempt(PearlBotConfig.PendingPull pull) { + awaitingClickConfirmation = true; + clickAttemptStartMs = System.currentTimeMillis(); + BARITONE.rightClickBlock(pull.blockX, pull.blockY, pull.blockZ).addExecutedListener(req -> { + if (activePull == null || !pull.ownerUuid.equals(activePull.ownerUuid)) return; + if (!awaitingClickConfirmation) return; + awaitingClickConfirmation = false; + + if (req.getNow()) { + completePullSuccess(pull); + } else { + warn("Interact with trapdoor for {} could not be confirmed; reporting failure", labelOf(pull)); + abortActivePull(PLUGIN_MESSAGES.pullFailed); + } + }); + } + + private void submitTrapdoorFacingRotation(PearlBotConfig.PendingPull pull) { + var rotation = RotationHelper.rotationTo(pull.blockX + 0.5, pull.blockY, pull.blockZ + 0.5); + INPUTS.submit(InputRequest.builder() + .owner(this) + .yaw(rotation.getX()) + .pitch(rotation.getY()) + .priority(FACE_TRAPDOOR_PRIORITY) + .build()); + } + + private void completePullSuccess(PearlBotConfig.PendingPull pull) { int tx = pull.blockX; int ty = pull.blockY; int tz = pull.blockZ; String label = labelOf(pull); - sendUseItemOn(tx, ty, tz); if (PLUGIN_CONFIG.reopenTrapdoors) { reopenTx = tx; reopenTy = ty; From 57c0351a16dd52b638ab2c6fa3bb86e43e1b6004 Mon Sep 17 00:00:00 2001 From: CrisisSheep <41985051+CrisisSheep@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:19:35 +1200 Subject: [PATCH 2/6] chore: bump version to 0.4.4 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9b56a90..9b87900 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -plugin_version=0.4.3 +plugin_version=0.4.4 plugin_name=PearlBot plugin_id=pearlbot # More info about MC Version support: https://wiki.2b2t.vc/Setup/#release-channels From c38e2d4f4f8f9e59c8c8c98f29a45a09aa71bc91 Mon Sep 17 00:00:00 2001 From: CrisisSheep <41985051+CrisisSheep@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:28:43 +1200 Subject: [PATCH 3/6] fix: don't double-count just-pulled chamber in remaining pearl count --- .../org/pearlbot/module/AutoPearlModule.java | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/pearlbot/module/AutoPearlModule.java b/src/main/java/org/pearlbot/module/AutoPearlModule.java index 782af6f..5e65617 100644 --- a/src/main/java/org/pearlbot/module/AutoPearlModule.java +++ b/src/main/java/org/pearlbot/module/AutoPearlModule.java @@ -518,13 +518,6 @@ public void checkAndEnforceMaxChambers(UUID ownerUuid) { enqueuePull(ownerUuid, name, chamber, "max chambers exceeded"); } - private int remainingPearlsFor(UUID ownerUuid) { - if (ownerUuid == null) return 0; - return (int) PLUGIN_CONFIG.chambers.values().stream() - .filter(c -> ownerUuid.equals(c.ownerUuid)) - .count(); - } - public boolean enqueuePull(UUID ownerUuid, String requesterName, PearlBotConfig.StasisChamber chamber) { return enqueuePull(ownerUuid, requesterName, chamber, null); } @@ -892,8 +885,13 @@ private void completePullSuccess(PearlBotConfig.PendingPull pull) { if (pull.source != null) pullEmbed.addField("Triggered by", pull.source); pullNotification(pullEmbed.successColor(), false); - // Chamber is still in the map until the entity despawns, so subtract 1 to compensate. - int remaining = Math.max(0, remainingPearlsFor(pull.ownerUuid) - 1); + // Don't rely on the pulled chamber having been removed from the map yet - the + // despawn packet that triggers removal races with this code. Exclude it by + // position explicitly instead of assuming it's (not) still present. + int remaining = (int) PLUGIN_CONFIG.chambers.values().stream() + .filter(c -> pull.ownerUuid.equals(c.ownerUuid)) + .filter(c -> c.x != tx || c.y != ty || c.z != tz) + .count(); if (pull.ownerName != null) { String tail = remaining == 1 ? "1 pearl" : remaining + " pearls"; sendWhisper(pull.ownerName, PLUGIN_MESSAGES.format(PLUGIN_MESSAGES.pulled, "remaining", tail)); From 3b4882715bab2299535d91f5f47aed4a47996a2d Mon Sep 17 00:00:00 2001 From: CrisisSheep <41985051+CrisisSheep@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:48:30 +1200 Subject: [PATCH 4/6] fix: lengthen whisper antispam suffix and move it into brackets --- src/main/java/org/pearlbot/module/AutoPearlModule.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/pearlbot/module/AutoPearlModule.java b/src/main/java/org/pearlbot/module/AutoPearlModule.java index 5e65617..11ad202 100644 --- a/src/main/java/org/pearlbot/module/AutoPearlModule.java +++ b/src/main/java/org/pearlbot/module/AutoPearlModule.java @@ -496,8 +496,8 @@ private String discordTriggerExample() { private void sendWhisper(String name, String message) { if (name == null || name.isBlank()) return; - String suffix = String.format("%08x", ThreadLocalRandom.current().nextInt()); - sendClientPacketAsync(ChatUtil.getWhisperChatPacket(name, message + " - " + suffix)); + String suffix = String.format("%016x", ThreadLocalRandom.current().nextLong()); + sendClientPacketAsync(ChatUtil.getWhisperChatPacket(name, message + " [" + suffix + "]")); } public void checkAndEnforceMaxChambers(UUID ownerUuid) { From 1b55b30448955eaaa3c852c157e0b846671efcbb Mon Sep 17 00:00:00 2001 From: CrisisSheep <41985051+CrisisSheep@users.noreply.github.com> Date: Thu, 23 Jul 2026 21:52:20 +1200 Subject: [PATCH 5/6] fix: extend whisper antispam suffix to 24 hex chars --- src/main/java/org/pearlbot/module/AutoPearlModule.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/pearlbot/module/AutoPearlModule.java b/src/main/java/org/pearlbot/module/AutoPearlModule.java index 11ad202..9e415b7 100644 --- a/src/main/java/org/pearlbot/module/AutoPearlModule.java +++ b/src/main/java/org/pearlbot/module/AutoPearlModule.java @@ -496,7 +496,8 @@ private String discordTriggerExample() { private void sendWhisper(String name, String message) { if (name == null || name.isBlank()) return; - String suffix = String.format("%016x", ThreadLocalRandom.current().nextLong()); + String suffix = String.format("%016x", ThreadLocalRandom.current().nextLong()) + + String.format("%08x", ThreadLocalRandom.current().nextInt()); sendClientPacketAsync(ChatUtil.getWhisperChatPacket(name, message + " [" + suffix + "]")); } From 8af47fe9d49b548ee76da25fcd36e168bcc83955 Mon Sep 17 00:00:00 2001 From: CrisisSheep <41985051+CrisisSheep@users.noreply.github.com> Date: Sun, 26 Jul 2026 01:03:22 +1200 Subject: [PATCH 6/6] fix: settle briefly before verified interact when owner already at trapdoor Previously fired the verified click immediately when positioning finished with the owner already online, before rotation/position had settled. Waits CLICK_SETTLE_DELAY_MS (150ms) first, continuing to re-face the trapdoor in the meantime. --- .../org/pearlbot/module/AutoPearlModule.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/pearlbot/module/AutoPearlModule.java b/src/main/java/org/pearlbot/module/AutoPearlModule.java index 9e415b7..282dad1 100644 --- a/src/main/java/org/pearlbot/module/AutoPearlModule.java +++ b/src/main/java/org/pearlbot/module/AutoPearlModule.java @@ -73,6 +73,7 @@ public class AutoPearlModule extends Module { private static final long IDLE_RETURN_DELAY_MS = 1500L; private static final long GHOST_PRUNE_GRACE_MS = 5_000L; private static final long CLICK_CONFIRM_TIMEOUT_MS = 3_000L; + private static final long CLICK_SETTLE_DELAY_MS = 150L; private static final int FACE_TRAPDOOR_PRIORITY = 3000; private static final String DISCORD_AUTH_CMD = "!auth"; private static final String INGAME_AUTH_CMD = "!auth"; @@ -90,6 +91,7 @@ public class AutoPearlModule extends Module { private long activePullStartMs = 0L; private volatile boolean readyAtTrapdoor = false; private volatile long readyAtMs = 0L; + private volatile long clickReadyAtMs = 0L; private long idleReturnAtMs = 0L; private int reopenStep = 0; @@ -577,6 +579,12 @@ private void tickPending() { } else if (!pearlPresentNear(activePull.blockX, activePull.blockY, activePull.blockZ)) { abortForEmptyChamber(activePull); return; + } else if (clickReadyAtMs > 0L) { + submitTrapdoorFacingRotation(activePull); + if (now >= clickReadyAtMs) { + clickReadyAtMs = 0L; + beginVerifiedClickAttempt(activePull); + } } else if (isOwnerOnline(activePull.ownerUuid)) { fireClick(); } else { @@ -657,6 +665,7 @@ private void clearActivePullState() { activePullStartMs = 0L; readyAtTrapdoor = false; readyAtMs = 0L; + clickReadyAtMs = 0L; awaitingClickConfirmation = false; clickAttemptStartMs = 0L; } @@ -789,6 +798,7 @@ private void executePull(PearlBotConfig.PendingPull pull) { activePull = pull; activePullStartMs = System.currentTimeMillis(); readyAtTrapdoor = false; + clickReadyAtMs = 0L; BARITONE.pathTo(new GoalNear(new BlockPos(tx, ty, tz), 9)).addExecutedListener(req -> { pf.allowBreak = prevAllowBreak; @@ -802,12 +812,8 @@ private void executePull(PearlBotConfig.PendingPull pull) { readyAtMs = System.currentTimeMillis(); if (isOwnerOnline(pull.ownerUuid)) { - info("Ready at trapdoor for {} - owner online, interacting", label); - if (!pearlPresentNear(pull.blockX, pull.blockY, pull.blockZ)) { - abortForEmptyChamber(pull); - } else { - beginVerifiedClickAttempt(pull); - } + info("Ready at trapdoor for {} - owner online, settling before interact", label); + clickReadyAtMs = readyAtMs + CLICK_SETTLE_DELAY_MS; } else { info("Ready at trapdoor for {} - waiting for owner online", label); }