diff --git a/.gitignore b/.gitignore index 09cd281f..af00e685 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ bin/ # fabric run/ + +/DEVNODE \ No newline at end of file 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..0eca80c9 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; @@ -31,7 +33,6 @@ public void onInitialize() { 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()); Modules.get().add(new CrystalMacro()); @@ -53,7 +54,6 @@ public void onInitialize() { 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()); @@ -86,7 +86,6 @@ public void onInitialize() { 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()); @@ -96,11 +95,19 @@ public void onInitialize() { 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()); + + } @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..34143165 --- /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; + } + + + String itemName = getItemName(mainHandItem); + + + List enchantmentStrings = getEnchantments(mainHandItem); + + + StringBuilder searchCommand = new StringBuilder("ah "); + searchCommand.append(itemName); + + + if (mainHandItem.getCount() == 64) { + searchCommand.append(" stack"); + } + + if (!enchantmentStrings.isEmpty()) { + searchCommand.append(" "); + searchCommand.append(String.join(" ", enchantmentStrings)); + } + + + String command = searchCommand.toString(); + info("Searching: /" + command); + mc.getNetworkHandler().sendChatCommand(command); + + return SINGLE_SUCCESS; + }); + } + + private String getItemName(ItemStack stack) { + + String itemId = stack.getItem().toString(); + + + if (itemId.contains(":")) { + itemId = itemId.split(":")[1]; + } + + return itemId.toLowerCase().replace(" ", "_"); + } + + private List getEnchantments(ItemStack stack) { + List result = new ArrayList<>(); + + + 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) { + + String enchantmentId = enchantmentEntry.getIdAsString(); + + + 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..35d28035 --- /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; + + + 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..a3d3a860 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,8 @@ import meteordevelopment.meteorclient.utils.render.color.SettingColor; import meteordevelopment.orbit.EventHandler; import net.minecraft.block.BlockState; +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 +29,12 @@ 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"); + private final Setting detectionMode = sgGeneral.add(new EnumSetting.Builder() .name("Detection Mode") .description("Choose what to detect: holes, tunnels, stairs, or all.") @@ -38,321 +43,587 @@ 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 block updates below Y=0 required to trigger an underground rescan of that chunk.") + .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() ); + 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() ); + 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() ); + 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) + + 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() ); + + 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)) - .build() - ); + .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 }; + + // ── 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 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(); + + // 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); + + // ── 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 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(); 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 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)) { + // Server re-sent data for a known chunk (underground reveal). + // Schedule an immediate targeted rescan next tick. + chunksNeedingRescan.add(key); + } + } + + } 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)); + + // 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) { + chunks.keySet().removeAll(toProcess); + } } + /** + * 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)); + 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++) + if (chunkKeys.contains(ChunkPos.toLong(cx, cz))) return true; + return false; + } + + // ========================================================================= + // TICK / RENDER + // ========================================================================= + @EventHandler private void onTick(TickEvent.Post event) { + // Process any chunks that received new underground data first. + processRescans(); + 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(); + + // 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) { - chunkSet.add((WorldChunk) chunk); - } - } - - removeBoxesOutsideRenderDistance(holes, chunkSet); - removeBoxesOutsideRenderDistance(tunnels, chunkSet); - removeBoxesOutsideRenderDistance(staircases, chunkSet); - removeBoxesOutsideRenderDistance(holes3x1, chunkSet); + for (Chunk chunk : Utils.chunks(true)) + if (chunk instanceof WorldChunk wc) chunkSet.add(wc); + 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 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: - renderHoles(event.renderer); - renderTunnels(event.renderer); - renderStaircases(event.renderer); - render3x1Holes(event.renderer); - break; - case HOLES_AND_TUNNELS: - renderHoles(event.renderer); - renderTunnels(event.renderer); - render3x1Holes(event.renderer); - break; - case HOLES_AND_STAIRCASES: - renderHoles(event.renderer); - renderStaircases(event.renderer); - render3x1Holes(event.renderer); - break; - case TUNNELS_AND_STAIRCASES: - renderTunnels(event.renderer); - renderStaircases(event.renderer); - break; - case HOLES: - renderHoles(event.renderer); + case ALL -> { + 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); render3x1Holes(event.renderer); - break; - case TUNNELS: - renderTunnels(event.renderer); - break; - case STAIRCASES: - renderStaircases(event.renderer); - break; - case HOLES_3X1_AND_TUNNELS: - renderHoles(event.renderer); + if (detectCoveredHoles.get()) renderCoveredHoles(event.renderer); + } + case HOLES_AND_STAIRCASES -> { + 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); + 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 -> { + 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; + 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; + 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 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 renderTunnels(Renderer3D r) { + // 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 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 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 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 +631,547 @@ 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) { + 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; } + + // NEW: After all diagonal tunnels are detected, compute neighbor relationships + computeDiagonalTunnelNeighbors(); } - 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 DETECTION + // ========================================================================= + + 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 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); } } } - 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 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); } } } - - // 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 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); } } } } - 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()); + // ========================================================================= + // 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; + + 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; + } + return !isPassableBlock(pos.offset(widthDir, width)); } - 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(); - } - while (isTunnelSection(currentPos, dir)) { - maxHeight = Math.max(maxHeight, getTunnelHeight(currentPos)); - endPos = currentPos.toImmutable(); - currentPos.move(dir); - stepCount++; + /** + * 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 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++; + 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(); + probe.move(dir.getOpposite()); + while (isValidTunnelCrossSection(probe, dir, width, refHeight)) { + canonicalStart.set(probe); + probe.move(dir.getOpposite()); + } } - 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); + // 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; + + // 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; + + 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()) 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) + )); } } - 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 DETECTION + // ========================================================================= + + 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 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 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); - } + List potential = new ArrayList<>(); + Direction checkDir = dir; + boolean turnRight = true; - if (turnRight) { - checkingDir = checkingDir.rotateYClockwise(); - currentPos.move(checkingDir.rotateYClockwise(), i); - turnRight = false; - } else { - checkingDir = checkingDir.rotateYCounterclockwise(); - currentPos.move(checkingDir.rotateYCounterclockwise(), i); - turnRight = true; + while (isDiagonalTunnelSection(cur, checkDir)) { + int height = getTunnelHeight(cur); + BlockPos.Mutable fill = cur.mutableCopy(); + + for (int k = 0; k < w; k++) { + // 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()); + } + + 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 (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.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; + }); + } } } } } - - 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; + + /** + * 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); } - if (wasPassableBlockFound) return false; - - return true; } - private int getTunnelHeight(BlockPos pos) { - int height = 0; - while (isPassableBlock(pos.up(height)) && height < maxTunnelHeight.get()) { - height++; + // ========================================================================= + // 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; } - 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); + 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)); } - currentPos.move(dir); - currentPos.move(Direction.UP); - stepCount++; } + } + } - for (Box stairsBox : potentialStaircaseBoxes) { - if (stepCount >= minStaircaseLength.get() && !staircases.contains(stairsBox) && !staircases.stream().anyMatch(existingStaircase -> existingStaircase.intersects(stairsBox))) { - staircases.add(stairsBox); - } - } + 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++; + return h; } private int getStaircaseHeight(BlockPos pos) { - int height = 0; - while (isPassableBlock(pos.up(height)) && height < maxStaircaseHeight.get()) { - height++; - } - return height; + 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[] 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; - } - } - } + 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; + for (int i = 0; i < height; i++) + if (isPassableBlock(pos.up(i).offset(dir))) 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); - } + if (airBlocks.get()) return state.isAir(); + VoxelShape shape = state.getCollisionShape(mc.world, pos); + return shape.isEmpty() || !VoxelShapes.fullCube().equals(shape); } + // ========================================================================= + // 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..88aa40a7 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/esp/PearlLandingPredictor.java @@ -0,0 +1,563 @@ +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.RenderUtils; +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 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.") + .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 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.") + .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; + UUID ownerUuid; + final Vector3d landingPos = new Vector3d(); + boolean isEstimated = 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; + } + } + + + private final Map> trackedPearls = new ConcurrentHashMap<>(); + private final Deque unknownPearls = new ArrayDeque<>(); + private final Set knownPearlIds = ConcurrentHashMap.newKeySet(); + private final ProjectileEntitySimulator simulator = new ProjectileEntitySimulator(); + + + 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; + + + UUID ownerUuid = pearl.getOwner() != null ? pearl.getOwner().getUuid() : null; + String ownerName = pearl.getOwner() != null ? pearl.getOwner().getName().getString() : "Unknown"; + + + 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 { + + synchronized (unknownPearls) { + unknownPearls.addFirst(entry); + while (unknownPearls.size() > maxPerPlayer.get()) unknownPearls.removeLast(); + } + } + + } else if (liveUpdate.get()) { + + 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) { + + if (entryToUpdate.isUnknown && pearl.getOwner() != null) { + entryToUpdate.ownerName = ownerName; + entryToUpdate.ownerUuid = ownerUuid; + entryToUpdate.isUnknown = false; + } + simulateLanding(pearl, entryToUpdate); + } + } + } + + + + 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 -> entry.shouldRemove(keep, timeout)); + } + + + trackedPearls.values().removeIf(Deque::isEmpty); + + + knownPearlIds.removeIf(id -> !activeIds.contains(id)); + } + + 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; + + + try { + if (seeThrough.get()) RenderSystem.disableDepthTest(); + + + for (Deque deque : trackedPearls.values()) { + synchronized (deque) { + for (PearlEntry entry : deque) { + renderPearlEntry(event.renderer, entry, half); + } + } + } + + + synchronized (unknownPearls) { + for (PearlEntry entry : unknownPearls) { + 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(); + } + + + 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); + } + + @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; + + + for (Deque deque : trackedPearls.values()) { + synchronized (deque) { + for (PearlEntry entry : deque) { + renderPearlName(entry, half); + } + } + } + + + synchronized (unknownPearls) { + for (PearlEntry entry : unknownPearls) { + renderPearlName(entry, half); + } + } + } + + private void renderPearlName(PearlEntry entry, double half) { + + 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..8fb670be --- /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(); + + + 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 + } + + + + private int getShopPrice() { + return switch (category.get()) { + case END -> switch (endItem.get()) { + case ENDER_CHEST -> 2500; + case ENDER_PEARL -> 75; + case END_STONE -> 8; + 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; + }; + }; + } + + + private int getDefaultMinPrice() { + return getShopPrice() + 1; + } + + + 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; + + + private long last_action_time = 0; + private static final long STUCK_TIMEOUT_MS = 5000; + + + private long buy_spam_start_time = 0; + private static final long BUY_SPAM_TIMEOUT_MS = 5000; + + + private int move_pass_count = 0; + private static final int MAX_MOVE_PASSES = 5; + + + 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."); + } + + + private double getEffectiveMinPrice() { + if (priceMode.get() == PriceMode.AUTO) { + return getDefaultMinPrice(); + } + + + 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; + } + + + private void recordAction() { + last_action_time = System.currentTimeMillis(); + } + + + 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(); + + + if (checkAndHandleStuck()) return; + + switch (stage) { + + 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 (!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 -> { + + 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(); + + + 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) { + + if (is_inventory_full()) { + mc.player.closeHandledScreen(); + stage = Stage.SHOP_EXIT; + stage_start = now; + recordAction(); + return; + } + + + 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 { + + 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(); + } + } + + + 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(); + + + 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(); + } + } + } + + + case ORDERS_SELECT -> { + if (mc.currentScreen instanceof GenericContainerScreen screen) { + ScreenHandler handler = screen.getScreenHandler(); + + + List slots_to_move = new ArrayList<>(); + + for (Slot slot : handler.slots) { + + if (slot.inventory == mc.player.getInventory()) { + int slot_index = slot.getIndex(); + + + 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()) { + + mc.player.closeHandledScreen(); + stage = Stage.ORDERS_CONFIRM; + stage_start = now; + recordAction(); + return; + } + + + for (int slot_id : slots_to_move) { + mc.interactionManager.clickSlot(handler.syncId, slot_id, 0, SlotActionType.QUICK_MOVE, mc.player); + } + + move_pass_count++; + recordAction(); + + + if (move_pass_count >= MAX_MOVE_PASSES) { + + mc.player.closeHandledScreen(); + stage = Stage.ORDERS_CONFIRM; + stage_start = now; + recordAction(); + } + + } + } + + 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 -> {} + } + } + + + + 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(); + } + + + private boolean isTargetItemStackable() { + Item item = getTargetMcItem(); + return item.getMaxCount() > 1; + } + + + 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; + } + + + + 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[] 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) { + + } + } + } + } + + return -1.0; + } + + + + 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 deliver_pattern = Pattern.compile("(?i)click to deliver\\s+\\.?([a-zA-Z0-9_]+)"); + + + 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)); - } - } -}