Everything handed over, in full. Pick a file; raw opens the plain text for copying.
The module to ship: the port with nine defects of the original fixed.
/** Ported into QuinnAddon from THM Addons — https://github.com/Magnus-Krill/THM-Addons* Source file: src/main/java/xyz/thm/addon/modules/Speedmine.java (branch 26.2).* The original header reads: "Copyright (c) THM Addons contributors. Credit the devs, keep the link.* By using this code you agree to the license terms and to keep your repo public."* Credit stays with the THM Addons contributors; thanks to mushek, and to BlackOut for the* Auto Mine target order this is based on.** PORT NOTES — behaviour is kept, the plumbing is not. QuinnAddon does not have THM's support* classes, so each one is replaced by the nearest thing Meteor already ships:* Mojang mappings -> Yarn mappings (mc.level -> mc.world, Serverbound* -> *C2SPacket).* THM InventoryManager -> a local server-slot tracker (UpdateSelectedSlotC2SPacket).* THM RangeUtils -> isInRange() / closestFace() below.* THM RenderUtilsTHM -> Render3DEvent.renderer.box().* THM hand-written mining formula -> Meteor BlockUtils.getBreakDelta(slot, state) (same maths).* THM ClientLevel mixin hook -> Meteor BlockUpdateEvent.* THM sequenced-packet accessor -> an invoker mixin on ClientPlayerInteractionManager.* PaketLimiter / ThmMembers / THMSystem / DescribedOption -> dropped, they are THM-only.* fromTag() settings migration -> dropped, QuinnAddon has no legacy Speedmine settings.* FIXES APPLIED ON TOP OF THE ORIGINAL (THM 26.2 behaves differently here):* 1. The break event is only cancelled when this module actually takes the block, so a click* that the module declines falls back to vanilla mining instead of doing nothing.* 2. requestBreak() validates the block, and a context that cannot make progress is dropped after* STALL_TIMEOUT_MS instead of occupying the primary slot forever.* 3. Every queue insert stores an immutable BlockPos.* 4. A context whose block changed to a different block is dropped instead of mining the old one.* 5. The enemy's feet use the floored Y, so a half-slab or snow layer no longer shifts the* surround ring one block up.* 6. Dead players are not targeted.* 7. The tool search starts from the slot already selected, so an equally good held tool is kept* instead of swapping to a lower hotbar index.* 8. The rebreak timeout is one constant instead of the same value written in ms and in ns.* 9. The "last broken" outline respects rebreak-mode Off.*/package com.example.addon.modules;import com.example.addon.QuinnAddon;import com.example.addon.mixin.ClientPlayerInteractionManagerAccessor;import com.example.addon.mixin.ClientWorldAccessor;import com.example.addon.mixin.PlayerMoveC2SPacketAccessor;import meteordevelopment.meteorclient.events.entity.player.StartBreakingBlockEvent;import meteordevelopment.meteorclient.events.game.GameLeftEvent;import meteordevelopment.meteorclient.events.packets.PacketEvent;import meteordevelopment.meteorclient.events.render.Render3DEvent;import meteordevelopment.meteorclient.events.world.BlockUpdateEvent;import meteordevelopment.meteorclient.events.world.TickEvent;import meteordevelopment.meteorclient.renderer.ShapeMode;import meteordevelopment.meteorclient.settings.*;import meteordevelopment.meteorclient.systems.friends.Friends;import meteordevelopment.meteorclient.systems.modules.Module;import meteordevelopment.meteorclient.utils.misc.Keybind;import meteordevelopment.meteorclient.utils.player.Rotations;import meteordevelopment.meteorclient.utils.render.color.Color;import meteordevelopment.meteorclient.utils.render.color.SettingColor;import meteordevelopment.meteorclient.utils.world.BlockUtils;import meteordevelopment.meteorclient.utils.world.TickRate;import meteordevelopment.orbit.EventHandler;import meteordevelopment.orbit.EventPriority;import net.minecraft.block.Block;import net.minecraft.block.BlockState;import net.minecraft.block.Blocks;import net.minecraft.entity.player.PlayerEntity;import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket;import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket;import net.minecraft.network.packet.c2s.play.UpdateSelectedSlotC2SPacket;import net.minecraft.util.Hand;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Box;import net.minecraft.util.math.Direction;import net.minecraft.util.math.Vec3d;import net.minecraft.world.WorldEvents;import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Comparator;import java.util.Deque;import java.util.HashMap;import java.util.List;import java.util.Locale;import java.util.Map;import java.util.function.Function;/** Packet miner with server-timed progress and sequenced client prediction. */public class Speedmine extends Module {private final SettingGroup sgMine = settings.getDefaultGroup();private final SettingGroup sgAuto = settings.createGroup("Auto Mine", false);private final SettingGroup sgRender = settings.createGroup("Render");private final SettingGroup sgAir = settings.createGroup("Air Miner", false);// ── Air Miner ─────────────────────────────────────────────────────────────private final Setting<Boolean> airMine = sgAir.add(new BoolSetting.Builder().name("air-mine").description("Mine at ground speed while airborne.").defaultValue(false).onChanged(enabled -> { if (!enabled) restoreAirGround(); }).build());private final Setting<Boolean> airWhileGliding = sgAir.add(new BoolSetting.Builder().name("air-while-gliding").description("Allow airborne mining while gliding.").defaultValue(false).visible(airMine::get).build());private final Setting<Double> airMaxSpeed = sgAir.add(new DoubleSetting.Builder().name("air-max-speed").description("Maximum horizontal speed for ground claims.").defaultValue(1).min(0).sliderRange(0, 3).visible(airMine::get).build());private final Setting<Integer> airClaimWindow = sgAir.add(new IntSetting.Builder().name("air-claim-window").description("Ticks to retain the mining ground claim.").defaultValue(2).min(1).sliderRange(1, 10).visible(airMine::get).build());private int airClaimLeft;private long airClaimTick = -1;private boolean airInjecting;// ── Auto Mine (target selection, ported from BlackOut's AutoMine) ─────────public final Setting<Boolean> autoMine = sgAuto.add(new BoolSetting.Builder().name("auto-mine").description("Automatically pick blocks to break around nearby enemies.").defaultValue(false).build());public final Setting<Keybind> autoMineBind = sgAuto.add(new KeybindSetting.Builder().name("auto-mine-bind").description("Hold this to auto-mine. Leave unbound to have auto-mine run whenever the module is on.").defaultValue(Keybind.none()).visible(autoMine::get).build());private final Setting<Boolean> debugAutoMine = sgAuto.add(new BoolSetting.Builder().name("debug-auto-mine").description("Report Auto Mine targets and idle reasons in chat.").defaultValue(false).onChanged(enabled -> { this.lastAutoMineStatus = null; this.nextAutoMineReportTick = 0; }).visible(autoMine::get).build());public final Setting<Boolean> autoMineOnly = sgAuto.add(new BoolSetting.Builder().name("auto-mine-only").description("Ignore blocks you click yourself — only auto-mine targets get broken.").defaultValue(false).visible(autoMine::get).build());public final Setting<Boolean> feetFirst = sgAuto.add(new BoolSetting.Builder().name("feet-first").description("Break the lowest targets first, so an enemy's feet go before their head. Applies to bedrock too.").defaultValue(false).visible(autoMine::get).build());public final Setting<Double> enemyRange = sgAuto.add(new DoubleSetting.Builder().name("enemy-range").description("How far away an enemy can be to be considered.").defaultValue(10).min(1).max(20).sliderRange(1, 20).visible(autoMine::get).build());public final Setting<Boolean> antiPhase = sgAuto.add(new BoolSetting.Builder().name("anti-phase").description("Mine the blocks an enemy is standing inside.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> antiSurround = sgAuto.add(new BoolSetting.Builder().name("anti-surround").description("Mine the blocks boxing an enemy in.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> neverMineOwn = sgAuto.add(new BoolSetting.Builder().name("never-mine-own").description("Never auto-mine blocks touching you.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> autoDoubleMine = sgAuto.add(new BoolSetting.Builder().name("auto-double-mine").description("Break two auto-mine targets at once, using double-break.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> mineBedrock = sgAuto.add(new BoolSetting.Builder().name("mine-bedrock").description("Also target bedrock, broken vanilla-style with hand swings instead of packets.").defaultValue(false).visible(autoMine::get).build());public final Setting<Boolean> bedrockOnly = sgAuto.add(new BoolSetting.Builder().name("bedrock-only").description("Only break bedrock, ignore every other block.").defaultValue(false).visible(() -> autoMine.get() && mineBedrock.get()).build());public final Setting<Boolean> bedrockWhileEating = sgAuto.add(new BoolSetting.Builder().name("bedrock-while-eating").description("Continue mining bedrock while eating.").defaultValue(false).visible(() -> autoMine.get() && mineBedrock.get()).build());public final Setting<Boolean> bedrockRotate = sgAuto.add(new BoolSetting.Builder().name("bedrock-rotate").description("Silently look at the bedrock before breaking it. Usually not needed.").defaultValue(false).visible(() -> autoMine.get() && mineBedrock.get()).build());// ── Mining ────────────────────────────────────────────────────────────────public final Setting<Boolean> rotate = sgMine.add(new BoolSetting.Builder().name("rotate").description("Silently look at each block before mining it. Bedrock has its own toggle.").defaultValue(false).build());public final Setting<Boolean> grimBypass = sgMine.add(new BoolSetting.Builder().name("grim-bypass").description("Send STOP_DESTROY_BLOCK before START to bypass Grim's sequence check.").defaultValue(true).build());public final Setting<Boolean> doubleBreak = sgMine.add(new BoolSetting.Builder().name("double-break").description("Track a primary and secondary block simultaneously.").defaultValue(true).build());public final Setting<Boolean> queueEnabled = sgMine.add(new BoolSetting.Builder().name("queue").description("Queue extra blocks when both break slots are occupied.").defaultValue(true).build());public final Setting<Double> breakThreshold = sgMine.add(new DoubleSetting.Builder().name("break-threshold").description("Mining progress needed before sending STOP.").defaultValue(0.7).min(0.1).max(1.0).decimalPlaces(2).build());public final Setting<Boolean> clientPrediction = sgMine.add(new BoolSetting.Builder().name("client-prediction").description("Predict completed breaks locally, including rebreaks.").defaultValue(true).build());public final Setting<Boolean> autoRebreak = sgMine.add(new BoolSetting.Builder().name("auto-rebreak").description("Rebreak the last position if a block reappears there.").defaultValue(true).build());public final Setting<RebreakMode> rebreakMode = sgMine.add(new EnumSetting.Builder<RebreakMode>().name("rebreak-mode").description("How instant rebreaks wait for server confirmation. Off: no instant rebreaks. Strict: wait for server confirmation between rebreaks. Strong: skip confirmation waits and TPS start delays. Bypass: Strict until a rebreak is confirmed, then Strong.").defaultValue(RebreakMode.Strong).visible(autoRebreak::get).build());public final Setting<RebreakTrigger> rebreakTrigger = sgMine.add(new EnumSetting.Builder<RebreakTrigger>().name("rebreak-trigger").description("Use client-world checks, optionally with early packet triggers. OnPacket: rebreak on server updates and client-world replacements. OnClientWorld: check for replacement blocks each client tick.").defaultValue(RebreakTrigger.OnPacket).visible(autoRebreak::get).build());private final Setting<Boolean> debugRebreak = sgMine.add(new BoolSetting.Builder().name("debug-rebreak").description("Show server-confirmed rebreaks per second.").defaultValue(false).onChanged(enabled -> resetRebreakMonitor()).build());public final Setting<Boolean> silentSwap = sgMine.add(new BoolSetting.Builder().name("silent-swap").description("Swap to the best tool via packet without visually changing your held item.").defaultValue(true).build());public final Setting<SwapMode> swapMode = sgMine.add(new EnumSetting.Builder<SwapMode>().name("swap-mode").description("Swap timing for instant breaks and rebreaks. Keep: hold the tool through the next tick and consecutive rebreaks. SameTick: restore the previous slot immediately after mining packets. EndOfTick: restore the selected slot at the end of the tick. ToolHold: hold the tool through server validation and three idle ticks.").defaultValue(SwapMode.SameTick).visible(silentSwap::get).onChanged(mode -> releaseHeldSlot()).build());public final Setting<Boolean> tpsSync = sgMine.add(new BoolSetting.Builder().name("tps-sync").description("Match mining progress and start rate to server TPS.").defaultValue(false).build());public final Setting<Boolean> multitask = sgMine.add(new BoolSetting.Builder().name("multitask").description("Continue mining while using items.").defaultValue(false).build());public final Setting<Double> range = sgMine.add(new DoubleSetting.Builder().name("range").description("Maximum block-breaking distance.").defaultValue(5.2).min(1).max(6).decimalPlaces(1).build());// ── Render ────────────────────────────────────────────────────────────────private final Setting<SettingColor> renderColor = sgRender.add(new ColorSetting.Builder().name("color").defaultValue(new SettingColor(225, 25, 25, 255)).build());private final Setting<SettingColor> bedrockColor = sgRender.add(new ColorSetting.Builder().name("bedrock-color").description("Color of the bedrock blocks currently being broken.").defaultValue(new SettingColor(255, 70, 70, 255)).visible(mineBedrock::get).build());private final Setting<ShapeMode> bedrockShape = sgRender.add(new EnumSetting.Builder<ShapeMode>().name("bedrock-shape").description("How the bedrock being broken is drawn.").defaultValue(ShapeMode.Both).visible(mineBedrock::get).build());// ── State ─────────────────────────────────────────────────────────────────public static Speedmine INSTANCE;/** Ticks with nothing left to mine before the slot is handed back to the client's real one. */private static final int IDLE_RELEASE_TICKS = 3;/** How long a context may make no progress before it is dropped (fix 2). */private static final long STALL_TIMEOUT_MS = 3000;/** Fix 8: one timeout, two units derived from it. */private static final long REBREAK_TIMEOUT_MS = 2000;private static final long REBREAK_TIMEOUT_NS = REBREAK_TIMEOUT_MS * 1_000_000L;private MineContext primary;private MineContext secondary;public BlockPos lastBrokenPos;private boolean lastBreakConfirmed;private boolean rebreakPending;private boolean bypassConfirmed;private long rebreakDeadlineMs;private BlockState packetReplacement;private long rebreakMonitorStartNs;private int rebreakCount;private String rebreakRate = "0.0";private final RebreakConfirmations rebreakConfirmations = new RebreakConfirmations();private BlockPos lastStartedPos;public final Deque<BlockPos> queue = new ArrayDeque<>();/** Hotbar slot this miner is holding on the server; -1 when it holds none. */private int heldSlot = -1;/** What the server currently believes is selected; -1 when it matches the client. */private int serverSlot = -1;private int idleTicks = 0;private final List<PendingBreak> pendingBreaks = new ArrayList<>(3);private long lastStartCreditMs;private double startCredit = 1;private long clientTick;private long lastMiningActionTick;private BlockPos bedrockPos;private String autoMineStatus;private String lastAutoMineStatus;private long nextAutoMineReportTick;private int autoAirRejected, autoSelfRejected, autoRangeRejected, autoDisabledRejected, autoUnbreakableRejected;// ── Constructor ───────────────────────────────────────────────────────────public Speedmine() {super(QuinnAddon.CATEGORY, "speedmine","Grim-safe packet miner with queue and double break. Ported from THM Addons, credit to the THM devs.");INSTANCE = this;}// ── Module lifecycle ──────────────────────────────────────────────────────@Overridepublic void onActivate() {airClaimLeft = 0;airClaimTick = -1;lastStartCreditMs = System.currentTimeMillis();startCredit = 1;clientTick = 0;lastMiningActionTick = 0;autoMineStatus = lastAutoMineStatus = null;nextAutoMineReportTick = 0;resetRebreakMonitor();}@Overridepublic void onDeactivate() {restoreAirGround();releaseHeldSlot();primary = null;secondary = null;lastBrokenPos = null;lastBreakConfirmed = false;rebreakPending = false;bypassConfirmed = false;packetReplacement = null;resetRebreakMonitor();lastStartedPos = null;bedrockPos = null;pendingBreaks.clear();queue.clear();}@EventHandlerprivate void onGameLeft(GameLeftEvent event) {heldSlot = -1;serverSlot = -1;onDeactivate();}// ── Air Miner ─────────────────────────────────────────────────────────────public boolean liftsAirMiningPenalty() {return isActive() && airMine.get() && mc.player != null && mc.getNetworkHandler() != null&& canClaimAir(mc.player.isOnGround(), mc.player.isGliding(), airWhileGliding.get(),mc.player.isCreative(), mc.player.isSpectator(),mc.player.getVelocity().horizontalLength(), airMaxSpeed.get());}static boolean canClaimAir(boolean onGround, boolean gliding, boolean allowGliding, boolean creative,boolean spectator, double speed, double maximum) {return !onGround && (!gliding || allowGliding) && !creative && !spectator && speed <= maximum;}@EventHandler(priority = EventPriority.LOWEST)private void onPacketSend(PacketEvent.Send event) {if (event.isCancelled() || mc.player == null || mc.getNetworkHandler() == null) return;// Keep our idea of the server's selected slot in sync with whatever actually goes out.if (event.packet instanceof UpdateSelectedSlotC2SPacket slot) {serverSlot = slot.getSelectedSlot();return;}if (airInjecting) return;if (event.packet instanceof PlayerActionC2SPacket action&& (action.getAction() == PlayerActionC2SPacket.Action.START_DESTROY_BLOCK|| action.getAction() == PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK)&& liftsAirMiningPenalty()) {if (airClaimTick != clientTick) {sendAirGround(true);airClaimTick = clientTick;}airClaimLeft = airClaimWindow.get();} else if (event.packet instanceof PlayerMoveC2SPacket move && airClaimLeft > 0 && liftsAirMiningPenalty()) {((PlayerMoveC2SPacketAccessor) move).quinn$setOnGround(true);}}private void sendAirGround(boolean onGround) {if (mc.player == null || mc.getNetworkHandler() == null) return;airInjecting = true;try {mc.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.Full(mc.player.getX(), mc.player.getY(), mc.player.getZ(),mc.player.getYaw(), mc.player.getPitch(), onGround, mc.player.horizontalCollision));} finally {airInjecting = false;}}private void restoreAirGround() {boolean claimed = airClaimLeft > 0 || airClaimTick != -1;airClaimLeft = 0;airClaimTick = -1;if (claimed && mc.player != null && mc.getNetworkHandler() != null) sendAirGround(mc.player.isOnGround());}// ── Server block updates (THM used a ClientLevel mixin; Meteor fires this for us) ──@EventHandlerprivate void onBlockUpdate(BlockUpdateEvent event) {if (mc.world == null || mc.player == null) return;BlockPos pos = event.pos;BlockState state = event.newState;confirmBreak(pos, state);if (!pos.equals(lastBrokenPos)) return;packetReplacement = state.isAir() ? null : state;if (rebreakTrigger.get() == RebreakTrigger.OnPacket && packetReplacement != null) {tryRebreak(pos, packetReplacement, true);}}private void confirmBreak(BlockPos pos, BlockState state) {if (!state.isAir()) return;if (rebreakConfirmations.confirm(pos, System.nanoTime())) rebreakCount++;if (pos.equals(lastStartedPos) && (pos.equals(lastBrokenPos) || (primary != null && pos.equals(primary.pos)))) {lastBrokenPos = pos.toImmutable();lastBreakConfirmed = true;if (rebreakPending) bypassConfirmed = true;rebreakPending = false;}pendingBreaks.removeIf(pending -> pending.pos().equals(pos));if (primary != null && pos.equals(primary.pos)) primary = null;if (secondary != null && pos.equals(secondary.pos)) secondary = null;}// ── Events ────────────────────────────────────────────────────────────────@EventHandlerprivate void onStartBreaking(StartBreakingBlockEvent event) {if (mc.world == null || mc.player == null) return;// Hands manual clicks straight back to vanilla — auto-mine owns the moduleif (autoMine.get() && autoMineOnly.get()) return;BlockState state = mc.world.getBlockState(event.blockPos);if (!BlockUtils.canBreak(event.blockPos, state)) return;if (outOfRange(event.blockPos)) return;// Fix 1: this block is already ours, so vanilla must not dig it, but there is nothing to start.if (isMining(event.blockPos)) {event.cancel();return;}// Fix 1: only take the click away from vanilla if the module actually accepted it.if (handleBlockClick(event.blockPos, state)) event.cancel();}@EventHandlerprivate void onTick(TickEvent.Pre event) {if (mc.world == null || mc.player == null) return;clientTick++;if (airClaimLeft > 0 && (--airClaimLeft == 0 || !liftsAirMiningPenalty())) restoreAirGround();long now = System.currentTimeMillis();startCredit = replenishStartCredit(startCredit, now - lastStartCreditMs, effectiveTps());lastStartCreditMs = now;pendingBreaks.removeIf(pending -> now >= pending.deadlineMs());if (rebreakPending && now >= rebreakDeadlineMs) rebreakPending = false;updateRebreakMonitor();boolean paused = shouldPauseMining();long progressNow = System.nanoTime();if (primary != null) primary.advance(progressNow, paused);if (secondary != null) secondary.advance(progressNow, paused);if (debugAutoMine.get()) {autoMineStatus = !autoMine.get() ? "Disabled.": !autoMineHeld() ? "Waiting for auto-mine-bind.": "Paused while using an item or eating.";}if (paused) {releaseHeldSlot();if (bedrockWhileEating.get() && isEating()) tickAutoMine();else bedrockPos = null;return;}int validationSlot = validationToolSlot();if (!silentSwap.get()) {releaseHeldSlot();if (validationSlot != -1) selectSlot(validationSlot);} else if (validationSlot != -1) holdTool(validationSlot);else if (swapMode.get() == SwapMode.SameTick) releaseHeldSlot();else if (swapMode.get() != SwapMode.EndOfTick) {if (primary != null) holdTool(primary.startSlot);else if (secondary != null) holdTool(secondary.startSlot);else if (heldSlot != -1 && !pendingBreaks.isEmpty()) holdTool(heldSlot);}if (primary != null && !shouldRemove(primary, now) && !primary.started) sendStart(primary);tickAutoMine();rebreakReplacement();pruneCompletedOrInvalid();if (secondary != null && secondary.progress() >= 1.0) finishBreak(secondary, silentSwap.get());if (primary != null && primary.progress() >= 1.0) finishBreak(primary, silentSwap.get());drainQueue();}@EventHandler(priority = EventPriority.LOWEST)private void onTickPost(TickEvent.Post event) {if (mc.player == null || mc.world == null) return;if (debugAutoMine.get()&& autoMineStatus != null && !autoMineStatus.equals(lastAutoMineStatus) && clientTick >= nextAutoMineReportTick) {info("Auto Mine: %s", autoMineStatus);lastAutoMineStatus = autoMineStatus;nextAutoMineReportTick = clientTick + 40;}// Placers can refill predicted air after our Pre handler.rebreakReplacement();if (heldSlot == -1) return;if (!silentSwap.get() || shouldPauseMining()) {releaseHeldSlot();return;}boolean mining = primary != null || secondary != null || !queue.isEmpty();boolean replacement = autoRebreak.get() && rebreakMode.get() != RebreakMode.Off&& rebreakMode.get().isPrimed(lastBreakConfirmed) && lastBrokenPos != null&& lastBrokenPos.equals(lastStartedPos)&& !outOfRange(lastBrokenPos) && BlockUtils.canBreak(lastBrokenPos, mc.world.getBlockState(lastBrokenPos));boolean pending = !pendingBreaks.isEmpty();idleTicks = mining || pending ? 0 : Math.min(IDLE_RELEASE_TICKS, idleTicks + 1);// Check after this tick's placements before releasing the keep-mode hold.SwapMode mode = validationToolSlot() != -1 ? SwapMode.ToolHold : swapMode.get();if (mode.shouldRelease(clientTick - lastMiningActionTick, mining, replacement, pending, idleTicks)) releaseHeldSlot();}@EventHandlerprivate void onRender(Render3DEvent event) {if (mc.world == null || mc.player == null) return;for (BlockPos pos : queue) renderBox(event, pos, 1, renderColor.get(), ShapeMode.Lines);if (bedrockPos != null) {renderBox(event, bedrockPos, 1, bedrockColor.get(), bedrockShape.get());}if (secondary != null) renderMineContext(event, secondary);if (primary != null) renderMineContext(event, primary);// Fix 9: every other rebreak check also tests the mode, so the outline should too.if (lastBrokenPos != null && autoRebreak.get() && rebreakMode.get() != RebreakMode.Off&& !mc.world.getBlockState(lastBrokenPos).isAir()) {renderBox(event, lastBrokenPos, 1, renderColor.get(), ShapeMode.Lines);}}// ── Core break logic ──────────────────────────────────────────────────────private void rebreakReplacement() {if (lastBrokenPos == null) return;boolean packet = rebreakTrigger.get() == RebreakTrigger.OnPacket && packetReplacement != null;tryRebreak(lastBrokenPos, packet ? packetReplacement : mc.world.getBlockState(lastBrokenPos), packet);}private void tryRebreak(BlockPos pos, BlockState state, boolean packet) {boolean strong = rebreakMode.get().isStrong(bypassConfirmed);if (mc.interactionManager == null || !autoRebreak.get() || !pos.equals(lastBrokenPos) || !pos.equals(lastStartedPos)|| !rebreakMode.get().canRebreak(lastBreakConfirmed, rebreakPending, bypassConfirmed)|| primary != null || secondary != null || hasPendingNormalBreak(pendingBreaks, strong ? pos : null)|| outOfRange(pos) || state.isAir() || !BlockUtils.canBreak(pos, state)) return;if (shouldPauseMining()) return;if (!strong && !canStart()) return;MineContext ctx = new MineContext(pos, state, false);ctx.rebreak = true;ctx.packetRebreak = packet;if (rotate.get()) lookAt(pos);if (finishBreak(ctx, silentSwap.get()) && !strong && tpsSync.get()) startCredit = Math.max(0, startCredit - 1);}/** @return true when this module took ownership of {@code pos} (mining it, or queueing it). */private boolean handleBlockClick(BlockPos pos, BlockState state) {if (isMining(pos)) return false;if (autoRebreak.get() && rebreakMode.get() != RebreakMode.Off && rebreakMode.get().isPrimed(lastBreakConfirmed)&& pos.equals(lastBrokenPos) && pos.equals(lastStartedPos)) {boolean packet = rebreakTrigger.get() == RebreakTrigger.OnPacket;if (packet && packetReplacement != null) tryRebreak(pos, packetReplacement, true);else tryRebreak(pos, state, false);return true;}if (shouldPauseMining()) return false;if (hasPendingNormalBreak()) {if (!queueEnabled.get()) return false;queue.addLast(pos.toImmutable());return true;}if (!canStart()) {queue.addLast(pos.toImmutable());return true;}boolean canAddSecondary = secondary == null && doubleBreak.get();if (primary == null) {primary = new MineContext(pos, state, true);sendStart(primary);return true;} else if (canAddSecondary) {if (!stopWithTool(primary, silentSwap.get())) {if (!queueEnabled.get()) return false;if (!queue.contains(pos)) queue.addLast(pos.toImmutable());return true;}secondary = primary;secondary.isPrimary = false;primary = new MineContext(pos, state, true);sendStart(primary);return true;} else {// Fix 3: this insert was the one place the original stored a possibly mutable BlockPos.if (!queueEnabled.get()) return false;if (!queue.contains(pos)) queue.addLast(pos.toImmutable());return true;}}private void pruneCompletedOrInvalid() {long now = System.currentTimeMillis();if (primary != null && shouldRemove(primary, now)) primary = null;if (secondary != null && shouldRemove(secondary, now)) secondary = null;queue.removeIf(this::shouldRemove);}private boolean shouldRemove(BlockPos pos) {return mc.world.getBlockState(pos).isAir() || outOfRange(pos);}/*** Fix 4: the context carries the state it started on, so a block that was swapped for a* different one must not keep being mined with the old hardness and the old particle id.* Fix 2: a context that cannot make progress at all (hardness below zero, no usable tool) would* otherwise never satisfy shouldRemove and would hold the primary slot forever.*/private boolean shouldRemove(MineContext ctx, long now) {if (shouldRemove(ctx.pos)) return true;if (mc.world.getBlockState(ctx.pos) != ctx.state) return true;return ctx.tickDamage <= 0 && now - ctx.startMs > STALL_TIMEOUT_MS;}private void drainQueue() {if (hasPendingNormalBreak() || queue.isEmpty() || !canStart()) return;if (primary == null || (doubleBreak.get() && secondary == null)) {BlockPos pos = queue.pollFirst();handleBlockClick(pos, mc.world.getBlockState(pos)); // return value is not needed here}}// ── Packet building ───────────────────────────────────────────────────────private void sendStart(MineContext ctx) {if (ctx.started || !ctx.active) return;if (rotate.get()) lookAt(ctx.pos);withMiningTool(ctx, silentSwap.get(), () -> {if (grimBypass.get() && !sendSequencedAction(PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK, ctx, false)) return;if (!sendSequencedAction(PlayerActionC2SPacket.Action.START_DESTROY_BLOCK, ctx, false)) return;ctx.started = true;ctx.instaBreak = ctx.tickDamage >= 1.0;ctx.timing.start(System.nanoTime(), ctx.tickDamage, effectiveTps());rebreakPending = false;bypassConfirmed = false;packetReplacement = null;lastBreakConfirmed = false;lastStartedPos = ctx.pos;if (tpsSync.get()) startCredit = Math.max(0, startCredit - 1);if (ctx.instaBreak) finishBreak(ctx, silentSwap.get());});}/** STOP must use the same tool as START. */private boolean stopWithTool(MineContext ctx, boolean silent) {if (!ctx.started || mc.world == null || mc.player == null) return false;withMiningTool(ctx, silent,() -> ctx.stopSent = sendSequencedAction(PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK, ctx, false));return ctx.stopSent;}private boolean finishBreak(MineContext ctx, boolean silent) {if (!ctx.active || (!ctx.started && !ctx.rebreak) || mc.world == null || mc.player == null|| mc.interactionManager == null) return false;boolean clientRemove = clientPrediction.get();// Instant STARTs do not prime the server's target for subsequent STOPs.PlayerActionC2SPacket.Action action = ctx.rebreak && ctx.instaBreak? PlayerActionC2SPacket.Action.START_DESTROY_BLOCK: PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK;withMiningTool(ctx, silent, () -> ctx.stopSent = sendSequencedAction(action, ctx, clientRemove));if (!ctx.stopSent) return false;if (ctx.requiresValidation() || (silent && swapMode.get() == SwapMode.ToolHold)) {double ticksPerMs = serverTicks(1, TickRate.INSTANCE.getTickRate()) * ctx.tickDamage;long deadline = System.currentTimeMillis() + (long) clamp(ticksPerMs > 0 ? 1 / ticksPerMs + 1000 : 30000, 2000, 30000);// At most two normal validations and the latest instant/rebreak hold.pendingBreaks.removeIf(pending -> !pending.normal() || pending.pos().equals(ctx.pos));pendingBreaks.add(new PendingBreak(ctx.pos, ctx.startSlot, ctx.requiresValidation(), !ctx.isPrimary, deadline));}if (clientRemove) {mc.world.syncWorldEvent(WorldEvents.BLOCK_BROKEN, ctx.pos, Block.getRawIdFromState(ctx.state));}if (!ctx.pos.equals(lastBrokenPos)) lastBreakConfirmed = false;lastBrokenPos = ctx.pos;if (ctx.rebreak) {rebreakPending = true;rebreakDeadlineMs = System.currentTimeMillis() + REBREAK_TIMEOUT_MS;packetReplacement = null;if (debugRebreak.get()) rebreakConfirmations.sent(ctx.pos, System.nanoTime(), lastBreakConfirmed);}ctx.active = false;if (ctx == primary) primary = null;else if (ctx == secondary) secondary = null;if (silent && hasPendingNormalBreak()) holdTool(validationToolSlot());return true;}/*** Sends one dig action inside the client's own prediction sequence, through vanilla's* sendSequencedPacket. Any block change made inside the callback is recorded against that* sequence, so a wrong guess is rolled back by the server's acknowledgement instead of leaving a* ghost block. The call needs an invoker mixin: the method is private and the pending-update* manager behind it is package-private.*/private boolean sendSequencedAction(PlayerActionC2SPacket.Action action, MineContext ctx, boolean predict) {if (mc.world == null || mc.player == null || mc.interactionManager == null) return false;((ClientPlayerInteractionManagerAccessor) mc.interactionManager).quinn$sendSequencedPacket(mc.world, sequence -> {if (predict) {// The replacement may still read as air locally, so tell the prediction bookkeeping// what the server really has before predicting air over it.if (ctx.packetRebreak) {((ClientWorldAccessor) mc.world).quinn$getPendingUpdateManager().addPendingUpdate(ctx.pos, ctx.state, mc.player);}mc.world.setBlockState(ctx.pos, Blocks.AIR.getDefaultState(), Block.NOTIFY_ALL);}return new PlayerActionC2SPacket(action, ctx.pos, Direction.DOWN, sequence);});return true;}// ── Silent swap ───────────────────────────────────────────────────────────private void withMiningTool(MineContext ctx, boolean silent, Runnable action) {SwapMode mode = swapMode.get().forBreak(ctx.instaBreak, ctx.rebreak);if (validationToolSlot() != -1) mode = SwapMode.ToolHold;if (silent && mode == SwapMode.SameTick) {releaseHeldSlot();withSilentTool(ctx.state, action);} else {if (silent) holdTool(ctx.startSlot);else equipBestTool(ctx.state);action.run();}int validationSlot = validationToolSlot();if (validationSlot != -1) {if (silent) holdTool(validationSlot);else selectSlot(validationSlot);}lastMiningActionTick = clientTick;}/** Restores the previous server slot, including an existing silent hold. */public void withSilentTool(BlockState state, Runnable action) {if (mc.player == null) { action.run(); return; }int best = findBestHotbarSlot(state);int prev = currentServerSlot();boolean swap = best != -1 && best != prev;if (swap) sendSlotPacket(best);try {action.run();} finally {if (swap && currentServerSlot() != prev) sendSlotPacket(prev);}}private int currentServerSlot() {if (serverSlot != -1) return serverSlot;return mc.player == null ? 0 : mc.player.getInventory().getSelectedSlot();}private void sendSlotPacket(int slot) {if (slot < 0 || mc.getNetworkHandler() == null) return;if (currentServerSlot() == slot) return;mc.getNetworkHandler().sendPacket(new UpdateSelectedSlotC2SPacket(slot));serverSlot = slot;}private void holdTool(int slot) {if (slot < 0 || mc.player == null) return;sendSlotPacket(slot);heldSlot = slot;}private void releaseHeldSlot() {if (heldSlot != -1 && mc.player != null && currentServerSlot() == heldSlot) {sendSlotPacket(mc.player.getInventory().getSelectedSlot());}heldSlot = -1;idleTicks = 0;}/** Non-silent swap: move the client's own selection too. */private void selectSlot(int slot) {if (slot < 0 || mc.player == null) return;mc.player.getInventory().setSelectedSlot(slot);sendSlotPacket(slot);}private void equipBestTool(BlockState state) {int slot = findBestHotbarSlot(state);if (slot != -1) selectSlot(slot);}/*** THM re-implemented vanilla's mining formula here. Meteor already has it, including efficiency,* haste, mining fatigue, water and the airborne penalty, so the port calls that instead.*/private int findBestHotbarSlot(BlockState state) {if (mc.player == null) return -1;// Fix 7: start from the slot already selected, so an equally fast tool that is in hand wins// the tie. The original started at -1, which both made the "no tool" result unreachable and// swapped to the lowest matching hotbar index on every tie.int best = mc.player.getInventory().getSelectedSlot();double bestSpeed = BlockUtils.getBreakDelta(best, state);for (int i = 0; i < 9; i++) {if (i == best) continue;double speed = BlockUtils.getBreakDelta(i, state);if (speed > bestSpeed) { bestSpeed = speed; best = i; }}return best;}// ── Auto Mine ─────────────────────────────────────────────────────────────/*** Picks blocks to break around nearby enemies, in BlackOut AutoMine's priority order, and hands* them to the normal packet miner via {@link #requestBreak(BlockPos)}. Bedrock can't be* packet-mined, so it goes down the vanilla progress+swing path instead.*/private void tickAutoMine() {if (!autoMine.get() || !autoMineHeld()) {bedrockPos = null;if (debugAutoMine.get()) autoMineStatus = !autoMine.get() ? "Disabled." : "Waiting for auto-mine-bind.";return;}bedrockPos = null;int budget = autoDoubleMine.get() && doubleBreak.get() ? 2 : 1;List<BlockPos> targets = findAutoTargets();// Auto-mine outranks the queue: drop the backlog instead of mining it firstif (!targets.isEmpty() && !shouldPauseMining()) queue.clear();for (BlockPos target : targets) {if (mc.world.getBlockState(target).getBlock() == Blocks.BEDROCK) {// Bedrock is mined one at a time — it's a vanilla progress bar, not a packet breakif (bedrockPos == null) mineBedrock(target);continue;}if (budget-- <= 0) break;// Both break slots busy: skip rather than queue — auto targets never enter the queueif (primary != null && (secondary != null || !doubleBreak.get())) continue;requestBreak(target);}if (debugAutoMine.get() && !targets.isEmpty()) {if (bedrockPos != null) autoMineStatus = "Targeting bedrock at " + bedrockPos.toShortString();else if (primary != null) autoMineStatus = "Targeting block at " + primary.pos.toShortString();else if (secondary != null) autoMineStatus = "Targeting block at " + secondary.pos.toShortString();else if (hasPendingNormalBreak()) autoMineStatus = "Waiting for server break confirmation.";else if (!canStart()) autoMineStatus = "Waiting for TPS mining credit.";else autoMineStatus = "Targets selected; waiting for mining slots.";}}/** A bound key is hold-to-mine; an unbound one means "always on". */private boolean autoMineHeld() {return !autoMineBind.get().isSet() || autoMineBind.get().isPressed();}/** Every valid target around nearby enemies, nearest first. */private List<BlockPos> findAutoTargets() {List<PlayerEntity> enemies = new ArrayList<>();int friends = 0, distant = 0, dead = 0;for (PlayerEntity player : mc.world.getPlayers()) {if (player == mc.player || player.isSpectator()) continue;// Fix 6: a corpse still sits in the player list for the whole death animation.if (!player.isAlive()) { dead++; continue; }if (Friends.get().isFriend(player)) { friends++; continue; }if (player.distanceTo(mc.player) > enemyRange.get()) { distant++; continue; }enemies.add(player);}if (enemies.isEmpty()) {if (debugAutoMine.get()) autoMineStatus = String.format(Locale.ROOT,"No eligible players (friends=%d, distant=%d, dead=%d).", friends, distant, dead);return List.of();}// Digging an enemy out beats chipping at their surroundautoAirRejected = autoSelfRejected = autoRangeRejected = autoDisabledRejected = autoUnbreakableRejected = 0;List<BlockPos> targets = new ArrayList<>();if (antiPhase.get()) targets.addAll(collect(enemies, this::phaseBlocks));if (targets.isEmpty() && antiSurround.get()) targets.addAll(collect(enemies, this::surroundBlocks));if (debugAutoMine.get() && targets.isEmpty()) {autoMineStatus = !antiPhase.get() && !antiSurround.get()? "Both anti-phase and anti-surround are disabled.": String.format(Locale.ROOT, "No target blocks (air=%d, own=%d, range=%d, disabled=%d, unbreakable=%d).",autoAirRejected, autoSelfRejected, autoRangeRejected, autoDisabledRejected, autoUnbreakableRejected);}return targets;}private List<BlockPos> collect(List<PlayerEntity> enemies, Function<PlayerEntity, List<BlockPos>> candidates) {List<BlockPos> out = new ArrayList<>();for (PlayerEntity enemy : enemies) {for (BlockPos pos : candidates.apply(enemy)) {if (pos == null) continue;if (outOfRange(pos)) { autoRangeRejected++; continue; }if (!out.contains(pos)) out.add(pos);}}Comparator<BlockPos> byDistance =Comparator.comparingDouble(pos -> mc.player.getEyePos().squaredDistanceTo(Vec3d.ofCenter(pos)));// Absolute Y, not per-enemy feet level — right for one enemy, good enough for a pile of themout.sort(feetFirst.get() ? Comparator.<BlockPos>comparingInt(BlockPos::getY).thenComparing(byDistance) : byDistance);return out;}/** Every block the enemy's hitbox overlaps. */private List<BlockPos> phaseBlocks(PlayerEntity enemy) {List<BlockPos> out = new ArrayList<>();Box box = enemy.getBoundingBox();for (BlockPos pos : BlockPos.iterate(BlockPos.ofFloored(box.minX, box.minY, box.minZ),BlockPos.ofFloored(box.maxX, box.maxY, box.maxZ))) {if (mineable(pos)) out.add(pos.toImmutable());}return out;}/** Everything boxing the enemy in: surround ring, head-level ring, and the block above their head. */private List<BlockPos> surroundBlocks(PlayerEntity enemy) {BlockPos feet = feet(enemy);BlockPos head = new BlockPos(enemy.getBlockX(), (int) Math.floor(enemy.getBoundingBox().maxY), enemy.getBlockZ());List<BlockPos> out = new ArrayList<>();for (Direction dir : Direction.Type.HORIZONTAL) {out.add(feet.offset(dir));out.add(head.offset(dir));}out.add(head.up());List<BlockPos> mineable = new ArrayList<>();for (BlockPos pos : out) if (mineable(pos)) mineable.add(pos);return mineable;}/*** Fix 5: the original rounded the Y, which lands one block too high whenever the enemy stands on* anything that puts them on a fractional Y of .5 or more (slab, snow layer, soul sand).*/private BlockPos feet(PlayerEntity enemy) {return new BlockPos(enemy.getBlockX(), enemy.getBlockY(), enemy.getBlockZ());}/** True if the block is inside or directly against our own hitbox — breaking it drops or exposes us. */private boolean touchesSelf(BlockPos pos) {return mc.player.getBoundingBox().expand(1).intersects(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1);}private boolean mineable(BlockPos pos) {BlockState state = mc.world.getBlockState(pos);if (state.isAir()) { autoAirRejected++; return false; }if (neverMineOwn.get() && touchesSelf(pos)) { autoSelfRejected++; return false; }// Bedrock's hardness is -1, so BlockUtils.canBreak always rejects it — it has to be checked firstif (state.isOf(Blocks.BEDROCK)) {if (!mineBedrock.get()) { autoDisabledRejected++; return false; }return true;}if (shouldPauseMining() || bedrockOnly.get()) { autoDisabledRejected++; return false; }if (!BlockUtils.canBreak(pos, state)) { autoUnbreakableRejected++; return false; }return true;}// ── Bedrock (vanilla progress + swing — packets can't break it) ───────────/*** Bedrock is a server-side progress bar driven by holding the dig, not something a START/STOP* packet pair can pop, so it goes through updateBlockBreakingProgress — which tracks exactly one* position, hence one block at a time. No tool swap: bedrock breaks at the same speed with* anything, so whatever is held works.*/private void mineBedrock(BlockPos pos) {if (mc.interactionManager == null || mc.player == null) return;bedrockPos = pos;if (bedrockRotate.get()) lookAt(pos);mc.interactionManager.updateBlockBreakingProgress(pos, closestFace(pos));mc.player.swingHand(Hand.MAIN_HAND);}// ── Pending validation ────────────────────────────────────────────────────/** A delayed secondary completes in a server tick, using the tool held then. */private int validationToolSlot() {return validationToolSlot(secondary != null && secondary.requiresValidation() ? secondary.startSlot : -1,pendingBreaks, primary != null && primary.requiresValidation() ? primary.startSlot : -1);}static int validationToolSlot(int secondarySlot, List<PendingBreak> pendingBreaks, int primarySlot) {for (PendingBreak pending : pendingBreaks) {if (pending.normal() && pending.secondary()) return pending.toolSlot();}if (secondarySlot != -1) return secondarySlot;for (PendingBreak pending : pendingBreaks) {if (pending.normal()) return pending.toolSlot();}return primarySlot;}private boolean hasPendingNormalBreak() {return hasPendingNormalBreak(pendingBreaks, null);}static boolean hasPendingNormalBreak(List<PendingBreak> pendingBreaks, BlockPos completedTarget) {return pendingBreaks.stream().anyMatch(pending -> pending.normal() && !pending.pos().equals(completedTarget));}record PendingBreak(BlockPos pos, int toolSlot, boolean normal, boolean secondary, long deadlineMs) {}// ── Util ─────────────────────────────────────────────────────────────────private boolean isEating() {return mc.player != null && mc.player.isUsingItem();}private boolean shouldPauseMining() {return shouldPauseMining(multitask.get(), mc.player != null && mc.player.isUsingItem());}static boolean shouldPauseMining(boolean multitask, boolean usingItem) {return !multitask && usingItem;}private void resetRebreakMonitor() {rebreakMonitorStartNs = System.nanoTime();rebreakCount = 0;rebreakRate = "0.0";rebreakConfirmations.clear();}private void updateRebreakMonitor() {if (!debugRebreak.get()) return;long now = System.nanoTime();rebreakConfirmations.expire(now);long elapsed = now - rebreakMonitorStartNs;if (elapsed < 1_000_000_000L) return;rebreakRate = String.format(Locale.ROOT, "%.1f", rebreakCount * 1_000_000_000.0 / elapsed);info("Rebreaks/s: %s confirmed", rebreakRate);rebreakMonitorStartNs = now;rebreakCount = 0;}@Overridepublic String getInfoString() {return debugRebreak.get() ? rebreakRate + " rebreaks/s" : null;}private float effectiveTps() {return tpsSync.get() ? TickRate.INSTANCE.getTickRate() : 20;}private boolean canStart() {return canStart(tpsSync.get(), effectiveTps(), startCredit);}static boolean canStart(boolean sync, float tps, double credit) {return !sync || !Float.isFinite(tps) || tps >= 20 || credit >= 1;}static double replenishStartCredit(double credit, long elapsedMillis, float tps) {// Retain fractional overflow so tick jitter does not discard earned starts.return Math.min(2, credit + serverTicks(elapsedMillis, tps));}static double serverTicks(long elapsedMillis, float tps) {return Math.max(0, elapsedMillis) / 1000.0 * clamp(Float.isFinite(tps) ? tps : 20, 0, 20);}static double clamp(double value, double min, double max) {return Math.max(min, Math.min(max, value));}public void requestBreak(BlockPos pos) {if (mc.world == null || mc.player == null) return;BlockState state = mc.world.getBlockState(pos);// Fix 2: the original only checked for air, so an unbreakable block handed in by another// module (bedrock, barrier) produced a context that could never finish.if (state.isAir() || outOfRange(pos) || !BlockUtils.canBreak(pos, state)) return;if (!isMining(pos)) handleBlockClick(pos, state);}public boolean isMining(BlockPos pos) {return (primary != null && primary.pos.equals(pos))|| (secondary != null && secondary.pos.equals(pos))|| pendingBreaks.stream().anyMatch(pending -> pending.normal() && pending.pos().equals(pos))|| queue.contains(pos);}/** Server-side only look at {@code pos} — the camera doesn't move. */private void lookAt(BlockPos pos) {Rotations.rotate(Rotations.getYaw(pos), Rotations.getPitch(pos));}public boolean outOfRange(BlockPos pos) {return !isInRange(range.get(), pos);}/** Distance from the eyes to the nearest point of the block, like the server measures it. */private boolean isInRange(double maximum, BlockPos pos) {if (mc.player == null) return false;Vec3d eyes = mc.player.getEyePos();double dx = Math.max(0, Math.max(pos.getX() - eyes.x, eyes.x - (pos.getX() + 1)));double dy = Math.max(0, Math.max(pos.getY() - eyes.y, eyes.y - (pos.getY() + 1)));double dz = Math.max(0, Math.max(pos.getZ() - eyes.z, eyes.z - (pos.getZ() + 1)));return dx * dx + dy * dy + dz * dz <= maximum * maximum;}/** The block face pointing closest to our eyes, used for the bedrock dig. */private Direction closestFace(BlockPos pos) {if (mc.player == null) return Direction.UP;Vec3d center = Vec3d.ofCenter(pos);Vec3d eyes = mc.player.getEyePos();Direction best = Direction.UP;double bestDot = -Double.MAX_VALUE;Vec3d toEyes = eyes.subtract(center).normalize();for (Direction dir : Direction.values()) {double dot = toEyes.x * dir.getOffsetX() + toEyes.y * dir.getOffsetY() + toEyes.z * dir.getOffsetZ();if (dot > bestDot) { bestDot = dot; best = dir; }}return best;}// ── Rendering ─────────────────────────────────────────────────────────────private void renderMineContext(Render3DEvent event, MineContext ctx) {double fraction = ctx.timing.renderFraction(System.nanoTime(), ctx.isPrimary ? breakThreshold.get() : 1);renderBox(event, ctx.pos, fraction, renderColor.get(), ShapeMode.Lines);}/** Draws the block shrunk toward its centre by {@code fraction}, as a progress indicator. */private void renderBox(Render3DEvent event, BlockPos pos, double fraction, SettingColor color, ShapeMode shape) {double shrink = (1 - clamp(fraction, 0, 1)) * 0.5;// THM fills the side at a third of the outline's alpha; Lines mode ignores it either way.Color side = new Color(color.r, color.g, color.b, color.a / 3);event.renderer.box(pos.getX() + shrink, pos.getY() + shrink, pos.getZ() + shrink,pos.getX() + 1 - shrink, pos.getY() + 1 - shrink, pos.getZ() + 1 - shrink,side, color, shape, 0);}// ── Enums ────────────────────────────────────────────────────────────────public enum RebreakTrigger {OnPacket,OnClientWorld}public enum RebreakMode {Off,Strict,Strong,Bypass;boolean isStrong(boolean confirmedRebreak) {return this == Strong || (this == Bypass && confirmedRebreak);}boolean isPrimed(boolean confirmedBreak) {// A break and replacement in one server tick can omit the air update.return confirmedBreak || this == Strong;}boolean canRebreak(boolean primed, boolean waiting, boolean confirmedRebreak) {return this != Off && isPrimed(primed) && (!waiting || isStrong(confirmedRebreak));}}public enum SwapMode {Keep("Keep for Next Tick"),SameTick("Same Tick"),EndOfTick("End of Tick"),ToolHold("Tool Hold");private final String title;SwapMode(String title) {this.title = title;}@Overridepublic String toString() {return title;}SwapMode forBreak(boolean instant, boolean primedRebreak) {return instant || primedRebreak ? this : ToolHold;}boolean shouldRelease(long ticksSinceAction, boolean mining, boolean replacement, boolean pending, int idleTicks) {return switch (this) {case SameTick, EndOfTick -> true;case Keep -> !mining && !replacement && ticksSinceAction >= 1;case ToolHold -> !mining && !pending && idleTicks >= IDLE_RELEASE_TICKS;};}}// ── Rebreak monitor ──────────────────────────────────────────────────────static final class RebreakConfirmations {private final Map<BlockPos, Long> awaiting = new HashMap<>();void sent(BlockPos pos, long now, boolean initialConfirmed) {// A late initial air response must not count as a successful Strong retry.if (!initialConfirmed) return;expire(now);awaiting.putIfAbsent(pos.toImmutable(), now + REBREAK_TIMEOUT_NS);}boolean confirm(BlockPos pos, long now) {Long deadline = awaiting.remove(pos);return deadline != null && now < deadline;}void expire(long now) {awaiting.values().removeIf(deadline -> now >= deadline);}void clear() {awaiting.clear();}}// ── Mining progress ──────────────────────────────────────────────────────static final class MiningProgress {private long lastNs;private double damage;private double rate;private boolean started;void start(long now, double delta, float tps) {lastNs = now;damage = Double.isFinite(delta) ? Math.max(0, delta) : 1;rate = damage * serverTicks(1000, tps) / 1_000_000_000.0;started = true;}void advance(long now, double delta, float tps, boolean paused) {if (!started) return;damage += Math.max(0, now - lastNs) * rate;lastNs = Math.max(lastNs, now);rate = paused || !Double.isFinite(delta) ? 0: Math.max(0, delta) * serverTicks(1000, tps) / 1_000_000_000.0;}double fraction(double threshold) {return clamp(damage / threshold, 0, 1);}double renderFraction(long now, double threshold) {// Extrapolate at most one client tick without changing mining work.return clamp((damage + clamp(now - lastNs, 0, 50_000_000L) * rate) / threshold, 0, 1);}}// ── MineContext ──────────────────────────────────────────────────────────public class MineContext {public final BlockPos pos;public final BlockState state;public final long startMs;public boolean isPrimary;public boolean instaBreak;/** Hotbar slot holding the tool this block was started with; -1 if none. */public final int startSlot;public boolean active = true;private boolean rebreak;/** True when this rebreak was triggered by a server update the client world has not shown yet. */private boolean packetRebreak;private boolean started;private boolean stopSent;private double tickDamage;private final MiningProgress timing = new MiningProgress();public MineContext(BlockPos pos, BlockState state, boolean isPrimary) {this.pos = pos.toImmutable();this.state = state;this.isPrimary = isPrimary;this.startSlot = findBestHotbarSlot(state);this.startMs = System.currentTimeMillis();this.tickDamage = calcDelta();this.instaBreak = tickDamage >= 1.0;}private boolean requiresValidation() {return !instaBreak && !rebreak;}public double progress() {return timing.fraction(isPrimary ? breakThreshold.get() : 1);}private void advance(long now, boolean paused) {tickDamage = paused ? 0 : calcDelta();timing.advance(now, tickDamage, effectiveTps(), paused);}/*** Per-tick mining damage for the tool this context started with. Meteor's helper already does* the whole vanilla formula; the only thing added here is cancelling its airborne penalty* when the ground claim is active.*/private double calcDelta() {if (mc.player == null || mc.world == null) return 0;int slot = startSlot < 0 ? mc.player.getInventory().getSelectedSlot() : startSlot;double delta = BlockUtils.getBreakDelta(slot, state);if (!Double.isFinite(delta)) return 1;if (delta <= 0) return 0;if (!mc.player.isOnGround() && liftsAirMiningPenalty()) delta *= 5;return delta;}}}
port-only to fixed. Every fix, and nothing else.
@@ -17,7 +17,20 @@* THM sequenced-packet accessor -> an invoker mixin on ClientPlayerInteractionManager.* PaketLimiter / ThmMembers / THMSystem / DescribedOption -> dropped, they are THM-only.* fromTag() settings migration -> dropped, QuinnAddon has no legacy Speedmine settings.- * The known defects of the original are NOT fixed here; see ../fixed/Speedmine.java for that.+ * FIXES APPLIED ON TOP OF THE ORIGINAL (THM 26.2 behaves differently here):+ * 1. The break event is only cancelled when this module actually takes the block, so a click+ * that the module declines falls back to vanilla mining instead of doing nothing.+ * 2. requestBreak() validates the block, and a context that cannot make progress is dropped after+ * STALL_TIMEOUT_MS instead of occupying the primary slot forever.+ * 3. Every queue insert stores an immutable BlockPos.+ * 4. A context whose block changed to a different block is dropped instead of mining the old one.+ * 5. The enemy's feet use the floored Y, so a half-slab or snow layer no longer shifts the+ * surround ring one block up.+ * 6. Dead players are not targeted.+ * 7. The tool search starts from the slot already selected, so an equally good held tool is kept+ * instead of swapping to a lower hotbar index.+ * 8. The rebreak timeout is one constant instead of the same value written in ms and in ns.+ * 9. The "last broken" outline respects rebreak-mode Off.*/package com.example.addon.modules;@@ -336,6 +349,11 @@/** Ticks with nothing left to mine before the slot is handed back to the client's real one. */private static final int IDLE_RELEASE_TICKS = 3;+ /** How long a context may make no progress before it is dropped (fix 2). */+ private static final long STALL_TIMEOUT_MS = 3000;+ /** Fix 8: one timeout, two units derived from it. */+ private static final long REBREAK_TIMEOUT_MS = 2000;+ private static final long REBREAK_TIMEOUT_NS = REBREAK_TIMEOUT_MS * 1_000_000L;private MineContext primary;private MineContext secondary;@@ -516,10 +534,13 @@BlockState state = mc.world.getBlockState(event.blockPos);if (!BlockUtils.canBreak(event.blockPos, state)) return;if (outOfRange(event.blockPos)) return;- event.cancel();- if (!isMining(event.blockPos)) {- handleBlockClick(event.blockPos, state);+ // Fix 1: this block is already ours, so vanilla must not dig it, but there is nothing to start.+ if (isMining(event.blockPos)) {+ event.cancel();+ return;}+ // Fix 1: only take the click away from vanilla if the module actually accepted it.+ if (handleBlockClick(event.blockPos, state)) event.cancel();}@EventHandler@@ -564,7 +585,7 @@else if (heldSlot != -1 && !pendingBreaks.isEmpty()) holdTool(heldSlot);}- if (primary != null && !primary.started && !shouldRemove(primary.pos)) sendStart(primary);+ if (primary != null && !shouldRemove(primary, now) && !primary.started) sendStart(primary);tickAutoMine();rebreakReplacement();@@ -616,7 +637,9 @@if (secondary != null) renderMineContext(event, secondary);if (primary != null) renderMineContext(event, primary);- if (lastBrokenPos != null && autoRebreak.get() && !mc.world.getBlockState(lastBrokenPos).isAir()) {+ // Fix 9: every other rebreak check also tests the mode, so the outline should too.+ if (lastBrokenPos != null && autoRebreak.get() && rebreakMode.get() != RebreakMode.Off+ && !mc.world.getBlockState(lastBrokenPos).isAir()) {renderBox(event, lastBrokenPos, 1, renderColor.get(), ShapeMode.Lines);}}@@ -645,24 +668,26 @@if (finishBreak(ctx, silentSwap.get()) && !strong && tpsSync.get()) startCredit = Math.max(0, startCredit - 1);}- private void handleBlockClick(BlockPos pos, BlockState state) {- if (isMining(pos)) return;+ /** @return true when this module took ownership of {@code pos} (mining it, or queueing it). */+ private boolean handleBlockClick(BlockPos pos, BlockState state) {+ if (isMining(pos)) return false;if (autoRebreak.get() && rebreakMode.get() != RebreakMode.Off && rebreakMode.get().isPrimed(lastBreakConfirmed)&& pos.equals(lastBrokenPos) && pos.equals(lastStartedPos)) {boolean packet = rebreakTrigger.get() == RebreakTrigger.OnPacket;if (packet && packetReplacement != null) tryRebreak(pos, packetReplacement, true);else tryRebreak(pos, state, false);- return;+ return true;}- if (shouldPauseMining()) return;+ if (shouldPauseMining()) return false;if (hasPendingNormalBreak()) {- if (queueEnabled.get()) queue.addLast(pos.toImmutable());- return;+ if (!queueEnabled.get()) return false;+ queue.addLast(pos.toImmutable());+ return true;}if (!canStart()) {queue.addLast(pos.toImmutable());- return;+ return true;}boolean canAddSecondary = secondary == null && doubleBreak.get();@@ -670,23 +695,30 @@if (primary == null) {primary = new MineContext(pos, state, true);sendStart(primary);+ return true;} else if (canAddSecondary) {if (!stopWithTool(primary, silentSwap.get())) {- if (queueEnabled.get() && !queue.contains(pos)) queue.addLast(pos.toImmutable());- return;+ if (!queueEnabled.get()) return false;+ if (!queue.contains(pos)) queue.addLast(pos.toImmutable());+ return true;}secondary = primary;secondary.isPrimary = false;primary = new MineContext(pos, state, true);sendStart(primary);+ return true;} else {- if (queueEnabled.get() && !queue.contains(pos)) queue.addLast(pos);+ // Fix 3: this insert was the one place the original stored a possibly mutable BlockPos.+ if (!queueEnabled.get()) return false;+ if (!queue.contains(pos)) queue.addLast(pos.toImmutable());+ return true;}}private void pruneCompletedOrInvalid() {- if (primary != null && shouldRemove(primary.pos)) primary = null;- if (secondary != null && shouldRemove(secondary.pos)) secondary = null;+ long now = System.currentTimeMillis();+ if (primary != null && shouldRemove(primary, now)) primary = null;+ if (secondary != null && shouldRemove(secondary, now)) secondary = null;queue.removeIf(this::shouldRemove);}@@ -694,12 +726,24 @@return mc.world.getBlockState(pos).isAir() || outOfRange(pos);}+ /**+ * Fix 4: the context carries the state it started on, so a block that was swapped for a+ * different one must not keep being mined with the old hardness and the old particle id.+ * Fix 2: a context that cannot make progress at all (hardness below zero, no usable tool) would+ * otherwise never satisfy shouldRemove and would hold the primary slot forever.+ */+ private boolean shouldRemove(MineContext ctx, long now) {+ if (shouldRemove(ctx.pos)) return true;+ if (mc.world.getBlockState(ctx.pos) != ctx.state) return true;+ return ctx.tickDamage <= 0 && now - ctx.startMs > STALL_TIMEOUT_MS;+ }+private void drainQueue() {if (hasPendingNormalBreak() || queue.isEmpty() || !canStart()) return;if (primary == null || (doubleBreak.get() && secondary == null)) {BlockPos pos = queue.pollFirst();- handleBlockClick(pos, mc.world.getBlockState(pos));+ handleBlockClick(pos, mc.world.getBlockState(pos)); // return value is not needed here}}@@ -761,7 +805,7 @@lastBrokenPos = ctx.pos;if (ctx.rebreak) {rebreakPending = true;- rebreakDeadlineMs = System.currentTimeMillis() + 2000;+ rebreakDeadlineMs = System.currentTimeMillis() + REBREAK_TIMEOUT_MS;packetReplacement = null;if (debugRebreak.get()) rebreakConfirmations.sent(ctx.pos, System.nanoTime(), lastBreakConfirmed);}@@ -875,9 +919,13 @@*/private int findBestHotbarSlot(BlockState state) {if (mc.player == null) return -1;- int best = -1;- double bestSpeed = -1;+ // Fix 7: start from the slot already selected, so an equally fast tool that is in hand wins+ // the tie. The original started at -1, which both made the "no tool" result unreachable and+ // swapped to the lowest matching hotbar index on every tie.+ int best = mc.player.getInventory().getSelectedSlot();+ double bestSpeed = BlockUtils.getBreakDelta(best, state);for (int i = 0; i < 9; i++) {+ if (i == best) continue;double speed = BlockUtils.getBreakDelta(i, state);if (speed > bestSpeed) { bestSpeed = speed; best = i; }}@@ -934,16 +982,18 @@/** Every valid target around nearby enemies, nearest first. */private List<BlockPos> findAutoTargets() {List<PlayerEntity> enemies = new ArrayList<>();- int friends = 0, distant = 0;+ int friends = 0, distant = 0, dead = 0;for (PlayerEntity player : mc.world.getPlayers()) {if (player == mc.player || player.isSpectator()) continue;+ // Fix 6: a corpse still sits in the player list for the whole death animation.+ if (!player.isAlive()) { dead++; continue; }if (Friends.get().isFriend(player)) { friends++; continue; }if (player.distanceTo(mc.player) > enemyRange.get()) { distant++; continue; }enemies.add(player);}if (enemies.isEmpty()) {if (debugAutoMine.get()) autoMineStatus = String.format(Locale.ROOT,- "No eligible players (friends=%d, distant=%d).", friends, distant);+ "No eligible players (friends=%d, distant=%d, dead=%d).", friends, distant, dead);return List.of();}@@ -1006,8 +1056,12 @@return mineable;}+ /**+ * Fix 5: the original rounded the Y, which lands one block too high whenever the enemy stands on+ * anything that puts them on a fractional Y of .5 or more (slab, snow layer, soul sand).+ */private BlockPos feet(PlayerEntity enemy) {- return new BlockPos(enemy.getBlockX(), (int) Math.round(enemy.getY()), enemy.getBlockZ());+ return new BlockPos(enemy.getBlockX(), enemy.getBlockY(), enemy.getBlockZ());}/** True if the block is inside or directly against our own hitbox — breaking it drops or exposes us. */@@ -1144,7 +1198,9 @@public void requestBreak(BlockPos pos) {if (mc.world == null || mc.player == null) return;BlockState state = mc.world.getBlockState(pos);- if (state.isAir()) return;+ // Fix 2: the original only checked for air, so an unbreakable block handed in by another+ // module (bedrock, barrier) produced a context that could never finish.+ if (state.isAir() || outOfRange(pos) || !BlockUtils.canBreak(pos, state)) return;if (!isMining(pos)) handleBlockClick(pos, state);}@@ -1273,7 +1329,7 @@// A late initial air response must not count as a successful Strong retry.if (!initialConfirmed) return;expire(now);- awaiting.putIfAbsent(pos.toImmutable(), now + 2_000_000_000L);+ awaiting.putIfAbsent(pos.toImmutable(), now + REBREAK_TIMEOUT_NS);}boolean confirm(BlockPos pos, long now) {
The original behaviour, reshaped for QuinnAddon. Reference point for the diff.
/** Ported into QuinnAddon from THM Addons — https://github.com/Magnus-Krill/THM-Addons* Source file: src/main/java/xyz/thm/addon/modules/Speedmine.java (branch 26.2).* The original header reads: "Copyright (c) THM Addons contributors. Credit the devs, keep the link.* By using this code you agree to the license terms and to keep your repo public."* Credit stays with the THM Addons contributors; thanks to mushek, and to BlackOut for the* Auto Mine target order this is based on.** PORT NOTES — behaviour is kept, the plumbing is not. QuinnAddon does not have THM's support* classes, so each one is replaced by the nearest thing Meteor already ships:* Mojang mappings -> Yarn mappings (mc.level -> mc.world, Serverbound* -> *C2SPacket).* THM InventoryManager -> a local server-slot tracker (UpdateSelectedSlotC2SPacket).* THM RangeUtils -> isInRange() / closestFace() below.* THM RenderUtilsTHM -> Render3DEvent.renderer.box().* THM hand-written mining formula -> Meteor BlockUtils.getBreakDelta(slot, state) (same maths).* THM ClientLevel mixin hook -> Meteor BlockUpdateEvent.* THM sequenced-packet accessor -> an invoker mixin on ClientPlayerInteractionManager.* PaketLimiter / ThmMembers / THMSystem / DescribedOption -> dropped, they are THM-only.* fromTag() settings migration -> dropped, QuinnAddon has no legacy Speedmine settings.* The known defects of the original are NOT fixed here; see ../fixed/Speedmine.java for that.*/package com.example.addon.modules;import com.example.addon.QuinnAddon;import com.example.addon.mixin.ClientPlayerInteractionManagerAccessor;import com.example.addon.mixin.ClientWorldAccessor;import com.example.addon.mixin.PlayerMoveC2SPacketAccessor;import meteordevelopment.meteorclient.events.entity.player.StartBreakingBlockEvent;import meteordevelopment.meteorclient.events.game.GameLeftEvent;import meteordevelopment.meteorclient.events.packets.PacketEvent;import meteordevelopment.meteorclient.events.render.Render3DEvent;import meteordevelopment.meteorclient.events.world.BlockUpdateEvent;import meteordevelopment.meteorclient.events.world.TickEvent;import meteordevelopment.meteorclient.renderer.ShapeMode;import meteordevelopment.meteorclient.settings.*;import meteordevelopment.meteorclient.systems.friends.Friends;import meteordevelopment.meteorclient.systems.modules.Module;import meteordevelopment.meteorclient.utils.misc.Keybind;import meteordevelopment.meteorclient.utils.player.Rotations;import meteordevelopment.meteorclient.utils.render.color.Color;import meteordevelopment.meteorclient.utils.render.color.SettingColor;import meteordevelopment.meteorclient.utils.world.BlockUtils;import meteordevelopment.meteorclient.utils.world.TickRate;import meteordevelopment.orbit.EventHandler;import meteordevelopment.orbit.EventPriority;import net.minecraft.block.Block;import net.minecraft.block.BlockState;import net.minecraft.block.Blocks;import net.minecraft.entity.player.PlayerEntity;import net.minecraft.network.packet.c2s.play.PlayerActionC2SPacket;import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket;import net.minecraft.network.packet.c2s.play.UpdateSelectedSlotC2SPacket;import net.minecraft.util.Hand;import net.minecraft.util.math.BlockPos;import net.minecraft.util.math.Box;import net.minecraft.util.math.Direction;import net.minecraft.util.math.Vec3d;import net.minecraft.world.WorldEvents;import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Comparator;import java.util.Deque;import java.util.HashMap;import java.util.List;import java.util.Locale;import java.util.Map;import java.util.function.Function;/** Packet miner with server-timed progress and sequenced client prediction. */public class Speedmine extends Module {private final SettingGroup sgMine = settings.getDefaultGroup();private final SettingGroup sgAuto = settings.createGroup("Auto Mine", false);private final SettingGroup sgRender = settings.createGroup("Render");private final SettingGroup sgAir = settings.createGroup("Air Miner", false);// ── Air Miner ─────────────────────────────────────────────────────────────private final Setting<Boolean> airMine = sgAir.add(new BoolSetting.Builder().name("air-mine").description("Mine at ground speed while airborne.").defaultValue(false).onChanged(enabled -> { if (!enabled) restoreAirGround(); }).build());private final Setting<Boolean> airWhileGliding = sgAir.add(new BoolSetting.Builder().name("air-while-gliding").description("Allow airborne mining while gliding.").defaultValue(false).visible(airMine::get).build());private final Setting<Double> airMaxSpeed = sgAir.add(new DoubleSetting.Builder().name("air-max-speed").description("Maximum horizontal speed for ground claims.").defaultValue(1).min(0).sliderRange(0, 3).visible(airMine::get).build());private final Setting<Integer> airClaimWindow = sgAir.add(new IntSetting.Builder().name("air-claim-window").description("Ticks to retain the mining ground claim.").defaultValue(2).min(1).sliderRange(1, 10).visible(airMine::get).build());private int airClaimLeft;private long airClaimTick = -1;private boolean airInjecting;// ── Auto Mine (target selection, ported from BlackOut's AutoMine) ─────────public final Setting<Boolean> autoMine = sgAuto.add(new BoolSetting.Builder().name("auto-mine").description("Automatically pick blocks to break around nearby enemies.").defaultValue(false).build());public final Setting<Keybind> autoMineBind = sgAuto.add(new KeybindSetting.Builder().name("auto-mine-bind").description("Hold this to auto-mine. Leave unbound to have auto-mine run whenever the module is on.").defaultValue(Keybind.none()).visible(autoMine::get).build());private final Setting<Boolean> debugAutoMine = sgAuto.add(new BoolSetting.Builder().name("debug-auto-mine").description("Report Auto Mine targets and idle reasons in chat.").defaultValue(false).onChanged(enabled -> { this.lastAutoMineStatus = null; this.nextAutoMineReportTick = 0; }).visible(autoMine::get).build());public final Setting<Boolean> autoMineOnly = sgAuto.add(new BoolSetting.Builder().name("auto-mine-only").description("Ignore blocks you click yourself — only auto-mine targets get broken.").defaultValue(false).visible(autoMine::get).build());public final Setting<Boolean> feetFirst = sgAuto.add(new BoolSetting.Builder().name("feet-first").description("Break the lowest targets first, so an enemy's feet go before their head. Applies to bedrock too.").defaultValue(false).visible(autoMine::get).build());public final Setting<Double> enemyRange = sgAuto.add(new DoubleSetting.Builder().name("enemy-range").description("How far away an enemy can be to be considered.").defaultValue(10).min(1).max(20).sliderRange(1, 20).visible(autoMine::get).build());public final Setting<Boolean> antiPhase = sgAuto.add(new BoolSetting.Builder().name("anti-phase").description("Mine the blocks an enemy is standing inside.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> antiSurround = sgAuto.add(new BoolSetting.Builder().name("anti-surround").description("Mine the blocks boxing an enemy in.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> neverMineOwn = sgAuto.add(new BoolSetting.Builder().name("never-mine-own").description("Never auto-mine blocks touching you.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> autoDoubleMine = sgAuto.add(new BoolSetting.Builder().name("auto-double-mine").description("Break two auto-mine targets at once, using double-break.").defaultValue(true).visible(autoMine::get).build());public final Setting<Boolean> mineBedrock = sgAuto.add(new BoolSetting.Builder().name("mine-bedrock").description("Also target bedrock, broken vanilla-style with hand swings instead of packets.").defaultValue(false).visible(autoMine::get).build());public final Setting<Boolean> bedrockOnly = sgAuto.add(new BoolSetting.Builder().name("bedrock-only").description("Only break bedrock, ignore every other block.").defaultValue(false).visible(() -> autoMine.get() && mineBedrock.get()).build());public final Setting<Boolean> bedrockWhileEating = sgAuto.add(new BoolSetting.Builder().name("bedrock-while-eating").description("Continue mining bedrock while eating.").defaultValue(false).visible(() -> autoMine.get() && mineBedrock.get()).build());public final Setting<Boolean> bedrockRotate = sgAuto.add(new BoolSetting.Builder().name("bedrock-rotate").description("Silently look at the bedrock before breaking it. Usually not needed.").defaultValue(false).visible(() -> autoMine.get() && mineBedrock.get()).build());// ── Mining ────────────────────────────────────────────────────────────────public final Setting<Boolean> rotate = sgMine.add(new BoolSetting.Builder().name("rotate").description("Silently look at each block before mining it. Bedrock has its own toggle.").defaultValue(false).build());public final Setting<Boolean> grimBypass = sgMine.add(new BoolSetting.Builder().name("grim-bypass").description("Send STOP_DESTROY_BLOCK before START to bypass Grim's sequence check.").defaultValue(true).build());public final Setting<Boolean> doubleBreak = sgMine.add(new BoolSetting.Builder().name("double-break").description("Track a primary and secondary block simultaneously.").defaultValue(true).build());public final Setting<Boolean> queueEnabled = sgMine.add(new BoolSetting.Builder().name("queue").description("Queue extra blocks when both break slots are occupied.").defaultValue(true).build());public final Setting<Double> breakThreshold = sgMine.add(new DoubleSetting.Builder().name("break-threshold").description("Mining progress needed before sending STOP.").defaultValue(0.7).min(0.1).max(1.0).decimalPlaces(2).build());public final Setting<Boolean> clientPrediction = sgMine.add(new BoolSetting.Builder().name("client-prediction").description("Predict completed breaks locally, including rebreaks.").defaultValue(true).build());public final Setting<Boolean> autoRebreak = sgMine.add(new BoolSetting.Builder().name("auto-rebreak").description("Rebreak the last position if a block reappears there.").defaultValue(true).build());public final Setting<RebreakMode> rebreakMode = sgMine.add(new EnumSetting.Builder<RebreakMode>().name("rebreak-mode").description("How instant rebreaks wait for server confirmation. Off: no instant rebreaks. Strict: wait for server confirmation between rebreaks. Strong: skip confirmation waits and TPS start delays. Bypass: Strict until a rebreak is confirmed, then Strong.").defaultValue(RebreakMode.Strong).visible(autoRebreak::get).build());public final Setting<RebreakTrigger> rebreakTrigger = sgMine.add(new EnumSetting.Builder<RebreakTrigger>().name("rebreak-trigger").description("Use client-world checks, optionally with early packet triggers. OnPacket: rebreak on server updates and client-world replacements. OnClientWorld: check for replacement blocks each client tick.").defaultValue(RebreakTrigger.OnPacket).visible(autoRebreak::get).build());private final Setting<Boolean> debugRebreak = sgMine.add(new BoolSetting.Builder().name("debug-rebreak").description("Show server-confirmed rebreaks per second.").defaultValue(false).onChanged(enabled -> resetRebreakMonitor()).build());public final Setting<Boolean> silentSwap = sgMine.add(new BoolSetting.Builder().name("silent-swap").description("Swap to the best tool via packet without visually changing your held item.").defaultValue(true).build());public final Setting<SwapMode> swapMode = sgMine.add(new EnumSetting.Builder<SwapMode>().name("swap-mode").description("Swap timing for instant breaks and rebreaks. Keep: hold the tool through the next tick and consecutive rebreaks. SameTick: restore the previous slot immediately after mining packets. EndOfTick: restore the selected slot at the end of the tick. ToolHold: hold the tool through server validation and three idle ticks.").defaultValue(SwapMode.SameTick).visible(silentSwap::get).onChanged(mode -> releaseHeldSlot()).build());public final Setting<Boolean> tpsSync = sgMine.add(new BoolSetting.Builder().name("tps-sync").description("Match mining progress and start rate to server TPS.").defaultValue(false).build());public final Setting<Boolean> multitask = sgMine.add(new BoolSetting.Builder().name("multitask").description("Continue mining while using items.").defaultValue(false).build());public final Setting<Double> range = sgMine.add(new DoubleSetting.Builder().name("range").description("Maximum block-breaking distance.").defaultValue(5.2).min(1).max(6).decimalPlaces(1).build());// ── Render ────────────────────────────────────────────────────────────────private final Setting<SettingColor> renderColor = sgRender.add(new ColorSetting.Builder().name("color").defaultValue(new SettingColor(225, 25, 25, 255)).build());private final Setting<SettingColor> bedrockColor = sgRender.add(new ColorSetting.Builder().name("bedrock-color").description("Color of the bedrock blocks currently being broken.").defaultValue(new SettingColor(255, 70, 70, 255)).visible(mineBedrock::get).build());private final Setting<ShapeMode> bedrockShape = sgRender.add(new EnumSetting.Builder<ShapeMode>().name("bedrock-shape").description("How the bedrock being broken is drawn.").defaultValue(ShapeMode.Both).visible(mineBedrock::get).build());// ── State ─────────────────────────────────────────────────────────────────public static Speedmine INSTANCE;/** Ticks with nothing left to mine before the slot is handed back to the client's real one. */private static final int IDLE_RELEASE_TICKS = 3;private MineContext primary;private MineContext secondary;public BlockPos lastBrokenPos;private boolean lastBreakConfirmed;private boolean rebreakPending;private boolean bypassConfirmed;private long rebreakDeadlineMs;private BlockState packetReplacement;private long rebreakMonitorStartNs;private int rebreakCount;private String rebreakRate = "0.0";private final RebreakConfirmations rebreakConfirmations = new RebreakConfirmations();private BlockPos lastStartedPos;public final Deque<BlockPos> queue = new ArrayDeque<>();/** Hotbar slot this miner is holding on the server; -1 when it holds none. */private int heldSlot = -1;/** What the server currently believes is selected; -1 when it matches the client. */private int serverSlot = -1;private int idleTicks = 0;private final List<PendingBreak> pendingBreaks = new ArrayList<>(3);private long lastStartCreditMs;private double startCredit = 1;private long clientTick;private long lastMiningActionTick;private BlockPos bedrockPos;private String autoMineStatus;private String lastAutoMineStatus;private long nextAutoMineReportTick;private int autoAirRejected, autoSelfRejected, autoRangeRejected, autoDisabledRejected, autoUnbreakableRejected;// ── Constructor ───────────────────────────────────────────────────────────public Speedmine() {super(QuinnAddon.CATEGORY, "speedmine","Grim-safe packet miner with queue and double break. Ported from THM Addons, credit to the THM devs.");INSTANCE = this;}// ── Module lifecycle ──────────────────────────────────────────────────────@Overridepublic void onActivate() {airClaimLeft = 0;airClaimTick = -1;lastStartCreditMs = System.currentTimeMillis();startCredit = 1;clientTick = 0;lastMiningActionTick = 0;autoMineStatus = lastAutoMineStatus = null;nextAutoMineReportTick = 0;resetRebreakMonitor();}@Overridepublic void onDeactivate() {restoreAirGround();releaseHeldSlot();primary = null;secondary = null;lastBrokenPos = null;lastBreakConfirmed = false;rebreakPending = false;bypassConfirmed = false;packetReplacement = null;resetRebreakMonitor();lastStartedPos = null;bedrockPos = null;pendingBreaks.clear();queue.clear();}@EventHandlerprivate void onGameLeft(GameLeftEvent event) {heldSlot = -1;serverSlot = -1;onDeactivate();}// ── Air Miner ─────────────────────────────────────────────────────────────public boolean liftsAirMiningPenalty() {return isActive() && airMine.get() && mc.player != null && mc.getNetworkHandler() != null&& canClaimAir(mc.player.isOnGround(), mc.player.isGliding(), airWhileGliding.get(),mc.player.isCreative(), mc.player.isSpectator(),mc.player.getVelocity().horizontalLength(), airMaxSpeed.get());}static boolean canClaimAir(boolean onGround, boolean gliding, boolean allowGliding, boolean creative,boolean spectator, double speed, double maximum) {return !onGround && (!gliding || allowGliding) && !creative && !spectator && speed <= maximum;}@EventHandler(priority = EventPriority.LOWEST)private void onPacketSend(PacketEvent.Send event) {if (event.isCancelled() || mc.player == null || mc.getNetworkHandler() == null) return;// Keep our idea of the server's selected slot in sync with whatever actually goes out.if (event.packet instanceof UpdateSelectedSlotC2SPacket slot) {serverSlot = slot.getSelectedSlot();return;}if (airInjecting) return;if (event.packet instanceof PlayerActionC2SPacket action&& (action.getAction() == PlayerActionC2SPacket.Action.START_DESTROY_BLOCK|| action.getAction() == PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK)&& liftsAirMiningPenalty()) {if (airClaimTick != clientTick) {sendAirGround(true);airClaimTick = clientTick;}airClaimLeft = airClaimWindow.get();} else if (event.packet instanceof PlayerMoveC2SPacket move && airClaimLeft > 0 && liftsAirMiningPenalty()) {((PlayerMoveC2SPacketAccessor) move).quinn$setOnGround(true);}}private void sendAirGround(boolean onGround) {if (mc.player == null || mc.getNetworkHandler() == null) return;airInjecting = true;try {mc.getNetworkHandler().sendPacket(new PlayerMoveC2SPacket.Full(mc.player.getX(), mc.player.getY(), mc.player.getZ(),mc.player.getYaw(), mc.player.getPitch(), onGround, mc.player.horizontalCollision));} finally {airInjecting = false;}}private void restoreAirGround() {boolean claimed = airClaimLeft > 0 || airClaimTick != -1;airClaimLeft = 0;airClaimTick = -1;if (claimed && mc.player != null && mc.getNetworkHandler() != null) sendAirGround(mc.player.isOnGround());}// ── Server block updates (THM used a ClientLevel mixin; Meteor fires this for us) ──@EventHandlerprivate void onBlockUpdate(BlockUpdateEvent event) {if (mc.world == null || mc.player == null) return;BlockPos pos = event.pos;BlockState state = event.newState;confirmBreak(pos, state);if (!pos.equals(lastBrokenPos)) return;packetReplacement = state.isAir() ? null : state;if (rebreakTrigger.get() == RebreakTrigger.OnPacket && packetReplacement != null) {tryRebreak(pos, packetReplacement, true);}}private void confirmBreak(BlockPos pos, BlockState state) {if (!state.isAir()) return;if (rebreakConfirmations.confirm(pos, System.nanoTime())) rebreakCount++;if (pos.equals(lastStartedPos) && (pos.equals(lastBrokenPos) || (primary != null && pos.equals(primary.pos)))) {lastBrokenPos = pos.toImmutable();lastBreakConfirmed = true;if (rebreakPending) bypassConfirmed = true;rebreakPending = false;}pendingBreaks.removeIf(pending -> pending.pos().equals(pos));if (primary != null && pos.equals(primary.pos)) primary = null;if (secondary != null && pos.equals(secondary.pos)) secondary = null;}// ── Events ────────────────────────────────────────────────────────────────@EventHandlerprivate void onStartBreaking(StartBreakingBlockEvent event) {if (mc.world == null || mc.player == null) return;// Hands manual clicks straight back to vanilla — auto-mine owns the moduleif (autoMine.get() && autoMineOnly.get()) return;BlockState state = mc.world.getBlockState(event.blockPos);if (!BlockUtils.canBreak(event.blockPos, state)) return;if (outOfRange(event.blockPos)) return;event.cancel();if (!isMining(event.blockPos)) {handleBlockClick(event.blockPos, state);}}@EventHandlerprivate void onTick(TickEvent.Pre event) {if (mc.world == null || mc.player == null) return;clientTick++;if (airClaimLeft > 0 && (--airClaimLeft == 0 || !liftsAirMiningPenalty())) restoreAirGround();long now = System.currentTimeMillis();startCredit = replenishStartCredit(startCredit, now - lastStartCreditMs, effectiveTps());lastStartCreditMs = now;pendingBreaks.removeIf(pending -> now >= pending.deadlineMs());if (rebreakPending && now >= rebreakDeadlineMs) rebreakPending = false;updateRebreakMonitor();boolean paused = shouldPauseMining();long progressNow = System.nanoTime();if (primary != null) primary.advance(progressNow, paused);if (secondary != null) secondary.advance(progressNow, paused);if (debugAutoMine.get()) {autoMineStatus = !autoMine.get() ? "Disabled.": !autoMineHeld() ? "Waiting for auto-mine-bind.": "Paused while using an item or eating.";}if (paused) {releaseHeldSlot();if (bedrockWhileEating.get() && isEating()) tickAutoMine();else bedrockPos = null;return;}int validationSlot = validationToolSlot();if (!silentSwap.get()) {releaseHeldSlot();if (validationSlot != -1) selectSlot(validationSlot);} else if (validationSlot != -1) holdTool(validationSlot);else if (swapMode.get() == SwapMode.SameTick) releaseHeldSlot();else if (swapMode.get() != SwapMode.EndOfTick) {if (primary != null) holdTool(primary.startSlot);else if (secondary != null) holdTool(secondary.startSlot);else if (heldSlot != -1 && !pendingBreaks.isEmpty()) holdTool(heldSlot);}if (primary != null && !primary.started && !shouldRemove(primary.pos)) sendStart(primary);tickAutoMine();rebreakReplacement();pruneCompletedOrInvalid();if (secondary != null && secondary.progress() >= 1.0) finishBreak(secondary, silentSwap.get());if (primary != null && primary.progress() >= 1.0) finishBreak(primary, silentSwap.get());drainQueue();}@EventHandler(priority = EventPriority.LOWEST)private void onTickPost(TickEvent.Post event) {if (mc.player == null || mc.world == null) return;if (debugAutoMine.get()&& autoMineStatus != null && !autoMineStatus.equals(lastAutoMineStatus) && clientTick >= nextAutoMineReportTick) {info("Auto Mine: %s", autoMineStatus);lastAutoMineStatus = autoMineStatus;nextAutoMineReportTick = clientTick + 40;}// Placers can refill predicted air after our Pre handler.rebreakReplacement();if (heldSlot == -1) return;if (!silentSwap.get() || shouldPauseMining()) {releaseHeldSlot();return;}boolean mining = primary != null || secondary != null || !queue.isEmpty();boolean replacement = autoRebreak.get() && rebreakMode.get() != RebreakMode.Off&& rebreakMode.get().isPrimed(lastBreakConfirmed) && lastBrokenPos != null&& lastBrokenPos.equals(lastStartedPos)&& !outOfRange(lastBrokenPos) && BlockUtils.canBreak(lastBrokenPos, mc.world.getBlockState(lastBrokenPos));boolean pending = !pendingBreaks.isEmpty();idleTicks = mining || pending ? 0 : Math.min(IDLE_RELEASE_TICKS, idleTicks + 1);// Check after this tick's placements before releasing the keep-mode hold.SwapMode mode = validationToolSlot() != -1 ? SwapMode.ToolHold : swapMode.get();if (mode.shouldRelease(clientTick - lastMiningActionTick, mining, replacement, pending, idleTicks)) releaseHeldSlot();}@EventHandlerprivate void onRender(Render3DEvent event) {if (mc.world == null || mc.player == null) return;for (BlockPos pos : queue) renderBox(event, pos, 1, renderColor.get(), ShapeMode.Lines);if (bedrockPos != null) {renderBox(event, bedrockPos, 1, bedrockColor.get(), bedrockShape.get());}if (secondary != null) renderMineContext(event, secondary);if (primary != null) renderMineContext(event, primary);if (lastBrokenPos != null && autoRebreak.get() && !mc.world.getBlockState(lastBrokenPos).isAir()) {renderBox(event, lastBrokenPos, 1, renderColor.get(), ShapeMode.Lines);}}// ── Core break logic ──────────────────────────────────────────────────────private void rebreakReplacement() {if (lastBrokenPos == null) return;boolean packet = rebreakTrigger.get() == RebreakTrigger.OnPacket && packetReplacement != null;tryRebreak(lastBrokenPos, packet ? packetReplacement : mc.world.getBlockState(lastBrokenPos), packet);}private void tryRebreak(BlockPos pos, BlockState state, boolean packet) {boolean strong = rebreakMode.get().isStrong(bypassConfirmed);if (mc.interactionManager == null || !autoRebreak.get() || !pos.equals(lastBrokenPos) || !pos.equals(lastStartedPos)|| !rebreakMode.get().canRebreak(lastBreakConfirmed, rebreakPending, bypassConfirmed)|| primary != null || secondary != null || hasPendingNormalBreak(pendingBreaks, strong ? pos : null)|| outOfRange(pos) || state.isAir() || !BlockUtils.canBreak(pos, state)) return;if (shouldPauseMining()) return;if (!strong && !canStart()) return;MineContext ctx = new MineContext(pos, state, false);ctx.rebreak = true;ctx.packetRebreak = packet;if (rotate.get()) lookAt(pos);if (finishBreak(ctx, silentSwap.get()) && !strong && tpsSync.get()) startCredit = Math.max(0, startCredit - 1);}private void handleBlockClick(BlockPos pos, BlockState state) {if (isMining(pos)) return;if (autoRebreak.get() && rebreakMode.get() != RebreakMode.Off && rebreakMode.get().isPrimed(lastBreakConfirmed)&& pos.equals(lastBrokenPos) && pos.equals(lastStartedPos)) {boolean packet = rebreakTrigger.get() == RebreakTrigger.OnPacket;if (packet && packetReplacement != null) tryRebreak(pos, packetReplacement, true);else tryRebreak(pos, state, false);return;}if (shouldPauseMining()) return;if (hasPendingNormalBreak()) {if (queueEnabled.get()) queue.addLast(pos.toImmutable());return;}if (!canStart()) {queue.addLast(pos.toImmutable());return;}boolean canAddSecondary = secondary == null && doubleBreak.get();if (primary == null) {primary = new MineContext(pos, state, true);sendStart(primary);} else if (canAddSecondary) {if (!stopWithTool(primary, silentSwap.get())) {if (queueEnabled.get() && !queue.contains(pos)) queue.addLast(pos.toImmutable());return;}secondary = primary;secondary.isPrimary = false;primary = new MineContext(pos, state, true);sendStart(primary);} else {if (queueEnabled.get() && !queue.contains(pos)) queue.addLast(pos);}}private void pruneCompletedOrInvalid() {if (primary != null && shouldRemove(primary.pos)) primary = null;if (secondary != null && shouldRemove(secondary.pos)) secondary = null;queue.removeIf(this::shouldRemove);}private boolean shouldRemove(BlockPos pos) {return mc.world.getBlockState(pos).isAir() || outOfRange(pos);}private void drainQueue() {if (hasPendingNormalBreak() || queue.isEmpty() || !canStart()) return;if (primary == null || (doubleBreak.get() && secondary == null)) {BlockPos pos = queue.pollFirst();handleBlockClick(pos, mc.world.getBlockState(pos));}}// ── Packet building ───────────────────────────────────────────────────────private void sendStart(MineContext ctx) {if (ctx.started || !ctx.active) return;if (rotate.get()) lookAt(ctx.pos);withMiningTool(ctx, silentSwap.get(), () -> {if (grimBypass.get() && !sendSequencedAction(PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK, ctx, false)) return;if (!sendSequencedAction(PlayerActionC2SPacket.Action.START_DESTROY_BLOCK, ctx, false)) return;ctx.started = true;ctx.instaBreak = ctx.tickDamage >= 1.0;ctx.timing.start(System.nanoTime(), ctx.tickDamage, effectiveTps());rebreakPending = false;bypassConfirmed = false;packetReplacement = null;lastBreakConfirmed = false;lastStartedPos = ctx.pos;if (tpsSync.get()) startCredit = Math.max(0, startCredit - 1);if (ctx.instaBreak) finishBreak(ctx, silentSwap.get());});}/** STOP must use the same tool as START. */private boolean stopWithTool(MineContext ctx, boolean silent) {if (!ctx.started || mc.world == null || mc.player == null) return false;withMiningTool(ctx, silent,() -> ctx.stopSent = sendSequencedAction(PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK, ctx, false));return ctx.stopSent;}private boolean finishBreak(MineContext ctx, boolean silent) {if (!ctx.active || (!ctx.started && !ctx.rebreak) || mc.world == null || mc.player == null|| mc.interactionManager == null) return false;boolean clientRemove = clientPrediction.get();// Instant STARTs do not prime the server's target for subsequent STOPs.PlayerActionC2SPacket.Action action = ctx.rebreak && ctx.instaBreak? PlayerActionC2SPacket.Action.START_DESTROY_BLOCK: PlayerActionC2SPacket.Action.STOP_DESTROY_BLOCK;withMiningTool(ctx, silent, () -> ctx.stopSent = sendSequencedAction(action, ctx, clientRemove));if (!ctx.stopSent) return false;if (ctx.requiresValidation() || (silent && swapMode.get() == SwapMode.ToolHold)) {double ticksPerMs = serverTicks(1, TickRate.INSTANCE.getTickRate()) * ctx.tickDamage;long deadline = System.currentTimeMillis() + (long) clamp(ticksPerMs > 0 ? 1 / ticksPerMs + 1000 : 30000, 2000, 30000);// At most two normal validations and the latest instant/rebreak hold.pendingBreaks.removeIf(pending -> !pending.normal() || pending.pos().equals(ctx.pos));pendingBreaks.add(new PendingBreak(ctx.pos, ctx.startSlot, ctx.requiresValidation(), !ctx.isPrimary, deadline));}if (clientRemove) {mc.world.syncWorldEvent(WorldEvents.BLOCK_BROKEN, ctx.pos, Block.getRawIdFromState(ctx.state));}if (!ctx.pos.equals(lastBrokenPos)) lastBreakConfirmed = false;lastBrokenPos = ctx.pos;if (ctx.rebreak) {rebreakPending = true;rebreakDeadlineMs = System.currentTimeMillis() + 2000;packetReplacement = null;if (debugRebreak.get()) rebreakConfirmations.sent(ctx.pos, System.nanoTime(), lastBreakConfirmed);}ctx.active = false;if (ctx == primary) primary = null;else if (ctx == secondary) secondary = null;if (silent && hasPendingNormalBreak()) holdTool(validationToolSlot());return true;}/*** Sends one dig action inside the client's own prediction sequence, through vanilla's* sendSequencedPacket. Any block change made inside the callback is recorded against that* sequence, so a wrong guess is rolled back by the server's acknowledgement instead of leaving a* ghost block. The call needs an invoker mixin: the method is private and the pending-update* manager behind it is package-private.*/private boolean sendSequencedAction(PlayerActionC2SPacket.Action action, MineContext ctx, boolean predict) {if (mc.world == null || mc.player == null || mc.interactionManager == null) return false;((ClientPlayerInteractionManagerAccessor) mc.interactionManager).quinn$sendSequencedPacket(mc.world, sequence -> {if (predict) {// The replacement may still read as air locally, so tell the prediction bookkeeping// what the server really has before predicting air over it.if (ctx.packetRebreak) {((ClientWorldAccessor) mc.world).quinn$getPendingUpdateManager().addPendingUpdate(ctx.pos, ctx.state, mc.player);}mc.world.setBlockState(ctx.pos, Blocks.AIR.getDefaultState(), Block.NOTIFY_ALL);}return new PlayerActionC2SPacket(action, ctx.pos, Direction.DOWN, sequence);});return true;}// ── Silent swap ───────────────────────────────────────────────────────────private void withMiningTool(MineContext ctx, boolean silent, Runnable action) {SwapMode mode = swapMode.get().forBreak(ctx.instaBreak, ctx.rebreak);if (validationToolSlot() != -1) mode = SwapMode.ToolHold;if (silent && mode == SwapMode.SameTick) {releaseHeldSlot();withSilentTool(ctx.state, action);} else {if (silent) holdTool(ctx.startSlot);else equipBestTool(ctx.state);action.run();}int validationSlot = validationToolSlot();if (validationSlot != -1) {if (silent) holdTool(validationSlot);else selectSlot(validationSlot);}lastMiningActionTick = clientTick;}/** Restores the previous server slot, including an existing silent hold. */public void withSilentTool(BlockState state, Runnable action) {if (mc.player == null) { action.run(); return; }int best = findBestHotbarSlot(state);int prev = currentServerSlot();boolean swap = best != -1 && best != prev;if (swap) sendSlotPacket(best);try {action.run();} finally {if (swap && currentServerSlot() != prev) sendSlotPacket(prev);}}private int currentServerSlot() {if (serverSlot != -1) return serverSlot;return mc.player == null ? 0 : mc.player.getInventory().getSelectedSlot();}private void sendSlotPacket(int slot) {if (slot < 0 || mc.getNetworkHandler() == null) return;if (currentServerSlot() == slot) return;mc.getNetworkHandler().sendPacket(new UpdateSelectedSlotC2SPacket(slot));serverSlot = slot;}private void holdTool(int slot) {if (slot < 0 || mc.player == null) return;sendSlotPacket(slot);heldSlot = slot;}private void releaseHeldSlot() {if (heldSlot != -1 && mc.player != null && currentServerSlot() == heldSlot) {sendSlotPacket(mc.player.getInventory().getSelectedSlot());}heldSlot = -1;idleTicks = 0;}/** Non-silent swap: move the client's own selection too. */private void selectSlot(int slot) {if (slot < 0 || mc.player == null) return;mc.player.getInventory().setSelectedSlot(slot);sendSlotPacket(slot);}private void equipBestTool(BlockState state) {int slot = findBestHotbarSlot(state);if (slot != -1) selectSlot(slot);}/*** THM re-implemented vanilla's mining formula here. Meteor already has it, including efficiency,* haste, mining fatigue, water and the airborne penalty, so the port calls that instead.*/private int findBestHotbarSlot(BlockState state) {if (mc.player == null) return -1;int best = -1;double bestSpeed = -1;for (int i = 0; i < 9; i++) {double speed = BlockUtils.getBreakDelta(i, state);if (speed > bestSpeed) { bestSpeed = speed; best = i; }}return best;}// ── Auto Mine ─────────────────────────────────────────────────────────────/*** Picks blocks to break around nearby enemies, in BlackOut AutoMine's priority order, and hands* them to the normal packet miner via {@link #requestBreak(BlockPos)}. Bedrock can't be* packet-mined, so it goes down the vanilla progress+swing path instead.*/private void tickAutoMine() {if (!autoMine.get() || !autoMineHeld()) {bedrockPos = null;if (debugAutoMine.get()) autoMineStatus = !autoMine.get() ? "Disabled." : "Waiting for auto-mine-bind.";return;}bedrockPos = null;int budget = autoDoubleMine.get() && doubleBreak.get() ? 2 : 1;List<BlockPos> targets = findAutoTargets();// Auto-mine outranks the queue: drop the backlog instead of mining it firstif (!targets.isEmpty() && !shouldPauseMining()) queue.clear();for (BlockPos target : targets) {if (mc.world.getBlockState(target).getBlock() == Blocks.BEDROCK) {// Bedrock is mined one at a time — it's a vanilla progress bar, not a packet breakif (bedrockPos == null) mineBedrock(target);continue;}if (budget-- <= 0) break;// Both break slots busy: skip rather than queue — auto targets never enter the queueif (primary != null && (secondary != null || !doubleBreak.get())) continue;requestBreak(target);}if (debugAutoMine.get() && !targets.isEmpty()) {if (bedrockPos != null) autoMineStatus = "Targeting bedrock at " + bedrockPos.toShortString();else if (primary != null) autoMineStatus = "Targeting block at " + primary.pos.toShortString();else if (secondary != null) autoMineStatus = "Targeting block at " + secondary.pos.toShortString();else if (hasPendingNormalBreak()) autoMineStatus = "Waiting for server break confirmation.";else if (!canStart()) autoMineStatus = "Waiting for TPS mining credit.";else autoMineStatus = "Targets selected; waiting for mining slots.";}}/** A bound key is hold-to-mine; an unbound one means "always on". */private boolean autoMineHeld() {return !autoMineBind.get().isSet() || autoMineBind.get().isPressed();}/** Every valid target around nearby enemies, nearest first. */private List<BlockPos> findAutoTargets() {List<PlayerEntity> enemies = new ArrayList<>();int friends = 0, distant = 0;for (PlayerEntity player : mc.world.getPlayers()) {if (player == mc.player || player.isSpectator()) continue;if (Friends.get().isFriend(player)) { friends++; continue; }if (player.distanceTo(mc.player) > enemyRange.get()) { distant++; continue; }enemies.add(player);}if (enemies.isEmpty()) {if (debugAutoMine.get()) autoMineStatus = String.format(Locale.ROOT,"No eligible players (friends=%d, distant=%d).", friends, distant);return List.of();}// Digging an enemy out beats chipping at their surroundautoAirRejected = autoSelfRejected = autoRangeRejected = autoDisabledRejected = autoUnbreakableRejected = 0;List<BlockPos> targets = new ArrayList<>();if (antiPhase.get()) targets.addAll(collect(enemies, this::phaseBlocks));if (targets.isEmpty() && antiSurround.get()) targets.addAll(collect(enemies, this::surroundBlocks));if (debugAutoMine.get() && targets.isEmpty()) {autoMineStatus = !antiPhase.get() && !antiSurround.get()? "Both anti-phase and anti-surround are disabled.": String.format(Locale.ROOT, "No target blocks (air=%d, own=%d, range=%d, disabled=%d, unbreakable=%d).",autoAirRejected, autoSelfRejected, autoRangeRejected, autoDisabledRejected, autoUnbreakableRejected);}return targets;}private List<BlockPos> collect(List<PlayerEntity> enemies, Function<PlayerEntity, List<BlockPos>> candidates) {List<BlockPos> out = new ArrayList<>();for (PlayerEntity enemy : enemies) {for (BlockPos pos : candidates.apply(enemy)) {if (pos == null) continue;if (outOfRange(pos)) { autoRangeRejected++; continue; }if (!out.contains(pos)) out.add(pos);}}Comparator<BlockPos> byDistance =Comparator.comparingDouble(pos -> mc.player.getEyePos().squaredDistanceTo(Vec3d.ofCenter(pos)));// Absolute Y, not per-enemy feet level — right for one enemy, good enough for a pile of themout.sort(feetFirst.get() ? Comparator.<BlockPos>comparingInt(BlockPos::getY).thenComparing(byDistance) : byDistance);return out;}/** Every block the enemy's hitbox overlaps. */private List<BlockPos> phaseBlocks(PlayerEntity enemy) {List<BlockPos> out = new ArrayList<>();Box box = enemy.getBoundingBox();for (BlockPos pos : BlockPos.iterate(BlockPos.ofFloored(box.minX, box.minY, box.minZ),BlockPos.ofFloored(box.maxX, box.maxY, box.maxZ))) {if (mineable(pos)) out.add(pos.toImmutable());}return out;}/** Everything boxing the enemy in: surround ring, head-level ring, and the block above their head. */private List<BlockPos> surroundBlocks(PlayerEntity enemy) {BlockPos feet = feet(enemy);BlockPos head = new BlockPos(enemy.getBlockX(), (int) Math.floor(enemy.getBoundingBox().maxY), enemy.getBlockZ());List<BlockPos> out = new ArrayList<>();for (Direction dir : Direction.Type.HORIZONTAL) {out.add(feet.offset(dir));out.add(head.offset(dir));}out.add(head.up());List<BlockPos> mineable = new ArrayList<>();for (BlockPos pos : out) if (mineable(pos)) mineable.add(pos);return mineable;}private BlockPos feet(PlayerEntity enemy) {return new BlockPos(enemy.getBlockX(), (int) Math.round(enemy.getY()), enemy.getBlockZ());}/** True if the block is inside or directly against our own hitbox — breaking it drops or exposes us. */private boolean touchesSelf(BlockPos pos) {return mc.player.getBoundingBox().expand(1).intersects(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1);}private boolean mineable(BlockPos pos) {BlockState state = mc.world.getBlockState(pos);if (state.isAir()) { autoAirRejected++; return false; }if (neverMineOwn.get() && touchesSelf(pos)) { autoSelfRejected++; return false; }// Bedrock's hardness is -1, so BlockUtils.canBreak always rejects it — it has to be checked firstif (state.isOf(Blocks.BEDROCK)) {if (!mineBedrock.get()) { autoDisabledRejected++; return false; }return true;}if (shouldPauseMining() || bedrockOnly.get()) { autoDisabledRejected++; return false; }if (!BlockUtils.canBreak(pos, state)) { autoUnbreakableRejected++; return false; }return true;}// ── Bedrock (vanilla progress + swing — packets can't break it) ───────────/*** Bedrock is a server-side progress bar driven by holding the dig, not something a START/STOP* packet pair can pop, so it goes through updateBlockBreakingProgress — which tracks exactly one* position, hence one block at a time. No tool swap: bedrock breaks at the same speed with* anything, so whatever is held works.*/private void mineBedrock(BlockPos pos) {if (mc.interactionManager == null || mc.player == null) return;bedrockPos = pos;if (bedrockRotate.get()) lookAt(pos);mc.interactionManager.updateBlockBreakingProgress(pos, closestFace(pos));mc.player.swingHand(Hand.MAIN_HAND);}// ── Pending validation ────────────────────────────────────────────────────/** A delayed secondary completes in a server tick, using the tool held then. */private int validationToolSlot() {return validationToolSlot(secondary != null && secondary.requiresValidation() ? secondary.startSlot : -1,pendingBreaks, primary != null && primary.requiresValidation() ? primary.startSlot : -1);}static int validationToolSlot(int secondarySlot, List<PendingBreak> pendingBreaks, int primarySlot) {for (PendingBreak pending : pendingBreaks) {if (pending.normal() && pending.secondary()) return pending.toolSlot();}if (secondarySlot != -1) return secondarySlot;for (PendingBreak pending : pendingBreaks) {if (pending.normal()) return pending.toolSlot();}return primarySlot;}private boolean hasPendingNormalBreak() {return hasPendingNormalBreak(pendingBreaks, null);}static boolean hasPendingNormalBreak(List<PendingBreak> pendingBreaks, BlockPos completedTarget) {return pendingBreaks.stream().anyMatch(pending -> pending.normal() && !pending.pos().equals(completedTarget));}record PendingBreak(BlockPos pos, int toolSlot, boolean normal, boolean secondary, long deadlineMs) {}// ── Util ─────────────────────────────────────────────────────────────────private boolean isEating() {return mc.player != null && mc.player.isUsingItem();}private boolean shouldPauseMining() {return shouldPauseMining(multitask.get(), mc.player != null && mc.player.isUsingItem());}static boolean shouldPauseMining(boolean multitask, boolean usingItem) {return !multitask && usingItem;}private void resetRebreakMonitor() {rebreakMonitorStartNs = System.nanoTime();rebreakCount = 0;rebreakRate = "0.0";rebreakConfirmations.clear();}private void updateRebreakMonitor() {if (!debugRebreak.get()) return;long now = System.nanoTime();rebreakConfirmations.expire(now);long elapsed = now - rebreakMonitorStartNs;if (elapsed < 1_000_000_000L) return;rebreakRate = String.format(Locale.ROOT, "%.1f", rebreakCount * 1_000_000_000.0 / elapsed);info("Rebreaks/s: %s confirmed", rebreakRate);rebreakMonitorStartNs = now;rebreakCount = 0;}@Overridepublic String getInfoString() {return debugRebreak.get() ? rebreakRate + " rebreaks/s" : null;}private float effectiveTps() {return tpsSync.get() ? TickRate.INSTANCE.getTickRate() : 20;}private boolean canStart() {return canStart(tpsSync.get(), effectiveTps(), startCredit);}static boolean canStart(boolean sync, float tps, double credit) {return !sync || !Float.isFinite(tps) || tps >= 20 || credit >= 1;}static double replenishStartCredit(double credit, long elapsedMillis, float tps) {// Retain fractional overflow so tick jitter does not discard earned starts.return Math.min(2, credit + serverTicks(elapsedMillis, tps));}static double serverTicks(long elapsedMillis, float tps) {return Math.max(0, elapsedMillis) / 1000.0 * clamp(Float.isFinite(tps) ? tps : 20, 0, 20);}static double clamp(double value, double min, double max) {return Math.max(min, Math.min(max, value));}public void requestBreak(BlockPos pos) {if (mc.world == null || mc.player == null) return;BlockState state = mc.world.getBlockState(pos);if (state.isAir()) return;if (!isMining(pos)) handleBlockClick(pos, state);}public boolean isMining(BlockPos pos) {return (primary != null && primary.pos.equals(pos))|| (secondary != null && secondary.pos.equals(pos))|| pendingBreaks.stream().anyMatch(pending -> pending.normal() && pending.pos().equals(pos))|| queue.contains(pos);}/** Server-side only look at {@code pos} — the camera doesn't move. */private void lookAt(BlockPos pos) {Rotations.rotate(Rotations.getYaw(pos), Rotations.getPitch(pos));}public boolean outOfRange(BlockPos pos) {return !isInRange(range.get(), pos);}/** Distance from the eyes to the nearest point of the block, like the server measures it. */private boolean isInRange(double maximum, BlockPos pos) {if (mc.player == null) return false;Vec3d eyes = mc.player.getEyePos();double dx = Math.max(0, Math.max(pos.getX() - eyes.x, eyes.x - (pos.getX() + 1)));double dy = Math.max(0, Math.max(pos.getY() - eyes.y, eyes.y - (pos.getY() + 1)));double dz = Math.max(0, Math.max(pos.getZ() - eyes.z, eyes.z - (pos.getZ() + 1)));return dx * dx + dy * dy + dz * dz <= maximum * maximum;}/** The block face pointing closest to our eyes, used for the bedrock dig. */private Direction closestFace(BlockPos pos) {if (mc.player == null) return Direction.UP;Vec3d center = Vec3d.ofCenter(pos);Vec3d eyes = mc.player.getEyePos();Direction best = Direction.UP;double bestDot = -Double.MAX_VALUE;Vec3d toEyes = eyes.subtract(center).normalize();for (Direction dir : Direction.values()) {double dot = toEyes.x * dir.getOffsetX() + toEyes.y * dir.getOffsetY() + toEyes.z * dir.getOffsetZ();if (dot > bestDot) { bestDot = dot; best = dir; }}return best;}// ── Rendering ─────────────────────────────────────────────────────────────private void renderMineContext(Render3DEvent event, MineContext ctx) {double fraction = ctx.timing.renderFraction(System.nanoTime(), ctx.isPrimary ? breakThreshold.get() : 1);renderBox(event, ctx.pos, fraction, renderColor.get(), ShapeMode.Lines);}/** Draws the block shrunk toward its centre by {@code fraction}, as a progress indicator. */private void renderBox(Render3DEvent event, BlockPos pos, double fraction, SettingColor color, ShapeMode shape) {double shrink = (1 - clamp(fraction, 0, 1)) * 0.5;// THM fills the side at a third of the outline's alpha; Lines mode ignores it either way.Color side = new Color(color.r, color.g, color.b, color.a / 3);event.renderer.box(pos.getX() + shrink, pos.getY() + shrink, pos.getZ() + shrink,pos.getX() + 1 - shrink, pos.getY() + 1 - shrink, pos.getZ() + 1 - shrink,side, color, shape, 0);}// ── Enums ────────────────────────────────────────────────────────────────public enum RebreakTrigger {OnPacket,OnClientWorld}public enum RebreakMode {Off,Strict,Strong,Bypass;boolean isStrong(boolean confirmedRebreak) {return this == Strong || (this == Bypass && confirmedRebreak);}boolean isPrimed(boolean confirmedBreak) {// A break and replacement in one server tick can omit the air update.return confirmedBreak || this == Strong;}boolean canRebreak(boolean primed, boolean waiting, boolean confirmedRebreak) {return this != Off && isPrimed(primed) && (!waiting || isStrong(confirmedRebreak));}}public enum SwapMode {Keep("Keep for Next Tick"),SameTick("Same Tick"),EndOfTick("End of Tick"),ToolHold("Tool Hold");private final String title;SwapMode(String title) {this.title = title;}@Overridepublic String toString() {return title;}SwapMode forBreak(boolean instant, boolean primedRebreak) {return instant || primedRebreak ? this : ToolHold;}boolean shouldRelease(long ticksSinceAction, boolean mining, boolean replacement, boolean pending, int idleTicks) {return switch (this) {case SameTick, EndOfTick -> true;case Keep -> !mining && !replacement && ticksSinceAction >= 1;case ToolHold -> !mining && !pending && idleTicks >= IDLE_RELEASE_TICKS;};}}// ── Rebreak monitor ──────────────────────────────────────────────────────static final class RebreakConfirmations {private final Map<BlockPos, Long> awaiting = new HashMap<>();void sent(BlockPos pos, long now, boolean initialConfirmed) {// A late initial air response must not count as a successful Strong retry.if (!initialConfirmed) return;expire(now);awaiting.putIfAbsent(pos.toImmutable(), now + 2_000_000_000L);}boolean confirm(BlockPos pos, long now) {Long deadline = awaiting.remove(pos);return deadline != null && now < deadline;}void expire(long now) {awaiting.values().removeIf(deadline -> now >= deadline);}void clear() {awaiting.clear();}}// ── Mining progress ──────────────────────────────────────────────────────static final class MiningProgress {private long lastNs;private double damage;private double rate;private boolean started;void start(long now, double delta, float tps) {lastNs = now;damage = Double.isFinite(delta) ? Math.max(0, delta) : 1;rate = damage * serverTicks(1000, tps) / 1_000_000_000.0;started = true;}void advance(long now, double delta, float tps, boolean paused) {if (!started) return;damage += Math.max(0, now - lastNs) * rate;lastNs = Math.max(lastNs, now);rate = paused || !Double.isFinite(delta) ? 0: Math.max(0, delta) * serverTicks(1000, tps) / 1_000_000_000.0;}double fraction(double threshold) {return clamp(damage / threshold, 0, 1);}double renderFraction(long now, double threshold) {// Extrapolate at most one client tick without changing mining work.return clamp((damage + clamp(now - lastNs, 0, 50_000_000L) * rate) / threshold, 0, 1);}}// ── MineContext ──────────────────────────────────────────────────────────public class MineContext {public final BlockPos pos;public final BlockState state;public final long startMs;public boolean isPrimary;public boolean instaBreak;/** Hotbar slot holding the tool this block was started with; -1 if none. */public final int startSlot;public boolean active = true;private boolean rebreak;/** True when this rebreak was triggered by a server update the client world has not shown yet. */private boolean packetRebreak;private boolean started;private boolean stopSent;private double tickDamage;private final MiningProgress timing = new MiningProgress();public MineContext(BlockPos pos, BlockState state, boolean isPrimary) {this.pos = pos.toImmutable();this.state = state;this.isPrimary = isPrimary;this.startSlot = findBestHotbarSlot(state);this.startMs = System.currentTimeMillis();this.tickDamage = calcDelta();this.instaBreak = tickDamage >= 1.0;}private boolean requiresValidation() {return !instaBreak && !rebreak;}public double progress() {return timing.fraction(isPrimary ? breakThreshold.get() : 1);}private void advance(long now, boolean paused) {tickDamage = paused ? 0 : calcDelta();timing.advance(now, tickDamage, effectiveTps(), paused);}/*** Per-tick mining damage for the tool this context started with. Meteor's helper already does* the whole vanilla formula; the only thing added here is cancelling its airborne penalty* when the ground claim is active.*/private double calcDelta() {if (mc.player == null || mc.world == null) return 0;int slot = startSlot < 0 ? mc.player.getInventory().getSelectedSlot() : startSlot;double delta = BlockUtils.getBreakDelta(slot, state);if (!Double.isFinite(delta)) return 1;if (delta <= 0) return 0;if (!mc.player.isOnGround() && liftsAirMiningPenalty()) delta *= 5;return delta;}}}
Required. Sequence numbers on the dig packets.
package com.example.addon.mixin;import net.minecraft.client.network.ClientPlayerInteractionManager;import net.minecraft.client.network.SequencedPacketCreator;import net.minecraft.client.world.ClientWorld;import org.spongepowered.asm.mixin.Mixin;import org.spongepowered.asm.mixin.gen.Invoker;/*** Speedmine sends its own dig packets, but they still have to carry the client's prediction* sequence number or the server's acknowledgement will roll the predicted break back.* Vanilla does that in {@code sendSequencedPacket}, which is private, and the underlying* {@code ClientWorld.getPendingUpdateManager()} is package-private, so this is the only way in.*/@Mixin(ClientPlayerInteractionManager.class)public interface ClientPlayerInteractionManagerAccessor {@Invoker("sendSequencedPacket")void quinn$sendSequencedPacket(ClientWorld world, SequencedPacketCreator packetCreator);}
Required. Keeps rebreak rollbacks honest.
package com.example.addon.mixin;import net.minecraft.client.network.PendingUpdateManager;import net.minecraft.client.world.ClientWorld;import org.spongepowered.asm.mixin.Mixin;import org.spongepowered.asm.mixin.gen.Invoker;/*** Reaches the client's prediction bookkeeping, which is package-private.** <p>A packet-triggered rebreak fires while the position still reads as air locally — that air is* our own prediction of the previous break — even though the server has just put a block back* there. Predicting another break would otherwise record "the server has air here", so a rebreak* the server rejects would roll back to air and leave a hole that isn't there.* Recording the replacement first makes the rollback restore the real block.*/@Mixin(ClientWorld.class)public interface ClientWorldAccessor {@Invoker("getPendingUpdateManager")PendingUpdateManager quinn$getPendingUpdateManager();}
Air Miner only. Flips onGround on outgoing move packets.
package com.example.addon.mixin;import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket;import org.spongepowered.asm.mixin.Mixin;import org.spongepowered.asm.mixin.Mutable;import org.spongepowered.asm.mixin.gen.Accessor;/*** Lets Speedmine's Air Miner claim that an outgoing move packet was sent while standing on the* ground, so the server does not apply vanilla's 5x airborne mining penalty.* The field is final, hence {@link Mutable}.*/@Mixin(PlayerMoveC2SPacket.class)public interface PlayerMoveC2SPacketAccessor {@Mutable@Accessor("onGround")void quinn$setOnGround(boolean onGround);}
Fixed: no longer runs with a container open, and leaves your held slot alone.
package com.example.addon.modules;import com.example.addon.QuinnAddon;import meteordevelopment.meteorclient.events.world.TickEvent;import meteordevelopment.meteorclient.settings.BoolSetting;import meteordevelopment.meteorclient.settings.IntSetting;import meteordevelopment.meteorclient.settings.ItemListSetting;import meteordevelopment.meteorclient.settings.Setting;import meteordevelopment.meteorclient.settings.SettingGroup;import meteordevelopment.meteorclient.systems.modules.Module;import meteordevelopment.meteorclient.utils.player.InvUtils;import meteordevelopment.orbit.EventHandler;import net.minecraft.item.Item;import net.minecraft.item.Items;import java.util.List;/*** Throws away junk as it piles up.** <p>Changes from the version this was built from:* <ul>* <li>The category. {@code Category} is the class, not the list of them, so* {@code Category.Misc} does not compile; this uses the addon's own category.</li>* <li>It no longer runs while a container is open. Slot indices are read against whatever* screen handler is active, so dropping with a chest or shulker open throws the* container's contents instead of yours.</li>* <li>The slot you are holding can be left alone, which is on by default — otherwise the* blocks you are placing with get thrown out of your hand.</li>* </ul>* The offhand is still not covered: it is outside the 0-35 range this walks.*/public class InventoryCleaner extends Module {private final SettingGroup sgGeneral = settings.getDefaultGroup();private final Setting<List<Item>> items = sgGeneral.add(new ItemListSetting.Builder().name("items").description("Items to throw away.").defaultValue(Items.DIRT,Items.COBBLESTONE,Items.ANDESITE,Items.DIORITE,Items.GRANITE).build());private final Setting<Integer> delay = sgGeneral.add(new IntSetting.Builder().name("delay").description("Delay in ticks between throwing items.").defaultValue(2).min(0).build());private final Setting<Boolean> skipSelected = sgGeneral.add(new BoolSetting.Builder().name("skip-selected-slot").description("Never throw the item you are currently holding.").defaultValue(true).build());private final Setting<Boolean> skipHotbar = sgGeneral.add(new BoolSetting.Builder().name("skip-hotbar").description("Only clean the main inventory and leave the whole hotbar alone.").defaultValue(false).build());private int timer;public InventoryCleaner() {super(QuinnAddon.CATEGORY, "inventory-cleaner", "Automatically throws away specified items.");}@Overridepublic void onActivate() {timer = 0;}@EventHandlerprivate void onTick(TickEvent.Post event) {if (mc.player == null) return;// With a container open, slot numbers address that container, not the player's inventory.if (mc.player.currentScreenHandler != mc.player.playerScreenHandler) return;if (timer > 0) {timer--;return;}for (int i = 9; i < 36; i++) {if (checkAndDrop(i)) return;}if (skipHotbar.get()) return;for (int i = 0; i < 9; i++) {if (checkAndDrop(i)) return;}}private boolean checkAndDrop(int slot) {if (mc.player == null) return false;if (skipSelected.get() && slot == mc.player.getInventory().getSelectedSlot()) return false;Item item = mc.player.getInventory().getStack(slot).getItem();if (item != Items.AIR && items.get().contains(item)) {InvUtils.drop().slot(slot);timer = delay.get();return true;}return false;}}
Fixed: cannot be triggered by another player's chat, has a cooldown, reads the name off the tab list.
package com.example.addon.modules;import com.example.addon.QuinnAddon;import meteordevelopment.meteorclient.events.game.ReceiveMessageEvent;import meteordevelopment.meteorclient.settings.BoolSetting;import meteordevelopment.meteorclient.settings.EnumSetting;import meteordevelopment.meteorclient.settings.IntSetting;import meteordevelopment.meteorclient.settings.Setting;import meteordevelopment.meteorclient.settings.SettingGroup;import meteordevelopment.meteorclient.settings.StringListSetting;import meteordevelopment.meteorclient.settings.StringSetting;import meteordevelopment.meteorclient.systems.friends.Friend;import meteordevelopment.meteorclient.systems.friends.Friends;import meteordevelopment.meteorclient.systems.modules.Module;import meteordevelopment.meteorclient.utils.player.ChatUtils;import meteordevelopment.orbit.EventHandler;import net.minecraft.client.network.PlayerListEntry;import net.minecraft.text.Text;import java.util.HashMap;import java.util.List;import java.util.Locale;import java.util.Map;/*** Accepts teleport requests.** <p>Changes from the version this was built from:* <ul>* <li>It referenced an {@code Addon} class and a {@code utils.Utils} helper that do not exist* here, so the category comes from {@link QuinnAddon} and the toggle messages are left to* Meteor, which already prints them.</li>* <li>The accept command is a setting. {@code /tpy} is not universal.</li>* <li><b>Another player can no longer trigger it by typing.</b> Matching the phrase anywhere in* chat meant anyone could say "Someone has requested to teleport to you" and have you run the* accept command at them. Messages that look like player chat are now ignored, and the name* has to belong to someone actually on the server.</li>* <li><b>A per-player cooldown.</b> Servers re-announce a pending request every so often, and the* original answered every announcement, which reads as chat spam and gets you kicked.</li>* <li>The name is matched against the tab list rather than assumed to be the first word, so a* rank prefix in the message no longer breaks it.</li>* </ul>*/public class AutoAccept extends Module {private final SettingGroup sgGeneral = settings.getDefaultGroup();public enum Mode {Friends,Whitelist,Blacklist,All}private final Setting<Mode> mode = sgGeneral.add(new EnumSetting.Builder<Mode>().name("mode").description("Which players to accept requests from.").defaultValue(Mode.Friends).build());private final Setting<List<String>> whitelist = sgGeneral.add(new StringListSetting.Builder().name("whitelist").description("List of players to accept.").visible(() -> mode.get() == Mode.Whitelist).build());private final Setting<List<String>> blacklist = sgGeneral.add(new StringListSetting.Builder().name("blacklist").description("List of players to ignore.").visible(() -> mode.get() == Mode.Blacklist).build());private final Setting<String> acceptCommand = sgGeneral.add(new StringSetting.Builder().name("accept-command").description("Command sent to accept, with the player name appended. Servers differ: /tpy, /tpaccept, /tpayes.").defaultValue("/tpy").build());private final Setting<Integer> cooldown = sgGeneral.add(new IntSetting.Builder().name("cooldown").description("Seconds to wait before answering the same player again. Servers re-announce pending requests.").defaultValue(30).min(0).sliderRange(0, 120).build());private final Setting<Boolean> requireOnline = sgGeneral.add(new BoolSetting.Builder().name("require-online-player").description("Only accept when the name in the message belongs to someone on the player list. Turn this off only if the server hides the list.").defaultValue(true).build());private final Setting<Boolean> ignoreChat = sgGeneral.add(new BoolSetting.Builder().name("ignore-player-chat").description("Ignore messages that look like somebody talking. Without this, anyone can make you accept by typing the request text.").defaultValue(true).build());private final Map<String, Long> lastAccepted = new HashMap<>();public AutoAccept() {super(QuinnAddon.CATEGORY, "auto-accept", "Automatically accepts teleport requests.");}@Overridepublic void onActivate() {lastAccepted.clear();}@EventHandlerprivate void onMessageReceive(ReceiveMessageEvent event) {if (mc.player == null) return;Text message = event.getMessage();String text = message.getString();if (!isRequest(text)) return;if (ignoreChat.get() && looksLikePlayerChat(text)) return;String playerName = extractPlayerName(text);if (playerName == null) return;if (playerName.equalsIgnoreCase(mc.player.getGameProfile().name())) return;if (!shouldAccept(playerName)) return;long now = System.currentTimeMillis();Long last = lastAccepted.get(playerName.toLowerCase(Locale.ROOT));if (last != null && now - last < cooldown.get() * 1000L) return;lastAccepted.put(playerName.toLowerCase(Locale.ROOT), now);info("Accepting teleport request from " + playerName);ChatUtils.sendPlayerMsg(acceptCommand.get() + " " + playerName);}private boolean isRequest(String text) {return text.contains("has requested to teleport")|| text.contains("to teleport to you")|| text.contains("has invited you to join them");}/*** Server announcements are not spoken by anyone, so the usual chat shapes are a good enough* tell: {@code <Name> ...}, {@code Name: ...} and {@code [Rank] Name » ...}. A server with an* unusual chat format could still slip through, which is why the name is checked as well.*/private boolean looksLikePlayerChat(String text) {int phrase = text.indexOf("has requested");if (phrase < 0) phrase = text.indexOf("to teleport to you");if (phrase < 0) phrase = text.indexOf("has invited you");if (phrase < 0) phrase = text.length();String before = text.substring(0, phrase);return before.contains("<")|| before.contains(":")|| before.contains("»")|| before.contains(">");}private boolean shouldAccept(String playerName) {switch (mode.get()) {case Friends:for (Friend friend : Friends.get()) {if (friend.name.equalsIgnoreCase(playerName)) return true;}return false;case Whitelist:for (String name : whitelist.get()) {if (name.equalsIgnoreCase(playerName)) return true;}return false;case Blacklist:for (String name : blacklist.get()) {if (name.equalsIgnoreCase(playerName)) return false;}return true;case All:return true;default:return false;}}/*** Picks the name out of the announcement by looking for a word that belongs to someone on the* server, which survives rank prefixes and colour codes. Falls back to the first word when the* player list cannot be trusted.*/private String extractPlayerName(String text) {String[] parts = text.split("[^A-Za-z0-9_]+");if (mc.getNetworkHandler() != null) {for (String part : parts) {if (part.length() < 3 || part.length() > 16) continue;for (PlayerListEntry entry : mc.getNetworkHandler().getPlayerList()) {String name = entry.getProfile().name();if (name != null && name.equalsIgnoreCase(part)) return name;}}}if (requireOnline.get()) return null;for (String part : parts) {if (!part.isEmpty()) return part;}return null;}}
The original, for comparison. Does not compile.
package com.example.addon.modules;import com.example.addon.QuinnAddon;import meteordevelopment.meteorclient.events.world.TickEvent;import meteordevelopment.meteorclient.settings.IntSetting;import meteordevelopment.meteorclient.settings.ItemListSetting;import meteordevelopment.meteorclient.settings.Setting;import meteordevelopment.meteorclient.settings.SettingGroup;import meteordevelopment.meteorclient.systems.modules.Module;import meteordevelopment.meteorclient.utils.player.InvUtils;import meteordevelopment.orbit.EventHandler;import net.minecraft.item.Item;import net.minecraft.item.Items;import java.util.List;/*** Throws away junk as it piles up. Supplied by Rei; the only change from the original is the* category — {@code Category} is the class, not the list of categories, so {@code Category.Misc}* does not compile. It sits in the addon's own category rather than Meteor's Misc.*/public class InventoryCleaner extends Module {private final SettingGroup sgGeneral = settings.getDefaultGroup();private final Setting<List<Item>> items = sgGeneral.add(new ItemListSetting.Builder().name("items").description("Items to throw away.").defaultValue(Items.DIRT,Items.COBBLESTONE,Items.ANDESITE,Items.DIORITE,Items.GRANITE).build());private final Setting<Integer> delay = sgGeneral.add(new IntSetting.Builder().name("delay").description("Delay in ticks between throwing items.").defaultValue(2).min(0).build());private int timer;public InventoryCleaner() {super(QuinnAddon.CATEGORY, "inventory-cleaner", "Automatically throws away specified items.");}@Overridepublic void onActivate() {timer = 0;}@EventHandlerprivate void onTick(TickEvent.Post event) {if (timer > 0) {timer--;return;}for (int i = 9; i < 36; i++) {if (checkAndDrop(i)) return;}for (int i = 0; i < 9; i++) {if (checkAndDrop(i)) return;}}private boolean checkAndDrop(int slot) {if (mc.player == null) return false;Item item = mc.player.getInventory().getStack(slot).getItem();if (item != Items.AIR && items.get().contains(item)) {InvUtils.drop().slot(slot);timer = delay.get();return true;}return false;}}
The original, for comparison. Does not compile.
package com.example.addon.modules;import com.example.addon.QuinnAddon;import meteordevelopment.meteorclient.events.game.ReceiveMessageEvent;import meteordevelopment.meteorclient.settings.EnumSetting;import meteordevelopment.meteorclient.settings.Setting;import meteordevelopment.meteorclient.settings.SettingGroup;import meteordevelopment.meteorclient.settings.StringListSetting;import meteordevelopment.meteorclient.settings.StringSetting;import meteordevelopment.meteorclient.systems.friends.Friend;import meteordevelopment.meteorclient.systems.friends.Friends;import meteordevelopment.meteorclient.systems.modules.Module;import meteordevelopment.meteorclient.utils.player.ChatUtils;import meteordevelopment.orbit.EventHandler;import net.minecraft.text.Text;import java.util.List;/*** Accepts teleport requests. Supplied by Rei; changed only where the original could not build here:* it referenced an {@code Addon} class and a {@code utils.Utils} helper that this addon does not* have, so the category comes from {@link QuinnAddon} and the toggle messages are left to Meteor,* which already prints them. The accept command is a setting because it is server-specific —* {@code /tpy} is not universal.*/public class AutoAccept extends Module {private final SettingGroup sgGeneral = settings.getDefaultGroup();public enum Mode {Friends,Whitelist,Blacklist,All}private final Setting<Mode> mode = sgGeneral.add(new EnumSetting.Builder<Mode>().name("mode").description("Which players to accept requests from.").defaultValue(Mode.Friends).build());private final Setting<List<String>> whitelist = sgGeneral.add(new StringListSetting.Builder().name("whitelist").description("List of players to accept.").visible(() -> mode.get() == Mode.Whitelist).build());private final Setting<List<String>> blacklist = sgGeneral.add(new StringListSetting.Builder().name("blacklist").description("List of players to ignore.").visible(() -> mode.get() == Mode.Blacklist).build());private final Setting<String> acceptCommand = sgGeneral.add(new StringSetting.Builder().name("accept-command").description("Command sent to accept, with the player name appended. Servers differ: /tpy, /tpaccept, /tpayes.").defaultValue("/tpy").build());public AutoAccept() {super(QuinnAddon.CATEGORY, "auto-accept", "Automatically accepts teleport requests.");}@EventHandlerprivate void onMessageReceive(ReceiveMessageEvent event) {Text message = event.getMessage();String text = message.getString();if (text.contains("has requested to teleport")|| text.contains("to teleport to you")|| text.contains("has invited you to join them")) {String playerName = extractPlayerName(text);if (playerName != null && shouldAccept(playerName)) {info("Accepting teleport request from " + playerName);ChatUtils.sendPlayerMsg(acceptCommand.get() + " " + playerName);}}}private boolean shouldAccept(String playerName) {switch (mode.get()) {case Friends:for (Friend friend : Friends.get()) {if (friend.name.equalsIgnoreCase(playerName)) return true;}return false;case Whitelist:for (String name : whitelist.get()) {if (name.equalsIgnoreCase(playerName)) return true;}return false;case Blacklist:for (String name : blacklist.get()) {if (name.equalsIgnoreCase(playerName)) return false;}return true;case All:return true;default:return false;}}/*** Takes the first word of the message as the requester's name. That holds when the server puts* the name first, and breaks when it prefixes a rank or a tag — worth checking against the* server this is used on.*/private String extractPlayerName(String text) {String[] parts = text.split(" ");if (parts.length > 0) {return parts[0];}return null;}}