From 8e11e246c8e8136320c1fe3f3e742d878f17da15 Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Tue, 17 Mar 2026 15:23:32 +0100 Subject: [PATCH 1/6] Add /temp to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 09cd281f..42eab877 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ bin/ # fabric run/ + +/temp \ No newline at end of file From 3bf95fba8abe6b86578a3405642da766553fd08b Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Sun, 29 Mar 2026 19:06:02 +0200 Subject: [PATCH 2/6] Initial commit --- changes_1.md | 52 + gradlew | 0 .../java/com/nnpg/glazed/GlazedAddon.java | 16 +- .../nnpg/glazed/commands/AHItemCommand.java | 100 ++ .../glazed/commands/SellHotbarCommand.java | 96 ++ .../glazed/managers/SellHotbarManager.java | 146 +++ .../modules/esp/HoleTunnelStairsESP.java | 1162 ++++++++++------- .../modules/esp/PearlLandingPredictor.java | 496 +++++++ .../modules/main/AutoBlazeRodOrder.java | 514 -------- .../glazed/modules/main/AutoShellOrder.java | 276 ---- .../glazed/modules/main/AutoShopOrder.java | 952 ++++++++++++++ .../glazed/modules/main/AutoShulkerOrder.java | 646 --------- .../modules/main/AutoShulkerShellOrder.java | 516 -------- .../glazed/modules/main/AutoTotemOrder.java | 646 --------- 14 files changed, 2561 insertions(+), 3057 deletions(-) create mode 100644 changes_1.md mode change 100644 => 100755 gradlew create mode 100644 src/main/java/com/nnpg/glazed/commands/AHItemCommand.java create mode 100644 src/main/java/com/nnpg/glazed/commands/SellHotbarCommand.java create mode 100644 src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java create mode 100644 src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java delete mode 100644 src/main/java/com/nnpg/glazed/modules/main/AutoBlazeRodOrder.java delete mode 100644 src/main/java/com/nnpg/glazed/modules/main/AutoShellOrder.java create mode 100644 src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java delete mode 100644 src/main/java/com/nnpg/glazed/modules/main/AutoShulkerOrder.java delete mode 100644 src/main/java/com/nnpg/glazed/modules/main/AutoShulkerShellOrder.java delete mode 100644 src/main/java/com/nnpg/glazed/modules/main/AutoTotemOrder.java diff --git a/changes_1.md b/changes_1.md new file mode 100644 index 00000000..445f893d --- /dev/null +++ b/changes_1.md @@ -0,0 +1,52 @@ +# πŸ› οΈ HoleTunnelStairsESP & Glazed Addon - Changelog + +## ✨ New Major Modules & Commands + +- **Auto Shop Order (`AutoShopOrder.java`)** + - **Consolidation:** Replaces legacy modules `AutoBlazeRodOrder`, `AutoShulkerOrder`, `AutoTotemOrder`, `AutoShulkerShellOrder`, and `AutoShellOrder`. + - **Logic:** Features a robust state-machine for navigating `/shop`, bulk buying, and identifying the most profitable `/orders`. + - **Features:** "AUTO" pricing (Shop + $1), custom price suffixes (K, M, B), player blacklist, and an anti-stuck system. +- **Pearl Landing Predictor (`PearlLandingPredictor.java`)** + - **Function:** Simulates Ender Pearl trajectories to predict and highlight landing points. + - **Features:** Player filtering, live updates for new chunks, and distinct rendering for unknown owners. + - **Visuals:** Now correctly renders both the landing destination box and the player’s name. +- **AH Sell Command (`.sell_hotbar`)** + - **Function:** Command-based version of `AHSell.java`. Automatically lists all hotbar items via `/ah sell `. +- **AH Item Command (`.ahitem`)** + - **Function:** Instantly searches the Auction House for the item in hand. + - **Smart Search:** Automatically appends enchantments (e.g., `sharpness 5`) and "stack" parameters to the search query. + +--- + +## πŸ”„ Module Improvements & Refactors + +### 🟦 HoleTunnelStairsESP (Major Overhaul) + +The `CoveredHole.java` module has been fully integrated into `HoleTunnelStairsESP` to centralize logic and reduce overhead. + +- **Variable Tunnel Width:** New `minTunnelWidth`/`maxTunnelWidth` settings (default 1-3) for detecting 1x1 to 3x3 tunnels. +- **Covered Hole Detection:** + - Identifies 1x1 and 1x3 holes covered by solid blocks. + - **Smart Filtering:** `only-player-covered` logic distinguishes player-made covers from natural terrain generation. + - **Dedicated Rendering:** Custom colors for covered holes to avoid visual confusion. +- **Dynamic Updates:** Added `undergroundUpdateThreshold` and packet listeners for Y < 0 updates on servers with dynamic chunk loading. +- **Optimization:** Hash-based deduplication (`tunnelHashes`) for O(1) lookups, preventing redundant rendering across chunk borders. + +--- + +## πŸ› Bug Fixes + +- **HoleTunnelStairsESP:** + - **Double Rendering:** Fixed via canonical start positions and hash-based deduplication. + - **Inconsistent Cross-sections:** Fixed ceiling height detection using the `refHeight` parameter. + - **1x3 Covered Holes:** Corrected detection logic to verify all three top blocks instead of just the start block. +- **Pearl Landing Predictor:** Fixed a bug where the landing box was not rendering (previously only the name label was visible). + +--- + +## πŸš€ Performance Improvements + +- **Scan Efficiency:** `HoleTunnelStairsESP` now uses a **2-pass instead of 3-pass** scan, combining length measurement and end-position recording. +- **Redundancy Check:** Implementation of `CANONICAL_TUNNEL_DIRS` (East/South only) to prevent scanning the same tunnel from opposite directions. +- **Thread Safety:** Integrated `ThreadLocal BitSet` for chunk processing, removing synchronization bottlenecks. +- **Caching:** Added `solidBlockCache` and `blockStateCache` to minimize expensive world-access calls. diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/nnpg/glazed/GlazedAddon.java b/src/main/java/com/nnpg/glazed/GlazedAddon.java index de3d0a99..dc22898f 100644 --- a/src/main/java/com/nnpg/glazed/GlazedAddon.java +++ b/src/main/java/com/nnpg/glazed/GlazedAddon.java @@ -3,7 +3,9 @@ import com.nnpg.glazed.modules.esp.*; import com.nnpg.glazed.modules.main.*; import com.nnpg.glazed.modules.pvp.*; +import com.nnpg.glazed.commands.*; import meteordevelopment.meteorclient.addons.MeteorAddon; +import meteordevelopment.meteorclient.commands.Commands; import meteordevelopment.meteorclient.systems.modules.Modules; import meteordevelopment.meteorclient.systems.modules.Category; import meteordevelopment.orbit.EventHandler; @@ -28,9 +30,7 @@ public void onInitialize() { Modules.get().add(new PlayerDetection()); Modules.get().add(new AHSniper()); Modules.get().add(new RTPer()); - Modules.get().add(new ShulkerDropper()); Modules.get().add(new AutoSell()); - Modules.get().add(new SpawnerDropper()); Modules.get().add(new AutoShulkerOrder()); Modules.get().add(new AutoOrder()); Modules.get().add(new HideScoreboard()); @@ -49,15 +49,12 @@ public void onInitialize() { Modules.get().add(new TabDetector()); Modules.get().add(new OrderSniper()); Modules.get().add(new LamaESP()); - Modules.get().add(new PillagerESP()); Modules.get().add(new HoleTunnelStairsESP()); - Modules.get().add(new CoveredHole()); Modules.get().add(new ClusterFinder()); Modules.get().add(new AutoShulkerShellOrder()); Modules.get().add(new EmergencySeller()); Modules.get().add(new RTPEndBaseFinder()); Modules.get().add(new ShopBuyer()); - Modules.get().add(new OrderDropper()); Modules.get().add(new CollectibleESP()); Modules.get().add(new SpawnerNotifier()); Modules.get().add(new VineESP()); @@ -83,8 +80,6 @@ public void onInitialize() { Modules.get().add(new HoverTotem()); Modules.get().add(new TunnelBaseFinder()); Modules.get().add(new AimAssist()); - Modules.get().add(new SkeletonESP()); - Modules.get().add(new RainNoti()); Modules.get().add(new AutoPearlChain()); Modules.get().add(new AutoBlazeRodOrder()); Modules.get().add(new BlazeRodDropper()); @@ -92,7 +87,6 @@ public void onInitialize() { Modules.get().add(new FakeScoreboard()); Modules.get().add(new AutoInvTotem()); Modules.get().add(new FreecamMining()); - Modules.get().add(new BedrockVoidESP()); Modules.get().add(new UIHelper()); Modules.get().add(new ShieldBreaker()); Modules.get().add(new InvisESP()); @@ -101,6 +95,12 @@ public void onInitialize() { Modules.get().add(new PremiumTunnelBaseFinder()); Modules.get().add(new AdminList()); Modules.get().add(new AutoTreeFarmer()); + Modules.get().add(new PearlLandingPredictor()); + Modules.get().add(new AutoShopOrder()); + Commands.add(new SellHotbarCommand()); + Commands.add(new AHItemCommand()); + + } @EventHandler diff --git a/src/main/java/com/nnpg/glazed/commands/AHItemCommand.java b/src/main/java/com/nnpg/glazed/commands/AHItemCommand.java new file mode 100644 index 00000000..f88168bd --- /dev/null +++ b/src/main/java/com/nnpg/glazed/commands/AHItemCommand.java @@ -0,0 +1,100 @@ +package com.nnpg.glazed.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import meteordevelopment.meteorclient.commands.Command; +import net.minecraft.component.type.ItemEnchantmentsComponent; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.command.CommandSource; + +import java.util.ArrayList; +import java.util.List; + +public class AHItemCommand extends Command { + + public AHItemCommand() { + super("ahitem", "Searches /ah for the item in your main hand with its enchantments."); + } + + @Override + public void build(LiteralArgumentBuilder builder) { + builder.executes(context -> { + if (mc.player == null) return SINGLE_SUCCESS; + + ItemStack mainHandItem = mc.player.getMainHandStack(); + + if (mainHandItem.isEmpty()) { + error("You are not holding any item in your main hand."); + return SINGLE_SUCCESS; + } + + // Get item name + String itemName = getItemName(mainHandItem); + + // Get enchantments using the new API + List enchantmentStrings = getEnchantments(mainHandItem); + + // Build the search command + StringBuilder searchCommand = new StringBuilder("ah "); + searchCommand.append(itemName); + + // Add "stack" suffix if item is a full stack (64 items) + if (mainHandItem.getCount() == 64) { + searchCommand.append(" stack"); + } + + if (!enchantmentStrings.isEmpty()) { + searchCommand.append(" "); + searchCommand.append(String.join(" ", enchantmentStrings)); + } + + // Send the command + String command = searchCommand.toString(); + info("Searching: /" + command); + mc.getNetworkHandler().sendChatCommand(command); + + return SINGLE_SUCCESS; + }); + } + + private String getItemName(ItemStack stack) { + // Get the item ID + String itemId = stack.getItem().toString(); + + // Remove namespace if present (e.g., "minecraft:diamond_sword" -> "diamond_sword") + if (itemId.contains(":")) { + itemId = itemId.split(":")[1]; + } + + return itemId.toLowerCase().replace(" ", "_"); + } + + private List getEnchantments(ItemStack stack) { + List result = new ArrayList<>(); + + // Use the new 1.20.5+ API + ItemEnchantmentsComponent enchantments = EnchantmentHelper.getEnchantments(stack); + + for (RegistryEntry entry : enchantments.getEnchantments()) { + int level = enchantments.getLevel(entry); + String enchantmentName = getEnchantmentName(entry); + result.add(enchantmentName + " " + level); + } + + return result; + } + + private String getEnchantmentName(RegistryEntry enchantmentEntry) { + // Get the enchantment ID from the registry + String enchantmentId = enchantmentEntry.getIdAsString(); + + // Remove namespace (e.g., "minecraft:protection" -> "protection") + if (enchantmentId.contains(":")) { + enchantmentId = enchantmentId.split(":")[1]; + } + + return enchantmentId.toLowerCase().replace(" ", "_"); + } +} diff --git a/src/main/java/com/nnpg/glazed/commands/SellHotbarCommand.java b/src/main/java/com/nnpg/glazed/commands/SellHotbarCommand.java new file mode 100644 index 00000000..307f1e27 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/commands/SellHotbarCommand.java @@ -0,0 +1,96 @@ +package com.nnpg.glazed.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.nnpg.glazed.managers.SellHotbarManager; +import meteordevelopment.meteorclient.commands.Command; +import net.minecraft.item.ItemStack; +import net.minecraft.command.CommandSource; + +public class SellHotbarCommand extends Command { + + public SellHotbarCommand() { + super("sell_hotbar", "Sells all items in your hotbar for a specified price. Supports K/M/B suffixes."); + } + + @Override + public void build(LiteralArgumentBuilder builder) { + builder.then(argument("price", StringArgumentType.word()) + .executes(context -> { + String price = StringArgumentType.getString(context, "price"); + + if (!isValidPrice(price)) { + error("Invalid price format: " + price + ". Use numbers with K/M/B suffixes (e.g., 30k, 1.5m, 2b)."); + return SINGLE_SUCCESS; + } + + if (SellHotbarManager.get().isRunning()) { + error("Already selling items. Please wait."); + return SINGLE_SUCCESS; + } + + if (!hasSellableItemsInHotbar()) { + error("No sellable items found in hotbar."); + return SINGLE_SUCCESS; + } + + info("Starting to sell hotbar items for " + formatPrice(parsePrice(price)) + " each."); + SellHotbarManager.get().start(price, true); + + return SINGLE_SUCCESS; + }) + ); + } + + private boolean hasSellableItemsInHotbar() { + if (mc.player == null) return false; + + for (int slot = 0; slot <= 8; slot++) { + ItemStack stack = mc.player.getInventory().getStack(slot); + if (!stack.isEmpty()) { + return true; + } + } + return false; + } + + private boolean isValidPrice(String priceStr) { + return parsePrice(priceStr) > 0; + } + + private double parsePrice(String priceStr) { + if (priceStr == null || priceStr.isEmpty()) return -1.0; + + String cleaned = priceStr.trim().toUpperCase(); + double multiplier = 1.0; + + if (cleaned.endsWith("B")) { + multiplier = 1_000_000_000.0; + cleaned = cleaned.substring(0, cleaned.length() - 1); + } else if (cleaned.endsWith("M")) { + multiplier = 1_000_000.0; + cleaned = cleaned.substring(0, cleaned.length() - 1); + } else if (cleaned.endsWith("K")) { + multiplier = 1_000.0; + cleaned = cleaned.substring(0, cleaned.length() - 1); + } + + try { + return Double.parseDouble(cleaned) * multiplier; + } catch (NumberFormatException e) { + return -1.0; + } + } + + private String formatPrice(double price) { + if (price >= 1_000_000_000) { + return String.format("%.2fB", price / 1_000_000_000); + } else if (price >= 1_000_000) { + return String.format("%.2fM", price / 1_000_000); + } else if (price >= 1_000) { + return String.format("%.2fK", price / 1_000); + } else { + return String.format("%.2f", price); + } + } +} diff --git a/src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java b/src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java new file mode 100644 index 00000000..e101e416 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java @@ -0,0 +1,146 @@ +package com.nnpg.glazed.managers; + +import com.nnpg.glazed.VersionUtil; +import meteordevelopment.meteorclient.MeteorClient; +import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.events.game.ReceiveMessageEvent; +import meteordevelopment.orbit.EventHandler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.item.ItemStack; +import net.minecraft.screen.GenericContainerScreenHandler; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.text.Text; + +public class SellHotbarManager { + private static SellHotbarManager instance; + + // mc instance manually since we don't extend Module/Command + private static final MinecraftClient mc = MinecraftClient.getInstance(); + + private int delayCounter = 0; + private boolean awaitingConfirmation = false; + private int currentSlot = 0; + private String currentPrice = "30k"; + private boolean isRunning = false; + private boolean notifications = true; + + public static SellHotbarManager get() { + if (instance == null) { + instance = new SellHotbarManager(); + MeteorClient.EVENT_BUS.subscribe(instance); + } + return instance; + } + + public boolean isRunning() { + return isRunning; + } + + public void start(String price, boolean notify) { + if (isRunning) return; + + currentPrice = price; + currentSlot = 0; + notifications = notify; + isRunning = true; + awaitingConfirmation = false; + delayCounter = 0; + + attemptSellCurrentSlot(); + } + + public void stop() { + reset(); + } + + @EventHandler + private void onTick(TickEvent.Pre event) { + if (!isRunning || mc.player == null) return; + + if (awaitingConfirmation) { + if (delayCounter > 0) { + delayCounter--; + return; + } + + ScreenHandler screenHandler = mc.player.currentScreenHandler; + + if (screenHandler instanceof GenericContainerScreenHandler handler) { + if (handler.getRows() == 3) { + ItemStack confirmButton = handler.getSlot(15).getStack(); + if (!confirmButton.isEmpty()) { + mc.interactionManager.clickSlot(handler.syncId, 15, 1, SlotActionType.QUICK_MOVE, mc.player); + if (notifications) info("Sold item in hotbar slot " + currentSlot + "."); + } + + awaitingConfirmation = false; + moveToNextSlot(); + } + } + } + } + + @EventHandler + private void onChatMessage(ReceiveMessageEvent event) { + if (!isRunning) return; + + String msg = event.getMessage().getString(); + if (msg.contains("You have too many listed items.")) { + warning("Sell limit reached! Stopping."); + reset(); + } + } + + private void attemptSellCurrentSlot() { + if (mc.player == null) { + reset(); + return; + } + + if (currentSlot > 8) { + if (notifications) info("Finished processing hotbar."); + reset(); + return; + } + + VersionUtil.setSelectedSlot(mc.player, currentSlot); + ItemStack stack = mc.player.getInventory().getStack(currentSlot); + + if (stack.isEmpty()) { + moveToNextSlot(); + return; + } + + if (notifications) { + info("Sending /ah sell " + currentPrice + " for slot " + currentSlot); + } + + mc.getNetworkHandler().sendChatCommand("ah sell " + currentPrice); + delayCounter = 10; + awaitingConfirmation = true; + } + + private void moveToNextSlot() { + currentSlot++; + attemptSellCurrentSlot(); + } + + private void reset() { + isRunning = false; + awaitingConfirmation = false; + currentSlot = 0; + } + + private void info(String message) { + if (mc.player != null) { + mc.player.sendMessage(Text.literal("Β§a[SellHotbar] Β§f" + message), false); + } + } + + private void warning(String message) { + if (mc.player != null) { + mc.player.sendMessage(Text.literal("Β§e[SellHotbar] Β§f" + message), false); + } + } +} diff --git a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java index d9a3324a..690ef79b 100644 --- a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java +++ b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java @@ -3,6 +3,7 @@ import com.nnpg.glazed.GlazedAddon; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import meteordevelopment.meteorclient.events.packets.PacketEvent; import meteordevelopment.meteorclient.events.render.Render3DEvent; import meteordevelopment.meteorclient.events.world.TickEvent; import meteordevelopment.meteorclient.renderer.Renderer3D; @@ -14,6 +15,9 @@ import meteordevelopment.meteorclient.utils.render.color.SettingColor; import meteordevelopment.orbit.EventHandler; import net.minecraft.block.BlockState; +import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; +import net.minecraft.network.packet.s2c.play.BlockUpdateS2CPacket; +import net.minecraft.network.packet.s2c.play.ChunkDataS2CPacket; import net.minecraft.util.math.*; import net.minecraft.util.shape.VoxelShape; import net.minecraft.util.shape.VoxelShapes; @@ -26,10 +30,13 @@ public class HoleTunnelStairsESP extends Module { private final SettingGroup sgGeneral = settings.getDefaultGroup(); - private final SettingGroup sgHParams = settings.createGroup("Hole Parameters"); - private final SettingGroup sgTParams = settings.createGroup("Tunnel Parameters"); - private final SettingGroup sgSParams = settings.createGroup("Stairs Parameters"); - private final SettingGroup sgRender = settings.createGroup("Rendering"); + private final SettingGroup sgHParams = settings.createGroup("Hole Parameters"); + private final SettingGroup sgTParams = settings.createGroup("Tunnel Parameters"); + private final SettingGroup sgSParams = settings.createGroup("Stairs Parameters"); + private final SettingGroup sgCParams = settings.createGroup("Covered Hole Parameters"); + private final SettingGroup sgRender = settings.createGroup("Rendering"); + + // == General == private final Setting detectionMode = sgGeneral.add(new EnumSetting.Builder() .name("Detection Mode") .description("Choose what to detect: holes, tunnels, stairs, or all.") @@ -38,321 +45,479 @@ public class HoleTunnelStairsESP extends Module { ); private final Setting maxChunks = sgGeneral.add(new IntSetting.Builder() .name("Chunks to process/tick") - .description("Amount of Chunks to process per tick") - .defaultValue(10) - .min(1) - .sliderRange(1, 100) + .description("Amount of chunks to process per tick.") + .defaultValue(10).min(1).sliderRange(1, 100) .build() ); private final Setting airBlocks = sgGeneral.add(new BoolSetting.Builder() .name("Detect only Air blocks as passable.") - .description("Only marks tunnels or holes if their blocks are air as oppose to if the blocks are passable.") - .defaultValue(false) + .description("Only marks tunnels/holes if their blocks are air rather than merely passable.") + .defaultValue(true) + .build() + ); + private final Setting undergroundUpdateThreshold = sgGeneral.add(new IntSetting.Builder() + .name("Underground Update Threshold") + .description("Amount of incoming blocks/updates below Y=0 required to trigger an underground rescan.") + .defaultValue(250).min(10).sliderMax(2000) .build() ); private final Setting minY = sgGeneral.add(new IntSetting.Builder() - .name("Detection Y Minimum OffSet") - .description("Scans blocks above or at this this many blocks from minimum build limit.") - .min(0) - .sliderRange(0,319) - .defaultValue(0) + .name("Detection Y Minimum Offset") + .description("Scans blocks at or above this many blocks from the minimum build limit.") + .min(0).sliderRange(0, 319).defaultValue(0) .build() ); private final Setting maxY = sgGeneral.add(new IntSetting.Builder() - .name("Detection Y Maximum OffSet") - .description("Scans blocks below or at this this many blocks from maximum build limit.") - .min(0) - .sliderRange(0,319) - .defaultValue(0) + .name("Detection Y Maximum Offset") + .description("Scans blocks at or below this many blocks from the maximum build limit.") + .min(0).sliderRange(0, 319).defaultValue(0) .build() ); + + // == Hole Parameters == private final Setting minHoleDepth = sgHParams.add(new IntSetting.Builder() .name("Min Hole Depth") - .description("Minimum depth for a hole to be detected") - .defaultValue(4) - .min(1) - .sliderMax(20) + .description("Minimum depth for a hole to be detected.") + .defaultValue(4).min(1).sliderMax(20) .build() ); + + // == Tunnel Parameters == private final Setting minTunnelLength = sgTParams.add(new IntSetting.Builder() .name("Min Tunnel Length") - .description("Minimum length for a tunnel to be detected") - .defaultValue(3) - .min(1) - .sliderMax(20) + .description("Minimum length for a tunnel to be detected.") + .defaultValue(6).min(1).sliderMax(20) .build() ); private final Setting minTunnelHeight = sgTParams.add(new IntSetting.Builder() .name("Min Tunnel Height") - .description("Minimum height of the tunnels to be detected") - .defaultValue(2) - .min(1) - .sliderMax(10) + .description("Minimum height of tunnels to detect.") + .defaultValue(1).min(1).sliderMax(10) .build() ); private final Setting maxTunnelHeight = sgTParams.add(new IntSetting.Builder() .name("Max Tunnel Height") - .description("Maximum height of the tunnels to be detected") - .defaultValue(3) - .min(2) - .sliderMax(10) + .description("Maximum height of tunnels to detect.") + .defaultValue(3).min(2).sliderMax(10) + .build() + ); + private final Setting minTunnelWidth = sgTParams.add(new IntSetting.Builder() + .name("Min Tunnel Width") + .description("Minimum width of straight tunnels to detect.") + .defaultValue(1).min(1).sliderMax(10) + .build() + ); + private final Setting maxTunnelWidth = sgTParams.add(new IntSetting.Builder() + .name("Max Tunnel Width") + .description("Maximum width of straight tunnels to detect.") + .defaultValue(3).min(1).sliderMax(10) .build() ); private final Setting diagonals = sgTParams.add(new BoolSetting.Builder() .name("Detect Diagonal Tunnels.") - .description("Detects diagonal tunnels when tunnels are selected to be detected.") + .description("Detects diagonal tunnels when tunnels are selected.") .defaultValue(true) .build() ); private final Setting minDiagonalLength = sgTParams.add(new IntSetting.Builder() .name("Min Diagonal Tunnel Length") - .description("Minimum length for diagonal tunnels to be detected") - .defaultValue(3) - .min(1) - .sliderMax(20) + .description("Minimum length for diagonal tunnels to be detected.") + .defaultValue(6).min(1).sliderMax(20) .visible(diagonals::get) .build() ); private final Setting minDiagonalWidth = sgTParams.add(new IntSetting.Builder() .name("Min Diagonal Tunnel Width") - .description("Minimum width for diagonal tunnels to be detected") - .defaultValue(2) - .min(2) - .sliderMax(10) + .description("Minimum width for diagonal tunnels to be detected.") + .defaultValue(1).min(1).sliderMax(10) .visible(diagonals::get) .build() ); private final Setting maxDiagonalWidth = sgTParams.add(new IntSetting.Builder() .name("Max Diagonal Tunnel Width") - .description("Maximum width for diagonal tunnels to be detected") - .defaultValue(4) - .min(2) - .sliderMax(10) + .description("Maximum width for diagonal tunnels to be detected.") + .defaultValue(3).min(1).sliderMax(10) .visible(diagonals::get) .build() ); + + // == Stairs Parameters == private final Setting minStaircaseLength = sgSParams.add(new IntSetting.Builder() .name("Min Staircase Length") - .description("Minimum length for a staircase to be detected") - .defaultValue(3) - .min(1) - .sliderMax(20) + .description("Minimum length for a staircase to be detected.") + .defaultValue(3).min(1).sliderMax(20) .build() ); private final Setting minStaircaseHeight = sgSParams.add(new IntSetting.Builder() .name("Min Staircase Height") - .description("Minimum height of the staircase to be detected") - .defaultValue(3) - .min(2) - .sliderMax(10) + .description("Minimum height of the staircase to be detected.") + .defaultValue(3).min(2).sliderMax(10) .build() ); private final Setting maxStaircaseHeight = sgSParams.add(new IntSetting.Builder() .name("Max Staircase Height") - .description("Maximum height of the staircase to be detected") - .defaultValue(5) - .min(2) - .sliderMax(10) + .description("Maximum height of the staircase to be detected.") + .defaultValue(5).min(2).sliderMax(10) .build() ); - private final Setting shapeMode = sgRender.add(new EnumSetting.Builder() - .name("shape-mode") - .description("How the shapes are rendered.") - .defaultValue(ShapeMode.Both) + + // == Covered Hole Parameters == + private final Setting detectCoveredHoles = sgCParams.add(new BoolSetting.Builder() + .name("detect-covered-holes") + .description("Detects and highlights holes that are covered by solid blocks.") + .defaultValue(true) .build() ); - private final Setting holeLineColor = sgRender.add(new ColorSetting.Builder() - .name("1x1-hole-line-color") - .description("The color of the lines for 1x1 holes being rendered.") - .defaultValue(new SettingColor(255, 0, 0, 95)) + private final Setting chatNotifications = sgCParams.add(new BoolSetting.Builder() + .name("chat-notifications") + .description("Send chat messages when covered holes are found.") + .defaultValue(true) + .visible(detectCoveredHoles::get) .build() ); - private final Setting holeSideColor = sgRender.add(new ColorSetting.Builder() - .name("1x1-hole-side-color") - .description("The color of the sides for 1x1 holes being rendered.") - .defaultValue(new SettingColor(255, 0, 0, 30)) + private final Setting onlyPlayerCovered = sgCParams.add(new BoolSetting.Builder() + .name("only-player-covered") + .description("Only detect holes that appear to be intentionally covered (e.g., by common building blocks).") + .defaultValue(true) + .visible(detectCoveredHoles::get) .build() ); + + // == Rendering == + private final Setting shapeMode = sgRender.add(new EnumSetting.Builder() + .name("shape-mode").defaultValue(ShapeMode.Both).build()); + private final Setting holeLineColor = sgRender.add(new ColorSetting.Builder() + .name("1x1-hole-line-color").defaultValue(new SettingColor(255, 0, 0, 95)).build()); + private final Setting holeSideColor = sgRender.add(new ColorSetting.Builder() + .name("1x1-hole-side-color").defaultValue(new SettingColor(255, 0, 0, 30)).build()); private final Setting hole3x1LineColor = sgRender.add(new ColorSetting.Builder() - .name("3x1-hole-line-color") - .description("The color of the lines for 3x1 holes being rendered.") - .defaultValue(new SettingColor(255, 165, 0, 95)) - .build() - ); + .name("3x1-hole-line-color").defaultValue(new SettingColor(255, 165, 0, 95)).build()); private final Setting hole3x1SideColor = sgRender.add(new ColorSetting.Builder() - .name("3x1-hole-side-color") - .description("The color of the sides for 3x1 holes being rendered.") - .defaultValue(new SettingColor(255, 165, 0, 30)) - .build() - ); + .name("3x1-hole-side-color").defaultValue(new SettingColor(255, 165, 0, 30)).build()); private final Setting tunnelLineColor = sgRender.add(new ColorSetting.Builder() - .name("tunnel-line-color") - .description("The color of the lines for the tunnels being rendered.") - .defaultValue(new SettingColor(0, 0, 255, 95)) - .build() - ); + .name("tunnel-line-color").defaultValue(new SettingColor(0, 0, 255, 95)).build()); private final Setting tunnelSideColor = sgRender.add(new ColorSetting.Builder() - .name("tunnel-side-color") - .description("The color of the sides for the tunnels being rendered.") - .defaultValue(new SettingColor(0, 0, 255, 30)) - .build() - ); + .name("tunnel-side-color").defaultValue(new SettingColor(0, 0, 255, 30)).build()); private final Setting staircaseLineColor = sgRender.add(new ColorSetting.Builder() - .name("staircase-line-color") - .description("The color of the lines for the staircases being rendered.") - .defaultValue(new SettingColor(255, 0, 255, 95)) - .build() - ); + .name("staircase-line-color").defaultValue(new SettingColor(255, 0, 255, 95)).build()); private final Setting staircaseSideColor = sgRender.add(new ColorSetting.Builder() - .name("staircase-side-color") - .description("The color of the sides for the staircases being rendered.") - .defaultValue(new SettingColor(255, 0, 255, 30)) + .name("staircase-side-color").defaultValue(new SettingColor(255, 0, 255, 30)).build()); + private final Setting coveredHoleLineColor = sgCParams.add(new ColorSetting.Builder() + .name("covered-hole-line-color").defaultValue(new SettingColor(255, 165, 0, 255)) + .visible(detectCoveredHoles::get) .build() ); + private final Setting coveredHoleSideColor = sgCParams.add(new ColorSetting.Builder() + .name("covered-hole-side-color").defaultValue(new SettingColor(255, 165, 0, 50)) + .visible(detectCoveredHoles::get).build()); private static final Direction[] DIRECTIONS = { Direction.EAST, Direction.WEST, Direction.NORTH, Direction.SOUTH }; - private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); - private final Queue chunkQueue = new LinkedList<>(); - private final Set holes = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final Set tunnels = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private static final Direction[] CANONICAL_TUNNEL_DIRS = { Direction.EAST, Direction.SOUTH }; + + // State + private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + private final Queue chunkQueue = new LinkedList<>(); + + private final Set holes = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set tunnels = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set staircases = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final Set holes3x1 = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set holes3x1 = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Map coveredHoles = new ConcurrentHashMap<>(); + private final Set notifiedHoles = ConcurrentHashMap.newKeySet(); + + private final Set tunnelHashes = ConcurrentHashMap.newKeySet(); + // coveredHoleHashes is not needed if coveredHoles is a ConcurrentHashMap and Box has proper equals/hashCode + // private final Set coveredHoleHashes = ConcurrentHashMap.newKeySet(); + + private final Set holeHashes = ConcurrentHashMap.newKeySet(); + private final Set hole3x1Hashes = ConcurrentHashMap.newKeySet(); + private final Set staircaseHashes = ConcurrentHashMap.newKeySet(); + + private final ThreadLocal visitedBlocksLocal = ThreadLocal.withInitial(BitSet::new); + + // Underground Update Tracking + private final Set pendingUndergroundChunks = ConcurrentHashMap.newKeySet(); + private int undergroundBlockUpdates = 0; + private boolean needsUndergroundRescan = false; + + // Caches for covered hole detection + private final Map solidBlockCache = new ConcurrentHashMap<>(); + private final Map blockStateCache = new ConcurrentHashMap<>(); public HoleTunnelStairsESP() { - super(GlazedAddon.esp, "hole-tunnel-stair-esp", "Finds and highlights holes and tunnels and stairs."); + super(GlazedAddon.esp, "hole-tunnel-stair-esp", "Finds and highlights holes, tunnels, and staircases."); } - public Set getHoles() { - return new HashSet<>(holes); - } + public Set getHoles() { return new HashSet<>(holes); } @Override - public void onDeactivate() { - chunks.clear(); + public void onDeactivate() { clearAll(); } + + private void clearAll() { + synchronized (chunks) { chunks.clear(); } chunkQueue.clear(); - holes.clear(); - tunnels.clear(); - staircases.clear(); - holes3x1.clear(); + holes.clear(); tunnels.clear(); staircases.clear(); holes3x1.clear(); coveredHoles.clear(); + tunnelHashes.clear(); holeHashes.clear(); hole3x1Hashes.clear(); staircaseHashes.clear(); + pendingUndergroundChunks.clear(); + undergroundBlockUpdates = 0; + needsUndergroundRescan = false; + notifiedHoles.clear(); + solidBlockCache.clear(); + blockStateCache.clear(); } + // ========================================================================= + // PACKET RECEIVE - TARGETED THRESHOLD LOGIC + // ========================================================================= + @EventHandler + private void onPacketReceive(PacketEvent.Receive event) { + if (event.packet instanceof BlockUpdateS2CPacket packet) { + if (packet.getPos().getY() < 0) { + pendingUndergroundChunks.add(ChunkPos.toLong(packet.getPos().getX() >> 4, packet.getPos().getZ() >> 4)); + undergroundBlockUpdates++; + } + } else if (event.packet instanceof ChunkDataS2CPacket packet) { + long key = ChunkPos.toLong(packet.getChunkX(), packet.getChunkZ()); + synchronized (chunks) { + if (chunks.containsKey(key)) { + pendingUndergroundChunks.add(key); + undergroundBlockUpdates += 200; + } + } + } + + if (undergroundBlockUpdates >= undergroundUpdateThreshold.get()) { + needsUndergroundRescan = true; + undergroundBlockUpdates = 0; + } + } + + private void triggerUndergroundRescan() { + // Snapshot ziehen, um Thread-Probleme zu verhindern + Set toProcess = new HashSet<>(pendingUndergroundChunks); + pendingUndergroundChunks.removeAll(toProcess); + + // 1. Chunks auf ein 3x3 Raster erweitern, um abgeschnittene Tunnel an Chunk-Grenzen zu reparieren + Set chunksToRescan = new HashSet<>(); + for (Long key : toProcess) { + int cx = (int) key.longValue(); + int cz = (int) (key.longValue() >> 32); + for (int dx = -1; dx <= 1; dx++) { + for (int dz = -1; dz <= 1; dz++) { + chunksToRescan.add(ChunkPos.toLong(cx + dx, cz + dz)); + } + } + } + + // 2. LΓ–SCHE NUR Boxen, die sich in DIESEN spezifischen Chunks befinden! + // Tunnel, die hinter dem Spieler liegen (und keine Updates bekommen), bleiben erhalten. + removeIntersectingUnderground(holes, holeHashes, chunksToRescan); + removeIntersectingUnderground(holes3x1, hole3x1Hashes, chunksToRescan); + removeIntersectingUnderground(staircases, staircaseHashes, chunksToRescan); + removeIntersectingUnderground(coveredHoles.keySet(), null, chunksToRescan); + + Iterator tunnelIter = tunnels.iterator(); + while (tunnelIter.hasNext()) { + Box b = tunnelIter.next(); + if (b.minY < 0 && intersectsChunk(b, chunksToRescan)) { + long h = BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ); + tunnelHashes.remove(h); + tunnelHashes.remove(h ^ Long.MIN_VALUE); + tunnelIter.remove(); + } + } + + // 3. Zielgerichtetes Rescannen: Nur diese betroffenen Chunks neu in die Queue werfen + synchronized (chunks) { + for (Long chunkKey : chunksToRescan) { + chunks.remove(chunkKey); + } + } + } + + private void removeIntersectingUnderground(Set boxes, Set hashes, Set chunksToRescan) { + Iterator iter = boxes.iterator(); + while (iter.hasNext()) { + Box b = iter.next(); // Box objects are immutable, so it's safe to iterate and remove + // For coveredHoles, we don't have a separate hash set, so hashes can be null. + // The Box itself is the identifier. + + if (b.minY < 0 && intersectsChunk(b, chunksToRescan)) { + hashes.remove(BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ)); + iter.remove(); + } + } + } + + // PrΓ€zise PrΓΌfung, ob eine Box in einen der zu updatenden Chunks hineinragt + private boolean intersectsChunk(Box b, Set chunkKeys) { + int minCx = ((int) Math.floor(b.minX)) >> 4; + int maxCx = ((int) Math.floor(b.maxX - 0.001)) >> 4; + int minCz = ((int) Math.floor(b.minZ)) >> 4; + int maxCz = ((int) Math.floor(b.maxZ - 0.001)) >> 4; + + for (int cx = minCx; cx <= maxCx; cx++) { + for (int cz = minCz; cz <= maxCz; cz++) { + if (chunkKeys.contains(ChunkPos.toLong(cx, cz))) return true; + } + } + return false; + } + + // ========================================================================= + // TICK / RENDER + // ========================================================================= @EventHandler private void onTick(TickEvent.Post event) { + if (needsUndergroundRescan) { + triggerUndergroundRescan(); + needsUndergroundRescan = false; + } + synchronized (chunks) { for (TChunk tChunk : chunks.values()) tChunk.marked = false; - for (Chunk chunk : Utils.chunks(true)) { long key = ChunkPos.toLong(chunk.getPos().x, chunk.getPos().z); - if (chunks.containsKey(key)) chunks.get(key).marked = true; - else if (!chunkQueue.contains(chunk)) { - chunkQueue.add(chunk); - } + else if (!chunkQueue.contains(chunk)) chunkQueue.add(chunk); } - processChunkQueue(); chunks.values().removeIf(tChunk -> !tChunk.marked); } removeBoxesOutsideRenderDistance(); } - private void removeBoxesOutsideRenderDistance() { - Set chunkSet = new HashSet<>(); - for (Chunk chunk : Utils.chunks(true)) { - if (chunk instanceof WorldChunk) { - chunkSet.add((WorldChunk) chunk); - } + private void clearOldCacheEntries() { + // Simple cache clearing strategy: clear if too large + // These caches are used by the searchChunk threads, so they need to be Concurrent. + // Clearing them entirely might cause temporary re-computation, but for ESP, + // it's generally acceptable as data is re-scanned frequently. + if (solidBlockCache.size() > 10000) { // Adjust size as needed + solidBlockCache.clear(); } + if (blockStateCache.size() > 10000) { // Adjust size as needed + blockStateCache.clear(); + } + } - removeBoxesOutsideRenderDistance(holes, chunkSet); - removeBoxesOutsideRenderDistance(tunnels, chunkSet); - removeBoxesOutsideRenderDistance(staircases, chunkSet); - removeBoxesOutsideRenderDistance(holes3x1, chunkSet); + private void removeBoxesOutsideRenderDistance() { + Set chunkSet = new HashSet<>(); + for (Chunk chunk : Utils.chunks(true)) + if (chunk instanceof WorldChunk wc) chunkSet.add(wc); + removeBoxesOutside(holes, chunkSet); + removeBoxesOutside(tunnels, chunkSet); + removeBoxesOutside(staircases, chunkSet); + removeBoxesOutside(coveredHoles.keySet(), chunkSet); + removeBoxesOutside(holes3x1, chunkSet); } - private void removeBoxesOutsideRenderDistance(Set boxSet, Set worldChunks) { + private void removeBoxesOutside(Set boxSet, Set worldChunks) { boxSet.removeIf(box -> { - BlockPos boxPos = new BlockPos((int)Math.floor(box.getCenter().getX()), (int)Math.floor(box.getCenter().getY()), (int)Math.floor(box.getCenter().getZ())); - assert mc.world != null; - return !worldChunks.contains(mc.world.getChunk(boxPos)); + BlockPos center = new BlockPos( + (int) Math.floor(box.getCenter().getX()), + (int) Math.floor(box.getCenter().getY()), + (int) Math.floor(box.getCenter().getZ())); + return !worldChunks.contains(mc.world.getChunk(center)); }); } @EventHandler private void onRender3D(Render3DEvent event) { switch (detectionMode.get()) { - case ALL: + case ALL -> { renderHoles(event.renderer); renderTunnels(event.renderer); renderStaircases(event.renderer); render3x1Holes(event.renderer); - break; - case HOLES_AND_TUNNELS: + if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); + } + case HOLES_AND_TUNNELS -> { renderHoles(event.renderer); renderTunnels(event.renderer); render3x1Holes(event.renderer); - break; - case HOLES_AND_STAIRCASES: + if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); + } + case HOLES_AND_STAIRCASES -> { renderHoles(event.renderer); renderStaircases(event.renderer); render3x1Holes(event.renderer); - break; - case TUNNELS_AND_STAIRCASES: + if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); + } + case TUNNELS_AND_STAIRCASES -> { renderTunnels(event.renderer); renderStaircases(event.renderer); - break; - case HOLES: + } + case HOLES -> { renderHoles(event.renderer); render3x1Holes(event.renderer); - break; - case TUNNELS: - renderTunnels(event.renderer); - break; - case STAIRCASES: - renderStaircases(event.renderer); - break; - case HOLES_3X1_AND_TUNNELS: + if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); + } + case TUNNELS -> renderTunnels(event.renderer); + case STAIRCASES -> renderStaircases(event.renderer); + case HOLES_3X1_AND_TUNNELS -> { renderHoles(event.renderer); render3x1Holes(event.renderer); renderTunnels(event.renderer); - break; + if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); + } + default -> { // Fallback for any other mode that might render holes + renderHoles(event.renderer); + renderTunnels(event.renderer); + renderStaircases(event.renderer); + render3x1Holes(event.renderer); + if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); + } } } - private void renderHoles(Renderer3D renderer) { - for (Box box : holes) { - renderer.box(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, holeSideColor.get(), holeLineColor.get(), shapeMode.get(), 0); + private void renderHoles(Renderer3D r) { + for (Box b : holes) { + if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; // Don't render if it's a covered hole + r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, holeSideColor.get(), holeLineColor.get(), shapeMode.get(), 0); } } - - private void render3x1Holes(Renderer3D renderer) { - for (Box box : holes3x1) { - renderer.box(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, hole3x1SideColor.get(), hole3x1LineColor.get(), shapeMode.get(), 0); + private void render3x1Holes(Renderer3D r) { + for (Box b : holes3x1) { + if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; // Don't render if it's a covered hole + r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, hole3x1SideColor.get(), hole3x1LineColor.get(), shapeMode.get(), 0); } } + private void renderTunnels(Renderer3D r) { + for (Box b : tunnels) + r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, tunnelSideColor.get(), tunnelLineColor.get(), shapeMode.get(), 0); + } - private void renderTunnels(Renderer3D renderer) { - for (Box box : tunnels) { - renderer.box(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, tunnelSideColor.get(), tunnelLineColor.get(), shapeMode.get(), 0); - } + private void renderStaircases(Renderer3D r) { + for (Box b : staircases) + r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, staircaseSideColor.get(), staircaseLineColor.get(), shapeMode.get(), 0); } - private void renderStaircases(Renderer3D renderer) { - for (Box box : staircases) { - renderer.box(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ, staircaseSideColor.get(), staircaseLineColor.get(), shapeMode.get(), 0); + private void renderCoveredHoles(Renderer3D r) { + for (Map.Entry entry : coveredHoles.entrySet()) { + Box hole = entry.getKey(); + CoveredHoleInfo info = entry.getValue(); + + // Render the hole + r.box(hole.minX, hole.minY, hole.minZ, hole.maxX, hole.maxY, hole.maxZ, + coveredHoleSideColor.get(), coveredHoleLineColor.get(), shapeMode.get(), 0); + + // Render the cover block + r.box(info.coverPos.getX(), info.coverPos.getY(), info.coverPos.getZ(), + info.coverPos.getX() + 1, info.coverPos.getY() + 1, info.coverPos.getZ() + 1, + coveredHoleSideColor.get(), coveredHoleLineColor.get(), shapeMode.get(), 0); } } + // ========================================================================= + // CHUNK PROCESSING + // ========================================================================= private void processChunkQueue() { - int maxChunksPerTick = maxChunks.get(); int processed = 0; - - while (!chunkQueue.isEmpty() && processed < maxChunksPerTick) { + while (!chunkQueue.isEmpty() && processed < maxChunks.get()) { Chunk chunk = chunkQueue.poll(); if (chunk != null) { TChunk tChunk = new TChunk(chunk.getPos().x, chunk.getPos().z); chunks.put(tChunk.getKey(), tChunk); - MeteorExecutor.execute(() -> searchChunk(chunk, tChunk)); processed++; } @@ -360,364 +525,459 @@ private void processChunkQueue() { } private void searchChunk(Chunk chunk, TChunk tChunk) { - var sections = chunk.getSectionArray(); - int Ymin = mc.world.getBottomY() + minY.get(); + int Ymin = mc.world.getBottomY() + minY.get(); int Ymax = mc.world.getTopYInclusive() - maxY.get(); - int Y = mc.world.getBottomY(); - for (ChunkSection section : sections) { - if (section != null && !section.isEmpty()) { + + BitSet visited = visitedBlocksLocal.get(); + visited.clear(); + + BlockPos.Mutable pos = new BlockPos.Mutable(); + BlockPos.Mutable floorPos = new BlockPos.Mutable(); + + for (int ySection = 0; ySection < chunk.getSectionArray().length; ySection++) { + ChunkSection section = chunk.getSectionArray()[ySection]; + if (section == null || section.isEmpty()) continue; + int sectionBaseY = mc.world.getBottomY() + (ySection * 16); + + for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { - for (int x = 0; x < 16; x++) { - for (int y = 0; y < 16; y++) { - int currentY = Y + y; - if (currentY <= Ymin || currentY >= Ymax) continue; - BlockPos pos = chunk.getPos().getBlockPos(x, currentY, z); - if (isPassableBlock(pos)) { - switch (detectionMode.get()) { - case ALL: - checkHole(pos, holes); - check3x1Hole(pos, holes3x1); - checkTunnel(pos); - if (diagonals.get()) checkDiagonalTunnel(pos); - checkStaircase(pos); - break; - case HOLES_AND_TUNNELS: - checkHole(pos, holes); - check3x1Hole(pos, holes3x1); - checkTunnel(pos); - if (diagonals.get()) checkDiagonalTunnel(pos); - break; - case HOLES_AND_STAIRCASES: - checkHole(pos, holes); - check3x1Hole(pos, holes3x1); - checkStaircase(pos); - break; - case TUNNELS_AND_STAIRCASES: - checkTunnel(pos); - if (diagonals.get()) checkDiagonalTunnel(pos); - checkStaircase(pos); - break; - case HOLES: - checkHole(pos, holes); - check3x1Hole(pos, holes3x1); - break; - case TUNNELS: - checkTunnel(pos); - if (diagonals.get()) checkDiagonalTunnel(pos); - break; - case STAIRCASES: - checkStaircase(pos); - break; - case HOLES_3X1_AND_TUNNELS: - checkHole(pos, holes); - check3x1Hole(pos, holes3x1); - checkTunnel(pos); - if (diagonals.get()) checkDiagonalTunnel(pos); - break; - } + for (int y = 0; y < 16; y++) { + int currentY = sectionBaseY + y; + if (currentY <= Ymin || currentY >= Ymax) continue; + if (visited.get(getLocalIndex(x, currentY, z, Ymin))) continue; + + pos.set(chunk.getPos().getStartX() + x, currentY, + chunk.getPos().getStartZ() + z); + if (!isPassableBlock(pos)) continue; + + floorPos.set(pos).move(Direction.DOWN); + boolean hasSolidFloor = !isPassableBlock(floorPos); + DetectionMode mode = detectionMode.get(); + + if (mode == DetectionMode.ALL || mode == DetectionMode.HOLES + || mode == DetectionMode.HOLES_AND_TUNNELS + || mode == DetectionMode.HOLES_AND_STAIRCASES + || mode == DetectionMode.HOLES_3X1_AND_TUNNELS) { + findAndAddHole(pos, visited, Ymin); + findAndAdd3x1Hole(pos, visited, Ymin); + } + if (hasSolidFloor) { // Tunnels and staircases require a solid floor + if (mode == DetectionMode.ALL || mode == DetectionMode.TUNNELS + || mode == DetectionMode.HOLES_AND_TUNNELS + || mode == DetectionMode.TUNNELS_AND_STAIRCASES + || mode == DetectionMode.HOLES_3X1_AND_TUNNELS) { + checkTunnelOptimized(pos, visited, Ymin); + if (diagonals.get()) checkDiagonalTunnel(pos, visited, Ymin); + } + if (mode == DetectionMode.ALL || mode == DetectionMode.STAIRCASES + || mode == DetectionMode.HOLES_AND_STAIRCASES + || mode == DetectionMode.TUNNELS_AND_STAIRCASES) { + checkStaircaseOptimized(pos, visited, Ymin); } } } } } - Y += 16; } } - private void checkHole(BlockPos pos, Set holes) { - if (isValidHoleSection(pos)) { - BlockPos.Mutable currentPos = pos.mutableCopy(); - while (isValidHoleSection(currentPos)) { - currentPos.move(Direction.UP); - } - if (currentPos.getY() - pos.getY() >= minHoleDepth.get()) { - Box holeBox = new Box( - pos.getX(), pos.getY(), pos.getZ(), - pos.getX() + 1, currentPos.getY(), pos.getZ() + 1 - ); - if (!holes.contains(holeBox) && holes.stream().noneMatch(existingHole -> existingHole.intersects(holeBox))) { - holes.add(holeBox); + private int getLocalIndex(int x, int y, int z, int yMin) { + return (x & 15) | ((z & 15) << 4) | ((y - yMin) << 8); + } + + // ========================================================================= + // HOLE CHECKS + // ========================================================================= + private void findAndAddHole(BlockPos pos, BitSet visited, int yMin) { + if (!isValidHoleSection(pos)) return; + BlockPos.Mutable cur = pos.mutableCopy(); + int depth = 0; + while (isValidHoleSection(cur)) { + visited.set(getLocalIndex(cur.getX() & 15, cur.getY(), cur.getZ() & 15, yMin)); + cur.move(Direction.UP); + depth++; + } + if (depth >= minHoleDepth.get()) { + long hash = BlockPos.asLong(pos.getX(), pos.getY(), pos.getZ()); + if (holeHashes.add(hash)) + { + Box newHoleBox = new Box(pos.getX(), pos.getY(), pos.getZ(), + pos.getX() + 1, cur.getY(), pos.getZ() + 1); + holes.add(newHoleBox); + if (detectCoveredHoles.get()) checkCoveredHole(newHoleBox); } - } } } - private void check3x1Hole(BlockPos pos, Set holes3x1) { - // Check 3x1 hole in X direction (3 blocks east-west, 1 block north-south) + private void findAndAdd3x1Hole(BlockPos pos, BitSet visited, int yMin) { if (isValid3x1HoleSectionX(pos)) { - BlockPos.Mutable currentPos = pos.mutableCopy(); - while (isValid3x1HoleSectionX(currentPos)) { - currentPos.move(Direction.UP); + BlockPos.Mutable cur = pos.mutableCopy(); + int depth = 0; + while (isValid3x1HoleSectionX(cur)) { + mark3x1Visited(cur, Direction.EAST, visited, yMin); + cur.move(Direction.UP); + depth++; } - if (currentPos.getY() - pos.getY() >= minHoleDepth.get()) { - Box holeBox = new Box( - pos.getX(), pos.getY(), pos.getZ(), - pos.getX() + 3, currentPos.getY(), pos.getZ() + 1 - ); - if (!holes3x1.contains(holeBox) && holes3x1.stream().noneMatch(existingHole -> existingHole.intersects(holeBox))) { - holes3x1.add(holeBox); - } + if (depth >= minHoleDepth.get()) { + long hash = BlockPos.asLong(pos.getX(), pos.getY(), pos.getZ()); + if (hole3x1Hashes.add(hash)) + { + Box newHoleBox = new Box(pos.getX(), pos.getY(), pos.getZ(), + pos.getX() + 3, cur.getY(), pos.getZ() + 1); + holes3x1.add(newHoleBox); + if (detectCoveredHoles.get()) checkCoveredHole(newHoleBox); + } } } - - // Check 3x1 hole in Z direction (1 block east-west, 3 blocks north-south) if (isValid3x1HoleSectionZ(pos)) { - BlockPos.Mutable currentPos = pos.mutableCopy(); - while (isValid3x1HoleSectionZ(currentPos)) { - currentPos.move(Direction.UP); + BlockPos.Mutable cur = pos.mutableCopy(); + int depth = 0; + while (isValid3x1HoleSectionZ(cur)) { + mark3x1Visited(cur, Direction.SOUTH, visited, yMin); + cur.move(Direction.UP); + depth++; } - if (currentPos.getY() - pos.getY() >= minHoleDepth.get()) { - Box holeBox = new Box( - pos.getX(), pos.getY(), pos.getZ(), - pos.getX() + 1, currentPos.getY(), pos.getZ() + 3 - ); - if (!holes3x1.contains(holeBox) && holes3x1.stream().noneMatch(existingHole -> existingHole.intersects(holeBox))) { - holes3x1.add(holeBox); - } + if (depth >= minHoleDepth.get()) { + long hash = BlockPos.asLong(pos.getX(), pos.getY(), pos.getZ()); + if (hole3x1Hashes.add(hash)) + { + Box newHoleBox = new Box(pos.getX(), pos.getY(), pos.getZ(), + pos.getX() + 1, cur.getY(), pos.getZ() + 3); + holes3x1.add(newHoleBox); + if (detectCoveredHoles.get()) checkCoveredHole(newHoleBox); + } } } } - private boolean isValidHoleSection(BlockPos pos) { - return isPassableBlock(pos) && !isPassableBlock(pos.north()) && !isPassableBlock(pos.south()) && !isPassableBlock(pos.east()) && !isPassableBlock(pos.west()); + private void mark3x1Visited(BlockPos pos, Direction widthDir, BitSet visited, int yMin) { + BlockPos.Mutable m = pos.mutableCopy(); + for (int i = 0; i < 3; i++) { + visited.set(getLocalIndex(m.getX() & 15, m.getY(), m.getZ() & 15, yMin)); + m.move(widthDir); + } } - private boolean isValid3x1HoleSectionX(BlockPos pos) { - // Check if this is part of a 3x1 hole in X direction - return isPassableBlock(pos) && - isPassableBlock(pos.east()) && - isPassableBlock(pos.east(2)) && - !isPassableBlock(pos.north()) && - !isPassableBlock(pos.south()) && - !isPassableBlock(pos.east(3)) && - !isPassableBlock(pos.west()) && - !isPassableBlock(pos.east().north()) && - !isPassableBlock(pos.east().south()) && - !isPassableBlock(pos.east(2).north()) && - !isPassableBlock(pos.east(2).south()); + // ========================================================================= + // STRAIGHT TUNNEL CHECK + // ========================================================================= + private boolean isValidTunnelCrossSection(BlockPos pos, Direction lengthDir, int width, int refHeight) { + Direction widthDir = (lengthDir.getAxis() == Direction.Axis.X) ? Direction.SOUTH : Direction.EAST; + Direction antiWidthDir = widthDir.getOpposite(); + + if (!isPassableBlock(pos)) return false; + if (isPassableBlock(pos.down())) return false; + if (isPassableBlock(pos.offset(antiWidthDir))) return false; + + if (getTunnelHeight(pos) != refHeight) return false; + if (isPassableBlock(pos.up(refHeight))) return false; + + for (int w = 1; w < width; w++) { + BlockPos wPos = pos.offset(widthDir, w); + if (!isPassableBlock(wPos)) return false; + if (isPassableBlock(wPos.down())) return false; + if (getTunnelHeight(wPos) != refHeight) return false; + if (isPassableBlock(wPos.up(refHeight))) return false; + } + + if (isPassableBlock(pos.offset(widthDir, width))) return false; + return true; } - private boolean isValid3x1HoleSectionZ(BlockPos pos) { - // Check if this is part of a 3x1 hole in Z direction - return isPassableBlock(pos) && - isPassableBlock(pos.south()) && - isPassableBlock(pos.south(2)) && - !isPassableBlock(pos.east()) && - !isPassableBlock(pos.west()) && - !isPassableBlock(pos.south(3)) && - !isPassableBlock(pos.north()) && - !isPassableBlock(pos.south().east()) && - !isPassableBlock(pos.south().west()) && - !isPassableBlock(pos.south(2).east()) && - !isPassableBlock(pos.south(2).west()); - } - - private void checkTunnel(BlockPos pos) { - for (Direction dir : DIRECTIONS) { - BlockPos.Mutable currentPos = pos.mutableCopy(); - int stepCount = 0; - BlockPos startPos = null; - BlockPos endPos = null; - int maxHeight = 0; - if (startPos == null && isTunnelSection(currentPos, dir)) { - startPos = currentPos.toImmutable(); + private void checkTunnelOptimized(BlockPos startPos, BitSet visited, int yMin) { + int chunkX = startPos.getX() >> 4; + int chunkZ = startPos.getZ() >> 4; + + for (Direction dir : CANONICAL_TUNNEL_DIRS) { + Direction widthDir = (dir.getAxis() == Direction.Axis.X) ? Direction.SOUTH : Direction.EAST; + Direction antiWidthDir = widthDir.getOpposite(); + + if (isPassableBlock(startPos.offset(antiWidthDir))) continue; + + int width = 0; + while (width < maxTunnelWidth.get() && isPassableBlock(startPos.offset(widthDir, width))) { + width++; } - while (isTunnelSection(currentPos, dir)) { - maxHeight = Math.max(maxHeight, getTunnelHeight(currentPos)); - endPos = currentPos.toImmutable(); - currentPos.move(dir); + if (width < minTunnelWidth.get() || width > maxTunnelWidth.get()) continue; + + int refHeight = getTunnelHeight(startPos); + if (refHeight < minTunnelHeight.get() || refHeight > maxTunnelHeight.get()) continue; + + if (!isValidTunnelCrossSection(startPos, dir, width, refHeight)) continue; + + BlockPos.Mutable canonicalStart = startPos.mutableCopy(); + { + BlockPos.Mutable probe = startPos.mutableCopy(); + probe.move(dir.getOpposite()); + while (isValidTunnelCrossSection(probe, dir, width, refHeight)) { + canonicalStart.set(probe); + probe.move(dir.getOpposite()); + } + } + + long hash = BlockPos.asLong(canonicalStart.getX(), canonicalStart.getY(), canonicalStart.getZ()); + if (dir.getAxis() == Direction.Axis.Z) hash ^= Long.MIN_VALUE; + + BlockPos.Mutable scanPos = canonicalStart.mutableCopy(); + BlockPos.Mutable lastValid = canonicalStart.mutableCopy(); + int stepCount = 0; + + while (isValidTunnelCrossSection(scanPos, dir, width, refHeight)) { + for (int w = 0; w < width; w++) { + int wx = scanPos.getX() + widthDir.getOffsetX() * w; + int wz = scanPos.getZ() + widthDir.getOffsetZ() * w; + if ((wx >> 4) == chunkX && (wz >> 4) == chunkZ) + visited.set(getLocalIndex(wx & 15, scanPos.getY(), wz & 15, yMin)); + } + lastValid.set(scanPos); + scanPos.move(dir); stepCount++; } - if (stepCount >= minTunnelLength.get() && maxHeight >= minTunnelHeight.get() && maxHeight <= maxTunnelHeight.get()) { - Box tunnelBox = new Box( - Math.min(startPos.getX(), endPos.getX()), - startPos.getY(), - Math.min(startPos.getZ(), endPos.getZ()), - Math.max(startPos.getX(), endPos.getX()) + 1, - startPos.getY() + maxHeight, - Math.max(startPos.getZ(), endPos.getZ()) + 1 - ); - - if (!tunnels.contains(tunnelBox) && tunnels.stream().noneMatch(existingTunnel -> existingTunnel.intersects(tunnelBox))) { - tunnels.add(tunnelBox); + if (stepCount >= minTunnelLength.get() && tunnelHashes.add(hash)) { + int x1 = canonicalStart.getX(), y1 = canonicalStart.getY(), z1 = canonicalStart.getZ(); + int x2, z2; + if (dir.getAxis() == Direction.Axis.X) { + x2 = lastValid.getX() + 1; + z2 = z1 + width; + } else { + x2 = x1 + width; + z2 = lastValid.getZ() + 1; } + tunnels.add(new Box(x1, y1, z1, x2, y1 + refHeight, z2)); } } } - private boolean isTunnelSection(BlockPos pos, Direction dir) { - int height = getTunnelHeight(pos); - if (height < minTunnelHeight.get() || height > maxTunnelHeight.get()) return false; - if (isPassableBlock(pos.down()) || isPassableBlock(pos.up(height))) return false; - Direction[] perpDirs = dir.getAxis() == Direction.Axis.X ? new Direction[]{Direction.NORTH, Direction.SOUTH} : new Direction[]{Direction.EAST, Direction.WEST}; - for (Direction perpDir : perpDirs) { - for (int i = 0; i < height; i++) { - if (isPassableBlock(pos.up(i).offset(perpDir))) { - return false; + // ========================================================================= + // STAIRCASE CHECK + // ========================================================================= + private void checkStaircaseOptimized(BlockPos pos, BitSet visited, int yMin) { + for (Direction dir : DIRECTIONS) { + BlockPos.Mutable cur = pos.mutableCopy(); + int stepCount = 0; + List potential = new ArrayList<>(); + + while (isStaircaseSection(cur, dir)) { + int height = getStaircaseHeight(cur); + potential.add(new Box(cur.getX(), cur.getY(), cur.getZ(), + cur.getX() + 1, cur.getY() + height, cur.getZ() + 1)); + visited.set(getLocalIndex(cur.getX() & 15, cur.getY(), cur.getZ() & 15, yMin)); + cur.move(dir); + cur.move(Direction.UP); + stepCount++; + } + if (stepCount >= minStaircaseLength.get()) { + for (Box b : potential) { + long hash = BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ); + if (staircaseHashes.add(hash)) staircases.add(b); } } } - return true; } - private void checkDiagonalTunnel(BlockPos pos) { + // ========================================================================= + // DIAGONAL TUNNEL CHECK + // ========================================================================= + private void checkDiagonalTunnel(BlockPos pos, BitSet visited, int yMin) { for (Direction dir : DIRECTIONS) { - for (int i = minDiagonalWidth.get() - 1; i < maxDiagonalWidth.get(); i++) { - BlockPos.Mutable currentPos = pos.mutableCopy(); + for (int w = minDiagonalWidth.get(); w <= maxDiagonalWidth.get(); w++) { + BlockPos.Mutable cur = pos.mutableCopy(); int stepCount = 0; - List potentialBoxes = new ArrayList<>(); - - Direction checkingDir = dir; - boolean turnRight = true; - - while (isDiagonalTunnelSection(currentPos, checkingDir)) { - int height = getTunnelHeight(currentPos); - Box tunnelBox = new Box( - currentPos.getX(), - currentPos.getY(), - currentPos.getZ(), - currentPos.getX() + 1, - currentPos.getY() + height, - currentPos.getZ() + 1 - ); - if (!potentialBoxes.contains(tunnelBox) && !potentialBoxes.stream().anyMatch(existingDiagonal -> existingDiagonal.intersects(tunnelBox))) { - potentialBoxes.add(tunnelBox); - } - - if (turnRight) { - checkingDir = checkingDir.rotateYClockwise(); - currentPos.move(checkingDir.rotateYClockwise(), i); - turnRight = false; - } else { - checkingDir = checkingDir.rotateYCounterclockwise(); - currentPos.move(checkingDir.rotateYCounterclockwise(), i); - turnRight = true; + List potential = new ArrayList<>(); + Direction checkDir = dir; + boolean turnRight = true; + + while (isDiagonalTunnelSection(cur, checkDir)) { + int height = getTunnelHeight(cur); + BlockPos.Mutable fill = cur.mutableCopy(); + for (int k = 0; k < w; k++) { + potential.add(new Box(fill.getX(), fill.getY(), fill.getZ(), + fill.getX() + 1, fill.getY() + height, fill.getZ() + 1)); + visited.set(getLocalIndex(fill.getX() & 15, fill.getY(), fill.getZ() & 15, yMin)); + fill.move(turnRight ? checkDir.rotateYClockwise() + : checkDir.rotateYCounterclockwise()); } + if (turnRight) { checkDir = checkDir.rotateYClockwise(); cur.move(checkDir, w); turnRight = false; } + else { checkDir = checkDir.rotateYCounterclockwise(); cur.move(checkDir, w); turnRight = true; } stepCount++; } - - if (stepCount / minDiagonalWidth.get() >= minDiagonalLength.get()) { - potentialBoxes.forEach(potentialBox -> { - if (!tunnels.contains(potentialBox) && tunnels.stream().noneMatch(existingDiagonal -> existingDiagonal.intersects(potentialBox))) { - tunnels.add(potentialBox); - } - }); + if (stepCount >= minDiagonalLength.get()) { + for (Box b : potential) { + long hash = BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ); + if (tunnelHashes.add(hash)) tunnels.add(b); + } } } } } + // ========================================================================= + // HELPER METHODS + // ========================================================================= + private int getTunnelHeight(BlockPos pos) { + int h = 0; + while (h < maxTunnelHeight.get() + 1 && isPassableBlock(pos.up(h))) h++; + return h; + } + + private int getStaircaseHeight(BlockPos pos) { + int h = 0; + while (h < maxStaircaseHeight.get() && isPassableBlock(pos.up(h))) h++; + return h; + } + + private boolean isValidHoleSection(BlockPos pos) { + return isPassableBlock(pos) + && !isPassableBlock(pos.north()) && !isPassableBlock(pos.south()) + && !isPassableBlock(pos.east()) && !isPassableBlock(pos.west()); + } + + private boolean isValid3x1HoleSectionX(BlockPos pos) { + return isPassableBlock(pos) && isPassableBlock(pos.east()) && isPassableBlock(pos.east(2)) + && !isPassableBlock(pos.north()) && !isPassableBlock(pos.south()) + && !isPassableBlock(pos.east(3)) && !isPassableBlock(pos.west()) + && !isPassableBlock(pos.east().north()) && !isPassableBlock(pos.east().south()) + && !isPassableBlock(pos.east(2).north()) && !isPassableBlock(pos.east(2).south()); + } + + private boolean isValid3x1HoleSectionZ(BlockPos pos) { + return isPassableBlock(pos) && isPassableBlock(pos.south()) && isPassableBlock(pos.south(2)) + && !isPassableBlock(pos.east()) && !isPassableBlock(pos.west()) + && !isPassableBlock(pos.south(3)) && !isPassableBlock(pos.north()) + && !isPassableBlock(pos.south().east()) && !isPassableBlock(pos.south().west()) + && !isPassableBlock(pos.south(2).east()) && !isPassableBlock(pos.south(2).west()); + } + + private boolean isStaircaseSection(BlockPos pos, Direction dir) { + int height = getStaircaseHeight(pos); + if (height < minStaircaseHeight.get() || height > maxStaircaseHeight.get()) return false; + if (isPassableBlock(pos.down()) || isPassableBlock(pos.up(height))) return false; + Direction[] perp = (dir.getAxis() == Direction.Axis.X) + ? new Direction[]{ Direction.NORTH, Direction.SOUTH } + : new Direction[]{ Direction.EAST, Direction.WEST }; + for (Direction p : perp) + for (int i = 0; i < height; i++) + if (isPassableBlock(pos.up(i).offset(p))) return false; + return true; + } + private boolean isDiagonalTunnelSection(BlockPos pos, Direction dir) { int height = getTunnelHeight(pos); if (height < minTunnelHeight.get() || height > maxTunnelHeight.get()) return false; if (isPassableBlock(pos.down()) || isPassableBlock(pos.up(height))) return false; - - boolean wasPassableBlockFound = false; - for (int i = 0; i < height; i++) { - if (isPassableBlock(pos.up(i).offset(dir))) wasPassableBlockFound = true; - } - if (wasPassableBlockFound) return false; - + for (int i = 0; i < height; i++) + if (isPassableBlock(pos.up(i).offset(dir))) return false; return true; } - private int getTunnelHeight(BlockPos pos) { - int height = 0; - while (isPassableBlock(pos.up(height)) && height < maxTunnelHeight.get()) { - height++; + private boolean isPassableBlock(BlockPos pos) { + BlockState state = mc.world.getBlockState(pos); + if (airBlocks.get()) return state.isAir(); + VoxelShape shape = state.getCollisionShape(mc.world, pos); + return shape.isEmpty() || !VoxelShapes.fullCube().equals(shape); + } + + // ========================================================================= + // COVERED HOLE CHECKS (Integrated from CoveredHole) + // ========================================================================= + private static class CoveredHoleInfo { + public final BlockPos coverPos; + public final Box holeBox; + + public CoveredHoleInfo(BlockPos coverPos, Box holeBox) { + this.coverPos = coverPos; + this.holeBox = holeBox; } - return height; } - private void checkStaircase(BlockPos pos) { - for (Direction dir : DIRECTIONS) { - BlockPos.Mutable currentPos = pos.mutableCopy(); - int stepCount = 0; - List potentialStaircaseBoxes = new ArrayList<>(); - - while (isStaircaseSection(currentPos, dir)) { - int height = getStaircaseHeight(currentPos); - Box stairsBox = new Box( - currentPos.getX(), - currentPos.getY(), - currentPos.getZ(), - currentPos.getX() + 1, - currentPos.getY() + height, - currentPos.getZ() + 1 - ); - if (!potentialStaircaseBoxes.contains(stairsBox) && !potentialStaircaseBoxes.stream().anyMatch(existingStaircase -> existingStaircase.intersects(stairsBox))) { - potentialStaircaseBoxes.add(stairsBox); - } - currentPos.move(dir); - currentPos.move(Direction.UP); - stepCount++; - } + private void checkCoveredHole(Box holeBox) { + if (!detectCoveredHoles.get()) return; + + BlockPos topPos = new BlockPos((int) holeBox.minX, (int) holeBox.maxY, (int) holeBox.minZ); + + if (isSolidBlockCached(topPos)) { + boolean isPlayerCovered = !onlyPlayerCovered.get() || isLikelyPlayerCovered(topPos, holeBox); - for (Box stairsBox : potentialStaircaseBoxes) { - if (stepCount >= minStaircaseLength.get() && !staircases.contains(stairsBox) && !staircases.stream().anyMatch(existingStaircase -> existingStaircase.intersects(stairsBox))) { - staircases.add(stairsBox); + if (isPlayerCovered) { + CoveredHoleInfo info = new CoveredHoleInfo(topPos, holeBox); + coveredHoles.put(holeBox, info); + + if (chatNotifications.get() && notifiedHoles.add(holeBox)) { + int depth = (int) (holeBox.maxY - holeBox.minY); + info(String.format("Covered Hole found at %s (depth: %d)", topPos.toShortString(), depth)); } } } } - private int getStaircaseHeight(BlockPos pos) { - int height = 0; - while (isPassableBlock(pos.up(height)) && height < maxStaircaseHeight.get()) { - height++; + private boolean isLikelyPlayerCovered(BlockPos coverPos, Box hole) { + BlockState coverBlock = getBlockStateCached(coverPos); + if (coverBlock == null) return false; + + if (isCommonBuildingBlock(coverBlock)) return true; + + int matchingBlocks = 0; + BlockPos[] checkPositions = { coverPos.north(), coverPos.south(), coverPos.east(), coverPos.west() }; + + for (BlockPos pos : checkPositions) { + BlockState state = getBlockStateCached(pos); + if (state != null && state.getBlock() == coverBlock.getBlock()) matchingBlocks++; } - return height; + return matchingBlocks < 2; // If less than 2 adjacent blocks are the same, it's likely player-placed. } - private boolean isStaircaseSection(BlockPos pos, Direction dir) { - int height = getStaircaseHeight(pos); - if (height < minStaircaseHeight.get() || height > maxStaircaseHeight.get()) return false; - if (isPassableBlock(pos.down()) || isPassableBlock(pos.up(height))) return false; - Direction[] perpDirs = dir.getAxis() == Direction.Axis.X ? new Direction[]{Direction.NORTH, Direction.SOUTH} : new Direction[]{Direction.EAST, Direction.WEST}; - for (Direction perpDir : perpDirs) { - for (int i = 0; i < height; i++) { - if (isPassableBlock(pos.up(i).offset(perpDir))) { - return false; - } + private boolean isCommonBuildingBlock(BlockState state) { + if (state == null) return false; + String blockName = state.getBlock().getTranslationKey().toLowerCase(); + return blockName.contains("cobblestone") || + blockName.contains("stone_brick") || + blockName.contains("plank") || + blockName.contains("log") || + blockName.contains("wool") || + blockName.contains("concrete") || + blockName.contains("terracotta") || + blockName.contains("glass"); + } + + // Caching methods for block states and solidity checks + private boolean isSolidBlockCached(BlockPos pos) { + if (mc.world == null) return false; + + return solidBlockCache.computeIfAbsent(pos, p -> { + try { + BlockState state = mc.world.getBlockState(p); + return state != null && state.isSolidBlock(mc.world, p); + } catch (Exception e) { + return false; } - } - return true; + }); } - private boolean isPassableBlock(BlockPos pos) { - BlockState state = mc.world.getBlockState(pos); - if (airBlocks.get()) { - return state.isAir(); - } else { - VoxelShape shape = state.getCollisionShape(mc.world, pos); - return shape.isEmpty() || !VoxelShapes.fullCube().equals(shape); - } + private BlockState getBlockStateCached(BlockPos pos) { + if (mc.world == null) return null; + + return blockStateCache.computeIfAbsent(pos, p -> { + try { + return mc.world.getBlockState(p); + } catch (Exception e) { + return null; + } + }); } + // ========================================================================= + // ENUMS / INNER CLASSES + // ========================================================================= public enum DetectionMode { - ALL, - HOLES_AND_TUNNELS, - HOLES_AND_STAIRCASES, - TUNNELS_AND_STAIRCASES, - HOLES, - TUNNELS, - STAIRCASES, - HOLES_3X1_AND_TUNNELS + ALL, HOLES_AND_TUNNELS, HOLES_AND_STAIRCASES, TUNNELS_AND_STAIRCASES, + HOLES, TUNNELS, STAIRCASES, HOLES_3X1_AND_TUNNELS } - private class TChunk { + private static class TChunk { private final int x, z; public boolean marked; - - public TChunk(int x, int z) { - this.x = x; - this.z = z; - this.marked = true; - } - - public long getKey() { - return ChunkPos.toLong(x, z); - } + public TChunk(int x, int z) { this.x = x; this.z = z; this.marked = true; } + public long getKey() { return ChunkPos.toLong(x, z); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java b/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java new file mode 100644 index 00000000..0e400e8e --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java @@ -0,0 +1,496 @@ +package com.nnpg.glazed.modules.esp; + +import com.mojang.blaze3d.systems.RenderSystem; +import com.nnpg.glazed.GlazedAddon; +import meteordevelopment.meteorclient.events.render.Render2DEvent; +import meteordevelopment.meteorclient.events.render.Render3DEvent; +import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.renderer.ShapeMode; +import meteordevelopment.meteorclient.renderer.text.TextRenderer; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.utils.entity.ProjectileEntitySimulator; +import meteordevelopment.meteorclient.utils.render.NametagUtils; +import meteordevelopment.meteorclient.utils.render.color.SettingColor; +import meteordevelopment.orbit.EventHandler; +import net.minecraft.entity.Entity; +import net.minecraft.entity.projectile.thrown.EnderPearlEntity; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.ChunkSectionPos; +import org.joml.Vector3d; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class PearlLandingPredictor extends Module { + + private final SettingGroup sgGeneral = settings.getDefaultGroup(); + private final SettingGroup sgFilter = settings.createGroup("Player Filter"); + private final SettingGroup sgRender = settings.createGroup("Render"); + private final SettingGroup sgColors = settings.createGroup("Colors"); + + public enum ListMode { Whitelist, Blacklist } + + private final Setting showSelf = sgGeneral.add(new BoolSetting.Builder() + .name("show-self") + .description("Also track your own thrown ender pearls.") + .defaultValue(false) + .build() + ); + + private final Setting maxPerPlayer = sgGeneral.add(new IntSetting.Builder() + .name("max-per-player") + .description("Maximum number of predicted landing spots shown per player. 1 = most recent pearl only.") + .defaultValue(1) + .min(1) + .sliderMax(5) + .build() + ); + + private final Setting liveUpdate = sgGeneral.add(new BoolSetting.Builder() + .name("live-update") + .description("Re-simulate the landing spot every tick. Needed for chunk-load updates.") + .defaultValue(true) + .build() + ); + + private final Setting simulationSteps = sgGeneral.add(new IntSetting.Builder() + .name("simulation-steps") + .description("Maximum simulation steps per pearl. Higher = more accurate for long-range throws.") + .defaultValue(1000) + .min(100) + .sliderMax(5000) + .build() + ); + + private final Setting advancedRender = sgGeneral.add(new BoolSetting.Builder() + .name("advanced-render-settings") + .description("Show advanced render and color settings.") + .defaultValue(false) + .build() + ); + + private final Setting listMode = sgFilter.add(new EnumSetting.Builder() + .name("list-mode") + .description("Whitelist: only track players on the list. Blacklist: track everyone except listed players.") + .defaultValue(ListMode.Blacklist) + .build() + ); + + private final Setting> playerList = sgFilter.add(new StringListSetting.Builder() + .name("player-list") + .description("Player names used for the whitelist or blacklist.") + .defaultValue(new ArrayList<>()) + .build() + ); + + private final Setting seeThrough = sgRender.add(new BoolSetting.Builder() + .name("see-through") + .description("Render the landing spot box through walls.") + .defaultValue(true) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting shapeMode = sgRender.add(new EnumSetting.Builder() + .name("shape-mode") + .description("How the landing spot box is rendered.") + .defaultValue(ShapeMode.Both) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting boxSize = sgRender.add(new DoubleSetting.Builder() + .name("box-size") + .description("Size of the landing spot indicator box.") + .defaultValue(0.3) + .min(0.1) + .sliderMax(1.0) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting showName = sgRender.add(new BoolSetting.Builder() + .name("show-name") + .description("Display the pearl owner's name above the landing spot.") + .defaultValue(true) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting nameScale = sgRender.add(new DoubleSetting.Builder() + .name("name-scale") + .description("Font size of the player name label.") + .defaultValue(1.0) + .min(0.3) + .sliderMax(3.0) + .visible(() -> advancedRender.get() && showName.get()) + .build() + ); + + private final Setting showEstimatedLabel = sgRender.add(new BoolSetting.Builder() + .name("show-estimated-label") + .description("Prefix the name with '~' when the landing spot is estimated because the target chunk is not loaded.") + .defaultValue(true) + .visible(() -> advancedRender.get() && showName.get()) + .build() + ); + + private final Setting sideColor = sgColors.add(new ColorSetting.Builder() + .name("side-color") + .description("Fill color of the landing spot box.") + .defaultValue(new SettingColor(255, 50, 50, 60)) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting lineColor = sgColors.add(new ColorSetting.Builder() + .name("line-color") + .description("Outline color of the landing spot box.") + .defaultValue(new SettingColor(255, 50, 50, 200)) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting estimatedSideColor = sgColors.add(new ColorSetting.Builder() + .name("estimated-side-color") + .description("Fill color when the landing spot is estimated (target chunk not loaded).") + .defaultValue(new SettingColor(255, 200, 0, 40)) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting estimatedLineColor = sgColors.add(new ColorSetting.Builder() + .name("estimated-line-color") + .description("Outline color when the landing spot is estimated.") + .defaultValue(new SettingColor(255, 200, 0, 180)) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting unknownSideColor = sgColors.add(new ColorSetting.Builder() + .name("unknown-side-color") + .description("Fill color when the pearl owner is unknown.") + .defaultValue(new SettingColor(128, 128, 128, 40)) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting unknownLineColor = sgColors.add(new ColorSetting.Builder() + .name("unknown-line-color") + .description("Outline color when the pearl owner is unknown.") + .defaultValue(new SettingColor(128, 128, 128, 180)) + .visible(() -> advancedRender.get()) + .build() + ); + + private final Setting nameColor = sgColors.add(new ColorSetting.Builder() + .name("name-color") + .description("Color of the player name label.") + .defaultValue(new SettingColor(255, 255, 255, 255)) + .visible(() -> advancedRender.get() && showName.get()) + .build() + ); + + private static class PearlEntry { + final int entityId; + String ownerName; // Not final - can be updated if owner becomes known + UUID ownerUuid; // Can be null for unknown pearls + final Vector3d landingPos = new Vector3d(); + boolean isEstimated = false; + boolean isUnknown = false; // True if pearl owner is unknown + long timestamp; + + PearlEntry(int entityId, String ownerName, UUID ownerUuid) { + this.entityId = entityId; + this.ownerName = ownerName; + this.ownerUuid = ownerUuid; + this.timestamp = System.currentTimeMillis(); + } + } + + // FIX #2: Use ConcurrentHashMap for thread safety + private final Map> trackedPearls = new ConcurrentHashMap<>(); + private final Deque unknownPearls = new ArrayDeque<>(); // Pearls with unknown owner + private final Set knownPearlIds = ConcurrentHashMap.newKeySet(); + private final ProjectileEntitySimulator simulator = new ProjectileEntitySimulator(); + + // FIX #8: Reusable Vector3d to avoid allocation every frame + private final Vector3d reusableScreenPos = new Vector3d(); + + public PearlLandingPredictor() { + super(GlazedAddon.esp, "pearl-landing-predictor", + "Predicts and displays the landing spot of ender pearls thrown by other players."); + } + + @Override + public void onActivate() { + trackedPearls.clear(); + unknownPearls.clear(); + knownPearlIds.clear(); + } + + @Override + public void onDeactivate() { + trackedPearls.clear(); + unknownPearls.clear(); + knownPearlIds.clear(); + } + + @EventHandler + private void onTick(TickEvent.Pre event) { + if (mc.player == null || mc.world == null) return; + + Set activeIds = new HashSet<>(); + Set activePlayers = new HashSet<>(); + + for (Entity entity : mc.world.getEntities()) { + if (!(entity instanceof EnderPearlEntity pearl)) continue; + if (!showSelf.get() && pearl.getOwner() == mc.player) continue; + + // FIX: Handle pearls with unknown owner + UUID ownerUuid = pearl.getOwner() != null ? pearl.getOwner().getUuid() : null; + String ownerName = pearl.getOwner() != null ? pearl.getOwner().getName().getString() : "Unknown"; + + // Check filter only if owner is known + if (pearl.getOwner() != null && !isAllowed(ownerName)) continue; + + if (pearl.getOwner() != null) { + activePlayers.add(ownerUuid); + } + + activeIds.add(pearl.getId()); + + if (!knownPearlIds.contains(pearl.getId())) { + knownPearlIds.add(pearl.getId()); + + PearlEntry entry = new PearlEntry(pearl.getId(), ownerName, ownerUuid); + entry.isUnknown = (pearl.getOwner() == null); + simulateLanding(pearl, entry); + + if (ownerUuid != null) { + Deque deque = trackedPearls.computeIfAbsent(ownerUuid, k -> new ArrayDeque<>()); + synchronized (deque) { + deque.addFirst(entry); + while (deque.size() > maxPerPlayer.get()) deque.removeLast(); + } + } else { + // Track unknown pearls separately + synchronized (unknownPearls) { + unknownPearls.addFirst(entry); + while (unknownPearls.size() > maxPerPlayer.get()) unknownPearls.removeLast(); + } + } + + } else if (liveUpdate.get()) { + // FIX #5: Better null handling with re-creation of missing entries + PearlEntry entryToUpdate = null; + + if (ownerUuid != null) { + Deque deque = trackedPearls.get(ownerUuid); + if (deque != null) { + synchronized (deque) { + for (PearlEntry entry : deque) { + if (entry.entityId == pearl.getId()) { + entryToUpdate = entry; + break; + } + } + } + } + } else { + synchronized (unknownPearls) { + for (PearlEntry entry : unknownPearls) { + if (entry.entityId == pearl.getId()) { + entryToUpdate = entry; + break; + } + } + } + } + + if (entryToUpdate != null) { + // Update owner info if it was previously unknown + if (entryToUpdate.isUnknown && pearl.getOwner() != null) { + entryToUpdate.ownerName = ownerName; + entryToUpdate.ownerUuid = ownerUuid; + entryToUpdate.isUnknown = false; + } + simulateLanding(pearl, entryToUpdate); + } + } + } + + // FIX #1: Clean up stale entries from trackedPearls + knownPearlIds.removeIf(id -> !activeIds.contains(id)); + + // Clean up unknown pearls that are no longer active + synchronized (unknownPearls) { + unknownPearls.removeIf(entry -> !activeIds.contains(entry.entityId)); + } + + // FIX #4: Clean up entries for disconnected players + // Remove tracked pearls for players no longer in the world + trackedPearls.keySet().removeIf(uuid -> !activePlayers.contains(uuid)); + } + + private boolean isAllowed(String name) { + boolean inList = playerList.get().stream().anyMatch(n -> n.equalsIgnoreCase(name)); + return switch (listMode.get()) { + case Whitelist -> inList; + case Blacklist -> !inList; + }; + } + + private void simulateLanding(EnderPearlEntity pearl, PearlEntry entry) { + if (!simulator.set(pearl, false)) return; + + int maxSteps = simulationSteps.get(); + boolean hitSomething = false; + boolean encounteredUnloadedChunk = false; + + for (int i = 0; i < maxSteps; i++) { + int chunkX = ChunkSectionPos.getSectionCoord(simulator.pos.x); + int chunkZ = ChunkSectionPos.getSectionCoord(simulator.pos.z); + + if (!mc.world.getChunkManager().isChunkLoaded(chunkX, chunkZ)) { + encounteredUnloadedChunk = true; + entry.landingPos.set(extrapolateInAir( + simulator.pos.x, simulator.pos.y, simulator.pos.z, + pearl.getVelocity().x, pearl.getVelocity().y, pearl.getVelocity().z + )); + entry.isEstimated = true; + return; + } + + HitResult result = simulator.tick(); + + if (result != null) { + entry.landingPos.set(simulator.pos); + entry.isEstimated = false; + hitSomething = true; + break; + } + } + + if (!hitSomething && !encounteredUnloadedChunk) { + entry.landingPos.set(simulator.pos); + entry.isEstimated = true; + } + } + + private Vector3d extrapolateInAir(double px, double py, double pz, + double vx, double vy, double vz) { + final double gravity = 0.03; + final double airDrag = 0.99; + + for (int i = 0; i < 2000; i++) { + vy -= gravity; + vx *= airDrag; + vy *= airDrag; + vz *= airDrag; + px += vx; + py += vy; + pz += vz; + if (py < (mc.world != null ? mc.world.getBottomY() : -64)) break; + } + + return new Vector3d(px, py, pz); + } + + @EventHandler + private void onRender3D(Render3DEvent event) { + if (mc.player == null || mc.world == null) return; + if (trackedPearls.isEmpty() && unknownPearls.isEmpty()) return; + + double half = boxSize.get() / 2.0; + + // FIX #3: Wrap depth test in try-finally for safety + try { + if (seeThrough.get()) RenderSystem.disableDepthTest(); + + // Render known player pearls + for (Deque deque : trackedPearls.values()) { + synchronized (deque) { + for (PearlEntry entry : deque) { // Pass event.renderer to renderPearlEntry + renderPearlEntry(event.renderer, entry, half); + } + } + } + + // Render unknown pearls + synchronized (unknownPearls) { + for (PearlEntry entry : unknownPearls) { // Pass event.renderer to renderPearlEntry + renderPearlEntry(event.renderer, entry, half); + } + } + } finally { + if (seeThrough.get()) RenderSystem.enableDepthTest(); + } + } + + private void renderPearlEntry(meteordevelopment.meteorclient.renderer.Renderer3D r, PearlEntry entry, double half) { + Vector3d pos = entry.landingPos; + SettingColor sc, lc; + + if (entry.isUnknown) { + sc = unknownSideColor.get(); + lc = unknownLineColor.get(); + } else if (entry.isEstimated) { + sc = estimatedSideColor.get(); + lc = estimatedLineColor.get(); + } else { + sc = sideColor.get(); + lc = lineColor.get(); + } + + // Render the box + r.box(pos.x - half, pos.y, pos.z - half, + pos.x + half, pos.y + boxSize.get(), pos.z + half, + sc, lc, shapeMode.get(), 0); + } + + @EventHandler + private void onRender2D(Render2DEvent event) { + if (!showName.get()) return; + if (mc.player == null || mc.world == null) return; + if (trackedPearls.isEmpty() && unknownPearls.isEmpty()) return; + + double half = boxSize.get() / 2.0; + + // Render known player pearl names + for (Deque deque : trackedPearls.values()) { + synchronized (deque) { + for (PearlEntry entry : deque) { + renderPearlName(entry, half); + } + } + } + + // Render unknown pearl names + synchronized (unknownPearls) { + for (PearlEntry entry : unknownPearls) { + renderPearlName(entry, half); + } + } + } + + private void renderPearlName(PearlEntry entry, double half) { + // FIX #8: Reuse Vector3d instead of creating new one + Vector3d pos = entry.landingPos; + reusableScreenPos.set(pos.x, pos.y + half + 0.15, pos.z); + + if (!NametagUtils.to2D(reusableScreenPos, nameScale.get())) return; + + String label = entry.ownerName; + if (showEstimatedLabel.get() && entry.isEstimated) label = "~" + label; + if (entry.isUnknown) label = "?" + label; + + NametagUtils.begin(reusableScreenPos); + TextRenderer.get().begin(nameScale.get(), false, true); + double textWidth = TextRenderer.get().getWidth(label); + TextRenderer.get().render(label, -textWidth / 2.0, 0, nameColor.get(), true); + TextRenderer.get().end(); + NametagUtils.end(); + } +} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/main/AutoBlazeRodOrder.java b/src/main/java/com/nnpg/glazed/modules/main/AutoBlazeRodOrder.java deleted file mode 100644 index d6bdc52a..00000000 --- a/src/main/java/com/nnpg/glazed/modules/main/AutoBlazeRodOrder.java +++ /dev/null @@ -1,514 +0,0 @@ -package com.nnpg.glazed.modules.main; - -import com.nnpg.glazed.GlazedAddon; -import meteordevelopment.meteorclient.events.world.TickEvent; -import meteordevelopment.meteorclient.settings.*; -import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.meteorclient.utils.player.ChatUtils; -import meteordevelopment.orbit.EventHandler; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.item.tooltip.TooltipType; -import net.minecraft.screen.ScreenHandler; -import net.minecraft.screen.slot.Slot; -import net.minecraft.screen.slot.SlotActionType; -import net.minecraft.text.Text; - -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class AutoBlazeRodOrder extends Module { - private final MinecraftClient mc = MinecraftClient.getInstance(); - - private enum Stage {NONE, SHOP, SHOP_CATEGORY, SHOP_ITEM, SHOP_GLASS_PANE, SHOP_BUY_ONE, SHOP_CHECK_FULL, SHOP_EXIT, WAIT, ORDERS, ORDERS_SELECT, ORDERS_EXIT, ORDERS_CONFIRM, ORDERS_FINAL_EXIT, CYCLE_PAUSE} - - private Stage stage = Stage.NONE; - private long stage_start = 0; - private static final long WAIT_TIME_MS = 50; - private int shell_move_index = 0; - private long last_shell_move_time = 0; - private int exit_count = 0; - private int final_exit_count = 0; - private long final_exit_start = 0; - - private final SettingGroup sg_general = settings.getDefaultGroup(); - private final SettingGroup sg_blacklist = settings.createGroup("Blacklist"); - - private final Setting min_price = sg_general.add(new StringSetting.Builder() - .name("min-price") - .description("Minimum price to deliver blaze rods for (supports K, M, B suffixes).") - .defaultValue("350.00001") - .build() - ); - - private final Setting notifications = sg_general.add(new BoolSetting.Builder() - .name("notifications") - .description("Show detailed price checking notifications.") - .defaultValue(true) - .build() - ); - - private final Setting speed_mode = sg_general.add(new BoolSetting.Builder() - .name("speed-mode") - .description("Maximum speed mode - removes most delays (may be unstable).") - .defaultValue(true) - .build() - ); - - private final Setting> blacklisted_players = sg_blacklist.add(new StringListSetting.Builder() - .name("blacklisted-players") - .description("Players whose orders will be ignored.") - .defaultValue(List.of()) - .build() - ); - - public AutoBlazeRodOrder() { - super(GlazedAddon.CATEGORY, "auto-blaze-rod-order", "Automatically buys and sells blaze rods in orders for profit (FAST MODE)"); - } - - @Override - public void onActivate() { - double parsed_price = parse_price(min_price.get()); - if (parsed_price == -1.0) { - if (notifications.get()) { - ChatUtils.error("Invalid minimum price format!"); - } - toggle(); - return; - } - - stage = Stage.SHOP; - stage_start = System.currentTimeMillis(); - shell_move_index = 0; - last_shell_move_time = 0; - exit_count = 0; - final_exit_count = 0; - - if (notifications.get()) { - info("πŸš€ FAST AutoBlazeRod Order activated! Minimum: %s", min_price.get()); - } - } - - @Override - public void onDeactivate() { - stage = Stage.NONE; - } - - @EventHandler - private void onTick(TickEvent.Post event) { - if (mc.player == null || mc.world == null) return; - long now = System.currentTimeMillis(); - - switch (stage) { - case SHOP -> { - ChatUtils.sendPlayerMsg("/shop"); - stage = Stage.SHOP_CATEGORY; - stage_start = now; - } - case SHOP_CATEGORY -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_shop_category(stack)) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_ITEM; - stage_start = now; - return; - } - } - if (now - stage_start > (speed_mode.get() ? 1000 : 3000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case SHOP_ITEM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_blaze_rod(stack) && slot.inventory != mc.player.getInventory()) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_GLASS_PANE; - stage_start = now; - return; - } - } - if (now - stage_start > (speed_mode.get() ? 300 : 1000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case SHOP_GLASS_PANE -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_glass_pane(stack) && stack.getCount() == 64) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_BUY_ONE; - stage_start = now; - return; - } - } - - if (now - stage_start > (speed_mode.get() ? 300 : 1000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case SHOP_BUY_ONE -> { - long wait_delay = speed_mode.get() ? 500 : 1000; - if (now - stage_start >= wait_delay) { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_green_glass(stack) && stack.getCount() == 1) { - int max_clicks = speed_mode.get() ? 50 : 30; - for (int i = 0; i < max_clicks; i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - if (is_inventory_full()) break; - } - stage = Stage.SHOP_CHECK_FULL; - stage_start = now; - return; - } - } - - if (now - stage_start > (speed_mode.get() ? 2000 : 3000)) { - stage = Stage.SHOP_GLASS_PANE; - stage_start = now; - } - } - } - } - case SHOP_CHECK_FULL -> { - mc.player.closeHandledScreen(); - stage = Stage.SHOP_EXIT; - stage_start = now; - } - case SHOP_EXIT -> { - if (mc.currentScreen == null) { - stage = Stage.WAIT; - stage_start = now; - } - if (now - stage_start > (speed_mode.get() ? 1000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - case WAIT -> { - long wait_time = speed_mode.get() ? 25 : WAIT_TIME_MS; - if (now - stage_start >= wait_time) { - ChatUtils.sendPlayerMsg("/orders blaze rod"); - stage = Stage.ORDERS; - stage_start = now; - } - } - case ORDERS -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_blaze_rod(stack)) { - if (is_blacklisted(get_order_player_name(stack))) continue; - double order_price = get_order_price(stack); - double min_price_value = parse_price(min_price.get()); - - if (order_price > 1500) { - continue; - } - - if (order_price >= min_price_value) { - if (notifications.get()) { - info("βœ… Found blaze rod order: %s", format_price(order_price)); - } - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.ORDERS_SELECT; - stage_start = now; - shell_move_index = 0; - last_shell_move_time = 0; - return; - } - } - } - if (now - stage_start > (speed_mode.get() ? 2000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case ORDERS_SELECT -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - if (shell_move_index >= 36) { - mc.player.closeHandledScreen(); - stage = Stage.ORDERS_CONFIRM; - stage_start = now; - shell_move_index = 0; - return; - } - - long move_delay = speed_mode.get() ? 10 : 100; - if (now - last_shell_move_time >= move_delay) { - int batch_size = speed_mode.get() ? 3 : 1; - - for (int batch = 0; batch < batch_size && shell_move_index < 36; batch++) { - ItemStack stack = mc.player.getInventory().getStack(shell_move_index); - if (is_blaze_rod(stack)) { - int player_slot_id = -1; - for (Slot slot : handler.slots) { - if (slot.inventory == mc.player.getInventory() && slot.getIndex() == shell_move_index) { - player_slot_id = slot.id; - break; - } - } - - if (player_slot_id != -1) { - mc.interactionManager.clickSlot(handler.syncId, player_slot_id, 0, SlotActionType.QUICK_MOVE, mc.player); - } - } - shell_move_index++; - } - last_shell_move_time = now; - } - } - } - case ORDERS_EXIT -> { - if (mc.currentScreen == null) { - exit_count++; - if (exit_count < 2) { - mc.player.closeHandledScreen(); - stage_start = now; - } else { - exit_count = 0; - stage = Stage.ORDERS_CONFIRM; - stage_start = now; - } - } - } - case ORDERS_CONFIRM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_green_glass(stack)) { - for (int i = 0; i < (speed_mode.get() ? 15 : 5); i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - stage = Stage.ORDERS_FINAL_EXIT; - stage_start = now; - final_exit_count = 0; - final_exit_start = now; - return; - } - } - if (now - stage_start > (speed_mode.get() ? 2000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case ORDERS_FINAL_EXIT -> { - long exit_delay = speed_mode.get() ? 50 : 200; - - if (final_exit_count == 0) { - if (System.currentTimeMillis() - final_exit_start >= exit_delay) { - mc.player.closeHandledScreen(); - final_exit_count++; - final_exit_start = System.currentTimeMillis(); - } - } else if (final_exit_count == 1) { - if (System.currentTimeMillis() - final_exit_start >= exit_delay) { - mc.player.closeHandledScreen(); - final_exit_count++; - final_exit_start = System.currentTimeMillis(); - } - } else { - final_exit_count = 0; - stage = Stage.CYCLE_PAUSE; - stage_start = System.currentTimeMillis(); - } - } - case CYCLE_PAUSE -> { - long cycle_wait = speed_mode.get() ? 25 : WAIT_TIME_MS; - if (now - stage_start >= cycle_wait) { - stage = Stage.SHOP; - stage_start = now; - } - } - case NONE -> { - } - } - } - - private boolean is_blacklisted(String playerName) { - if (playerName == null || blacklisted_players.get().isEmpty()) return false; - return blacklisted_players.get().stream().anyMatch(p -> p.equalsIgnoreCase(playerName)); - } - - private String get_order_player_name(ItemStack stack) { - if (stack.isEmpty()) return null; - Item.TooltipContext ctx = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(ctx, mc.player, TooltipType.BASIC); - Pattern[] patterns = { - Pattern.compile("(?i)player\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)from\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)by\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)seller\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)owner\\s*:\\s*([a-zA-Z0-9_]+)") - }; - for (Text line : tooltip) { - String text = line.getString(); - for (Pattern p : patterns) { - Matcher m = p.matcher(text); - if (m.find()) { - String name = m.group(1); - if (name.length() >= 3 && name.length() <= 16) return name; - } - } - } - return null; - } - - private double get_order_price(ItemStack stack) { - if (stack.isEmpty()) { - return -1.0; - } - - Item.TooltipContext tooltip_context = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(tooltip_context, mc.player, TooltipType.BASIC); - - return parse_tooltip_price(tooltip); - } - - private double parse_tooltip_price(List tooltip) { - if (tooltip == null || tooltip.isEmpty()) { - return -1.0; - } - - Pattern[] price_patterns = { - Pattern.compile("\\$([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)price\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)pay\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)reward\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("([\\d,]+(?:\\.[\\d]+)?)([kmb])?\\s*coins?", Pattern.CASE_INSENSITIVE), - Pattern.compile("\\b([\\d,]+(?:\\.[\\d]+)?)([kmb])\\b", Pattern.CASE_INSENSITIVE) - }; - - for (Text line : tooltip) { - String text = line.getString(); - - for (Pattern pattern : price_patterns) { - Matcher matcher = pattern.matcher(text); - if (matcher.find()) { - String number_str = matcher.group(1).replace(",", ""); - String suffix = ""; - if (matcher.groupCount() >= 2 && matcher.group(2) != null) { - suffix = matcher.group(2).toLowerCase(); - } - - try { - double base_price = Double.parseDouble(number_str); - double multiplier = 1.0; - - switch (suffix) { - case "k" -> multiplier = 1_000.0; - case "m" -> multiplier = 1_000_000.0; - case "b" -> multiplier = 1_000_000_000.0; - } - - return base_price * multiplier; - } catch (NumberFormatException e) { - } - } - } - } - - return -1.0; - } - - private double parse_price(String price_str) { - if (price_str == null || price_str.isEmpty()) { - return -1.0; - } - - String cleaned = price_str.trim().toLowerCase().replace(",", ""); - double multiplier = 1.0; - - if (cleaned.endsWith("b")) { - multiplier = 1_000_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("m")) { - multiplier = 1_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("k")) { - multiplier = 1_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } - - try { - return Double.parseDouble(cleaned) * multiplier; - } catch (NumberFormatException e) { - return -1.0; - } - } - - private String format_price(double price) { - if (price >= 1_000_000_000) { - return String.format("$%.1fB", price / 1_000_000_000.0); - } else if (price >= 1_000_000) { - return String.format("$%.1fM", price / 1_000_000.0); - } else if (price >= 1_000) { - return String.format("$%.1fK", price / 1_000.0); - } else { - return String.format("$%.0f", price); - } - } - - private boolean is_shop_category(ItemStack stack) { - if (stack.isEmpty()) return false; - String name = stack.getName().getString().toLowerCase(Locale.ROOT); - return stack.getItem() == Items.NETHERRACK || name.contains("nether"); - } - - private boolean is_blaze_rod(ItemStack stack) { - if (stack.isEmpty()) return false; - return stack.getItem() == Items.BLAZE_ROD; - } - - private boolean is_glass_pane(ItemStack stack) { - String item_name = stack.getItem().getName().getString().toLowerCase(); - return item_name.contains("glass") && item_name.contains("pane"); - } - - private boolean is_green_glass(ItemStack stack) { - return stack.getItem() == Items.LIME_STAINED_GLASS_PANE || stack.getItem() == Items.GREEN_STAINED_GLASS_PANE; - } - - private boolean is_inventory_full() { - for (int i = 9; i <= 35; i++) { - ItemStack stack = mc.player.getInventory().getStack(i); - if (stack.isEmpty()) return false; - } - return true; - } -} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/main/AutoShellOrder.java b/src/main/java/com/nnpg/glazed/modules/main/AutoShellOrder.java deleted file mode 100644 index e31e524f..00000000 --- a/src/main/java/com/nnpg/glazed/modules/main/AutoShellOrder.java +++ /dev/null @@ -1,276 +0,0 @@ -package com.nnpg.glazed.modules.main; - -import com.nnpg.glazed.GlazedAddon; -import meteordevelopment.meteorclient.events.world.TickEvent; -import meteordevelopment.meteorclient.settings.*; -import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.meteorclient.utils.player.ChatUtils; -import meteordevelopment.orbit.EventHandler; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.item.tooltip.TooltipType; -import net.minecraft.screen.ScreenHandler; -import net.minecraft.screen.slot.Slot; -import net.minecraft.screen.slot.SlotActionType; -import net.minecraft.text.Text; - -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class AutoShellOrder extends Module { - private final MinecraftClient mc = MinecraftClient.getInstance(); - - private enum Stage {NONE, SHOP, SHOP_END, SHOP_SHELL, SHOP_CONFIRM, SHOP_CHECK_FULL, SHOP_EXIT, WAIT, ORDERS, ORDERS_SELECT, ORDERS_EXIT, ORDERS_CONFIRM, ORDERS_FINAL_EXIT, CYCLE_PAUSE, TARGET_ORDERS} - - private Stage stage = Stage.NONE; - private long stageStart = 0; - private static final long WAIT_TIME_MS = 50; - private int shellMoveIndex = 0; - private long lastShellMoveTime = 0; - private int exitCount = 0; - private int finalExitCount = 0; - private long finalExitStart = 0; - - private String targetPlayer = ""; - private boolean isTargetingActive = false; - - private final SettingGroup sgGeneral = settings.getDefaultGroup(); - private final SettingGroup sgTargeting = settings.createGroup("Player Targeting"); - - private final Setting minPrice = sgGeneral.add(new StringSetting.Builder() - .name("min-price") - .description("Minimum price to deliver shells for (supports K, M, B suffixes).") - .defaultValue("50") - .build() - ); - - private final Setting notifications = sgGeneral.add(new BoolSetting.Builder() - .name("notifications") - .description("Show detailed price checking notifications.") - .defaultValue(true) - .build() - ); - - private final Setting speedMode = sgGeneral.add(new BoolSetting.Builder() - .name("speed-mode") - .description("Maximum speed mode - removes most delays (may be unstable).") - .defaultValue(true) - .build() - ); - - private final Setting enableTargeting = sgTargeting.add(new BoolSetting.Builder() - .name("enable-targeting") - .description("Enable targeting a specific player (ignores minimum price).") - .defaultValue(false) - .build() - ); - - private final Setting targetPlayerName = sgTargeting.add(new StringSetting.Builder() - .name("target-player") - .description("Specific player name to target for orders.") - .defaultValue("") - .visible(() -> enableTargeting.get()) - .build() - ); - - private final Setting targetOnlyMode = sgTargeting.add(new BoolSetting.Builder() - .name("target-only-mode") - .description("Only look for orders from the targeted player, ignore all others.") - .defaultValue(false) - .visible(() -> enableTargeting.get()) - .build() - ); - - private final Setting> blacklistedPlayers = sgTargeting.add(new StringListSetting.Builder() - .name("blacklisted-players") - .description("Players whose orders will be ignored.") - .defaultValue(List.of()) - .build() - ); - - public AutoShellOrder() { - super(GlazedAddon.CATEGORY, "auto-shell-order", "Automatically buys shulker shells and sells them in orders with player targeting"); - } - - @Override - public void onActivate() { - double parsedPrice = parsePrice(minPrice.get()); - if (parsedPrice == -1.0 && !enableTargeting.get()) { - if (notifications.get()) ChatUtils.error("Invalid minimum price format!"); - toggle(); - return; - } - - updateTargetPlayer(); - stage = Stage.SHOP; - stageStart = System.currentTimeMillis(); - shellMoveIndex = 0; - lastShellMoveTime = 0; - exitCount = 0; - finalExitCount = 0; - - if (notifications.get()) { - String modeInfo = isTargetingActive ? String.format(" | Targeting: %s", targetPlayer) : ""; - info("πŸš€ FAST AutoShellOrder activated! Minimum: %s%s", minPrice.get(), modeInfo); - } - } - - @Override - public void onDeactivate() { - stage = Stage.NONE; - } - - private void updateTargetPlayer() { - targetPlayer = ""; - isTargetingActive = false; - if (enableTargeting.get() && !targetPlayerName.get().trim().isEmpty()) { - targetPlayer = targetPlayerName.get().trim(); - isTargetingActive = true; - if (notifications.get()) info("🎯 Targeting enabled for player: %s", targetPlayer); - } - } - - @EventHandler - private void onTick(TickEvent.Post event) { - if (mc.player == null || mc.world == null) return; - long now = System.currentTimeMillis(); - - switch (stage) { - case TARGET_ORDERS -> { - ChatUtils.sendPlayerMsg("/orders " + targetPlayer); - stage = Stage.ORDERS; - stageStart = now; - if (notifications.get()) info("πŸ” Checking orders for: %s", targetPlayer); - } - case SHOP -> { - ChatUtils.sendPlayerMsg("/shop"); - stage = Stage.SHOP_END; - stageStart = now; - } - case SHOP_END -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isEndStone(stack)) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_SHELL; - stageStart = now; - return; - } - } - } - } - case SHOP_SHELL -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isShell(stack)) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_CONFIRM; - stageStart = now; - return; - } - } - } - } - case ORDERS -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isShell(stack)) { - if (isBlacklisted(getOrderPlayerName(stack))) continue; - double orderPrice = getOrderPrice(stack); - if (orderPrice >= parsePrice(minPrice.get())) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.ORDERS_SELECT; - stageStart = now; - if (notifications.get()) info("βœ… Found shell order: %s", formatPrice(orderPrice)); - return; - } - } - } - } - } - default -> {} - } - } - - private boolean isBlacklisted(String playerName) { - if (playerName == null || blacklistedPlayers.get().isEmpty()) return false; - return blacklistedPlayers.get().stream().anyMatch(p -> p.equalsIgnoreCase(playerName)); - } - - private String getOrderPlayerName(ItemStack stack) { - if (stack.isEmpty()) return null; - List tooltip = stack.getTooltip(Item.TooltipContext.create(mc.world), mc.player, TooltipType.BASIC); - Pattern[] patterns = { - Pattern.compile("(?i)player\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)from\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)by\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)seller\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)owner\\s*:\\s*([a-zA-Z0-9_]+)") - }; - for (Text line : tooltip) { - String text = line.getString(); - for (Pattern p : patterns) { - Matcher m = p.matcher(text); - if (m.find()) { - String name = m.group(1); - if (name.length() >= 3 && name.length() <= 16) return name; - } - } - } - return null; - } - - private boolean isShell(ItemStack stack) { - return stack.getItem() == Items.SHULKER_SHELL; - } - - private boolean isEndStone(ItemStack stack) { - return stack.getItem() == Items.END_STONE; - } - - private double getOrderPrice(ItemStack stack) { - if (stack.isEmpty()) return -1.0; - Item.TooltipContext tooltipContext = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(tooltipContext, mc.player, TooltipType.BASIC); - return parseTooltipPrice(tooltip); - } - - private double parseTooltipPrice(List tooltip) { - if (tooltip == null || tooltip.isEmpty()) return -1.0; - Pattern pattern = Pattern.compile("\\$([\\d,]+)"); - for (Text line : tooltip) { - Matcher matcher = pattern.matcher(line.getString()); - if (matcher.find()) { - try { - return Double.parseDouble(matcher.group(1).replace(",", "")); - } catch (NumberFormatException ignored) {} - } - } - return -1.0; - } - - private double parsePrice(String priceStr) { - try { return Double.parseDouble(priceStr.replace(",", "")); } - catch (NumberFormatException e) { return -1.0; } - } - - private String formatPrice(double price) { - if (price >= 1_000_000) return String.format("$%.1fM", price / 1_000_000); - if (price >= 1_000) return String.format("$%.1fK", price / 1_000); - return String.format("$%.0f", price); - } - - public void info(String message, Object... args) { - if (notifications.get()) ChatUtils.info(String.format(message, args)); - } -} diff --git a/src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java b/src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java new file mode 100644 index 00000000..e9b40249 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java @@ -0,0 +1,952 @@ +package com.nnpg.glazed.modules.main; + +import com.nnpg.glazed.GlazedAddon; +import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.utils.player.ChatUtils; +import meteordevelopment.orbit.EventHandler; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.item.tooltip.TooltipType; +import net.minecraft.screen.ScreenHandler; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.text.Text; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AutoShopOrder extends Module { + private final MinecraftClient mc = MinecraftClient.getInstance(); + + // ==================== CATEGORIES & ITEMS ==================== + public enum ShopCategory { END, NETHER, GEAR, FOOD } + + public enum PriceMode { AUTO, CUSTOM } + + public enum EndItem { + ENDER_CHEST, ENDER_PEARL, END_STONE, DRAGON_BREATH, END_ROD, + CHORUS_FRUIT, POPPED_CHORUS_FRUIT, SHULKER_SHELL, SHULKER_BOX + } + + public enum NetherItem { + BLAZE_ROD, NETHER_WART, GLOWSTONE_DUST, MAGMA_CREAM, GHAST_TEAR, + NETHER_QUARTZ, SOUL_SAND, MAGMA_BLOCK, CRYING_OBSIDIAN + } + + public enum GearItem { + OBSIDIAN, END_CRYSTAL, RESPAWN_ANCHOR, GLOWSTONE, TOTEM_OF_UNDYING, + ENDER_PEARL, GOLDEN_APPLE, EXPERIENCE_BOTTLE, TIPPED_ARROW + } + + public enum FoodItem { + POTATO, SWEET_BERRIES, MELON_SLICE, CARROT, APPLE, + COOKED_CHICKEN, COOKED_BEEF, GOLDEN_CARROT, GOLDEN_APPLE + } + + // ==================== SHOP PRICES TABLE ==================== + // Prices per item from /shop (per unit, not per stack) + private int getShopPrice() { + return switch (category.get()) { + case END -> switch (endItem.get()) { + case ENDER_CHEST -> 2500; + case ENDER_PEARL -> 75; + case END_STONE -> 8; // $128 for 16 = $8 each + case DRAGON_BREATH -> 1000; + case END_ROD -> 100; + case CHORUS_FRUIT -> 108; + case POPPED_CHORUS_FRUIT -> 24; + case SHULKER_SHELL -> 350; + case SHULKER_BOX -> 800; + }; + case NETHER -> switch (netherItem.get()) { + case BLAZE_ROD -> 150; + case NETHER_WART -> 96; + case GLOWSTONE_DUST -> 15; + case MAGMA_CREAM -> 96; + case GHAST_TEAR -> 350; + case NETHER_QUARTZ -> 30; + case SOUL_SAND -> 50; + case MAGMA_BLOCK -> 35; + case CRYING_OBSIDIAN -> 150; + }; + case GEAR -> switch (gearItem.get()) { + case OBSIDIAN -> 100; + case END_CRYSTAL -> 350; + case RESPAWN_ANCHOR -> 1000; + case GLOWSTONE -> 100; + case TOTEM_OF_UNDYING -> 1500; + case ENDER_PEARL -> 75; + case GOLDEN_APPLE -> 250; + case EXPERIENCE_BOTTLE -> 100; + case TIPPED_ARROW -> 500; + }; + case FOOD -> switch (foodItem.get()) { + case POTATO -> 96; + case SWEET_BERRIES -> 50; + case MELON_SLICE -> 36; + case CARROT -> 96; + case APPLE -> 25; + case COOKED_CHICKEN -> 48; + case COOKED_BEEF -> 35; + case GOLDEN_CARROT -> 120; + case GOLDEN_APPLE -> 250; + }; + }; + } + + // Get default min price (shop price + $1) + private int getDefaultMinPrice() { + return getShopPrice() + 1; + } + + // ==================== STAGES ==================== + private enum Stage { + NONE, + SHOP_OPEN, + SHOP_CATEGORY, + SHOP_ITEM, + SHOP_SET_STACK, + SHOP_WAIT_FOR_BUY_SCREEN, + SHOP_BUY_SPAM, + SHOP_EXIT, + WAIT, + ORDERS_OPEN, + ORDERS_SELECT, + ORDERS_VERIFY_EMPTY, + ORDERS_CONFIRM, + ORDERS_FINAL_EXIT, + CYCLE_PAUSE + } + + private Stage stage = Stage.NONE; + private long stage_start = 0; + private static final long WAIT_TIME_MS = 50; + private int final_exit_count = 0; + private long final_exit_start = 0; + private int confirm_slot_id = -1; + private int buy_screen_retry_count = 0; + private static final int MAX_BUY_RETRIES = 20; + + // Anti-stuck system + private long last_action_time = 0; + private static final long STUCK_TIMEOUT_MS = 5000; // 5 seconds + + // Buy spam tracking + private long buy_spam_start_time = 0; + private static final long BUY_SPAM_TIMEOUT_MS = 5000; + + // Move attempts + private int move_pass_count = 0; + private static final int MAX_MOVE_PASSES = 5; + + // ==================== SETTINGS ==================== + private final SettingGroup sg_general = settings.getDefaultGroup(); + private final SettingGroup sg_blacklist = settings.createGroup("Blacklist"); + + private final Setting category = sg_general.add(new EnumSetting.Builder() + .name("category") + .description("Select the shop category.") + .defaultValue(ShopCategory.FOOD) + .build() + ); + + private final Setting endItem = sg_general.add(new EnumSetting.Builder() + .name("end-item") + .description("Select item from END category.") + .defaultValue(EndItem.SHULKER_SHELL) + .visible(() -> category.get() == ShopCategory.END) + .build() + ); + + private final Setting netherItem = sg_general.add(new EnumSetting.Builder() + .name("nether-item") + .description("Select item from NETHER category.") + .defaultValue(NetherItem.BLAZE_ROD) + .visible(() -> category.get() == ShopCategory.NETHER) + .build() + ); + + private final Setting gearItem = sg_general.add(new EnumSetting.Builder() + .name("gear-item") + .description("Select item from GEAR category.") + .defaultValue(GearItem.TOTEM_OF_UNDYING) + .visible(() -> category.get() == ShopCategory.GEAR) + .build() + ); + + private final Setting foodItem = sg_general.add(new EnumSetting.Builder() + .name("food-item") + .description("Select item from FOOD category.") + .defaultValue(FoodItem.COOKED_CHICKEN) + .visible(() -> category.get() == ShopCategory.FOOD) + .build() + ); + + private final Setting priceMode = sg_general.add(new EnumSetting.Builder() + .name("min-price") + .description("Auto uses shop_price + $1, Custom lets you set your own.") + .defaultValue(PriceMode.AUTO) + .build() + ); + + private final Setting customPrice = sg_general.add(new StringSetting.Builder() + .name("custom-price") + .description("Custom minimum price (supports K, M, B suffixes).") + .defaultValue("50") + .visible(() -> priceMode.get() == PriceMode.CUSTOM) + .build() + ); + + private final Setting click_delay = sg_general.add(new IntSetting.Builder() + .name("click-delay") + .description("Delay in milliseconds between GUI clicks (except confirm spam).") + .defaultValue(50) + .min(0) + .max(500) + .sliderMax(200) + .build() + ); + + private final Setting notifications = sg_general.add(new BoolSetting.Builder() + .name("notifications") + .description("Show detailed notifications.") + .defaultValue(true) + .build() + ); + + private final Setting> blacklisted_players = sg_blacklist.add(new StringListSetting.Builder() + .name("blacklisted-players") + .description("Players whose orders will be ignored.") + .defaultValue(List.of()) + .build() + ); + + public AutoShopOrder() { + super(GlazedAddon.CATEGORY, "Auto Shop Order", "Auto Shop Order - Buys items from shop and delivers to orders automatically."); + } + + // Get effective minimum price + private double getEffectiveMinPrice() { + if (priceMode.get() == PriceMode.AUTO) { + return getDefaultMinPrice(); + } + + // Custom mode + String priceStr = customPrice.get().trim(); + double parsed = parse_price(priceStr); + if (parsed < 0) { + return getDefaultMinPrice(); + } + return parsed; + } + + @Override + public void onActivate() { + double effective_min = getEffectiveMinPrice(); + + stage = Stage.SHOP_OPEN; + stage_start = System.currentTimeMillis(); + last_action_time = System.currentTimeMillis(); + final_exit_count = 0; + confirm_slot_id = -1; + buy_screen_retry_count = 0; + buy_spam_start_time = 0; + move_pass_count = 0; + + if (notifications.get()) { + info("AutoShopOrder activated! Item: %s | Shop: $%d | Min: %s", + getSearchKeyword(), getShopPrice(), format_price(effective_min)); + } + } + + @Override + public void onDeactivate() { + stage = Stage.NONE; + } + + // Record an action was performed (resets anti-stuck timer) + private void recordAction() { + last_action_time = System.currentTimeMillis(); + } + + // Check if we're stuck and should reset + private boolean checkAndHandleStuck() { + if (mc.currentScreen instanceof GenericContainerScreen) { + long now = System.currentTimeMillis(); + if (now - last_action_time > STUCK_TIMEOUT_MS) { + if (notifications.get()) { + info("Stuck detected - resetting..."); + } + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + last_action_time = now; + return true; + } + } + return false; + } + + @EventHandler + private void onTick(TickEvent.Post event) { + if (mc.player == null || mc.world == null) return; + long now = System.currentTimeMillis(); + + // Anti-stuck check at the start of each tick + if (checkAndHandleStuck()) return; + + switch (stage) { + // ==================== SHOP PHASE ==================== + case SHOP_OPEN -> { + ChatUtils.sendPlayerMsg("/shop"); + stage = Stage.SHOP_CATEGORY; + stage_start = now; + recordAction(); + } + + case SHOP_CATEGORY -> { + if (mc.currentScreen instanceof GenericContainerScreen screen) { + if (now - stage_start < click_delay.get()) return; + + ScreenHandler handler = screen.getScreenHandler(); + for (Slot slot : handler.slots) { + ItemStack stack = slot.getStack(); + if (!stack.isEmpty() && isCategoryIcon(stack)) { + mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); + stage = Stage.SHOP_ITEM; + stage_start = now; + recordAction(); + return; + } + } + if (now - stage_start > 3000) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + } + + case SHOP_ITEM -> { + if (mc.currentScreen instanceof GenericContainerScreen screen) { + if (now - stage_start < click_delay.get()) return; + + ScreenHandler handler = screen.getScreenHandler(); + for (Slot slot : handler.slots) { + ItemStack stack = slot.getStack(); + if (!stack.isEmpty() && isTargetItem(stack) && slot.inventory != mc.player.getInventory()) { + mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); + + // If item is not stackable, skip to buy screen directly + if (!isTargetItemStackable()) { + stage = Stage.SHOP_WAIT_FOR_BUY_SCREEN; + stage_start = now; + confirm_slot_id = -1; + buy_screen_retry_count = 0; + recordAction(); + } else { + stage = Stage.SHOP_SET_STACK; + stage_start = now; + recordAction(); + } + return; + } + } + if (now - stage_start > 1000) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + } + + case SHOP_SET_STACK -> { + if (mc.currentScreen instanceof GenericContainerScreen screen) { + if (now - stage_start < click_delay.get()) return; + + ScreenHandler handler = screen.getScreenHandler(); + for (Slot slot : handler.slots) { + ItemStack stack = slot.getStack(); + if (!stack.isEmpty() && is_glass_pane(stack) && stack.getCount() == 64) { + mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); + stage = Stage.SHOP_WAIT_FOR_BUY_SCREEN; + stage_start = now; + confirm_slot_id = -1; + buy_screen_retry_count = 0; + recordAction(); + return; + } + } + if (now - stage_start > 1000) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + } + + case SHOP_WAIT_FOR_BUY_SCREEN -> { + if (now - stage_start < 50) return; + + if (mc.currentScreen instanceof GenericContainerScreen screen) { + ScreenHandler handler = screen.getScreenHandler(); + + for (Slot slot : handler.slots) { + ItemStack stack = slot.getStack(); + if (!stack.isEmpty() && is_green_glass(stack) && stack.getCount() == 1) { + confirm_slot_id = slot.id; + stage = Stage.SHOP_BUY_SPAM; + stage_start = now; + buy_spam_start_time = now; + recordAction(); + return; + } + } + + buy_screen_retry_count++; + if (buy_screen_retry_count > MAX_BUY_RETRIES) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + } + + case SHOP_BUY_SPAM -> { + // Check for timeout - prevent stuck + if (now - buy_spam_start_time > BUY_SPAM_TIMEOUT_MS) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + return; + } + + if (!(mc.currentScreen instanceof GenericContainerScreen screen)) { + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + return; + } + + ScreenHandler handler = screen.getScreenHandler(); + + // Re-find confirm button if lost + if (confirm_slot_id == -1) { + for (Slot slot : handler.slots) { + ItemStack stack = slot.getStack(); + if (!stack.isEmpty() && is_green_glass(stack) && stack.getCount() == 1) { + confirm_slot_id = slot.id; + break; + } + } + } + + if (confirm_slot_id != -1) { + // Check if inventory is full + if (is_inventory_full()) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_EXIT; + stage_start = now; + recordAction(); + return; + } + + // Click confirm button 2 times per tick + mc.interactionManager.clickSlot(handler.syncId, confirm_slot_id, 0, SlotActionType.PICKUP, mc.player); + mc.interactionManager.clickSlot(handler.syncId, confirm_slot_id, 0, SlotActionType.PICKUP, mc.player); + recordAction(); + } else { + // Button not found - retry search next tick + buy_screen_retry_count++; + if (buy_screen_retry_count > MAX_BUY_RETRIES) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + } + + case SHOP_EXIT -> { + if (mc.currentScreen == null) { + stage = Stage.WAIT; + stage_start = now; + recordAction(); + } + if (now - stage_start > 3000) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + + // ==================== ORDERS PHASE ==================== + case WAIT -> { + if (now - stage_start >= click_delay.get()) { + ChatUtils.sendPlayerMsg("/orders " + getSearchKeyword()); + stage = Stage.ORDERS_OPEN; + stage_start = now; + recordAction(); + } + } + + case ORDERS_OPEN -> { + if (mc.currentScreen instanceof GenericContainerScreen screen) { + if (now - stage_start < click_delay.get()) return; + + ScreenHandler handler = screen.getScreenHandler(); + + // Find best order (highest price) + Slot best_order = null; + double best_price = -1; + double min_price_value = getEffectiveMinPrice(); + + for (Slot slot : handler.slots) { + ItemStack stack = slot.getStack(); + if (!stack.isEmpty() && isTargetItem(stack)) { + String player_name = get_order_player_name(stack); + if (is_blacklisted(player_name)) continue; + + double order_price = get_order_price(stack); + + if (order_price >= min_price_value && order_price > best_price) { + best_price = order_price; + best_order = slot; + } + } + } + + if (best_order != null) { + if (notifications.get()) { + info("Found order: %s", format_price(best_price)); + } + mc.interactionManager.clickSlot(handler.syncId, best_order.id, 0, SlotActionType.PICKUP, mc.player); + stage = Stage.ORDERS_SELECT; + stage_start = now; + move_pass_count = 0; + recordAction(); + return; + } + + if (now - stage_start > 3000) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + } + + // BATCH MOVE: Move all items at once in the same tick + case ORDERS_SELECT -> { + if (mc.currentScreen instanceof GenericContainerScreen screen) { + ScreenHandler handler = screen.getScreenHandler(); + + // Collect all slots with target items first + List slots_to_move = new ArrayList<>(); + + for (Slot slot : handler.slots) { + // Only check player inventory slots (not container slots) + if (slot.inventory == mc.player.getInventory()) { + int slot_index = slot.getIndex(); + // Main inventory is slots 9-35, hotbar is 0-8 + // We want to move from player inventory to container + if (slot_index >= 0 && slot_index < 36) { + ItemStack stack = slot.getStack(); + if (isTargetItem(stack)) { + slots_to_move.add(slot.id); + } + } + } + } + + if (slots_to_move.isEmpty()) { + // No items to move - proceed to confirm + mc.player.closeHandledScreen(); + stage = Stage.ORDERS_CONFIRM; + stage_start = now; + recordAction(); + return; + } + + // BATCH MOVE: Send all QUICK_MOVE packets in rapid succession (same tick) + for (int slot_id : slots_to_move) { + mc.interactionManager.clickSlot(handler.syncId, slot_id, 0, SlotActionType.QUICK_MOVE, mc.player); + } + + move_pass_count++; + recordAction(); + + // Check after move if items remain + if (move_pass_count >= MAX_MOVE_PASSES) { + // Max passes - continue anyway + mc.player.closeHandledScreen(); + stage = Stage.ORDERS_CONFIRM; + stage_start = now; + recordAction(); + } + // Otherwise stay in ORDERS_SELECT to check again next tick + } + } + + case ORDERS_CONFIRM -> { + if (mc.currentScreen instanceof GenericContainerScreen screen) { + if (now - stage_start < click_delay.get()) return; + + ScreenHandler handler = screen.getScreenHandler(); + for (Slot slot : handler.slots) { + ItemStack stack = slot.getStack(); + if (!stack.isEmpty() && is_green_glass(stack)) { + for (int i = 0; i < 5; i++) { + mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); + } + stage = Stage.ORDERS_FINAL_EXIT; + stage_start = now; + final_exit_count = 0; + final_exit_start = now; + recordAction(); + return; + } + } + if (now - stage_start > 3000) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + } + + case ORDERS_FINAL_EXIT -> { + long exit_delay = click_delay.get(); + + if (final_exit_count == 0) { + if (System.currentTimeMillis() - final_exit_start >= exit_delay) { + mc.player.closeHandledScreen(); + final_exit_count++; + final_exit_start = System.currentTimeMillis(); + recordAction(); + } + } else if (final_exit_count == 1) { + if (System.currentTimeMillis() - final_exit_start >= exit_delay) { + mc.player.closeHandledScreen(); + final_exit_count++; + final_exit_start = System.currentTimeMillis(); + recordAction(); + } + } else { + final_exit_count = 0; + stage = Stage.CYCLE_PAUSE; + stage_start = System.currentTimeMillis(); + recordAction(); + } + } + + case CYCLE_PAUSE -> { + if (now - stage_start >= WAIT_TIME_MS) { + stage = Stage.SHOP_OPEN; + stage_start = now; + recordAction(); + } + } + + case NONE -> {} + } + } + + // ==================== HELPER METHODS ==================== + + private boolean isCategoryIcon(ItemStack stack) { + String name = stack.getName().getString().toLowerCase(); + return switch (category.get()) { + case END -> name.contains("ᴇɴᴅ") || name.contains("end") || stack.getItem() == Items.END_STONE; + case NETHER -> name.contains("Ι΄α΄‡α΄›Κœα΄‡Κ€") || name.contains("nether") || stack.getItem() == Items.NETHERRACK; + case GEAR -> name.contains("ɒᴇᴀʀ") || name.contains("gear") || stack.getItem() == Items.TOTEM_OF_UNDYING; + case FOOD -> name.contains("κœ°α΄α΄α΄…") || name.contains("food") || stack.getItem() == Items.COOKED_BEEF; + }; + } + + private boolean isTargetItem(ItemStack stack) { + return !stack.isEmpty() && stack.getItem() == getTargetMcItem(); + } + + // Check if target item is stackable + private boolean isTargetItemStackable() { + Item item = getTargetMcItem(); + return item.getMaxCount() > 1; + } + + // Check if any target items remain in inventory + private boolean hasTargetItemsInInventory() { + for (int i = 0; i < 36; i++) { + if (isTargetItem(mc.player.getInventory().getStack(i))) { + return true; + } + } + return false; + } + + private Item getTargetMcItem() { + return switch (category.get()) { + case END -> switch (endItem.get()) { + case ENDER_CHEST -> Items.ENDER_CHEST; + case ENDER_PEARL -> Items.ENDER_PEARL; + case END_STONE -> Items.END_STONE; + case DRAGON_BREATH -> Items.DRAGON_BREATH; + case END_ROD -> Items.END_ROD; + case CHORUS_FRUIT -> Items.CHORUS_FRUIT; + case POPPED_CHORUS_FRUIT -> Items.POPPED_CHORUS_FRUIT; + case SHULKER_SHELL -> Items.SHULKER_SHELL; + case SHULKER_BOX -> Items.SHULKER_BOX; + }; + case NETHER -> switch (netherItem.get()) { + case BLAZE_ROD -> Items.BLAZE_ROD; + case NETHER_WART -> Items.NETHER_WART; + case GLOWSTONE_DUST -> Items.GLOWSTONE_DUST; + case MAGMA_CREAM -> Items.MAGMA_CREAM; + case GHAST_TEAR -> Items.GHAST_TEAR; + case NETHER_QUARTZ -> Items.QUARTZ; + case SOUL_SAND -> Items.SOUL_SAND; + case MAGMA_BLOCK -> Items.MAGMA_BLOCK; + case CRYING_OBSIDIAN -> Items.CRYING_OBSIDIAN; + }; + case GEAR -> switch (gearItem.get()) { + case OBSIDIAN -> Items.OBSIDIAN; + case END_CRYSTAL -> Items.END_CRYSTAL; + case RESPAWN_ANCHOR -> Items.RESPAWN_ANCHOR; + case GLOWSTONE -> Items.GLOWSTONE; + case TOTEM_OF_UNDYING -> Items.TOTEM_OF_UNDYING; + case ENDER_PEARL -> Items.ENDER_PEARL; + case GOLDEN_APPLE -> Items.GOLDEN_APPLE; + case EXPERIENCE_BOTTLE -> Items.EXPERIENCE_BOTTLE; + case TIPPED_ARROW -> Items.TIPPED_ARROW; + }; + case FOOD -> switch (foodItem.get()) { + case POTATO -> Items.POTATO; + case SWEET_BERRIES -> Items.SWEET_BERRIES; + case MELON_SLICE -> Items.MELON_SLICE; + case CARROT -> Items.CARROT; + case APPLE -> Items.APPLE; + case COOKED_CHICKEN -> Items.COOKED_CHICKEN; + case COOKED_BEEF -> Items.COOKED_BEEF; + case GOLDEN_CARROT -> Items.GOLDEN_CARROT; + case GOLDEN_APPLE -> Items.GOLDEN_APPLE; + }; + }; + } + + private String getSearchKeyword() { + return switch (category.get()) { + case END -> switch (endItem.get()) { + case ENDER_CHEST -> "ender chest"; + case ENDER_PEARL -> "ender pearl"; + case END_STONE -> "end stone"; + case DRAGON_BREATH -> "dragon breath"; + case END_ROD -> "end rod"; + case CHORUS_FRUIT -> "chorus fruit"; + case POPPED_CHORUS_FRUIT -> "popped chorus fruit"; + case SHULKER_SHELL -> "shulker shell"; + case SHULKER_BOX -> "shulker box"; + }; + case NETHER -> switch (netherItem.get()) { + case BLAZE_ROD -> "blaze rod"; + case NETHER_WART -> "nether wart"; + case GLOWSTONE_DUST -> "glowstone dust"; + case MAGMA_CREAM -> "magma cream"; + case GHAST_TEAR -> "ghast tear"; + case NETHER_QUARTZ -> "quartz"; + case SOUL_SAND -> "soul sand"; + case MAGMA_BLOCK -> "magma block"; + case CRYING_OBSIDIAN -> "crying obsidian"; + }; + case GEAR -> switch (gearItem.get()) { + case OBSIDIAN -> "obsidian"; + case END_CRYSTAL -> "end crystal"; + case RESPAWN_ANCHOR -> "respawn anchor"; + case GLOWSTONE -> "glowstone"; + case TOTEM_OF_UNDYING -> "totem of undying"; + case ENDER_PEARL -> "ender pearl"; + case GOLDEN_APPLE -> "golden apple"; + case EXPERIENCE_BOTTLE -> "experience bottle"; + case TIPPED_ARROW -> "tipped arrow"; + }; + case FOOD -> switch (foodItem.get()) { + case POTATO -> "potato"; + case SWEET_BERRIES -> "sweet berries"; + case MELON_SLICE -> "melon slice"; + case CARROT -> "carrot"; + case APPLE -> "apple"; + case COOKED_CHICKEN -> "cooked chicken"; + case COOKED_BEEF -> "cooked beef"; + case GOLDEN_CARROT -> "golden carrot"; + case GOLDEN_APPLE -> "golden apple"; + }; + }; + } + + private boolean is_glass_pane(ItemStack stack) { + String item_name = stack.getItem().getName().getString().toLowerCase(); + return item_name.contains("glass") && item_name.contains("pane"); + } + + private boolean is_green_glass(ItemStack stack) { + return stack.getItem() == Items.LIME_STAINED_GLASS_PANE || stack.getItem() == Items.GREEN_STAINED_GLASS_PANE; + } + + private boolean is_inventory_full() { + for (int i = 9; i <= 35; i++) { + if (mc.player.getInventory().getStack(i).isEmpty()) return false; + } + return true; + } + + // ==================== PRICE PARSING ==================== + + private double parse_price(String price_str) { + if (price_str == null || price_str.isEmpty()) { + return -1.0; + } + + String cleaned = price_str.trim().toLowerCase().replace(",", ""); + double multiplier = 1.0; + + if (cleaned.endsWith("b")) { + multiplier = 1_000_000_000.0; + cleaned = cleaned.substring(0, cleaned.length() - 1); + } else if (cleaned.endsWith("m")) { + multiplier = 1_000_000.0; + cleaned = cleaned.substring(0, cleaned.length() - 1); + } else if (cleaned.endsWith("k")) { + multiplier = 1_000.0; + cleaned = cleaned.substring(0, cleaned.length() - 1); + } + + try { + return Double.parseDouble(cleaned) * multiplier; + } catch (NumberFormatException e) { + return -1.0; + } + } + + private String format_price(double price) { + if (price >= 1_000_000_000) { + return String.format("$%.1fB", price / 1_000_000_000.0); + } else if (price >= 1_000_000) { + return String.format("$%.1fM", price / 1_000_000.0); + } else if (price >= 1_000) { + return String.format("$%.1fK", price / 1_000.0); + } else { + return String.format("$%.0f", price); + } + } + + private double get_order_price(ItemStack stack) { + if (stack.isEmpty()) { + return -1.0; + } + + Item.TooltipContext tooltip_context = Item.TooltipContext.create(mc.world); + List tooltip = stack.getTooltip(tooltip_context, mc.player, TooltipType.BASIC); + + return parse_tooltip_price(tooltip); + } + + private double parse_tooltip_price(List tooltip) { + if (tooltip == null || tooltip.isEmpty()) { + return -1.0; + } + + // Pattern to match "$5 each", "$1.5K each", etc. + Pattern[] price_patterns = { + Pattern.compile("\\$([\\d,]+(?:\\.\\d+)?)([kmbKMB])?\\s*each", Pattern.CASE_INSENSITIVE), + Pattern.compile("\\$([\\d,]+(?:\\.\\d+)?)([kmbKMB])?", Pattern.CASE_INSENSITIVE), + Pattern.compile("(?i)price\\s*:\\s*([\\d,]+(?:\\.\\d+)?)([kmbKMB])?", Pattern.CASE_INSENSITIVE), + Pattern.compile("(?i)pay\\s*:\\s*([\\d,]+(?:\\.\\d+)?)([kmbKMB])?", Pattern.CASE_INSENSITIVE), + Pattern.compile("(?i)reward\\s*:\\s*([\\d,]+(?:\\.\\d+)?)([kmbKMB])?", Pattern.CASE_INSENSITIVE), + Pattern.compile("([\\d,]+(?:\\.\\d+)?)([kmbKMB])?\\s*coins?", Pattern.CASE_INSENSITIVE) + }; + + for (Text line : tooltip) { + String text = line.getString(); + + for (Pattern pattern : price_patterns) { + Matcher matcher = pattern.matcher(text); + if (matcher.find()) { + String number_str = matcher.group(1).replace(",", ""); + String suffix = ""; + if (matcher.groupCount() >= 2 && matcher.group(2) != null) { + suffix = matcher.group(2).toLowerCase(); + } + + try { + double base_price = Double.parseDouble(number_str); + double multiplier = 1.0; + + switch (suffix) { + case "k" -> multiplier = 1_000.0; + case "m" -> multiplier = 1_000_000.0; + case "b" -> multiplier = 1_000_000_000.0; + } + + return base_price * multiplier; + } catch (NumberFormatException e) { + // Continue to next pattern + } + } + } + } + + return -1.0; + } + + // ==================== BLACKLIST ==================== + + private boolean is_blacklisted(String playerName) { + if (playerName == null || blacklisted_players.get().isEmpty()) return false; + return blacklisted_players.get().stream().anyMatch(p -> p.equalsIgnoreCase(playerName)); + } + + private String get_order_player_name(ItemStack stack) { + if (stack.isEmpty()) return null; + Item.TooltipContext ctx = Item.TooltipContext.create(mc.world); + List tooltip = stack.getTooltip(ctx, mc.player, TooltipType.BASIC); + + // Pattern for "Click to deliver .Grumm7587 Cooked Chicken" + Pattern deliver_pattern = Pattern.compile("(?i)click to deliver\\s+\\.?([a-zA-Z0-9_]+)"); + + // Standard patterns + Pattern[] patterns = { + deliver_pattern, + Pattern.compile("(?i)player\\s*:\\s*([a-zA-Z0-9_]+)"), + Pattern.compile("(?i)from\\s*:\\s*([a-zA-Z0-9_]+)"), + Pattern.compile("(?i)by\\s*:\\s*([a-zA-Z0-9_]+)"), + Pattern.compile("(?i)seller\\s*:\\s*([a-zA-Z0-9_]+)"), + Pattern.compile("(?i)owner\\s*:\\s*([a-zA-Z0-9_]+)") + }; + + for (Text line : tooltip) { + String text = line.getString(); + for (Pattern p : patterns) { + Matcher m = p.matcher(text); + if (m.find()) { + String name = m.group(1); + if (name.length() >= 3 && name.length() <= 16) return name; + } + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/main/AutoShulkerOrder.java b/src/main/java/com/nnpg/glazed/modules/main/AutoShulkerOrder.java deleted file mode 100644 index 9882c901..00000000 --- a/src/main/java/com/nnpg/glazed/modules/main/AutoShulkerOrder.java +++ /dev/null @@ -1,646 +0,0 @@ -package com.nnpg.glazed.modules.main; - -import com.nnpg.glazed.GlazedAddon; -import meteordevelopment.meteorclient.events.world.TickEvent; -import meteordevelopment.meteorclient.settings.*; -import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.meteorclient.utils.player.ChatUtils; -import meteordevelopment.orbit.EventHandler; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.item.tooltip.TooltipType; -import net.minecraft.screen.ScreenHandler; -import net.minecraft.screen.slot.Slot; -import net.minecraft.screen.slot.SlotActionType; -import net.minecraft.text.Text; - -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class AutoShulkerOrder extends Module { - private final MinecraftClient mc = MinecraftClient.getInstance(); - - private enum Stage {NONE, SHOP, SHOP_END, SHOP_SHULKER, SHOP_CONFIRM, SHOP_CHECK_FULL, SHOP_EXIT, WAIT, ORDERS, ORDERS_SELECT, ORDERS_EXIT, ORDERS_CONFIRM, ORDERS_FINAL_EXIT, CYCLE_PAUSE, TARGET_ORDERS} - - private Stage stage = Stage.NONE; - private long stageStart = 0; - private static final long WAIT_TIME_MS = 50; - private int shulkerMoveIndex = 0; - private long lastShulkerMoveTime = 0; - private int exitCount = 0; - private int finalExitCount = 0; - private long finalExitStart = 0; - private int bulkBuyCount = 0; - private static final int MAX_BULK_BUY = 5; - - // Player targeting variables - private String targetPlayer = ""; - private boolean isTargetingActive = false; - - // Settings - private final SettingGroup sgGeneral = settings.getDefaultGroup(); - private final SettingGroup sgTargeting = settings.createGroup("Player Targeting"); - - private final Setting minPrice = sgGeneral.add(new StringSetting.Builder() - .name("min-price") - .description("Minimum price to deliver shulkers for (supports K, M, B suffixes).") - .defaultValue("850") - .build() - ); - - private final Setting notifications = sgGeneral.add(new BoolSetting.Builder() - .name("notifications") - .description("Show detailed price checking notifications.") - .defaultValue(true) - .build() - ); - - private final Setting speedMode = sgGeneral.add(new BoolSetting.Builder() - .name("speed-mode") - .description("Maximum speed mode - removes most delays (may be unstable).") - .defaultValue(true) - .build() - ); - - // New targeting settings - private final Setting enableTargeting = sgTargeting.add(new BoolSetting.Builder() - .name("enable-targeting") - .description("Enable targeting a specific player (ignores minimum price).") - .defaultValue(false) - .build() - ); - - private final Setting targetPlayerName = sgTargeting.add(new StringSetting.Builder() - .name("target-player") - .description("Specific player name to target for orders.") - .defaultValue("") - .visible(() -> enableTargeting.get()) - .build() - ); - - private final Setting targetOnlyMode = sgTargeting.add(new BoolSetting.Builder() - .name("target-only-mode") - .description("Only look for orders from the targeted player, ignore all others.") - .defaultValue(false) - .visible(() -> enableTargeting.get()) - .build() - ); - - private final Setting> blacklistedPlayers = sgTargeting.add(new StringListSetting.Builder() - .name("blacklisted-players") - .description("Players whose orders will be ignored.") - .defaultValue(List.of()) - .build() - ); - - public AutoShulkerOrder() { - super(GlazedAddon.CATEGORY, "auto-shulker-order", "Automatically buys shulkers and sells them in orders for profit with player targeting"); - } - - @Override - public void onActivate() { - double parsedPrice = parsePrice(minPrice.get()); - if (parsedPrice == -1.0 && !enableTargeting.get()) { - if (notifications.get()) { - ChatUtils.error("Invalid minimum price format!"); - } - toggle(); - return; - } - - // Setup target player - updateTargetPlayer(); - - stage = Stage.SHOP; // Always start with shop to buy shulkers first - stageStart = System.currentTimeMillis(); - shulkerMoveIndex = 0; - lastShulkerMoveTime = 0; - exitCount = 0; - finalExitCount = 0; - bulkBuyCount = 0; - - if (notifications.get()) { - String modeInfo = isTargetingActive ? - String.format(" | Targeting: %s", targetPlayer) : ""; - info("πŸš€ FAST AutoShulkerOrder activated! Minimum: %s%s", minPrice.get(), modeInfo); - } - } - - @Override - public void onDeactivate() { - stage = Stage.NONE; - } - - private void updateTargetPlayer() { - targetPlayer = ""; - isTargetingActive = false; - - if (enableTargeting.get() && !targetPlayerName.get().trim().isEmpty()) { - targetPlayer = targetPlayerName.get().trim(); - isTargetingActive = true; - - if (notifications.get()) { - info("🎯 Targeting enabled for player: %s", targetPlayer); - } - } else { - if (notifications.get() && enableTargeting.get()) { - info("⚠️ Targeting disabled - no player name provided"); - } - } - } - - @EventHandler - private void onTick(TickEvent.Post event) { - if (mc.player == null || mc.world == null) return; - long now = System.currentTimeMillis(); - - switch (stage) { - case TARGET_ORDERS -> { - ChatUtils.sendPlayerMsg("/orders " + targetPlayer); - stage = Stage.ORDERS; - stageStart = now; - - if (notifications.get()) { - info("πŸ” Checking orders for: %s", targetPlayer); - } - } - case SHOP -> { - ChatUtils.sendPlayerMsg("/shop"); - stage = Stage.SHOP_END; - stageStart = now; - } - case SHOP_END -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isEndStone(stack)) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_SHULKER; - stageStart = now; - bulkBuyCount = 0; - return; - } - } - if (now - stageStart > (speedMode.get() ? 1000 : 3000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stageStart = now; - } - } - } - case SHOP_SHULKER -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - boolean foundShulker = false; - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isShulkerBox(stack)) { - // CONTROLLED BUYING - slower to prevent overshooting - int clickCount = speedMode.get() ? 10 : 5; // Reduced from 64/27 to 10/5 - for (int i = 0; i < clickCount; i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - foundShulker = true; - bulkBuyCount++; - break; - } - } - - if (foundShulker) { - stage = Stage.SHOP_CONFIRM; - stageStart = now; - return; - } - if (now - stageStart > (speedMode.get() ? 500 : 1500)) { // Slightly longer wait - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stageStart = now; - } - } - } - case SHOP_CONFIRM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - boolean foundGreen = false; - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isGreenGlass(stack)) { - // CONTROLLED CONFIRM - fewer clicks to prevent over-confirming - for (int i = 0; i < (speedMode.get() ? 3 : 2); i++) { // Reduced from 10/5 to 3/2 - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - foundGreen = true; - break; - } - } - if (foundGreen) { - stage = Stage.SHOP_CHECK_FULL; - stageStart = now; - return; - } - if (now - stageStart > (speedMode.get() ? 200 : 800)) { // Slightly longer wait - stage = Stage.SHOP_SHULKER; - stageStart = now; - } - } - } - case SHOP_CHECK_FULL -> { - // Add a small delay before checking inventory to let transactions process - if (now - stageStart > (speedMode.get() ? 100 : 200)) { - if (isInventoryFull()) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP_EXIT; - stageStart = now; - } else { - // Small pause before buying more to prevent rapid-fire purchases - if (now - stageStart > (speedMode.get() ? 200 : 400)) { - stage = Stage.SHOP_SHULKER; - stageStart = now; - } - } - } - } - case SHOP_EXIT -> { - if (mc.currentScreen == null) { - stage = Stage.WAIT; - stageStart = now; - } - if (now - stageStart > (speedMode.get() ? 1000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stageStart = now; - } - } - case WAIT -> { - long waitTime = speedMode.get() ? 25 : WAIT_TIME_MS; - if (now - stageStart >= waitTime) { - // Only use target orders if targeting is enabled AND we have a valid target - if (isTargetingActive && !targetPlayer.isEmpty()) { - stage = Stage.TARGET_ORDERS; - } else { - ChatUtils.sendPlayerMsg("/orders shulker"); - stage = Stage.ORDERS; - } - stageStart = now; - } - } - case ORDERS -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - boolean foundOrder = false; - - // Add delay in speed mode to ensure GUI is fully loaded - if (speedMode.get() && now - stageStart < 200) { - return; // Wait 200ms for GUI to stabilize in speed mode - } - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isShulkerBox(stack) && isPurple(stack)) { - boolean shouldTakeOrder = false; - String orderPlayer = getOrderPlayerName(stack); - - if (isBlacklisted(orderPlayer)) continue; - - boolean isTargetedOrder = isTargetingActive && - orderPlayer != null && - orderPlayer.equalsIgnoreCase(targetPlayer); - - if (isTargetedOrder) { - shouldTakeOrder = true; - if (notifications.get()) { - double orderPrice = getOrderPrice(stack); - info("🎯 Found TARGET order from %s: %s", orderPlayer, - orderPrice > 0 ? formatPrice(orderPrice) : "Unknown price"); - } - } else if (!targetOnlyMode.get()) { - // Regular price check for non-targeted orders - double orderPrice = getOrderPrice(stack); - double minPriceValue = parsePrice(minPrice.get()); - - if (orderPrice >= minPriceValue) { - shouldTakeOrder = true; - if (notifications.get()) { - info("βœ… Found order: %s", formatPrice(orderPrice)); - } - } - } - - if (shouldTakeOrder) { - // Click on the order to select it - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - - // Wait a moment before moving to selection phase - stage = Stage.ORDERS_SELECT; - stageStart = now + (speedMode.get() ? 100 : 50); // Small delay to let selection register - shulkerMoveIndex = 0; - lastShulkerMoveTime = 0; - foundOrder = true; - - if (notifications.get()) { - info("πŸ”„ Selected order, preparing to transfer items..."); - } - return; - } - } - } - - if (!foundOrder && now - stageStart > (speedMode.get() ? 3000 : 5000)) { // Longer wait in speed mode - mc.player.closeHandledScreen(); - stage = Stage.SHOP; // Always go back to shop after checking orders - stageStart = now; - } - } - } - case ORDERS_SELECT -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - if (shulkerMoveIndex >= 36) { - mc.player.closeHandledScreen(); - stage = Stage.ORDERS_CONFIRM; - stageStart = now; - shulkerMoveIndex = 0; - return; - } - - long moveDelay = speedMode.get() ? 10 : 100; - if (now - lastShulkerMoveTime >= moveDelay) { - int batchSize = speedMode.get() ? 3 : 1; - - for (int batch = 0; batch < batchSize && shulkerMoveIndex < 36; batch++) { - ItemStack stack = mc.player.getInventory().getStack(shulkerMoveIndex); - if (isShulkerBox(stack)) { - int playerSlotId = -1; - for (Slot slot : handler.slots) { - if (slot.inventory == mc.player.getInventory() && slot.getIndex() == shulkerMoveIndex) { - playerSlotId = slot.id; - break; - } - } - - if (playerSlotId != -1) { - mc.interactionManager.clickSlot(handler.syncId, playerSlotId, 0, SlotActionType.QUICK_MOVE, mc.player); - } - } - shulkerMoveIndex++; - } - lastShulkerMoveTime = now; - } - } - } - case ORDERS_EXIT -> { - if (mc.currentScreen == null) { - exitCount++; - if (exitCount < 2) { - mc.player.closeHandledScreen(); - stageStart = now; - } else { - exitCount = 0; - stage = Stage.ORDERS_CONFIRM; - stageStart = now; - } - } - } - case ORDERS_CONFIRM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isGreenGlass(stack)) { - for (int i = 0; i < (speedMode.get() ? 15 : 5); i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - stage = Stage.ORDERS_FINAL_EXIT; - stageStart = now; - finalExitCount = 0; - finalExitStart = now; - - if (notifications.get()) { - info("βœ… Order completed! Going back to shop to buy more shulkers..."); - } - return; - } - } - if (now - stageStart > (speedMode.get() ? 2000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; // Go directly to shop if confirmation fails - stageStart = now; - } - } - } - case ORDERS_FINAL_EXIT -> { - long exitDelay = speedMode.get() ? 50 : 200; - - if (finalExitCount == 0) { - if (System.currentTimeMillis() - finalExitStart >= exitDelay) { - mc.player.closeHandledScreen(); - finalExitCount++; - finalExitStart = System.currentTimeMillis(); - } - } else if (finalExitCount == 1) { - if (System.currentTimeMillis() - finalExitStart >= exitDelay) { - mc.player.closeHandledScreen(); - finalExitCount++; - finalExitStart = System.currentTimeMillis(); - } - } else { - finalExitCount = 0; - stage = Stage.CYCLE_PAUSE; - stageStart = System.currentTimeMillis(); - } - } - case CYCLE_PAUSE -> { - long cycleWait = speedMode.get() ? 10 : 25; // Very fast cycle restart - if (now - stageStart >= cycleWait) { - // Always go back to shop to buy more shulkers - updateTargetPlayer(); // Refresh target player - stage = Stage.SHOP; - stageStart = now; - } - } - case NONE -> { - } - } - } - - // New method to extract player name from order tooltip - private String getOrderPlayerName(ItemStack stack) { - if (stack.isEmpty()) { - return null; - } - - Item.TooltipContext tooltipContext = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(tooltipContext, mc.player, TooltipType.BASIC); - - for (Text line : tooltip) { - String text = line.getString(); - - // Look for patterns like "Player: PlayerName" or "From: PlayerName" or "By: PlayerName" - Pattern[] namePatterns = { - Pattern.compile("(?i)player\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)from\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)by\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)seller\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)owner\\s*:\\s*([a-zA-Z0-9_]+)"), - // Generic pattern for username-like strings - Pattern.compile("\\b([a-zA-Z0-9_]{3,16})\\b") - }; - - for (Pattern pattern : namePatterns) { - Matcher matcher = pattern.matcher(text); - if (matcher.find()) { - String playerName = matcher.group(1); - // Basic validation for Minecraft usernames - if (playerName.length() >= 3 && playerName.length() <= 16 && - playerName.matches("[a-zA-Z0-9_]+")) { - return playerName; - } - } - } - } - - return null; - } - - // Price parsing methods (unchanged) - private double getOrderPrice(ItemStack stack) { - if (stack.isEmpty()) { - return -1.0; - } - - Item.TooltipContext tooltipContext = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(tooltipContext, mc.player, TooltipType.BASIC); - - return parseTooltipPrice(tooltip); - } - - private double parseTooltipPrice(List tooltip) { - if (tooltip == null || tooltip.isEmpty()) { - return -1.0; - } - - Pattern[] pricePatterns = { - Pattern.compile("\\$([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)price\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)pay\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)reward\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("([\\d,]+(?:\\.[\\d]+)?)([kmb])?\\s*coins?", Pattern.CASE_INSENSITIVE), - Pattern.compile("\\b([\\d,]+(?:\\.[\\d]+)?)([kmb])\\b", Pattern.CASE_INSENSITIVE) - }; - - for (Text line : tooltip) { - String text = line.getString(); - - for (Pattern pattern : pricePatterns) { - Matcher matcher = pattern.matcher(text); - if (matcher.find()) { - String numberStr = matcher.group(1).replace(",", ""); - String suffix = ""; - if (matcher.groupCount() >= 2 && matcher.group(2) != null) { - suffix = matcher.group(2).toLowerCase(); - } - - try { - double basePrice = Double.parseDouble(numberStr); - double multiplier = 1.0; - - switch (suffix) { - case "k" -> multiplier = 1_000.0; - case "m" -> multiplier = 1_000_000.0; - case "b" -> multiplier = 1_000_000_000.0; - } - - return basePrice * multiplier; - } catch (NumberFormatException e) { - // Continue to next pattern - } - } - } - } - - return -1.0; - } - - private double parsePrice(String priceStr) { - if (priceStr == null || priceStr.isEmpty()) { - return -1.0; - } - - String cleaned = priceStr.trim().toLowerCase().replace(",", ""); - double multiplier = 1.0; - - if (cleaned.endsWith("b")) { - multiplier = 1_000_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("m")) { - multiplier = 1_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("k")) { - multiplier = 1_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } - - try { - return Double.parseDouble(cleaned) * multiplier; - } catch (NumberFormatException e) { - return -1.0; - } - } - - private String formatPrice(double price) { - if (price >= 1_000_000_000) { - return String.format("$%.1fB", price / 1_000_000_000.0); - } else if (price >= 1_000_000) { - return String.format("$%.1fM", price / 1_000_000.0); - } else if (price >= 1_000) { - return String.format("$%.1fK", price / 1_000.0); - } else { - return String.format("$%.0f", price); - } - } - - // Helper methods (unchanged) - private boolean isBlacklisted(String playerName) { - if (playerName == null || blacklistedPlayers.get().isEmpty()) return false; - return blacklistedPlayers.get().stream().anyMatch(p -> p.equalsIgnoreCase(playerName)); - } - - private boolean isEndStone(ItemStack stack) { - return stack.getItem() == Items.END_STONE || stack.getName().getString().toLowerCase(Locale.ROOT).contains("end"); - } - - private boolean isShulkerBox(ItemStack stack) { - return !stack.isEmpty() && stack.getItem().getName().getString().toLowerCase(Locale.ROOT).contains("shulker box"); - } - - private boolean isPurple(ItemStack stack) { - return stack.getItem() == Items.SHULKER_BOX; - } - - private boolean isGreenGlass(ItemStack stack) { - return stack.getItem() == Items.LIME_STAINED_GLASS_PANE || stack.getItem() == Items.GREEN_STAINED_GLASS_PANE; - } - - private boolean isInventoryFull() { - for (int i = 9; i <= 35; i++) { - ItemStack stack = mc.player.getInventory().getStack(i); - if (stack.isEmpty()) return false; - } - return true; - } - - // Utility method to add info messages - public void info(String message, Object... args) { - if (notifications.get()) { - ChatUtils.info(String.format(message, args)); - } - } -} diff --git a/src/main/java/com/nnpg/glazed/modules/main/AutoShulkerShellOrder.java b/src/main/java/com/nnpg/glazed/modules/main/AutoShulkerShellOrder.java deleted file mode 100644 index e80dd634..00000000 --- a/src/main/java/com/nnpg/glazed/modules/main/AutoShulkerShellOrder.java +++ /dev/null @@ -1,516 +0,0 @@ -package com.nnpg.glazed.modules.main; - -import com.nnpg.glazed.GlazedAddon; -import meteordevelopment.meteorclient.events.world.TickEvent; -import meteordevelopment.meteorclient.settings.*; -import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.meteorclient.utils.player.ChatUtils; -import meteordevelopment.orbit.EventHandler; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.item.tooltip.TooltipType; -import net.minecraft.screen.ScreenHandler; -import net.minecraft.screen.slot.Slot; -import net.minecraft.screen.slot.SlotActionType; -import net.minecraft.text.Text; - -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class AutoShulkerShellOrder extends Module { - private final MinecraftClient mc = MinecraftClient.getInstance(); - - private enum Stage {NONE, SHOP, SHOP_END, SHOP_SHULKER_SHELLS, SHOP_GLASS_PANE, SHOP_BUY_ONE, SHOP_CHECK_FULL, SHOP_EXIT, WAIT, ORDERS, ORDERS_SELECT, ORDERS_EXIT, ORDERS_CONFIRM, ORDERS_FINAL_EXIT, CYCLE_PAUSE} - - private Stage stage = Stage.NONE; - private long stage_start = 0; - private static final long WAIT_TIME_MS = 50; - private int shell_move_index = 0; - private long last_shell_move_time = 0; - private int exit_count = 0; - private int final_exit_count = 0; - private long final_exit_start = 0; - - private final SettingGroup sg_general = settings.getDefaultGroup(); - private final SettingGroup sg_blacklist = settings.createGroup("Blacklist"); - - private final Setting min_price = sg_general.add(new StringSetting.Builder() - .name("min-price") - .description("Minimum price to deliver shulker shells for (supports K, M, B suffixes).") - .defaultValue("350.00001") - .build() - ); - - private final Setting notifications = sg_general.add(new BoolSetting.Builder() - .name("notifications") - .description("Show detailed price checking notifications.") - .defaultValue(true) - .build() - ); - - private final Setting speed_mode = sg_general.add(new BoolSetting.Builder() - .name("speed-mode") - .description("Maximum speed mode - removes most delays (may be unstable).") - .defaultValue(true) - .build() - ); - - private final Setting> blacklisted_players = sg_blacklist.add(new StringListSetting.Builder() - .name("blacklisted-players") - .description("Players whose orders will be ignored.") - .defaultValue(List.of()) - .build() - ); - - public AutoShulkerShellOrder() { - super(GlazedAddon.CATEGORY, "auto-shulker-shell-order", "Automatically buys shulker shells and sells them in orders for profit (FAST MODE)"); - } - - @Override - public void onActivate() { - double parsed_price = parse_price(min_price.get()); - if (parsed_price == -1.0) { - if (notifications.get()) { - ChatUtils.error("Invalid minimum price format!"); - } - toggle(); - return; - } - - stage = Stage.SHOP; - stage_start = System.currentTimeMillis(); - shell_move_index = 0; - last_shell_move_time = 0; - exit_count = 0; - final_exit_count = 0; - - if (notifications.get()) { - info("πŸš€ FAST AutoShulkerShellOrder activated! Minimum: %s", min_price.get()); - } - } - - @Override - public void onDeactivate() { - stage = Stage.NONE; - } - - @EventHandler - private void onTick(TickEvent.Post event) { - if (mc.player == null || mc.world == null) return; - long now = System.currentTimeMillis(); - - switch (stage) { - case SHOP -> { - ChatUtils.sendPlayerMsg("/shop"); - stage = Stage.SHOP_END; - stage_start = now; - } - case SHOP_END -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_end_stone(stack)) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_SHULKER_SHELLS; - stage_start = now; - return; - } - } - if (now - stage_start > (speed_mode.get() ? 1000 : 3000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case SHOP_SHULKER_SHELLS -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_shulker_shell(stack) && slot.inventory != mc.player.getInventory()) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_GLASS_PANE; - stage_start = now; - return; - } - } - if (now - stage_start > (speed_mode.get() ? 300 : 1000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case SHOP_GLASS_PANE -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_glass_pane(stack) && stack.getCount() == 64) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_BUY_ONE; - stage_start = now; - return; - } - } - - if (now - stage_start > (speed_mode.get() ? 300 : 1000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case SHOP_BUY_ONE -> { - long wait_delay = speed_mode.get() ? 500 : 1000; - if (now - stage_start >= wait_delay) { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_green_glass(stack) && stack.getCount() == 1) { - int max_clicks = speed_mode.get() ? 50 : 30; - for (int i = 0; i < max_clicks; i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - if (is_inventory_full()) break; - } - stage = Stage.SHOP_CHECK_FULL; - stage_start = now; - return; - } - } - - if (now - stage_start > (speed_mode.get() ? 2000 : 3000)) { - stage = Stage.SHOP_GLASS_PANE; - stage_start = now; - } - } - } - } - case SHOP_CHECK_FULL -> { - mc.player.closeHandledScreen(); - stage = Stage.SHOP_EXIT; - stage_start = now; - } - case SHOP_EXIT -> { - if (mc.currentScreen == null) { - stage = Stage.WAIT; - stage_start = now; - } - if (now - stage_start > (speed_mode.get() ? 1000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - case WAIT -> { - long wait_time = speed_mode.get() ? 25 : WAIT_TIME_MS; - if (now - stage_start >= wait_time) { - ChatUtils.sendPlayerMsg("/orders shulker shell"); - stage = Stage.ORDERS; - stage_start = now; - } - } - case ORDERS -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_shulker_shell(stack)) { - if (is_blacklisted(get_order_player_name(stack))) continue; - double order_price = get_order_price(stack); - double min_price_value = parse_price(min_price.get()); - - if (order_price > 1500) { - continue; - } - - if (order_price >= min_price_value) { - if (notifications.get()) { - info("βœ… Found shell order: %s", format_price(order_price)); - } - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.ORDERS_SELECT; - stage_start = now; - shell_move_index = 0; - last_shell_move_time = 0; - return; - } - } - } - if (now - stage_start > (speed_mode.get() ? 2000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case ORDERS_SELECT -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - if (shell_move_index >= 36) { - mc.player.closeHandledScreen(); - stage = Stage.ORDERS_CONFIRM; - stage_start = now; - shell_move_index = 0; - return; - } - - long move_delay = speed_mode.get() ? 10 : 100; - if (now - last_shell_move_time >= move_delay) { - int batch_size = speed_mode.get() ? 3 : 1; - - for (int batch = 0; batch < batch_size && shell_move_index < 36; batch++) { - ItemStack stack = mc.player.getInventory().getStack(shell_move_index); - if (is_shulker_shell(stack)) { - int player_slot_id = -1; - for (Slot slot : handler.slots) { - if (slot.inventory == mc.player.getInventory() && slot.getIndex() == shell_move_index) { - player_slot_id = slot.id; - break; - } - } - - if (player_slot_id != -1) { - mc.interactionManager.clickSlot(handler.syncId, player_slot_id, 0, SlotActionType.QUICK_MOVE, mc.player); - } - } - shell_move_index++; - } - last_shell_move_time = now; - } - } - } - case ORDERS_EXIT -> { - if (mc.currentScreen == null) { - exit_count++; - if (exit_count < 2) { - mc.player.closeHandledScreen(); - stage_start = now; - } else { - exit_count = 0; - stage = Stage.ORDERS_CONFIRM; - stage_start = now; - } - } - } - case ORDERS_CONFIRM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && is_green_glass(stack)) { - for (int i = 0; i < (speed_mode.get() ? 15 : 5); i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - stage = Stage.ORDERS_FINAL_EXIT; - stage_start = now; - final_exit_count = 0; - final_exit_start = now; - return; - } - } - if (now - stage_start > (speed_mode.get() ? 2000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stage_start = now; - } - } - } - case ORDERS_FINAL_EXIT -> { - long exit_delay = speed_mode.get() ? 50 : 200; - - if (final_exit_count == 0) { - if (System.currentTimeMillis() - final_exit_start >= exit_delay) { - mc.player.closeHandledScreen(); - final_exit_count++; - final_exit_start = System.currentTimeMillis(); - } - } else if (final_exit_count == 1) { - if (System.currentTimeMillis() - final_exit_start >= exit_delay) { - mc.player.closeHandledScreen(); - final_exit_count++; - final_exit_start = System.currentTimeMillis(); - } - } else { - final_exit_count = 0; - stage = Stage.CYCLE_PAUSE; - stage_start = System.currentTimeMillis(); - } - } - case CYCLE_PAUSE -> { - long cycle_wait = speed_mode.get() ? 25 : WAIT_TIME_MS; - if (now - stage_start >= cycle_wait) { - stage = Stage.SHOP; - stage_start = now; - } - } - case NONE -> { - } - } - } - - private boolean is_blacklisted(String playerName) { - if (playerName == null || blacklisted_players.get().isEmpty()) return false; - return blacklisted_players.get().stream().anyMatch(p -> p.equalsIgnoreCase(playerName)); - } - - private String get_order_player_name(ItemStack stack) { - if (stack.isEmpty()) return null; - Item.TooltipContext ctx = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(ctx, mc.player, TooltipType.BASIC); - Pattern[] patterns = { - Pattern.compile("(?i)player\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)from\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)by\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)seller\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)owner\\s*:\\s*([a-zA-Z0-9_]+)") - }; - for (Text line : tooltip) { - String text = line.getString(); - for (Pattern p : patterns) { - Matcher m = p.matcher(text); - if (m.find()) { - String name = m.group(1); - if (name.length() >= 3 && name.length() <= 16) return name; - } - } - } - return null; - } - - private double get_order_price(ItemStack stack) { - if (stack.isEmpty()) { - return -1.0; - } - - Item.TooltipContext tooltip_context = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(tooltip_context, mc.player, TooltipType.BASIC); - - return parse_tooltip_price(tooltip); - } - - private double parse_tooltip_price(List tooltip) { - if (tooltip == null || tooltip.isEmpty()) { - return -1.0; - } - - Pattern[] price_patterns = { - Pattern.compile("\\$([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)price\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)pay\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)reward\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("([\\d,]+(?:\\.[\\d]+)?)([kmb])?\\s*coins?", Pattern.CASE_INSENSITIVE), - Pattern.compile("\\b([\\d,]+(?:\\.[\\d]+)?)([kmb])\\b", Pattern.CASE_INSENSITIVE) - }; - - for (Text line : tooltip) { - String text = line.getString(); - - for (Pattern pattern : price_patterns) { - Matcher matcher = pattern.matcher(text); - if (matcher.find()) { - String number_str = matcher.group(1).replace(",", ""); - String suffix = ""; - if (matcher.groupCount() >= 2 && matcher.group(2) != null) { - suffix = matcher.group(2).toLowerCase(); - } - - try { - double base_price = Double.parseDouble(number_str); - double multiplier = 1.0; - - switch (suffix) { - case "k" -> multiplier = 1_000.0; - case "m" -> multiplier = 1_000_000.0; - case "b" -> multiplier = 1_000_000_000.0; - } - - return base_price * multiplier; - } catch (NumberFormatException e) { - } - } - } - } - - return -1.0; - } - - private double parse_price(String price_str) { - if (price_str == null || price_str.isEmpty()) { - return -1.0; - } - - String cleaned = price_str.trim().toLowerCase().replace(",", ""); - double multiplier = 1.0; - - if (cleaned.endsWith("b")) { - multiplier = 1_000_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("m")) { - multiplier = 1_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("k")) { - multiplier = 1_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } - - try { - return Double.parseDouble(cleaned) * multiplier; - } catch (NumberFormatException e) { - return -1.0; - } - } - - private String format_price(double price) { - if (price >= 1_000_000_000) { - return String.format("$%.1fB", price / 1_000_000_000.0); - } else if (price >= 1_000_000) { - return String.format("$%.1fM", price / 1_000_000.0); - } else if (price >= 1_000) { - return String.format("$%.1fK", price / 1_000.0); - } else { - return String.format("$%.0f", price); - } - } - - private boolean is_end_stone(ItemStack stack) { - return stack.getItem() == Items.END_STONE || stack.getName().getString().toLowerCase(Locale.ROOT).contains("end"); - } - - private boolean is_shulker_shell(ItemStack stack) { - return !stack.isEmpty() && stack.getItem() == Items.SHULKER_SHELL; - } - - private boolean is_glass_pane(ItemStack stack) { - String item_name = stack.getItem().getName().getString().toLowerCase(); - return item_name.contains("glass") && item_name.contains("pane"); - } - - private boolean is_buy_button(ItemStack stack) { - String display_name = stack.getName().getString().toLowerCase(); - return display_name.contains("buy") && display_name.contains("one"); - } - - private boolean is_green_glass(ItemStack stack) { - return stack.getItem() == Items.LIME_STAINED_GLASS_PANE || stack.getItem() == Items.GREEN_STAINED_GLASS_PANE; - } - - private boolean is_inventory_full() { - for (int i = 9; i <= 35; i++) { - ItemStack stack = mc.player.getInventory().getStack(i); - if (stack.isEmpty()) return false; - } - return true; - } -} diff --git a/src/main/java/com/nnpg/glazed/modules/main/AutoTotemOrder.java b/src/main/java/com/nnpg/glazed/modules/main/AutoTotemOrder.java deleted file mode 100644 index 256e179f..00000000 --- a/src/main/java/com/nnpg/glazed/modules/main/AutoTotemOrder.java +++ /dev/null @@ -1,646 +0,0 @@ -package com.nnpg.glazed.modules.main; - -import com.nnpg.glazed.GlazedAddon; -import meteordevelopment.meteorclient.events.world.TickEvent; -import meteordevelopment.meteorclient.settings.*; -import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.meteorclient.utils.player.ChatUtils; -import meteordevelopment.orbit.EventHandler; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.screen.ingame.GenericContainerScreen; -import net.minecraft.item.Item; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.item.tooltip.TooltipType; -import net.minecraft.screen.ScreenHandler; -import net.minecraft.screen.slot.Slot; -import net.minecraft.screen.slot.SlotActionType; -import net.minecraft.text.Text; - -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class AutoTotemOrder extends Module { - private final MinecraftClient mc = MinecraftClient.getInstance(); - - private enum Stage {NONE, SHOP, SHOP_GEAR, SHOP_TOTEM, SHOP_CONFIRM, SHOP_CHECK_FULL, SHOP_EXIT, WAIT, ORDERS, ORDERS_SELECT, ORDERS_EXIT, ORDERS_CONFIRM, ORDERS_FINAL_EXIT, CYCLE_PAUSE, TARGET_ORDERS} - - private Stage stage = Stage.NONE; - private long stageStart = 0; - private static final long WAIT_TIME_MS = 50; - private int shulkerMoveIndex = 0; - private long lastShulkerMoveTime = 0; - private int exitCount = 0; - private int finalExitCount = 0; - private long finalExitStart = 0; - private int bulkBuyCount = 0; - private static final int MAX_BULK_BUY = 5; - - // Player targeting variables - private String targetPlayer = ""; - private boolean isTargetingActive = false; - - // Settings - private final SettingGroup sgGeneral = settings.getDefaultGroup(); - private final SettingGroup sgTargeting = settings.createGroup("Player Targeting"); - - private final Setting minPrice = sgGeneral.add(new StringSetting.Builder() - .name("min-price") - .description("Minimum price to deliver totems for (supports K, M, B suffixes).") - .defaultValue("850") - .build() - ); - - private final Setting notifications = sgGeneral.add(new BoolSetting.Builder() - .name("notifications") - .description("Show detailed price checking notifications.") - .defaultValue(true) - .build() - ); - - private final Setting speedMode = sgGeneral.add(new BoolSetting.Builder() - .name("speed-mode") - .description("Maximum speed mode - removes most delays (may be unstable).") - .defaultValue(true) - .build() - ); - - // New targeting settings - private final Setting enableTargeting = sgTargeting.add(new BoolSetting.Builder() - .name("enable-targeting") - .description("Enable targeting a specific player (ignores minimum price).") - .defaultValue(false) - .build() - ); - - private final Setting targetPlayerName = sgTargeting.add(new StringSetting.Builder() - .name("target-player") - .description("Specific player name to target for orders.") - .defaultValue("") - .visible(() -> enableTargeting.get()) - .build() - ); - - private final Setting targetOnlyMode = sgTargeting.add(new BoolSetting.Builder() - .name("target-only-mode") - .description("Only look for orders from the targeted player, ignore all others.") - .defaultValue(false) - .visible(() -> enableTargeting.get()) - .build() - ); - - private final Setting> blacklistedPlayers = sgTargeting.add(new StringListSetting.Builder() - .name("blacklisted-players") - .description("Players whose orders will be ignored.") - .defaultValue(List.of()) - .build() - ); - - public AutoTotemOrder() { - super(GlazedAddon.CATEGORY, "auto-totem-order", "Automatically buys totems and sells them in orders for profit with player targeting"); - } - - @Override - public void onActivate() { - double parsedPrice = parsePrice(minPrice.get()); - if (parsedPrice == -1.0 && !enableTargeting.get()) { - if (notifications.get()) { - ChatUtils.error("Invalid minimum price format!"); - } - toggle(); - return; - } - - // Setup target player - updateTargetPlayer(); - - stage = Stage.SHOP; // Always start with shop to buy totems first - stageStart = System.currentTimeMillis(); - shulkerMoveIndex = 0; - lastShulkerMoveTime = 0; - exitCount = 0; - finalExitCount = 0; - bulkBuyCount = 0; - - if (notifications.get()) { - String modeInfo = isTargetingActive ? - String.format(" | Targeting: %s", targetPlayer) : ""; - info("πŸš€ FAST AutoTotemOrder activated! Minimum: %s%s", minPrice.get(), modeInfo); - } - } - - @Override - public void onDeactivate() { - stage = Stage.NONE; - } - - private void updateTargetPlayer() { - targetPlayer = ""; - isTargetingActive = false; - - if (enableTargeting.get() && !targetPlayerName.get().trim().isEmpty()) { - targetPlayer = targetPlayerName.get().trim(); - isTargetingActive = true; - - if (notifications.get()) { - info("🎯 Targeting enabled for player: %s", targetPlayer); - } - } else { - if (notifications.get() && enableTargeting.get()) { - info("⚠️ Targeting disabled - no player name provided"); - } - } - } - - @EventHandler - private void onTick(TickEvent.Post event) { - if (mc.player == null || mc.world == null) return; - long now = System.currentTimeMillis(); - - switch (stage) { - case TARGET_ORDERS -> { - ChatUtils.sendPlayerMsg("/orders " + targetPlayer); - stage = Stage.ORDERS; - stageStart = now; - - if (notifications.get()) { - info("πŸ” Checking orders for: %s", targetPlayer); - } - } - case SHOP -> { - ChatUtils.sendPlayerMsg("/shop"); - stage = Stage.SHOP_GEAR; - stageStart = now; - } - case SHOP_GEAR -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isTotemOfUndying(stack)) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - stage = Stage.SHOP_TOTEM; - stageStart = now; - bulkBuyCount = 0; - return; - } - } - if (now - stageStart > (speedMode.get() ? 1000 : 3000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stageStart = now; - } - } - } - case SHOP_TOTEM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - boolean foundTotem = false; - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isTotemOfUndying(stack)) { - // CONTROLLED BUYING - slower to prevent overshooting - int clickCount = speedMode.get() ? 10 : 5; // Reduced from 64/27 to 10/5 - for (int i = 0; i < clickCount; i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - foundTotem = true; - bulkBuyCount++; - break; - } - } - - if (foundTotem) { - stage = Stage.SHOP_CONFIRM; - stageStart = now; - return; - } - if (now - stageStart > (speedMode.get() ? 500 : 1500)) { // Slightly longer wait - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stageStart = now; - } - } - } - case SHOP_CONFIRM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - boolean foundGreen = false; - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isGreenGlass(stack)) { - // CONTROLLED CONFIRM - fewer clicks to prevent over-confirming - for (int i = 0; i < (speedMode.get() ? 3 : 2); i++) { // Reduced from 10/5 to 3/2 - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - foundGreen = true; - break; - } - } - if (foundGreen) { - stage = Stage.SHOP_CHECK_FULL; - stageStart = now; - return; - } - if (now - stageStart > (speedMode.get() ? 200 : 800)) { // Slightly longer wait - stage = Stage.SHOP_TOTEM; - stageStart = now; - } - } - } - case SHOP_CHECK_FULL -> { - // Add a small delay before checking inventory to let transactions process - if (now - stageStart > (speedMode.get() ? 100 : 200)) { - if (isInventoryFull()) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP_EXIT; - stageStart = now; - } else { - // Small pause before buying more to prevent rapid-fire purchases - if (now - stageStart > (speedMode.get() ? 200 : 400)) { - stage = Stage.SHOP_TOTEM; - stageStart = now; - } - } - } - } - case SHOP_EXIT -> { - if (mc.currentScreen == null) { - stage = Stage.WAIT; - stageStart = now; - } - if (now - stageStart > (speedMode.get() ? 1000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; - stageStart = now; - } - } - case WAIT -> { - long waitTime = speedMode.get() ? 25 : WAIT_TIME_MS; - if (now - stageStart >= waitTime) { - // Only use target orders if targeting is enabled AND we have a valid target - if (isTargetingActive && !targetPlayer.isEmpty()) { - stage = Stage.TARGET_ORDERS; - } else { - ChatUtils.sendPlayerMsg("/orders totem"); - stage = Stage.ORDERS; - } - stageStart = now; - } - } - case ORDERS -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - boolean foundOrder = false; - - // Add delay in speed mode to ensure GUI is fully loaded - if (speedMode.get() && now - stageStart < 200) { - return; // Wait 200ms for GUI to stabilize in speed mode - } - - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isTotemOfUndying(stack) && isPurple(stack)) { - boolean shouldTakeOrder = false; - String orderPlayer = getOrderPlayerName(stack); - - if (isBlacklisted(orderPlayer)) continue; - - boolean isTargetedOrder = isTargetingActive && - orderPlayer != null && - orderPlayer.equalsIgnoreCase(targetPlayer); - - if (isTargetedOrder) { - shouldTakeOrder = true; - if (notifications.get()) { - double orderPrice = getOrderPrice(stack); - info("🎯 Found TARGET order from %s: %s", orderPlayer, - orderPrice > 0 ? formatPrice(orderPrice) : "Unknown price"); - } - } else if (!targetOnlyMode.get()) { - // Regular price check for non-targeted orders - double orderPrice = getOrderPrice(stack); - double minPriceValue = parsePrice(minPrice.get()); - - if (orderPrice >= minPriceValue) { - shouldTakeOrder = true; - if (notifications.get()) { - info("βœ… Found order: %s", formatPrice(orderPrice)); - } - } - } - - if (shouldTakeOrder) { - // Click on the order to select it - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - - // Wait a moment before moving to selection phase - stage = Stage.ORDERS_SELECT; - stageStart = now + (speedMode.get() ? 100 : 50); // Small delay to let selection register - shulkerMoveIndex = 0; - lastShulkerMoveTime = 0; - foundOrder = true; - - if (notifications.get()) { - info("πŸ”„ Selected order, preparing to transfer items..."); - } - return; - } - } - } - - if (!foundOrder && now - stageStart > (speedMode.get() ? 3000 : 5000)) { // Longer wait in speed mode - mc.player.closeHandledScreen(); - stage = Stage.SHOP; // Always go back to shop after checking orders - stageStart = now; - } - } - } - case ORDERS_SELECT -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - - if (shulkerMoveIndex >= 36) { - mc.player.closeHandledScreen(); - stage = Stage.ORDERS_CONFIRM; - stageStart = now; - shulkerMoveIndex = 0; - return; - } - - long moveDelay = speedMode.get() ? 10 : 100; - if (now - lastShulkerMoveTime >= moveDelay) { - int batchSize = speedMode.get() ? 3 : 1; - - for (int batch = 0; batch < batchSize && shulkerMoveIndex < 36; batch++) { - ItemStack stack = mc.player.getInventory().getStack(shulkerMoveIndex); - if (isTotemOfUndying(stack)) { - int playerSlotId = -1; - for (Slot slot : handler.slots) { - if (slot.inventory == mc.player.getInventory() && slot.getIndex() == shulkerMoveIndex) { - playerSlotId = slot.id; - break; - } - } - - if (playerSlotId != -1) { - mc.interactionManager.clickSlot(handler.syncId, playerSlotId, 0, SlotActionType.QUICK_MOVE, mc.player); - } - } - shulkerMoveIndex++; - } - lastShulkerMoveTime = now; - } - } - } - case ORDERS_EXIT -> { - if (mc.currentScreen == null) { - exitCount++; - if (exitCount < 2) { - mc.player.closeHandledScreen(); - stageStart = now; - } else { - exitCount = 0; - stage = Stage.ORDERS_CONFIRM; - stageStart = now; - } - } - } - case ORDERS_CONFIRM -> { - if (mc.currentScreen instanceof GenericContainerScreen screen) { - ScreenHandler handler = screen.getScreenHandler(); - for (Slot slot : handler.slots) { - ItemStack stack = slot.getStack(); - if (!stack.isEmpty() && isGreenGlass(stack)) { - for (int i = 0; i < (speedMode.get() ? 15 : 5); i++) { - mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - } - stage = Stage.ORDERS_FINAL_EXIT; - stageStart = now; - finalExitCount = 0; - finalExitStart = now; - - if (notifications.get()) { - info("βœ… Order completed! Going back to shop to buy more totems..."); - } - return; - } - } - if (now - stageStart > (speedMode.get() ? 2000 : 5000)) { - mc.player.closeHandledScreen(); - stage = Stage.SHOP; // Go directly to shop if confirmation fails - stageStart = now; - } - } - } - case ORDERS_FINAL_EXIT -> { - long exitDelay = speedMode.get() ? 50 : 200; - - if (finalExitCount == 0) { - if (System.currentTimeMillis() - finalExitStart >= exitDelay) { - mc.player.closeHandledScreen(); - finalExitCount++; - finalExitStart = System.currentTimeMillis(); - } - } else if (finalExitCount == 1) { - if (System.currentTimeMillis() - finalExitStart >= exitDelay) { - mc.player.closeHandledScreen(); - finalExitCount++; - finalExitStart = System.currentTimeMillis(); - } - } else { - finalExitCount = 0; - stage = Stage.CYCLE_PAUSE; - stageStart = System.currentTimeMillis(); - } - } - case CYCLE_PAUSE -> { - long cycleWait = speedMode.get() ? 10 : 25; // Very fast cycle restart - if (now - stageStart >= cycleWait) { - // Always go back to shop to buy more totems - updateTargetPlayer(); // Refresh target player - stage = Stage.SHOP; - stageStart = now; - } - } - case NONE -> { - } - } - } - - // New method to extract player name from order tooltip - private String getOrderPlayerName(ItemStack stack) { - if (stack.isEmpty()) { - return null; - } - - Item.TooltipContext tooltipContext = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(tooltipContext, mc.player, TooltipType.BASIC); - - for (Text line : tooltip) { - String text = line.getString(); - - // Look for patterns like "Player: PlayerName" or "From: PlayerName" or "By: PlayerName" - Pattern[] namePatterns = { - Pattern.compile("(?i)player\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)from\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)by\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)seller\\s*:\\s*([a-zA-Z0-9_]+)"), - Pattern.compile("(?i)owner\\s*:\\s*([a-zA-Z0-9_]+)"), - // Generic pattern for username-like strings - Pattern.compile("\\b([a-zA-Z0-9_]{3,16})\\b") - }; - - for (Pattern pattern : namePatterns) { - Matcher matcher = pattern.matcher(text); - if (matcher.find()) { - String playerName = matcher.group(1); - // Basic validation for Minecraft usernames - if (playerName.length() >= 3 && playerName.length() <= 16 && - playerName.matches("[a-zA-Z0-9_]+")) { - return playerName; - } - } - } - } - - return null; - } - - // Price parsing methods (unchanged) - private double getOrderPrice(ItemStack stack) { - if (stack.isEmpty()) { - return -1.0; - } - - Item.TooltipContext tooltipContext = Item.TooltipContext.create(mc.world); - List tooltip = stack.getTooltip(tooltipContext, mc.player, TooltipType.BASIC); - - return parseTooltipPrice(tooltip); - } - - private double parseTooltipPrice(List tooltip) { - if (tooltip == null || tooltip.isEmpty()) { - return -1.0; - } - - Pattern[] pricePatterns = { - Pattern.compile("\\$([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)price\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)pay\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("(?i)reward\\s*:\\s*([\\d,]+(?:\\.[\\d]+)?)([kmb])?", Pattern.CASE_INSENSITIVE), - Pattern.compile("([\\d,]+(?:\\.[\\d]+)?)([kmb])?\\s*coins?", Pattern.CASE_INSENSITIVE), - Pattern.compile("\\b([\\d,]+(?:\\.[\\d]+)?)([kmb])\\b", Pattern.CASE_INSENSITIVE) - }; - - for (Text line : tooltip) { - String text = line.getString(); - - for (Pattern pattern : pricePatterns) { - Matcher matcher = pattern.matcher(text); - if (matcher.find()) { - String numberStr = matcher.group(1).replace(",", ""); - String suffix = ""; - if (matcher.groupCount() >= 2 && matcher.group(2) != null) { - suffix = matcher.group(2).toLowerCase(); - } - - try { - double basePrice = Double.parseDouble(numberStr); - double multiplier = 1.0; - - switch (suffix) { - case "k" -> multiplier = 1_000.0; - case "m" -> multiplier = 1_000_000.0; - case "b" -> multiplier = 1_000_000_000.0; - } - - return basePrice * multiplier; - } catch (NumberFormatException e) { - // Continue to next pattern - } - } - } - } - - return -1.0; - } - - private double parsePrice(String priceStr) { - if (priceStr == null || priceStr.isEmpty()) { - return -1.0; - } - - String cleaned = priceStr.trim().toLowerCase().replace(",", ""); - double multiplier = 1.0; - - if (cleaned.endsWith("b")) { - multiplier = 1_000_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("m")) { - multiplier = 1_000_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } else if (cleaned.endsWith("k")) { - multiplier = 1_000.0; - cleaned = cleaned.substring(0, cleaned.length() - 1); - } - - try { - return Double.parseDouble(cleaned) * multiplier; - } catch (NumberFormatException e) { - return -1.0; - } - } - - private String formatPrice(double price) { - if (price >= 1_000_000_000) { - return String.format("$%.1fB", price / 1_000_000_000.0); - } else if (price >= 1_000_000) { - return String.format("$%.1fM", price / 1_000_000.0); - } else if (price >= 1_000) { - return String.format("$%.1fK", price / 1_000.0); - } else { - return String.format("$%.0f", price); - } - } - - // Helper methods (unchanged) - private boolean isBlacklisted(String playerName) { - if (playerName == null || blacklistedPlayers.get().isEmpty()) return false; - return blacklistedPlayers.get().stream().anyMatch(p -> p.equalsIgnoreCase(playerName)); - } - - private boolean isTotemOfUndying(ItemStack stack) { - return stack.getItem() == Items.TOTEM_OF_UNDYING; - } - - private boolean isShulkerBox(ItemStack stack) { - return !stack.isEmpty() && stack.getItem().getName().getString().toLowerCase(Locale.ROOT).contains("shulker box"); - } - - private boolean isPurple(ItemStack stack) { - return stack.getItem() == Items.TOTEM_OF_UNDYING; - } - - private boolean isGreenGlass(ItemStack stack) { - return stack.getItem() == Items.LIME_STAINED_GLASS_PANE || stack.getItem() == Items.GREEN_STAINED_GLASS_PANE; - } - - private boolean isInventoryFull() { - for (int i = 9; i <= 35; i++) { - ItemStack stack = mc.player.getInventory().getStack(i); - if (stack.isEmpty()) return false; - } - return true; - } - - // Utility method to add info messages - public void info(String message, Object... args) { - if (notifications.get()) { - ChatUtils.info(String.format(message, args)); - } - } -} From b3840096131b7196fc13e2c95dc18f576f4f47be Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Sun, 29 Mar 2026 19:14:40 +0200 Subject: [PATCH 3/6] command fix --- changes_1.md | 52 ------- .../nnpg/glazed/commands/AHItemCommand.java | 20 +-- .../glazed/managers/SellHotbarManager.java | 2 +- .../modules/esp/HoleTunnelStairsESP.java | 130 +++++++++--------- .../modules/esp/PearlLandingPredictor.java | 48 +++---- .../glazed/modules/main/AutoShopOrder.java | 88 ++++++------ 6 files changed, 144 insertions(+), 196 deletions(-) delete mode 100644 changes_1.md diff --git a/changes_1.md b/changes_1.md deleted file mode 100644 index 445f893d..00000000 --- a/changes_1.md +++ /dev/null @@ -1,52 +0,0 @@ -# πŸ› οΈ HoleTunnelStairsESP & Glazed Addon - Changelog - -## ✨ New Major Modules & Commands - -- **Auto Shop Order (`AutoShopOrder.java`)** - - **Consolidation:** Replaces legacy modules `AutoBlazeRodOrder`, `AutoShulkerOrder`, `AutoTotemOrder`, `AutoShulkerShellOrder`, and `AutoShellOrder`. - - **Logic:** Features a robust state-machine for navigating `/shop`, bulk buying, and identifying the most profitable `/orders`. - - **Features:** "AUTO" pricing (Shop + $1), custom price suffixes (K, M, B), player blacklist, and an anti-stuck system. -- **Pearl Landing Predictor (`PearlLandingPredictor.java`)** - - **Function:** Simulates Ender Pearl trajectories to predict and highlight landing points. - - **Features:** Player filtering, live updates for new chunks, and distinct rendering for unknown owners. - - **Visuals:** Now correctly renders both the landing destination box and the player’s name. -- **AH Sell Command (`.sell_hotbar`)** - - **Function:** Command-based version of `AHSell.java`. Automatically lists all hotbar items via `/ah sell `. -- **AH Item Command (`.ahitem`)** - - **Function:** Instantly searches the Auction House for the item in hand. - - **Smart Search:** Automatically appends enchantments (e.g., `sharpness 5`) and "stack" parameters to the search query. - ---- - -## πŸ”„ Module Improvements & Refactors - -### 🟦 HoleTunnelStairsESP (Major Overhaul) - -The `CoveredHole.java` module has been fully integrated into `HoleTunnelStairsESP` to centralize logic and reduce overhead. - -- **Variable Tunnel Width:** New `minTunnelWidth`/`maxTunnelWidth` settings (default 1-3) for detecting 1x1 to 3x3 tunnels. -- **Covered Hole Detection:** - - Identifies 1x1 and 1x3 holes covered by solid blocks. - - **Smart Filtering:** `only-player-covered` logic distinguishes player-made covers from natural terrain generation. - - **Dedicated Rendering:** Custom colors for covered holes to avoid visual confusion. -- **Dynamic Updates:** Added `undergroundUpdateThreshold` and packet listeners for Y < 0 updates on servers with dynamic chunk loading. -- **Optimization:** Hash-based deduplication (`tunnelHashes`) for O(1) lookups, preventing redundant rendering across chunk borders. - ---- - -## πŸ› Bug Fixes - -- **HoleTunnelStairsESP:** - - **Double Rendering:** Fixed via canonical start positions and hash-based deduplication. - - **Inconsistent Cross-sections:** Fixed ceiling height detection using the `refHeight` parameter. - - **1x3 Covered Holes:** Corrected detection logic to verify all three top blocks instead of just the start block. -- **Pearl Landing Predictor:** Fixed a bug where the landing box was not rendering (previously only the name label was visible). - ---- - -## πŸš€ Performance Improvements - -- **Scan Efficiency:** `HoleTunnelStairsESP` now uses a **2-pass instead of 3-pass** scan, combining length measurement and end-position recording. -- **Redundancy Check:** Implementation of `CANONICAL_TUNNEL_DIRS` (East/South only) to prevent scanning the same tunnel from opposite directions. -- **Thread Safety:** Integrated `ThreadLocal BitSet` for chunk processing, removing synchronization bottlenecks. -- **Caching:** Added `solidBlockCache` and `blockStateCache` to minimize expensive world-access calls. diff --git a/src/main/java/com/nnpg/glazed/commands/AHItemCommand.java b/src/main/java/com/nnpg/glazed/commands/AHItemCommand.java index f88168bd..34143165 100644 --- a/src/main/java/com/nnpg/glazed/commands/AHItemCommand.java +++ b/src/main/java/com/nnpg/glazed/commands/AHItemCommand.java @@ -30,17 +30,17 @@ public void build(LiteralArgumentBuilder builder) { return SINGLE_SUCCESS; } - // Get item name + String itemName = getItemName(mainHandItem); - // Get enchantments using the new API + List enchantmentStrings = getEnchantments(mainHandItem); - // Build the search command + StringBuilder searchCommand = new StringBuilder("ah "); searchCommand.append(itemName); - // Add "stack" suffix if item is a full stack (64 items) + if (mainHandItem.getCount() == 64) { searchCommand.append(" stack"); } @@ -50,7 +50,7 @@ public void build(LiteralArgumentBuilder builder) { searchCommand.append(String.join(" ", enchantmentStrings)); } - // Send the command + String command = searchCommand.toString(); info("Searching: /" + command); mc.getNetworkHandler().sendChatCommand(command); @@ -60,10 +60,10 @@ public void build(LiteralArgumentBuilder builder) { } private String getItemName(ItemStack stack) { - // Get the item ID + String itemId = stack.getItem().toString(); - // Remove namespace if present (e.g., "minecraft:diamond_sword" -> "diamond_sword") + if (itemId.contains(":")) { itemId = itemId.split(":")[1]; } @@ -74,7 +74,7 @@ private String getItemName(ItemStack stack) { private List getEnchantments(ItemStack stack) { List result = new ArrayList<>(); - // Use the new 1.20.5+ API + ItemEnchantmentsComponent enchantments = EnchantmentHelper.getEnchantments(stack); for (RegistryEntry entry : enchantments.getEnchantments()) { @@ -87,10 +87,10 @@ private List getEnchantments(ItemStack stack) { } private String getEnchantmentName(RegistryEntry enchantmentEntry) { - // Get the enchantment ID from the registry + String enchantmentId = enchantmentEntry.getIdAsString(); - // Remove namespace (e.g., "minecraft:protection" -> "protection") + if (enchantmentId.contains(":")) { enchantmentId = enchantmentId.split(":")[1]; } diff --git a/src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java b/src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java index e101e416..35d28035 100644 --- a/src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java +++ b/src/main/java/com/nnpg/glazed/managers/SellHotbarManager.java @@ -15,7 +15,7 @@ public class SellHotbarManager { private static SellHotbarManager instance; - // mc instance manually since we don't extend Module/Command + private static final MinecraftClient mc = MinecraftClient.getInstance(); private int delayCounter = 0; diff --git a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java index 690ef79b..c42d8653 100644 --- a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java +++ b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java @@ -36,7 +36,7 @@ public class HoleTunnelStairsESP extends Module { private final SettingGroup sgCParams = settings.createGroup("Covered Hole Parameters"); private final SettingGroup sgRender = settings.createGroup("Rendering"); - // == General == + private final Setting detectionMode = sgGeneral.add(new EnumSetting.Builder() .name("Detection Mode") .description("Choose what to detect: holes, tunnels, stairs, or all.") @@ -74,7 +74,7 @@ public class HoleTunnelStairsESP extends Module { .build() ); - // == Hole Parameters == + private final Setting minHoleDepth = sgHParams.add(new IntSetting.Builder() .name("Min Hole Depth") .description("Minimum depth for a hole to be detected.") @@ -82,7 +82,7 @@ public class HoleTunnelStairsESP extends Module { .build() ); - // == Tunnel Parameters == + private final Setting minTunnelLength = sgTParams.add(new IntSetting.Builder() .name("Min Tunnel Length") .description("Minimum length for a tunnel to be detected.") @@ -141,7 +141,7 @@ public class HoleTunnelStairsESP extends Module { .build() ); - // == Stairs Parameters == + private final Setting minStaircaseLength = sgSParams.add(new IntSetting.Builder() .name("Min Staircase Length") .description("Minimum length for a staircase to be detected.") @@ -161,7 +161,7 @@ public class HoleTunnelStairsESP extends Module { .build() ); - // == Covered Hole Parameters == + private final Setting detectCoveredHoles = sgCParams.add(new BoolSetting.Builder() .name("detect-covered-holes") .description("Detects and highlights holes that are covered by solid blocks.") @@ -183,7 +183,7 @@ public class HoleTunnelStairsESP extends Module { .build() ); - // == Rendering == + private final Setting shapeMode = sgRender.add(new EnumSetting.Builder() .name("shape-mode").defaultValue(ShapeMode.Both).build()); private final Setting holeLineColor = sgRender.add(new ColorSetting.Builder() @@ -214,7 +214,7 @@ public class HoleTunnelStairsESP extends Module { private static final Direction[] DIRECTIONS = { Direction.EAST, Direction.WEST, Direction.NORTH, Direction.SOUTH }; private static final Direction[] CANONICAL_TUNNEL_DIRS = { Direction.EAST, Direction.SOUTH }; - // State + private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); private final Queue chunkQueue = new LinkedList<>(); @@ -226,8 +226,8 @@ public class HoleTunnelStairsESP extends Module { private final Set notifiedHoles = ConcurrentHashMap.newKeySet(); private final Set tunnelHashes = ConcurrentHashMap.newKeySet(); - // coveredHoleHashes is not needed if coveredHoles is a ConcurrentHashMap and Box has proper equals/hashCode - // private final Set coveredHoleHashes = ConcurrentHashMap.newKeySet(); + + private final Set holeHashes = ConcurrentHashMap.newKeySet(); private final Set hole3x1Hashes = ConcurrentHashMap.newKeySet(); @@ -235,12 +235,12 @@ public class HoleTunnelStairsESP extends Module { private final ThreadLocal visitedBlocksLocal = ThreadLocal.withInitial(BitSet::new); - // Underground Update Tracking + private final Set pendingUndergroundChunks = ConcurrentHashMap.newKeySet(); private int undergroundBlockUpdates = 0; private boolean needsUndergroundRescan = false; - // Caches for covered hole detection + private final Map solidBlockCache = new ConcurrentHashMap<>(); private final Map blockStateCache = new ConcurrentHashMap<>(); @@ -266,9 +266,9 @@ private void clearAll() { blockStateCache.clear(); } - // ========================================================================= - // PACKET RECEIVE - TARGETED THRESHOLD LOGIC - // ========================================================================= + + + @EventHandler private void onPacketReceive(PacketEvent.Receive event) { if (event.packet instanceof BlockUpdateS2CPacket packet) { @@ -293,11 +293,11 @@ private void onPacketReceive(PacketEvent.Receive event) { } private void triggerUndergroundRescan() { - // Snapshot ziehen, um Thread-Probleme zu verhindern + Set toProcess = new HashSet<>(pendingUndergroundChunks); pendingUndergroundChunks.removeAll(toProcess); - // 1. Chunks auf ein 3x3 Raster erweitern, um abgeschnittene Tunnel an Chunk-Grenzen zu reparieren + Set chunksToRescan = new HashSet<>(); for (Long key : toProcess) { int cx = (int) key.longValue(); @@ -309,8 +309,8 @@ private void triggerUndergroundRescan() { } } - // 2. LΓ–SCHE NUR Boxen, die sich in DIESEN spezifischen Chunks befinden! - // Tunnel, die hinter dem Spieler liegen (und keine Updates bekommen), bleiben erhalten. + + removeIntersectingUnderground(holes, holeHashes, chunksToRescan); removeIntersectingUnderground(holes3x1, hole3x1Hashes, chunksToRescan); removeIntersectingUnderground(staircases, staircaseHashes, chunksToRescan); @@ -327,7 +327,7 @@ private void triggerUndergroundRescan() { } } - // 3. Zielgerichtetes Rescannen: Nur diese betroffenen Chunks neu in die Queue werfen + synchronized (chunks) { for (Long chunkKey : chunksToRescan) { chunks.remove(chunkKey); @@ -338,9 +338,9 @@ private void triggerUndergroundRescan() { private void removeIntersectingUnderground(Set boxes, Set hashes, Set chunksToRescan) { Iterator iter = boxes.iterator(); while (iter.hasNext()) { - Box b = iter.next(); // Box objects are immutable, so it's safe to iterate and remove - // For coveredHoles, we don't have a separate hash set, so hashes can be null. - // The Box itself is the identifier. + Box b = iter.next(); + + if (b.minY < 0 && intersectsChunk(b, chunksToRescan)) { hashes.remove(BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ)); @@ -349,7 +349,7 @@ private void removeIntersectingUnderground(Set boxes, Set hashes, Set } } - // PrΓ€zise PrΓΌfung, ob eine Box in einen der zu updatenden Chunks hineinragt + private boolean intersectsChunk(Box b, Set chunkKeys) { int minCx = ((int) Math.floor(b.minX)) >> 4; int maxCx = ((int) Math.floor(b.maxX - 0.001)) >> 4; @@ -364,9 +364,9 @@ private boolean intersectsChunk(Box b, Set chunkKeys) { return false; } - // ========================================================================= - // TICK / RENDER - // ========================================================================= + + + @EventHandler private void onTick(TickEvent.Post event) { if (needsUndergroundRescan) { @@ -388,14 +388,14 @@ private void onTick(TickEvent.Post event) { } private void clearOldCacheEntries() { - // Simple cache clearing strategy: clear if too large - // These caches are used by the searchChunk threads, so they need to be Concurrent. - // Clearing them entirely might cause temporary re-computation, but for ESP, - // it's generally acceptable as data is re-scanned frequently. - if (solidBlockCache.size() > 10000) { // Adjust size as needed + + + + + if (solidBlockCache.size() > 10000) { solidBlockCache.clear(); } - if (blockStateCache.size() > 10000) { // Adjust size as needed + if (blockStateCache.size() > 10000) { blockStateCache.clear(); } } @@ -460,7 +460,7 @@ private void onRender3D(Render3DEvent event) { renderTunnels(event.renderer); if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); } - default -> { // Fallback for any other mode that might render holes + default -> { renderHoles(event.renderer); renderTunnels(event.renderer); renderStaircases(event.renderer); @@ -472,13 +472,13 @@ private void onRender3D(Render3DEvent event) { private void renderHoles(Renderer3D r) { for (Box b : holes) { - if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; // Don't render if it's a covered hole + if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, holeSideColor.get(), holeLineColor.get(), shapeMode.get(), 0); } } private void render3x1Holes(Renderer3D r) { for (Box b : holes3x1) { - if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; // Don't render if it's a covered hole + if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, hole3x1SideColor.get(), hole3x1LineColor.get(), shapeMode.get(), 0); } } @@ -497,20 +497,20 @@ private void renderCoveredHoles(Renderer3D r) { Box hole = entry.getKey(); CoveredHoleInfo info = entry.getValue(); - // Render the hole + r.box(hole.minX, hole.minY, hole.minZ, hole.maxX, hole.maxY, hole.maxZ, coveredHoleSideColor.get(), coveredHoleLineColor.get(), shapeMode.get(), 0); - // Render the cover block + r.box(info.coverPos.getX(), info.coverPos.getY(), info.coverPos.getZ(), info.coverPos.getX() + 1, info.coverPos.getY() + 1, info.coverPos.getZ() + 1, coveredHoleSideColor.get(), coveredHoleLineColor.get(), shapeMode.get(), 0); } } - // ========================================================================= - // CHUNK PROCESSING - // ========================================================================= + + + private void processChunkQueue() { int processed = 0; while (!chunkQueue.isEmpty() && processed < maxChunks.get()) { @@ -561,7 +561,7 @@ private void searchChunk(Chunk chunk, TChunk tChunk) { findAndAddHole(pos, visited, Ymin); findAndAdd3x1Hole(pos, visited, Ymin); } - if (hasSolidFloor) { // Tunnels and staircases require a solid floor + if (hasSolidFloor) { if (mode == DetectionMode.ALL || mode == DetectionMode.TUNNELS || mode == DetectionMode.HOLES_AND_TUNNELS || mode == DetectionMode.TUNNELS_AND_STAIRCASES @@ -585,9 +585,9 @@ private int getLocalIndex(int x, int y, int z, int yMin) { return (x & 15) | ((z & 15) << 4) | ((y - yMin) << 8); } - // ========================================================================= - // HOLE CHECKS - // ========================================================================= + + + private void findAndAddHole(BlockPos pos, BitSet visited, int yMin) { if (!isValidHoleSection(pos)) return; BlockPos.Mutable cur = pos.mutableCopy(); @@ -658,9 +658,9 @@ private void mark3x1Visited(BlockPos pos, Direction widthDir, BitSet visited, in } } - // ========================================================================= - // STRAIGHT TUNNEL CHECK - // ========================================================================= + + + private boolean isValidTunnelCrossSection(BlockPos pos, Direction lengthDir, int width, int refHeight) { Direction widthDir = (lengthDir.getAxis() == Direction.Axis.X) ? Direction.SOUTH : Direction.EAST; Direction antiWidthDir = widthDir.getOpposite(); @@ -685,7 +685,7 @@ private boolean isValidTunnelCrossSection(BlockPos pos, Direction lengthDir, int } private void checkTunnelOptimized(BlockPos startPos, BitSet visited, int yMin) { - int chunkX = startPos.getX() >> 4; + int chunkX = startPos.getX() >> 4; int chunkZ = startPos.getZ() >> 4; for (Direction dir : CANONICAL_TUNNEL_DIRS) { @@ -749,9 +749,9 @@ private void checkTunnelOptimized(BlockPos startPos, BitSet visited, int yMin) { } } - // ========================================================================= - // STAIRCASE CHECK - // ========================================================================= + + + private void checkStaircaseOptimized(BlockPos pos, BitSet visited, int yMin) { for (Direction dir : DIRECTIONS) { BlockPos.Mutable cur = pos.mutableCopy(); @@ -776,9 +776,9 @@ private void checkStaircaseOptimized(BlockPos pos, BitSet visited, int yMin) { } } - // ========================================================================= - // DIAGONAL TUNNEL CHECK - // ========================================================================= + + + private void checkDiagonalTunnel(BlockPos pos, BitSet visited, int yMin) { for (Direction dir : DIRECTIONS) { for (int w = minDiagonalWidth.get(); w <= maxDiagonalWidth.get(); w++) { @@ -812,9 +812,9 @@ private void checkDiagonalTunnel(BlockPos pos, BitSet visited, int yMin) { } } - // ========================================================================= - // HELPER METHODS - // ========================================================================= + + + private int getTunnelHeight(BlockPos pos) { int h = 0; while (h < maxTunnelHeight.get() + 1 && isPassableBlock(pos.up(h))) h++; @@ -878,9 +878,9 @@ private boolean isPassableBlock(BlockPos pos) { return shape.isEmpty() || !VoxelShapes.fullCube().equals(shape); } - // ========================================================================= - // COVERED HOLE CHECKS (Integrated from CoveredHole) - // ========================================================================= + + + private static class CoveredHoleInfo { public final BlockPos coverPos; public final Box holeBox; @@ -924,7 +924,7 @@ private boolean isLikelyPlayerCovered(BlockPos coverPos, Box hole) { BlockState state = getBlockStateCached(pos); if (state != null && state.getBlock() == coverBlock.getBlock()) matchingBlocks++; } - return matchingBlocks < 2; // If less than 2 adjacent blocks are the same, it's likely player-placed. + return matchingBlocks < 2; } private boolean isCommonBuildingBlock(BlockState state) { @@ -940,7 +940,7 @@ private boolean isCommonBuildingBlock(BlockState state) { blockName.contains("glass"); } - // Caching methods for block states and solidity checks + private boolean isSolidBlockCached(BlockPos pos) { if (mc.world == null) return false; @@ -966,9 +966,9 @@ private BlockState getBlockStateCached(BlockPos pos) { }); } - // ========================================================================= - // ENUMS / INNER CLASSES - // ========================================================================= + + + public enum DetectionMode { ALL, HOLES_AND_TUNNELS, HOLES_AND_STAIRCASES, TUNNELS_AND_STAIRCASES, HOLES, TUNNELS, STAIRCASES, HOLES_3X1_AND_TUNNELS diff --git a/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java b/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java index 0e400e8e..4f481e10 100644 --- a/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java +++ b/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java @@ -194,11 +194,11 @@ public enum ListMode { Whitelist, Blacklist } private static class PearlEntry { final int entityId; - String ownerName; // Not final - can be updated if owner becomes known - UUID ownerUuid; // Can be null for unknown pearls + String ownerName; + UUID ownerUuid; final Vector3d landingPos = new Vector3d(); boolean isEstimated = false; - boolean isUnknown = false; // True if pearl owner is unknown + boolean isUnknown = false; long timestamp; PearlEntry(int entityId, String ownerName, UUID ownerUuid) { @@ -209,13 +209,13 @@ private static class PearlEntry { } } - // FIX #2: Use ConcurrentHashMap for thread safety + private final Map> trackedPearls = new ConcurrentHashMap<>(); - private final Deque unknownPearls = new ArrayDeque<>(); // Pearls with unknown owner + private final Deque unknownPearls = new ArrayDeque<>(); private final Set knownPearlIds = ConcurrentHashMap.newKeySet(); private final ProjectileEntitySimulator simulator = new ProjectileEntitySimulator(); - // FIX #8: Reusable Vector3d to avoid allocation every frame + private final Vector3d reusableScreenPos = new Vector3d(); public PearlLandingPredictor() { @@ -248,11 +248,11 @@ private void onTick(TickEvent.Pre event) { if (!(entity instanceof EnderPearlEntity pearl)) continue; if (!showSelf.get() && pearl.getOwner() == mc.player) continue; - // FIX: Handle pearls with unknown owner + UUID ownerUuid = pearl.getOwner() != null ? pearl.getOwner().getUuid() : null; String ownerName = pearl.getOwner() != null ? pearl.getOwner().getName().getString() : "Unknown"; - // Check filter only if owner is known + if (pearl.getOwner() != null && !isAllowed(ownerName)) continue; if (pearl.getOwner() != null) { @@ -275,7 +275,7 @@ private void onTick(TickEvent.Pre event) { while (deque.size() > maxPerPlayer.get()) deque.removeLast(); } } else { - // Track unknown pearls separately + synchronized (unknownPearls) { unknownPearls.addFirst(entry); while (unknownPearls.size() > maxPerPlayer.get()) unknownPearls.removeLast(); @@ -283,7 +283,7 @@ private void onTick(TickEvent.Pre event) { } } else if (liveUpdate.get()) { - // FIX #5: Better null handling with re-creation of missing entries + PearlEntry entryToUpdate = null; if (ownerUuid != null) { @@ -310,7 +310,7 @@ private void onTick(TickEvent.Pre event) { } if (entryToUpdate != null) { - // Update owner info if it was previously unknown + if (entryToUpdate.isUnknown && pearl.getOwner() != null) { entryToUpdate.ownerName = ownerName; entryToUpdate.ownerUuid = ownerUuid; @@ -321,16 +321,16 @@ private void onTick(TickEvent.Pre event) { } } - // FIX #1: Clean up stale entries from trackedPearls + knownPearlIds.removeIf(id -> !activeIds.contains(id)); - // Clean up unknown pearls that are no longer active + synchronized (unknownPearls) { unknownPearls.removeIf(entry -> !activeIds.contains(entry.entityId)); } - // FIX #4: Clean up entries for disconnected players - // Remove tracked pearls for players no longer in the world + + trackedPearls.keySet().removeIf(uuid -> !activePlayers.contains(uuid)); } @@ -405,22 +405,22 @@ private void onRender3D(Render3DEvent event) { double half = boxSize.get() / 2.0; - // FIX #3: Wrap depth test in try-finally for safety + try { if (seeThrough.get()) RenderSystem.disableDepthTest(); - // Render known player pearls + for (Deque deque : trackedPearls.values()) { synchronized (deque) { - for (PearlEntry entry : deque) { // Pass event.renderer to renderPearlEntry + for (PearlEntry entry : deque) { renderPearlEntry(event.renderer, entry, half); } } } - // Render unknown pearls + synchronized (unknownPearls) { - for (PearlEntry entry : unknownPearls) { // Pass event.renderer to renderPearlEntry + for (PearlEntry entry : unknownPearls) { renderPearlEntry(event.renderer, entry, half); } } @@ -444,7 +444,7 @@ private void renderPearlEntry(meteordevelopment.meteorclient.renderer.Renderer3D lc = lineColor.get(); } - // Render the box + r.box(pos.x - half, pos.y, pos.z - half, pos.x + half, pos.y + boxSize.get(), pos.z + half, sc, lc, shapeMode.get(), 0); @@ -458,7 +458,7 @@ private void onRender2D(Render2DEvent event) { double half = boxSize.get() / 2.0; - // Render known player pearl names + for (Deque deque : trackedPearls.values()) { synchronized (deque) { for (PearlEntry entry : deque) { @@ -467,7 +467,7 @@ private void onRender2D(Render2DEvent event) { } } - // Render unknown pearl names + synchronized (unknownPearls) { for (PearlEntry entry : unknownPearls) { renderPearlName(entry, half); @@ -476,7 +476,7 @@ private void onRender2D(Render2DEvent event) { } private void renderPearlName(PearlEntry entry, double half) { - // FIX #8: Reuse Vector3d instead of creating new one + Vector3d pos = entry.landingPos; reusableScreenPos.set(pos.x, pos.y + half + 0.15, pos.z); diff --git a/src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java b/src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java index e9b40249..8fb670be 100644 --- a/src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java +++ b/src/main/java/com/nnpg/glazed/modules/main/AutoShopOrder.java @@ -25,7 +25,7 @@ public class AutoShopOrder extends Module { private final MinecraftClient mc = MinecraftClient.getInstance(); - // ==================== CATEGORIES & ITEMS ==================== + public enum ShopCategory { END, NETHER, GEAR, FOOD } public enum PriceMode { AUTO, CUSTOM } @@ -50,14 +50,14 @@ public enum FoodItem { COOKED_CHICKEN, COOKED_BEEF, GOLDEN_CARROT, GOLDEN_APPLE } - // ==================== SHOP PRICES TABLE ==================== - // Prices per item from /shop (per unit, not per stack) + + private int getShopPrice() { return switch (category.get()) { case END -> switch (endItem.get()) { case ENDER_CHEST -> 2500; case ENDER_PEARL -> 75; - case END_STONE -> 8; // $128 for 16 = $8 each + case END_STONE -> 8; case DRAGON_BREATH -> 1000; case END_ROD -> 100; case CHORUS_FRUIT -> 108; @@ -101,12 +101,12 @@ private int getShopPrice() { }; } - // Get default min price (shop price + $1) + private int getDefaultMinPrice() { return getShopPrice() + 1; } - // ==================== STAGES ==================== + private enum Stage { NONE, SHOP_OPEN, @@ -134,19 +134,19 @@ private enum Stage { private int buy_screen_retry_count = 0; private static final int MAX_BUY_RETRIES = 20; - // Anti-stuck system + private long last_action_time = 0; - private static final long STUCK_TIMEOUT_MS = 5000; // 5 seconds + private static final long STUCK_TIMEOUT_MS = 5000; + - // Buy spam tracking private long buy_spam_start_time = 0; private static final long BUY_SPAM_TIMEOUT_MS = 5000; - // Move attempts + private int move_pass_count = 0; private static final int MAX_MOVE_PASSES = 5; - // ==================== SETTINGS ==================== + private final SettingGroup sg_general = settings.getDefaultGroup(); private final SettingGroup sg_blacklist = settings.createGroup("Blacklist"); @@ -232,13 +232,13 @@ public AutoShopOrder() { super(GlazedAddon.CATEGORY, "Auto Shop Order", "Auto Shop Order - Buys items from shop and delivers to orders automatically."); } - // Get effective minimum price + private double getEffectiveMinPrice() { if (priceMode.get() == PriceMode.AUTO) { return getDefaultMinPrice(); } - // Custom mode + String priceStr = customPrice.get().trim(); double parsed = parse_price(priceStr); if (parsed < 0) { @@ -271,12 +271,12 @@ public void onDeactivate() { stage = Stage.NONE; } - // Record an action was performed (resets anti-stuck timer) + private void recordAction() { last_action_time = System.currentTimeMillis(); } - // Check if we're stuck and should reset + private boolean checkAndHandleStuck() { if (mc.currentScreen instanceof GenericContainerScreen) { long now = System.currentTimeMillis(); @@ -299,11 +299,11 @@ private void onTick(TickEvent.Post event) { if (mc.player == null || mc.world == null) return; long now = System.currentTimeMillis(); - // Anti-stuck check at the start of each tick + if (checkAndHandleStuck()) return; switch (stage) { - // ==================== SHOP PHASE ==================== + case SHOP_OPEN -> { ChatUtils.sendPlayerMsg("/shop"); stage = Stage.SHOP_CATEGORY; @@ -345,7 +345,7 @@ private void onTick(TickEvent.Post event) { if (!stack.isEmpty() && isTargetItem(stack) && slot.inventory != mc.player.getInventory()) { mc.interactionManager.clickSlot(handler.syncId, slot.id, 0, SlotActionType.PICKUP, mc.player); - // If item is not stackable, skip to buy screen directly + if (!isTargetItemStackable()) { stage = Stage.SHOP_WAIT_FOR_BUY_SCREEN; stage_start = now; @@ -424,7 +424,7 @@ private void onTick(TickEvent.Post event) { } case SHOP_BUY_SPAM -> { - // Check for timeout - prevent stuck + if (now - buy_spam_start_time > BUY_SPAM_TIMEOUT_MS) { mc.player.closeHandledScreen(); stage = Stage.SHOP_OPEN; @@ -442,7 +442,7 @@ private void onTick(TickEvent.Post event) { ScreenHandler handler = screen.getScreenHandler(); - // Re-find confirm button if lost + if (confirm_slot_id == -1) { for (Slot slot : handler.slots) { ItemStack stack = slot.getStack(); @@ -454,7 +454,7 @@ private void onTick(TickEvent.Post event) { } if (confirm_slot_id != -1) { - // Check if inventory is full + if (is_inventory_full()) { mc.player.closeHandledScreen(); stage = Stage.SHOP_EXIT; @@ -463,12 +463,12 @@ private void onTick(TickEvent.Post event) { return; } - // Click confirm button 2 times per tick + mc.interactionManager.clickSlot(handler.syncId, confirm_slot_id, 0, SlotActionType.PICKUP, mc.player); mc.interactionManager.clickSlot(handler.syncId, confirm_slot_id, 0, SlotActionType.PICKUP, mc.player); recordAction(); } else { - // Button not found - retry search next tick + buy_screen_retry_count++; if (buy_screen_retry_count > MAX_BUY_RETRIES) { mc.player.closeHandledScreen(); @@ -493,7 +493,7 @@ private void onTick(TickEvent.Post event) { } } - // ==================== ORDERS PHASE ==================== + case WAIT -> { if (now - stage_start >= click_delay.get()) { ChatUtils.sendPlayerMsg("/orders " + getSearchKeyword()); @@ -509,7 +509,7 @@ private void onTick(TickEvent.Post event) { ScreenHandler handler = screen.getScreenHandler(); - // Find best order (highest price) + Slot best_order = null; double best_price = -1; double min_price_value = getEffectiveMinPrice(); @@ -550,20 +550,20 @@ private void onTick(TickEvent.Post event) { } } - // BATCH MOVE: Move all items at once in the same tick + case ORDERS_SELECT -> { if (mc.currentScreen instanceof GenericContainerScreen screen) { ScreenHandler handler = screen.getScreenHandler(); - // Collect all slots with target items first + List slots_to_move = new ArrayList<>(); for (Slot slot : handler.slots) { - // Only check player inventory slots (not container slots) + if (slot.inventory == mc.player.getInventory()) { int slot_index = slot.getIndex(); - // Main inventory is slots 9-35, hotbar is 0-8 - // We want to move from player inventory to container + + if (slot_index >= 0 && slot_index < 36) { ItemStack stack = slot.getStack(); if (isTargetItem(stack)) { @@ -574,7 +574,7 @@ private void onTick(TickEvent.Post event) { } if (slots_to_move.isEmpty()) { - // No items to move - proceed to confirm + mc.player.closeHandledScreen(); stage = Stage.ORDERS_CONFIRM; stage_start = now; @@ -582,7 +582,7 @@ private void onTick(TickEvent.Post event) { return; } - // BATCH MOVE: Send all QUICK_MOVE packets in rapid succession (same tick) + for (int slot_id : slots_to_move) { mc.interactionManager.clickSlot(handler.syncId, slot_id, 0, SlotActionType.QUICK_MOVE, mc.player); } @@ -590,15 +590,15 @@ private void onTick(TickEvent.Post event) { move_pass_count++; recordAction(); - // Check after move if items remain + if (move_pass_count >= MAX_MOVE_PASSES) { - // Max passes - continue anyway + mc.player.closeHandledScreen(); stage = Stage.ORDERS_CONFIRM; stage_start = now; recordAction(); } - // Otherwise stay in ORDERS_SELECT to check again next tick + } } @@ -667,7 +667,7 @@ private void onTick(TickEvent.Post event) { } } - // ==================== HELPER METHODS ==================== + private boolean isCategoryIcon(ItemStack stack) { String name = stack.getName().getString().toLowerCase(); @@ -683,13 +683,13 @@ private boolean isTargetItem(ItemStack stack) { return !stack.isEmpty() && stack.getItem() == getTargetMcItem(); } - // Check if target item is stackable + private boolean isTargetItemStackable() { Item item = getTargetMcItem(); return item.getMaxCount() > 1; } - // Check if any target items remain in inventory + private boolean hasTargetItemsInInventory() { for (int i = 0; i < 36; i++) { if (isTargetItem(mc.player.getInventory().getStack(i))) { @@ -813,7 +813,7 @@ private boolean is_inventory_full() { return true; } - // ==================== PRICE PARSING ==================== + private double parse_price(String price_str) { if (price_str == null || price_str.isEmpty()) { @@ -869,7 +869,7 @@ private double parse_tooltip_price(List tooltip) { return -1.0; } - // Pattern to match "$5 each", "$1.5K each", etc. + Pattern[] price_patterns = { Pattern.compile("\\$([\\d,]+(?:\\.\\d+)?)([kmbKMB])?\\s*each", Pattern.CASE_INSENSITIVE), Pattern.compile("\\$([\\d,]+(?:\\.\\d+)?)([kmbKMB])?", Pattern.CASE_INSENSITIVE), @@ -903,7 +903,7 @@ private double parse_tooltip_price(List tooltip) { return base_price * multiplier; } catch (NumberFormatException e) { - // Continue to next pattern + } } } @@ -912,7 +912,7 @@ private double parse_tooltip_price(List tooltip) { return -1.0; } - // ==================== BLACKLIST ==================== + private boolean is_blacklisted(String playerName) { if (playerName == null || blacklisted_players.get().isEmpty()) return false; @@ -924,10 +924,10 @@ private String get_order_player_name(ItemStack stack) { Item.TooltipContext ctx = Item.TooltipContext.create(mc.world); List tooltip = stack.getTooltip(ctx, mc.player, TooltipType.BASIC); - // Pattern for "Click to deliver .Grumm7587 Cooked Chicken" + Pattern deliver_pattern = Pattern.compile("(?i)click to deliver\\s+\\.?([a-zA-Z0-9_]+)"); - // Standard patterns + Pattern[] patterns = { deliver_pattern, Pattern.compile("(?i)player\\s*:\\s*([a-zA-Z0-9_]+)"), From 15f2f64cc725e48aabb7fb74a53ddf4176700bee Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Sun, 29 Mar 2026 19:23:50 +0200 Subject: [PATCH 4/6] GlazedAddon.java updates --- src/main/java/com/nnpg/glazed/GlazedAddon.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/nnpg/glazed/GlazedAddon.java b/src/main/java/com/nnpg/glazed/GlazedAddon.java index dc22898f..0eca80c9 100644 --- a/src/main/java/com/nnpg/glazed/GlazedAddon.java +++ b/src/main/java/com/nnpg/glazed/GlazedAddon.java @@ -30,8 +30,9 @@ public void onInitialize() { Modules.get().add(new PlayerDetection()); Modules.get().add(new AHSniper()); Modules.get().add(new RTPer()); + Modules.get().add(new ShulkerDropper()); Modules.get().add(new AutoSell()); - Modules.get().add(new AutoShulkerOrder()); + Modules.get().add(new SpawnerDropper()); Modules.get().add(new AutoOrder()); Modules.get().add(new HideScoreboard()); Modules.get().add(new CrystalMacro()); @@ -49,12 +50,14 @@ public void onInitialize() { Modules.get().add(new TabDetector()); Modules.get().add(new OrderSniper()); Modules.get().add(new LamaESP()); + Modules.get().add(new PillagerESP()); Modules.get().add(new HoleTunnelStairsESP()); + Modules.get().add(new CoveredHole()); Modules.get().add(new ClusterFinder()); - Modules.get().add(new AutoShulkerShellOrder()); Modules.get().add(new EmergencySeller()); Modules.get().add(new RTPEndBaseFinder()); Modules.get().add(new ShopBuyer()); + Modules.get().add(new OrderDropper()); Modules.get().add(new CollectibleESP()); Modules.get().add(new SpawnerNotifier()); Modules.get().add(new VineESP()); @@ -80,23 +83,27 @@ public void onInitialize() { Modules.get().add(new HoverTotem()); Modules.get().add(new TunnelBaseFinder()); Modules.get().add(new AimAssist()); + Modules.get().add(new SkeletonESP()); + Modules.get().add(new RainNoti()); Modules.get().add(new AutoPearlChain()); - Modules.get().add(new AutoBlazeRodOrder()); Modules.get().add(new BlazeRodDropper()); Modules.get().add(new BreachSwap()); Modules.get().add(new FakeScoreboard()); Modules.get().add(new AutoInvTotem()); Modules.get().add(new FreecamMining()); + Modules.get().add(new BedrockVoidESP()); Modules.get().add(new UIHelper()); Modules.get().add(new ShieldBreaker()); Modules.get().add(new InvisESP()); - Modules.get().add(new AutoTotemOrder()); Modules.get().add(new LightESP()); Modules.get().add(new PremiumTunnelBaseFinder()); Modules.get().add(new AdminList()); Modules.get().add(new AutoTreeFarmer()); Modules.get().add(new PearlLandingPredictor()); Modules.get().add(new AutoShopOrder()); + + + // Commands Commands.add(new SellHotbarCommand()); Commands.add(new AHItemCommand()); From c377d3c62ec63b8bade718d8929d542e3a476fc3 Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Sat, 4 Apr 2026 18:13:50 +0200 Subject: [PATCH 5/6] Hole Tunnel Stairs Improvement --- .../modules/esp/HoleTunnelStairsESP.java | 856 +++++++++++------- 1 file changed, 525 insertions(+), 331 deletions(-) diff --git a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java index c42d8653..61c0ac52 100644 --- a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java +++ b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java @@ -15,7 +15,6 @@ import meteordevelopment.meteorclient.utils.render.color.SettingColor; import meteordevelopment.orbit.EventHandler; import net.minecraft.block.BlockState; -import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen; import net.minecraft.network.packet.s2c.play.BlockUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.ChunkDataS2CPacket; import net.minecraft.util.math.*; @@ -36,7 +35,6 @@ public class HoleTunnelStairsESP extends Module { private final SettingGroup sgCParams = settings.createGroup("Covered Hole Parameters"); private final SettingGroup sgRender = settings.createGroup("Rendering"); - private final Setting detectionMode = sgGeneral.add(new EnumSetting.Builder() .name("Detection Mode") .description("Choose what to detect: holes, tunnels, stairs, or all.") @@ -57,7 +55,7 @@ public class HoleTunnelStairsESP extends Module { ); private final Setting undergroundUpdateThreshold = sgGeneral.add(new IntSetting.Builder() .name("Underground Update Threshold") - .description("Amount of incoming blocks/updates below Y=0 required to trigger an underground rescan.") + .description("Amount of incoming block updates below Y=0 required to trigger an underground rescan of that chunk.") .defaultValue(250).min(10).sliderMax(2000) .build() ); @@ -74,7 +72,6 @@ public class HoleTunnelStairsESP extends Module { .build() ); - private final Setting minHoleDepth = sgHParams.add(new IntSetting.Builder() .name("Min Hole Depth") .description("Minimum depth for a hole to be detected.") @@ -82,7 +79,6 @@ public class HoleTunnelStairsESP extends Module { .build() ); - private final Setting minTunnelLength = sgTParams.add(new IntSetting.Builder() .name("Min Tunnel Length") .description("Minimum length for a tunnel to be detected.") @@ -141,7 +137,6 @@ public class HoleTunnelStairsESP extends Module { .build() ); - private final Setting minStaircaseLength = sgSParams.add(new IntSetting.Builder() .name("Min Staircase Length") .description("Minimum length for a staircase to be detected.") @@ -160,8 +155,7 @@ public class HoleTunnelStairsESP extends Module { .defaultValue(5).min(2).sliderMax(10) .build() ); - - + private final Setting detectCoveredHoles = sgCParams.add(new BoolSetting.Builder() .name("detect-covered-holes") .description("Detects and highlights holes that are covered by solid blocks.") @@ -182,8 +176,7 @@ public class HoleTunnelStairsESP extends Module { .visible(detectCoveredHoles::get) .build() ); - - + private final Setting shapeMode = sgRender.add(new EnumSetting.Builder() .name("shape-mode").defaultValue(ShapeMode.Both).build()); private final Setting holeLineColor = sgRender.add(new ColorSetting.Builder() @@ -204,9 +197,7 @@ public class HoleTunnelStairsESP extends Module { .name("staircase-side-color").defaultValue(new SettingColor(255, 0, 255, 30)).build()); private final Setting coveredHoleLineColor = sgCParams.add(new ColorSetting.Builder() .name("covered-hole-line-color").defaultValue(new SettingColor(255, 165, 0, 255)) - .visible(detectCoveredHoles::get) - .build() - ); + .visible(detectCoveredHoles::get).build()); private final Setting coveredHoleSideColor = sgCParams.add(new ColorSetting.Builder() .name("covered-hole-side-color").defaultValue(new SettingColor(255, 165, 0, 50)) .visible(detectCoveredHoles::get).build()); @@ -214,36 +205,57 @@ public class HoleTunnelStairsESP extends Module { private static final Direction[] DIRECTIONS = { Direction.EAST, Direction.WEST, Direction.NORTH, Direction.SOUTH }; private static final Direction[] CANONICAL_TUNNEL_DIRS = { Direction.EAST, Direction.SOUTH }; - + // ── Chunk tracking ──────────────────────────────────────────────────────── private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); private final Queue chunkQueue = new LinkedList<>(); - + + // ── Detection result stores ─────────────────────────────────────────────── private final Set holes = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final Set tunnels = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Set staircases = Collections.newSetFromMap(new ConcurrentHashMap<>()); - private final Set holes3x1 = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private final Set holes3x1 = Collections.newSetFromMap(new ConcurrentHashMap<>()); private final Map coveredHoles = new ConcurrentHashMap<>(); private final Set notifiedHoles = ConcurrentHashMap.newKeySet(); - private final Set tunnelHashes = ConcurrentHashMap.newKeySet(); - - + // Tunnels: hash β†’ Box. Using ConcurrentHashMap lets us call merge() to + // *atomically extend* an existing box when a new underground chunk reveals + // that the tunnel continues further β€” no delete-and-re-add, no flash. + // Diagonal tunnel boxes are stored here too via putIfAbsent (they don't extend). + private final ConcurrentHashMap tunnelBoxMap = new ConcurrentHashMap<>(); + + // NEW: Separate storage for diagonal tunnels with neighbor info + private final ConcurrentHashMap diagonalTunnelBoxMap = new ConcurrentHashMap<>(); private final Set holeHashes = ConcurrentHashMap.newKeySet(); private final Set hole3x1Hashes = ConcurrentHashMap.newKeySet(); private final Set staircaseHashes = ConcurrentHashMap.newKeySet(); - - private final ThreadLocal visitedBlocksLocal = ThreadLocal.withInitial(BitSet::new); - - private final Set pendingUndergroundChunks = ConcurrentHashMap.newKeySet(); - private int undergroundBlockUpdates = 0; - private boolean needsUndergroundRescan = false; + private final ThreadLocal visitedBlocksLocal = ThreadLocal.withInitial(BitSet::new); - - private final Map solidBlockCache = new ConcurrentHashMap<>(); + // ── Underground rescan state ────────────────────────────────────────────── + // + // Two sources of "this chunk needs rescanning": + // + // A) ChunkDataS2CPacket for a KNOWN chunk: the server re-sent full chunk + // data for a chunk we already scanned (underground reveal). We add it + // directly to chunksNeedingRescan β€” no threshold, immediate next tick. + // + // B) BlockUpdateS2CPacket for y < 0: the server sends individual block + // updates underground (also part of the reveal mechanism). We batch + // these via a per-chunk counter; only when enough accumulate for one + // chunk do we add it to chunksNeedingRescan. This avoids rescanning + // a chunk for every single block packet. + // + // Crucially we ONLY ever rescan the specific chunk(s) that received new data + // β€” never their neighbours. Neighbour tunnels are extended via tunnelBoxMap + // .merge() (see checkTunnelOptimized) without needing a neighbour rescan. + private final Set chunksNeedingRescan = ConcurrentHashMap.newKeySet(); + private final Map undergroundUpdateCounts = new ConcurrentHashMap<>(); + + // ── Block-state caches (for covered-hole detection) ─────────────────────── + private final Map solidBlockCache = new ConcurrentHashMap<>(); private final Map blockStateCache = new ConcurrentHashMap<>(); + // ========================================================================= public HoleTunnelStairsESP() { super(GlazedAddon.esp, "hole-tunnel-stair-esp", "Finds and highlights holes, tunnels, and staircases."); } @@ -256,123 +268,149 @@ public HoleTunnelStairsESP() { private void clearAll() { synchronized (chunks) { chunks.clear(); } chunkQueue.clear(); - holes.clear(); tunnels.clear(); staircases.clear(); holes3x1.clear(); coveredHoles.clear(); - tunnelHashes.clear(); holeHashes.clear(); hole3x1Hashes.clear(); staircaseHashes.clear(); - pendingUndergroundChunks.clear(); - undergroundBlockUpdates = 0; - needsUndergroundRescan = false; - notifiedHoles.clear(); + holes.clear(); staircases.clear(); holes3x1.clear(); + coveredHoles.clear(); notifiedHoles.clear(); + tunnelBoxMap.clear(); + diagonalTunnelBoxMap.clear(); + holeHashes.clear(); hole3x1Hashes.clear(); staircaseHashes.clear(); + chunksNeedingRescan.clear(); + undergroundUpdateCounts.clear(); solidBlockCache.clear(); blockStateCache.clear(); } - - - + // ========================================================================= + // PACKET HANDLING + // ========================================================================= + @EventHandler private void onPacketReceive(PacketEvent.Receive event) { - if (event.packet instanceof BlockUpdateS2CPacket packet) { - if (packet.getPos().getY() < 0) { - pendingUndergroundChunks.add(ChunkPos.toLong(packet.getPos().getX() >> 4, packet.getPos().getZ() >> 4)); - undergroundBlockUpdates++; - } - } else if (event.packet instanceof ChunkDataS2CPacket packet) { + if (event.packet instanceof ChunkDataS2CPacket packet) { long key = ChunkPos.toLong(packet.getChunkX(), packet.getChunkZ()); synchronized (chunks) { + // Only react when we already processed this chunk. + // Genuinely new chunks are handled via the normal onTick queue path. if (chunks.containsKey(key)) { - pendingUndergroundChunks.add(key); - undergroundBlockUpdates += 200; + // Server re-sent data for a known chunk (underground reveal). + // Schedule an immediate targeted rescan next tick. + chunksNeedingRescan.add(key); } } - } - if (undergroundBlockUpdates >= undergroundUpdateThreshold.get()) { - needsUndergroundRescan = true; - undergroundBlockUpdates = 0; - } - } - - private void triggerUndergroundRescan() { - - Set toProcess = new HashSet<>(pendingUndergroundChunks); - pendingUndergroundChunks.removeAll(toProcess); - - - Set chunksToRescan = new HashSet<>(); - for (Long key : toProcess) { - int cx = (int) key.longValue(); - int cz = (int) (key.longValue() >> 32); - for (int dx = -1; dx <= 1; dx++) { - for (int dz = -1; dz <= 1; dz++) { - chunksToRescan.add(ChunkPos.toLong(cx + dx, cz + dz)); + } else if (event.packet instanceof BlockUpdateS2CPacket packet) { + BlockPos pos = packet.getPos(); + if (pos.getY() < 0) { + long key = ChunkPos.toLong(pos.getX() >> 4, pos.getZ() >> 4); + // Count underground block updates per chunk. Trigger a rescan + // only when the count crosses the configured threshold. + int count = undergroundUpdateCounts.merge(key, 1, Integer::sum); + if (count >= undergroundUpdateThreshold.get()) { + undergroundUpdateCounts.remove(key); + synchronized (chunks) { + if (chunks.containsKey(key)) { + chunksNeedingRescan.add(key); + } + } } } } + } + // ========================================================================= + // TARGETED RESCAN (only the specific chunk(s) that received new data) + // ========================================================================= + + /** + * Processes chunks that received new underground block data. + * + *

Design rationale β€” no neighbour expansion:
+ * The old code expanded each affected chunk to a 3Γ—3 neighbourhood, which + * caused already-scanned neighbour chunks to lose their boxes and briefly + * disappear (the "flash" bug). + * + *

With the new {@code tunnelBoxMap.merge()} design we don't need to touch + * neighbours at all. When the newly-scanned chunk B discovers a tunnel whose + * canonical start is in neighbour chunk A, the forward walk reads world blocks + * freely (no chunk boundary limit) and computes the full new end-position. + * {@code merge} then atomically extends the existing box A had + * registered β€” the old box is replaced in one step, so there is no frame + * where it is absent. + * + *

What we remove before rescanning: + *

    + *
  • Hole/staircase boxes whose bounds fall inside the rescan chunk column β€” + * these are per-column structures and must be rediscovered from scratch.
  • + *
  • Tunnel boxes that intersect the rescan chunk column β€” they will + * be re-measured (and potentially extended) during the rescan. The + * {@code merge} call will silently no-op if the new box is not larger.
  • + *
+ */ + private void processRescans() { + if (chunksNeedingRescan.isEmpty()) return; + + // Snapshot and drain atomically. + Set toProcess = new HashSet<>(chunksNeedingRescan); + chunksNeedingRescan.removeAll(toProcess); + + // Remove hole / staircase boxes that belong to the affected chunks. + removeBoxesInChunks(holes, holeHashes, toProcess); + removeBoxesInChunks(holes3x1, hole3x1Hashes, toProcess); + removeBoxesInChunks(staircases, staircaseHashes, toProcess); + + // Remove covered-hole entries for affected chunks (null-safe β€” no hash set needed). + coveredHoles.keySet().removeIf(b -> intersectsChunk(b, toProcess)); + + // Remove tunnel boxes that touch the affected chunk columns so they will + // be re-measured (and extended if necessary) during the upcoming rescan. + // tunnelBoxMap keys are the dedup hashes; removing via values() removes + // the map entry entirely, so the hash is also freed. + tunnelBoxMap.values().removeIf(b -> intersectsChunk(b, toProcess)); - - removeIntersectingUnderground(holes, holeHashes, chunksToRescan); - removeIntersectingUnderground(holes3x1, hole3x1Hashes, chunksToRescan); - removeIntersectingUnderground(staircases, staircaseHashes, chunksToRescan); - removeIntersectingUnderground(coveredHoles.keySet(), null, chunksToRescan); - - Iterator tunnelIter = tunnels.iterator(); - while (tunnelIter.hasNext()) { - Box b = tunnelIter.next(); - if (b.minY < 0 && intersectsChunk(b, chunksToRescan)) { - long h = BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ); - tunnelHashes.remove(h); - tunnelHashes.remove(h ^ Long.MIN_VALUE); - tunnelIter.remove(); - } - } + // NEW: Remove diagonal tunnel boxes in affected chunks + diagonalTunnelBoxMap.values().removeIf(dtb -> intersectsChunk(dtb.box, toProcess)); - + // Remove from the chunks map. On the next onTick pass the chunks will be + // absent and therefore re-queued for a fresh searchChunk call. synchronized (chunks) { - for (Long chunkKey : chunksToRescan) { - chunks.remove(chunkKey); - } + chunks.keySet().removeAll(toProcess); } } - private void removeIntersectingUnderground(Set boxes, Set hashes, Set chunksToRescan) { - Iterator iter = boxes.iterator(); - while (iter.hasNext()) { - Box b = iter.next(); - - - - if (b.minY < 0 && intersectsChunk(b, chunksToRescan)) { + /** + * Removes boxes from {@code boxes} that intersect any chunk in {@code chunkKeys}, + * and removes the corresponding entry from {@code hashes} (if non-null). + */ + private void removeBoxesInChunks(Set boxes, Set hashes, Set chunkKeys) { + boxes.removeIf(b -> { + if (!intersectsChunk(b, chunkKeys)) return false; + if (hashes != null) hashes.remove(BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ)); - iter.remove(); - } - } + return true; + }); } - + /** + * Returns true if box {@code b} overlaps with any chunk column in {@code chunkKeys}. + */ private boolean intersectsChunk(Box b, Set chunkKeys) { int minCx = ((int) Math.floor(b.minX)) >> 4; int maxCx = ((int) Math.floor(b.maxX - 0.001)) >> 4; int minCz = ((int) Math.floor(b.minZ)) >> 4; int maxCz = ((int) Math.floor(b.maxZ - 0.001)) >> 4; - - for (int cx = minCx; cx <= maxCx; cx++) { - for (int cz = minCz; cz <= maxCz; cz++) { + for (int cx = minCx; cx <= maxCx; cx++) + for (int cz = minCz; cz <= maxCz; cz++) if (chunkKeys.contains(ChunkPos.toLong(cx, cz))) return true; - } - } return false; } - - - + // ========================================================================= + // TICK / RENDER + // ========================================================================= + @EventHandler private void onTick(TickEvent.Post event) { - if (needsUndergroundRescan) { - triggerUndergroundRescan(); - needsUndergroundRescan = false; - } + // Process any chunks that received new underground data first. + processRescans(); synchronized (chunks) { for (TChunk tChunk : chunks.values()) tChunk.marked = false; @@ -385,30 +423,35 @@ private void onTick(TickEvent.Post event) { chunks.values().removeIf(tChunk -> !tChunk.marked); } removeBoxesOutsideRenderDistance(); - } - private void clearOldCacheEntries() { - - - - - if (solidBlockCache.size() > 10000) { - solidBlockCache.clear(); - } - if (blockStateCache.size() > 10000) { - blockStateCache.clear(); - } + // Periodically trim the block-state caches to avoid unbounded growth. + if (solidBlockCache.size() > 10_000) solidBlockCache.clear(); + if (blockStateCache.size() > 10_000) blockStateCache.clear(); } private void removeBoxesOutsideRenderDistance() { Set chunkSet = new HashSet<>(); for (Chunk chunk : Utils.chunks(true)) if (chunk instanceof WorldChunk wc) chunkSet.add(wc); - removeBoxesOutside(holes, chunkSet); - removeBoxesOutside(tunnels, chunkSet); - removeBoxesOutside(staircases, chunkSet); - removeBoxesOutside(coveredHoles.keySet(), chunkSet); - removeBoxesOutside(holes3x1, chunkSet); + removeBoxesOutside(holes, chunkSet); + removeBoxesOutside(staircases, chunkSet); + removeBoxesOutside(holes3x1, chunkSet); + removeBoxesOutside(coveredHoles.keySet(),chunkSet); + tunnelBoxMap.values().removeIf(b -> { + BlockPos center = new BlockPos( + (int) Math.floor((b.minX + b.maxX) / 2), + (int) Math.floor((b.minY + b.maxY) / 2), + (int) Math.floor((b.minZ + b.maxZ) / 2)); + return !chunkSet.contains(mc.world.getChunk(center)); + }); + // NEW: Remove diagonal tunnels outside render distance + diagonalTunnelBoxMap.values().removeIf(dtb -> { + BlockPos center = new BlockPos( + (int) Math.floor((dtb.box.minX + dtb.box.maxX) / 2), + (int) Math.floor((dtb.box.minY + dtb.box.maxY) / 2), + (int) Math.floor((dtb.box.minZ + dtb.box.maxZ) / 2)); + return !chunkSet.contains(mc.world.getChunk(center)); + }); } private void removeBoxesOutside(Set boxSet, Set worldChunks) { @@ -425,46 +468,35 @@ private void removeBoxesOutside(Set boxSet, Set worldChunks) { private void onRender3D(Render3DEvent event) { switch (detectionMode.get()) { case ALL -> { - renderHoles(event.renderer); - renderTunnels(event.renderer); - renderStaircases(event.renderer); - render3x1Holes(event.renderer); + renderHoles(event.renderer); renderTunnels(event.renderer); + renderStaircases(event.renderer); render3x1Holes(event.renderer); if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); } case HOLES_AND_TUNNELS -> { - renderHoles(event.renderer); - renderTunnels(event.renderer); + renderHoles(event.renderer); renderTunnels(event.renderer); render3x1Holes(event.renderer); if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); } case HOLES_AND_STAIRCASES -> { - renderHoles(event.renderer); - renderStaircases(event.renderer); + renderHoles(event.renderer); renderStaircases(event.renderer); render3x1Holes(event.renderer); if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); } - case TUNNELS_AND_STAIRCASES -> { - renderTunnels(event.renderer); - renderStaircases(event.renderer); - } - case HOLES -> { - renderHoles(event.renderer); - render3x1Holes(event.renderer); + case TUNNELS_AND_STAIRCASES -> { renderTunnels(event.renderer); renderStaircases(event.renderer); } + case HOLES -> { + renderHoles(event.renderer); render3x1Holes(event.renderer); if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); } case TUNNELS -> renderTunnels(event.renderer); case STAIRCASES -> renderStaircases(event.renderer); case HOLES_3X1_AND_TUNNELS -> { - renderHoles(event.renderer); - render3x1Holes(event.renderer); + renderHoles(event.renderer); render3x1Holes(event.renderer); renderTunnels(event.renderer); if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); } - default -> { - renderHoles(event.renderer); - renderTunnels(event.renderer); - renderStaircases(event.renderer); - render3x1Holes(event.renderer); + default -> { + renderHoles(event.renderer); renderTunnels(event.renderer); + renderStaircases(event.renderer); render3x1Holes(event.renderer); if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); } } @@ -472,45 +504,119 @@ private void onRender3D(Render3DEvent event) { private void renderHoles(Renderer3D r) { for (Box b : holes) { - if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; + if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, holeSideColor.get(), holeLineColor.get(), shapeMode.get(), 0); } } private void render3x1Holes(Renderer3D r) { for (Box b : holes3x1) { - if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; + if (detectCoveredHoles.get() && coveredHoles.containsKey(b)) continue; r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, hole3x1SideColor.get(), hole3x1LineColor.get(), shapeMode.get(), 0); } } + private void renderTunnels(Renderer3D r) { - for (Box b : tunnels) + // Render straight tunnels (existing behavior) + for (Box b : tunnelBoxMap.values()) r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, tunnelSideColor.get(), tunnelLineColor.get(), shapeMode.get(), 0); + + // NEW: Render diagonal tunnels with neighbor-culling + renderDiagonalTunnelsOptimized(r); } - + + /** + * Renders diagonal tunnels using neighbor-culling to avoid overlapping faces. + * Only renders faces that are not shared with adjacent tunnel segments. + */ + private void renderDiagonalTunnelsOptimized(Renderer3D r) { + SettingColor sideColor = tunnelSideColor.get(); + SettingColor lineColor = tunnelLineColor.get(); + ShapeMode mode = shapeMode.get(); + + for (DiagonalTunnelBox dtb : diagonalTunnelBoxMap.values()) { + Box b = dtb.box; + double x1 = b.minX, y1 = b.minY, z1 = b.minZ; + double x2 = b.maxX, y2 = b.maxY, z2 = b.maxZ; + + // Determine which faces to render based on neighbors + boolean renderNegX = !dtb.hasNegX; + boolean renderPosX = !dtb.hasPosX; + boolean renderNegY = !dtb.hasNegY; + boolean renderPosY = !dtb.hasPosY; + boolean renderNegZ = !dtb.hasNegZ; + boolean renderPosZ = !dtb.hasPosZ; + + // Render sides (quads) - only if no neighbor on that side + if (mode.lines() || mode.sides()) { + // Bottom face (Y-) + if (renderNegY && mode.sides()) { + r.quadHorizontal(x1, y1, z1, x2, z2, sideColor); + } + // Top face (Y+) + if (renderPosY && mode.sides()) { + r.quadHorizontal(x1, y2, z1, x2, z2, sideColor); + } + // North face (Z-) + if (renderNegZ && mode.sides()) { + r.quadVertical(x1, y1, z1, x2, y2, z1, sideColor); + } + // South face (Z+) + if (renderPosZ && mode.sides()) { + r.quadVertical(x1, y1, z2, x2, y2, z2, sideColor); + } + // West face (X-) + if (renderNegX && mode.sides()) { + r.quadVertical(x1, y1, z1, x1, y2, z2, sideColor); + } + // East face (X+) + if (renderPosX && mode.sides()) { + r.quadVertical(x2, y1, z1, x2, y2, z2, sideColor); + } + } + + // Render edges (lines) - only if at least one adjacent face is exposed + if (mode.lines()) { + // Bottom edges + if (renderNegY || renderNegZ) r.line(x1, y1, z1, x2, y1, z1, lineColor); // North + if (renderNegY || renderPosZ) r.line(x1, y1, z2, x2, y1, z2, lineColor); // South + if (renderNegY || renderNegX) r.line(x1, y1, z1, x1, y1, z2, lineColor); // West + if (renderNegY || renderPosX) r.line(x2, y1, z1, x2, y1, z2, lineColor); // East + + // Top edges + if (renderPosY || renderNegZ) r.line(x1, y2, z1, x2, y2, z1, lineColor); // North + if (renderPosY || renderPosZ) r.line(x1, y2, z2, x2, y2, z2, lineColor); // South + if (renderPosY || renderNegX) r.line(x1, y2, z1, x1, y2, z2, lineColor); // West + if (renderPosY || renderPosX) r.line(x2, y2, z1, x2, y2, z2, lineColor); // East + + // Vertical edges + if (renderNegX || renderNegZ) r.line(x1, y1, z1, x1, y2, z1, lineColor); // NW + if (renderPosX || renderNegZ) r.line(x2, y1, z1, x2, y2, z1, lineColor); // NE + if (renderNegX || renderPosZ) r.line(x1, y1, z2, x1, y2, z2, lineColor); // SW + if (renderPosX || renderPosZ) r.line(x2, y1, z2, x2, y2, z2, lineColor); // SE + } + } + } + private void renderStaircases(Renderer3D r) { for (Box b : staircases) r.box(b.minX, b.minY, b.minZ, b.maxX, b.maxY, b.maxZ, staircaseSideColor.get(), staircaseLineColor.get(), shapeMode.get(), 0); } - private void renderCoveredHoles(Renderer3D r) { for (Map.Entry entry : coveredHoles.entrySet()) { Box hole = entry.getKey(); CoveredHoleInfo info = entry.getValue(); - - r.box(hole.minX, hole.minY, hole.minZ, hole.maxX, hole.maxY, hole.maxZ, coveredHoleSideColor.get(), coveredHoleLineColor.get(), shapeMode.get(), 0); - - r.box(info.coverPos.getX(), info.coverPos.getY(), info.coverPos.getZ(), info.coverPos.getX() + 1, info.coverPos.getY() + 1, info.coverPos.getZ() + 1, coveredHoleSideColor.get(), coveredHoleLineColor.get(), shapeMode.get(), 0); } } - - - + // ========================================================================= + // CHUNK PROCESSING + // ========================================================================= + private void processChunkQueue() { int processed = 0; while (!chunkQueue.isEmpty() && processed < maxChunks.get()) { @@ -546,8 +652,7 @@ private void searchChunk(Chunk chunk, TChunk tChunk) { if (currentY <= Ymin || currentY >= Ymax) continue; if (visited.get(getLocalIndex(x, currentY, z, Ymin))) continue; - pos.set(chunk.getPos().getStartX() + x, currentY, - chunk.getPos().getStartZ() + z); + pos.set(chunk.getPos().getStartX() + x, currentY, chunk.getPos().getStartZ() + z); if (!isPassableBlock(pos)) continue; floorPos.set(pos).move(Direction.DOWN); @@ -561,7 +666,7 @@ private void searchChunk(Chunk chunk, TChunk tChunk) { findAndAddHole(pos, visited, Ymin); findAndAdd3x1Hole(pos, visited, Ymin); } - if (hasSolidFloor) { + if (hasSolidFloor) { if (mode == DetectionMode.ALL || mode == DetectionMode.TUNNELS || mode == DetectionMode.HOLES_AND_TUNNELS || mode == DetectionMode.TUNNELS_AND_STAIRCASES @@ -579,15 +684,19 @@ private void searchChunk(Chunk chunk, TChunk tChunk) { } } } + + // NEW: After all diagonal tunnels are detected, compute neighbor relationships + computeDiagonalTunnelNeighbors(); } private int getLocalIndex(int x, int y, int z, int yMin) { return (x & 15) | ((z & 15) << 4) | ((y - yMin) << 8); } - - - + // ========================================================================= + // HOLE DETECTION + // ========================================================================= + private void findAndAddHole(BlockPos pos, BitSet visited, int yMin) { if (!isValidHoleSection(pos)) return; BlockPos.Mutable cur = pos.mutableCopy(); @@ -599,13 +708,11 @@ private void findAndAddHole(BlockPos pos, BitSet visited, int yMin) { } if (depth >= minHoleDepth.get()) { long hash = BlockPos.asLong(pos.getX(), pos.getY(), pos.getZ()); - if (holeHashes.add(hash)) - { - Box newHoleBox = new Box(pos.getX(), pos.getY(), pos.getZ(), - pos.getX() + 1, cur.getY(), pos.getZ() + 1); - holes.add(newHoleBox); - if (detectCoveredHoles.get()) checkCoveredHole(newHoleBox); - } + if (holeHashes.add(hash)) { + Box box = new Box(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, cur.getY(), pos.getZ() + 1); + holes.add(box); + if (detectCoveredHoles.get()) checkCoveredHole(box); + } } } @@ -620,13 +727,11 @@ private void findAndAdd3x1Hole(BlockPos pos, BitSet visited, int yMin) { } if (depth >= minHoleDepth.get()) { long hash = BlockPos.asLong(pos.getX(), pos.getY(), pos.getZ()); - if (hole3x1Hashes.add(hash)) - { - Box newHoleBox = new Box(pos.getX(), pos.getY(), pos.getZ(), - pos.getX() + 3, cur.getY(), pos.getZ() + 1); - holes3x1.add(newHoleBox); - if (detectCoveredHoles.get()) checkCoveredHole(newHoleBox); - } + if (hole3x1Hashes.add(hash)) { + Box box = new Box(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 3, cur.getY(), pos.getZ() + 1); + holes3x1.add(box); + if (detectCoveredHoles.get()) checkCoveredHole(box); + } } } if (isValid3x1HoleSectionZ(pos)) { @@ -639,13 +744,11 @@ private void findAndAdd3x1Hole(BlockPos pos, BitSet visited, int yMin) { } if (depth >= minHoleDepth.get()) { long hash = BlockPos.asLong(pos.getX(), pos.getY(), pos.getZ()); - if (hole3x1Hashes.add(hash)) - { - Box newHoleBox = new Box(pos.getX(), pos.getY(), pos.getZ(), - pos.getX() + 1, cur.getY(), pos.getZ() + 3); - holes3x1.add(newHoleBox); - if (detectCoveredHoles.get()) checkCoveredHole(newHoleBox); - } + if (hole3x1Hashes.add(hash)) { + Box box = new Box(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, cur.getY(), pos.getZ() + 3); + holes3x1.add(box); + if (detectCoveredHoles.get()) checkCoveredHole(box); + } } } } @@ -658,19 +761,19 @@ private void mark3x1Visited(BlockPos pos, Direction widthDir, BitSet visited, in } } - - - + // ========================================================================= + // TUNNEL DETECTION + // ========================================================================= + private boolean isValidTunnelCrossSection(BlockPos pos, Direction lengthDir, int width, int refHeight) { Direction widthDir = (lengthDir.getAxis() == Direction.Axis.X) ? Direction.SOUTH : Direction.EAST; Direction antiWidthDir = widthDir.getOpposite(); - if (!isPassableBlock(pos)) return false; - if (isPassableBlock(pos.down())) return false; - if (isPassableBlock(pos.offset(antiWidthDir))) return false; - - if (getTunnelHeight(pos) != refHeight) return false; - if (isPassableBlock(pos.up(refHeight))) return false; + if (!isPassableBlock(pos)) return false; + if (isPassableBlock(pos.down())) return false; + if (isPassableBlock(pos.offset(antiWidthDir))) return false; + if (getTunnelHeight(pos) != refHeight) return false; + if (isPassableBlock(pos.up(refHeight))) return false; for (int w = 1; w < width; w++) { BlockPos wPos = pos.offset(widthDir, w); @@ -679,32 +782,48 @@ private boolean isValidTunnelCrossSection(BlockPos pos, Direction lengthDir, int if (getTunnelHeight(wPos) != refHeight) return false; if (isPassableBlock(wPos.up(refHeight))) return false; } - - if (isPassableBlock(pos.offset(widthDir, width))) return false; - return true; + return !isPassableBlock(pos.offset(widthDir, width)); } + /** + * Detects straight tunnels and registers them in {@code tunnelBoxMap}. + * + *

Tunnel extension via merge()

+ * When an underground chunk is rescanned and a tunnel turns out to extend + * further than the previously registered box, {@code tunnelBoxMap.merge()} + * atomically replaces the old (shorter) box with the new (longer) one. + * Because the map entry is updated in a single atomic step the render thread + * always sees a valid box β€” there is no frame where the box is absent + * (no "flash"). + * + *

The merge function only grows the box along the tunnel's length axis + * (min/max on X and Z); the Y dimensions are kept from the first registration + * because they reflect the actual ceiling height measured at scan time. + */ private void checkTunnelOptimized(BlockPos startPos, BitSet visited, int yMin) { - int chunkX = startPos.getX() >> 4; + int chunkX = startPos.getX() >> 4; int chunkZ = startPos.getZ() >> 4; for (Direction dir : CANONICAL_TUNNEL_DIRS) { Direction widthDir = (dir.getAxis() == Direction.Axis.X) ? Direction.SOUTH : Direction.EAST; Direction antiWidthDir = widthDir.getOpposite(); + // Fast pre-check: canonical left-edge guard. if (isPassableBlock(startPos.offset(antiWidthDir))) continue; + // Measure width. int width = 0; - while (width < maxTunnelWidth.get() && isPassableBlock(startPos.offset(widthDir, width))) { - width++; - } + while (width < maxTunnelWidth.get() && isPassableBlock(startPos.offset(widthDir, width))) width++; if (width < minTunnelWidth.get() || width > maxTunnelWidth.get()) continue; + // Reference height β€” passed into every cross-section call so height is + // re-used without recomputation and enforces uniformity along the tunnel. int refHeight = getTunnelHeight(startPos); if (refHeight < minTunnelHeight.get() || refHeight > maxTunnelHeight.get()) continue; if (!isValidTunnelCrossSection(startPos, dir, width, refHeight)) continue; + // Pass 1: walk backward to canonical start (same for all chunks touching this tunnel). BlockPos.Mutable canonicalStart = startPos.mutableCopy(); { BlockPos.Mutable probe = startPos.mutableCopy(); @@ -715,10 +834,14 @@ private void checkTunnelOptimized(BlockPos startPos, BitSet visited, int yMin) { } } + // Dedup hash encodes position + axis (sign bit distinguishes X vs Z tunnels + // that share the same corner block). long hash = BlockPos.asLong(canonicalStart.getX(), canonicalStart.getY(), canonicalStart.getZ()); if (dir.getAxis() == Direction.Axis.Z) hash ^= Long.MIN_VALUE; - BlockPos.Mutable scanPos = canonicalStart.mutableCopy(); + // Pass 2: walk forward β€” mark visited blocks in this chunk, count steps, + // record end position. + BlockPos.Mutable scanPos = canonicalStart.mutableCopy(); BlockPos.Mutable lastValid = canonicalStart.mutableCopy(); int stepCount = 0; @@ -734,24 +857,29 @@ private void checkTunnelOptimized(BlockPos startPos, BitSet visited, int yMin) { stepCount++; } - if (stepCount >= minTunnelLength.get() && tunnelHashes.add(hash)) { - int x1 = canonicalStart.getX(), y1 = canonicalStart.getY(), z1 = canonicalStart.getZ(); - int x2, z2; - if (dir.getAxis() == Direction.Axis.X) { - x2 = lastValid.getX() + 1; - z2 = z1 + width; - } else { - x2 = x1 + width; - z2 = lastValid.getZ() + 1; - } - tunnels.add(new Box(x1, y1, z1, x2, y1 + refHeight, z2)); - } + if (stepCount < minTunnelLength.get()) continue; + + int x1 = canonicalStart.getX(), y1 = canonicalStart.getY(), z1 = canonicalStart.getZ(); + int x2, z2; + if (dir.getAxis() == Direction.Axis.X) { x2 = lastValid.getX() + 1; z2 = z1 + width; } + else { x2 = x1 + width; z2 = lastValid.getZ() + 1; } + + final Box newBox = new Box(x1, y1, z1, x2, y1 + refHeight, z2); + + // Atomically register or extend the box. If a shorter box was registered + // by an earlier chunk scan, merge() replaces it with the larger one. + // The render thread sees a valid box at all times (no flash). + tunnelBoxMap.merge(hash, newBox, (oldBox, nb) -> new Box( + Math.min(oldBox.minX, nb.minX), oldBox.minY, Math.min(oldBox.minZ, nb.minZ), + Math.max(oldBox.maxX, nb.maxX), oldBox.maxY, Math.max(oldBox.maxZ, nb.maxZ) + )); } } - - - + // ========================================================================= + // STAIRCASE DETECTION + // ========================================================================= + private void checkStaircaseOptimized(BlockPos pos, BitSet visited, int yMin) { for (Direction dir : DIRECTIONS) { BlockPos.Mutable cur = pos.mutableCopy(); @@ -760,8 +888,7 @@ private void checkStaircaseOptimized(BlockPos pos, BitSet visited, int yMin) { while (isStaircaseSection(cur, dir)) { int height = getStaircaseHeight(cur); - potential.add(new Box(cur.getX(), cur.getY(), cur.getZ(), - cur.getX() + 1, cur.getY() + height, cur.getZ() + 1)); + potential.add(new Box(cur.getX(), cur.getY(), cur.getZ(), cur.getX() + 1, cur.getY() + height, cur.getZ() + 1)); visited.set(getLocalIndex(cur.getX() & 15, cur.getY(), cur.getZ() & 15, yMin)); cur.move(dir); cur.move(Direction.UP); @@ -776,45 +903,199 @@ private void checkStaircaseOptimized(BlockPos pos, BitSet visited, int yMin) { } } - - - + // ========================================================================= + // DIAGONAL TUNNEL DETECTION + // ========================================================================= + + /** + * NEW: Data class for diagonal tunnel boxes with neighbor information. + * Stores which sides have adjacent tunnel segments for culling. + */ + private static class DiagonalTunnelBox { + public final Box box; + public final int x, y, z; // Grid position + public final int width, height; + public final Direction dir; // Primary direction + public final boolean turnRight; + + // Neighbor flags - true if another tunnel segment is adjacent on this side + public boolean hasNegX, hasPosX, hasNegY, hasPosY, hasNegZ, hasPosZ; + + public DiagonalTunnelBox(Box box, int x, int y, int z, int width, int height, + Direction dir, boolean turnRight) { + this.box = box; + this.x = x; + this.y = y; + this.z = z; + this.width = width; + this.height = height; + this.dir = dir; + this.turnRight = turnRight; + } + + /** + * Computes hash based on grid position for neighbor lookup. + */ + public long getHash() { + return BlockPos.asLong(x, y, z); + } + } + private void checkDiagonalTunnel(BlockPos pos, BitSet visited, int yMin) { for (Direction dir : DIRECTIONS) { for (int w = minDiagonalWidth.get(); w <= maxDiagonalWidth.get(); w++) { BlockPos.Mutable cur = pos.mutableCopy(); int stepCount = 0; - List potential = new ArrayList<>(); + List potential = new ArrayList<>(); Direction checkDir = dir; boolean turnRight = true; while (isDiagonalTunnelSection(cur, checkDir)) { int height = getTunnelHeight(cur); BlockPos.Mutable fill = cur.mutableCopy(); + for (int k = 0; k < w; k++) { - potential.add(new Box(fill.getX(), fill.getY(), fill.getZ(), - fill.getX() + 1, fill.getY() + height, fill.getZ() + 1)); + // Create box for this segment + Box box = new Box(fill.getX(), fill.getY(), fill.getZ(), + fill.getX() + 1, fill.getY() + height, fill.getZ() + 1); + + // Store grid position for neighbor detection + int gx = fill.getX(); + int gy = fill.getY(); + int gz = fill.getZ(); + + DiagonalTunnelBox dtb = new DiagonalTunnelBox(box, gx, gy, gz, w, height, checkDir, turnRight); + potential.add(dtb); + visited.set(getLocalIndex(fill.getX() & 15, fill.getY(), fill.getZ() & 15, yMin)); - fill.move(turnRight ? checkDir.rotateYClockwise() - : checkDir.rotateYCounterclockwise()); + fill.move(turnRight ? checkDir.rotateYClockwise() : checkDir.rotateYCounterclockwise()); + } + + if (turnRight) { + checkDir = checkDir.rotateYClockwise(); + cur.move(checkDir, w); + turnRight = false; + } else { + checkDir = checkDir.rotateYCounterclockwise(); + cur.move(checkDir, w); + turnRight = true; } - if (turnRight) { checkDir = checkDir.rotateYClockwise(); cur.move(checkDir, w); turnRight = false; } - else { checkDir = checkDir.rotateYCounterclockwise(); cur.move(checkDir, w); turnRight = true; } stepCount++; } + if (stepCount >= minDiagonalLength.get()) { - for (Box b : potential) { - long hash = BlockPos.asLong((int) b.minX, (int) b.minY, (int) b.minZ); - if (tunnelHashes.add(hash)) tunnels.add(b); + for (DiagonalTunnelBox dtb : potential) { + long hash = dtb.getHash(); + // Use merge to handle overlapping segments from different scan passes + diagonalTunnelBoxMap.merge(hash, dtb, (old, neu) -> { + // Keep the larger one if they overlap, or combine them + if (neu.box.getXLength() * neu.box.getYLength() * neu.box.getZLength() > + old.box.getXLength() * old.box.getYLength() * old.box.getZLength()) { + return neu; + } + return old; + }); } } } } } - - - + /** + * NEW: Computes neighbor relationships for all diagonal tunnel boxes. + * This is called after all diagonal tunnels are detected in a chunk. + */ + private void computeDiagonalTunnelNeighbors() { + // For each diagonal tunnel box, check all 6 neighbors + for (DiagonalTunnelBox dtb : diagonalTunnelBoxMap.values()) { + // Check X neighbors + long negXHash = BlockPos.asLong(dtb.x - 1, dtb.y, dtb.z); + long posXHash = BlockPos.asLong(dtb.x + 1, dtb.y, dtb.z); + dtb.hasNegX = diagonalTunnelBoxMap.containsKey(negXHash); + dtb.hasPosX = diagonalTunnelBoxMap.containsKey(posXHash); + + // Check Y neighbors + long negYHash = BlockPos.asLong(dtb.x, dtb.y - 1, dtb.z); + long posYHash = BlockPos.asLong(dtb.x, dtb.y + 1, dtb.z); + dtb.hasNegY = diagonalTunnelBoxMap.containsKey(negYHash); + dtb.hasPosY = diagonalTunnelBoxMap.containsKey(posYHash); + + // Check Z neighbors + long negZHash = BlockPos.asLong(dtb.x, dtb.y, dtb.z - 1); + long posZHash = BlockPos.asLong(dtb.x, dtb.y, dtb.z + 1); + dtb.hasNegZ = diagonalTunnelBoxMap.containsKey(negZHash); + dtb.hasPosZ = diagonalTunnelBoxMap.containsKey(posZHash); + } + } + + // ========================================================================= + // COVERED HOLE LOGIC + // ========================================================================= + + private static class CoveredHoleInfo { + public final BlockPos coverPos; + public final Box holeBox; + public CoveredHoleInfo(BlockPos coverPos, Box holeBox) { + this.coverPos = coverPos; this.holeBox = holeBox; + } + } + + private void checkCoveredHole(Box holeBox) { + if (!detectCoveredHoles.get()) return; + BlockPos topPos = new BlockPos((int) holeBox.minX, (int) holeBox.maxY, (int) holeBox.minZ); + if (isSolidBlockCached(topPos)) { + boolean isPlayerCovered = !onlyPlayerCovered.get() || isLikelyPlayerCovered(topPos, holeBox); + if (isPlayerCovered) { + CoveredHoleInfo info = new CoveredHoleInfo(topPos, holeBox); + coveredHoles.put(holeBox, info); + if (chatNotifications.get() && notifiedHoles.add(holeBox)) { + int depth = (int) (holeBox.maxY - holeBox.minY); + info(String.format("Covered Hole found at %s (depth: %d)", topPos.toShortString(), depth)); + } + } + } + } + + private boolean isLikelyPlayerCovered(BlockPos coverPos, Box hole) { + BlockState coverBlock = getBlockStateCached(coverPos); + if (coverBlock == null) return false; + if (isCommonBuildingBlock(coverBlock)) return true; + int matchingBlocks = 0; + for (BlockPos p : new BlockPos[]{ coverPos.north(), coverPos.south(), coverPos.east(), coverPos.west() }) { + BlockState state = getBlockStateCached(p); + if (state != null && state.getBlock() == coverBlock.getBlock()) matchingBlocks++; + } + return matchingBlocks < 2; + } + + private boolean isCommonBuildingBlock(BlockState state) { + if (state == null) return false; + String name = state.getBlock().getTranslationKey().toLowerCase(); + return name.contains("cobblestone") || name.contains("stone_brick") || name.contains("plank") + || name.contains("log") || name.contains("wool") || name.contains("concrete") + || name.contains("terracotta") || name.contains("glass"); + } + + private boolean isSolidBlockCached(BlockPos pos) { + if (mc.world == null) return false; + return solidBlockCache.computeIfAbsent(pos, p -> { + try { BlockState s = mc.world.getBlockState(p); return s != null && s.isSolidBlock(mc.world, p); } + catch (Exception e) { return false; } + }); + } + + private BlockState getBlockStateCached(BlockPos pos) { + if (mc.world == null) return null; + return blockStateCache.computeIfAbsent(pos, p -> { + try { return mc.world.getBlockState(p); } + catch (Exception e) { return null; } + }); + } + + // ========================================================================= + // HELPER METHODS + // ========================================================================= + private int getTunnelHeight(BlockPos pos) { int h = 0; while (h < maxTunnelHeight.get() + 1 && isPassableBlock(pos.up(h))) h++; @@ -837,16 +1118,16 @@ private boolean isValid3x1HoleSectionX(BlockPos pos) { return isPassableBlock(pos) && isPassableBlock(pos.east()) && isPassableBlock(pos.east(2)) && !isPassableBlock(pos.north()) && !isPassableBlock(pos.south()) && !isPassableBlock(pos.east(3)) && !isPassableBlock(pos.west()) - && !isPassableBlock(pos.east().north()) && !isPassableBlock(pos.east().south()) - && !isPassableBlock(pos.east(2).north()) && !isPassableBlock(pos.east(2).south()); + && !isPassableBlock(pos.east().north()) && !isPassableBlock(pos.east().south()) + && !isPassableBlock(pos.east(2).north()) && !isPassableBlock(pos.east(2).south()); } private boolean isValid3x1HoleSectionZ(BlockPos pos) { return isPassableBlock(pos) && isPassableBlock(pos.south()) && isPassableBlock(pos.south(2)) && !isPassableBlock(pos.east()) && !isPassableBlock(pos.west()) && !isPassableBlock(pos.south(3)) && !isPassableBlock(pos.north()) - && !isPassableBlock(pos.south().east()) && !isPassableBlock(pos.south().west()) - && !isPassableBlock(pos.south(2).east()) && !isPassableBlock(pos.south(2).west()); + && !isPassableBlock(pos.south().east()) && !isPassableBlock(pos.south().west()) + && !isPassableBlock(pos.south(2).east()) && !isPassableBlock(pos.south(2).west()); } private boolean isStaircaseSection(BlockPos pos, Direction dir) { @@ -878,97 +1159,10 @@ private boolean isPassableBlock(BlockPos pos) { return shape.isEmpty() || !VoxelShapes.fullCube().equals(shape); } - - - - private static class CoveredHoleInfo { - public final BlockPos coverPos; - public final Box holeBox; - - public CoveredHoleInfo(BlockPos coverPos, Box holeBox) { - this.coverPos = coverPos; - this.holeBox = holeBox; - } - } - - private void checkCoveredHole(Box holeBox) { - if (!detectCoveredHoles.get()) return; - - BlockPos topPos = new BlockPos((int) holeBox.minX, (int) holeBox.maxY, (int) holeBox.minZ); - - if (isSolidBlockCached(topPos)) { - boolean isPlayerCovered = !onlyPlayerCovered.get() || isLikelyPlayerCovered(topPos, holeBox); - - if (isPlayerCovered) { - CoveredHoleInfo info = new CoveredHoleInfo(topPos, holeBox); - coveredHoles.put(holeBox, info); - - if (chatNotifications.get() && notifiedHoles.add(holeBox)) { - int depth = (int) (holeBox.maxY - holeBox.minY); - info(String.format("Covered Hole found at %s (depth: %d)", topPos.toShortString(), depth)); - } - } - } - } - - private boolean isLikelyPlayerCovered(BlockPos coverPos, Box hole) { - BlockState coverBlock = getBlockStateCached(coverPos); - if (coverBlock == null) return false; - - if (isCommonBuildingBlock(coverBlock)) return true; - - int matchingBlocks = 0; - BlockPos[] checkPositions = { coverPos.north(), coverPos.south(), coverPos.east(), coverPos.west() }; - - for (BlockPos pos : checkPositions) { - BlockState state = getBlockStateCached(pos); - if (state != null && state.getBlock() == coverBlock.getBlock()) matchingBlocks++; - } - return matchingBlocks < 2; - } - - private boolean isCommonBuildingBlock(BlockState state) { - if (state == null) return false; - String blockName = state.getBlock().getTranslationKey().toLowerCase(); - return blockName.contains("cobblestone") || - blockName.contains("stone_brick") || - blockName.contains("plank") || - blockName.contains("log") || - blockName.contains("wool") || - blockName.contains("concrete") || - blockName.contains("terracotta") || - blockName.contains("glass"); - } - - - private boolean isSolidBlockCached(BlockPos pos) { - if (mc.world == null) return false; - - return solidBlockCache.computeIfAbsent(pos, p -> { - try { - BlockState state = mc.world.getBlockState(p); - return state != null && state.isSolidBlock(mc.world, p); - } catch (Exception e) { - return false; - } - }); - } - - private BlockState getBlockStateCached(BlockPos pos) { - if (mc.world == null) return null; + // ========================================================================= + // ENUMS / INNER CLASSES + // ========================================================================= - return blockStateCache.computeIfAbsent(pos, p -> { - try { - return mc.world.getBlockState(p); - } catch (Exception e) { - return null; - } - }); - } - - - - public enum DetectionMode { ALL, HOLES_AND_TUNNELS, HOLES_AND_STAIRCASES, TUNNELS_AND_STAIRCASES, HOLES, TUNNELS, STAIRCASES, HOLES_3X1_AND_TUNNELS From 1c8f2a5ace7b7d9a858b0d7aa695b884bb872cdc Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Thu, 23 Apr 2026 14:51:42 +0200 Subject: [PATCH 6/6] PerlLandingPredictor addions and HoletunnelStairsEsp fix --- .gitignore | 2 +- .../modules/esp/HoleTunnelStairsESP.java | 4 +- .../modules/esp/PearlLandingPredictor.java | 79 +++++++++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 42eab877..af00e685 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,4 @@ bin/ run/ -/temp \ No newline at end of file +/DEVNODE \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java index 61c0ac52..a3d3a860 100644 --- a/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java +++ b/src/main/java/com/nnpg/glazed/modules/esp/HoleTunnelStairsESP.java @@ -989,8 +989,8 @@ private void checkDiagonalTunnel(BlockPos pos, BitSet visited, int yMin) { // Use merge to handle overlapping segments from different scan passes diagonalTunnelBoxMap.merge(hash, dtb, (old, neu) -> { // Keep the larger one if they overlap, or combine them - if (neu.box.getXLength() * neu.box.getYLength() * neu.box.getZLength() > - old.box.getXLength() * old.box.getYLength() * old.box.getZLength()) { + if ((neu.box.maxX - neu.box.minX) * (neu.box.maxY - neu.box.minY) * (neu.box.maxZ - neu.box.minZ) > + (old.box.maxX - old.box.minX) * (old.box.maxY - old.box.minY) * (old.box.maxZ - old.box.minZ)) { return neu; } return old; diff --git a/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java b/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java index 4f481e10..88aa40a7 100644 --- a/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java +++ b/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java @@ -11,6 +11,7 @@ import meteordevelopment.meteorclient.systems.modules.Module; import meteordevelopment.meteorclient.utils.entity.ProjectileEntitySimulator; import meteordevelopment.meteorclient.utils.render.NametagUtils; +import meteordevelopment.meteorclient.utils.render.RenderUtils; import meteordevelopment.meteorclient.utils.render.color.SettingColor; import meteordevelopment.orbit.EventHandler; import net.minecraft.entity.Entity; @@ -47,6 +48,23 @@ public enum ListMode { Whitelist, Blacklist } .build() ); + private final Setting keepLastPos = sgGeneral.add(new BoolSetting.Builder() + .name("keep-last-pos") + .description("Keep rendering the landing spot after the player has landed.") + .defaultValue(true) + .build() + ); + + private final Setting keepLastPosTimeout = sgGeneral.add(new IntSetting.Builder() + .name("keep-last-pos-timeout") + .description("How long (in seconds) to keep rendering the last landing spot.") + .defaultValue(10) + .min(1) + .sliderMax(60) + .visible(keepLastPos::get) + .build() + ); + private final Setting liveUpdate = sgGeneral.add(new BoolSetting.Builder() .name("live-update") .description("Re-simulate the landing spot every tick. Needed for chunk-load updates.") @@ -110,6 +128,13 @@ public enum ListMode { Whitelist, Blacklist } .build() ); + private final Setting tracers = sgRender.add(new BoolSetting.Builder() + .name("tracers") + .description("Draw a line to the landing spot.") + .defaultValue(false) + .build() + ); + private final Setting showName = sgRender.add(new BoolSetting.Builder() .name("show-name") .description("Display the pearl owner's name above the landing spot.") @@ -194,19 +219,28 @@ public enum ListMode { Whitelist, Blacklist } private static class PearlEntry { final int entityId; - String ownerName; - UUID ownerUuid; + String ownerName; + UUID ownerUuid; final Vector3d landingPos = new Vector3d(); boolean isEstimated = false; - boolean isUnknown = false; + boolean isUnknown = false; long timestamp; + boolean isRemoved = false; + long removedTime = 0; + PearlEntry(int entityId, String ownerName, UUID ownerUuid) { this.entityId = entityId; this.ownerName = ownerName; this.ownerUuid = ownerUuid; this.timestamp = System.currentTimeMillis(); } + + boolean shouldRemove(boolean keep, int timeoutSec) { + if (!isRemoved) return false; + if (!keep) return true; + return System.currentTimeMillis() - removedTime > timeoutSec * 1000L; + } } @@ -322,16 +356,44 @@ private void onTick(TickEvent.Pre event) { } - knownPearlIds.removeIf(id -> !activeIds.contains(id)); + for (Deque deque : trackedPearls.values()) { + synchronized (deque) { + for (PearlEntry entry : deque) { + if (!activeIds.contains(entry.entityId) && !entry.isRemoved) { + entry.isRemoved = true; + entry.removedTime = System.currentTimeMillis(); + } + } + } + } + synchronized (unknownPearls) { + for (PearlEntry entry : unknownPearls) { + if (!activeIds.contains(entry.entityId) && !entry.isRemoved) { + entry.isRemoved = true; + entry.removedTime = System.currentTimeMillis(); + } + } + } + + boolean keep = keepLastPos.get(); + int timeout = keepLastPosTimeout.get(); + + for (Deque deque : trackedPearls.values()) { + synchronized (deque) { + deque.removeIf(entry -> entry.shouldRemove(keep, timeout)); + } + } synchronized (unknownPearls) { - unknownPearls.removeIf(entry -> !activeIds.contains(entry.entityId)); + unknownPearls.removeIf(entry -> entry.shouldRemove(keep, timeout)); } + + trackedPearls.values().removeIf(Deque::isEmpty); - trackedPearls.keySet().removeIf(uuid -> !activePlayers.contains(uuid)); + knownPearlIds.removeIf(id -> !activeIds.contains(id)); } private boolean isAllowed(String name) { @@ -445,6 +507,11 @@ private void renderPearlEntry(meteordevelopment.meteorclient.renderer.Renderer3D } + if (tracers.get()) { + r.line(RenderUtils.center.x, RenderUtils.center.y, RenderUtils.center.z, pos.x, pos.y + half, pos.z, lc); + } + + r.box(pos.x - half, pos.y, pos.z - half, pos.x + half, pos.y + boxSize.get(), pos.z + half, sc, lc, shapeMode.get(), 0);