diff --git a/gradle.properties b/gradle.properties index 05b7bb1..6011903 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,6 +8,6 @@ yarn_mappings=1.21.11+build.3 loader_version=0.18.2 # Mod Properties -mod_version=2.6.6 +mod_version=2.6.7 maven_group=cqb13.NumbyHack archives_base_name=Numby-Hack diff --git a/src/main/java/cqb13/NumbyHack/modules/general/TanukiEgapFinder.java b/src/main/java/cqb13/NumbyHack/modules/general/TanukiEgapFinder.java index daeddf5..7397073 100644 --- a/src/main/java/cqb13/NumbyHack/modules/general/TanukiEgapFinder.java +++ b/src/main/java/cqb13/NumbyHack/modules/general/TanukiEgapFinder.java @@ -3,11 +3,17 @@ import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; -import java.io.PrintWriter; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; import cqb13.NumbyHack.NumbyHack; +import meteordevelopment.meteorclient.events.world.ChunkDataEvent; import meteordevelopment.meteorclient.events.world.TickEvent; import meteordevelopment.meteorclient.settings.BoolSetting; +import meteordevelopment.meteorclient.settings.DoubleSetting; +import meteordevelopment.meteorclient.settings.IntSetting; import meteordevelopment.meteorclient.settings.Setting; import meteordevelopment.meteorclient.settings.SettingGroup; import meteordevelopment.meteorclient.systems.modules.Module; @@ -23,25 +29,17 @@ import net.minecraft.util.math.BlockPos; /** - * from Tanuki + * Original from Tanuki: + * https://gitlab.com/Walaryne/tanuki/-/blob/master/src/main/java/minegame159/meteorclient/modules/misc/EgapFinder.java */ -// https://gitlab.com/Walaryne/tanuki/-/blob/master/src/main/java/minegame159/meteorclient/modules/misc/EgapFinder.java - public class TanukiEgapFinder extends Module { - private final SettingGroup sgDefault = settings.getDefaultGroup(); + private static final String OUTPUT_FILE_NAME = "egap-coords"; - private final Setting coords = sgDefault.add(new BoolSetting.Builder() - .name("coords") - .description("Sends the coords in the message in case you're lazy to look at your .minecraft folder.") - .defaultValue(true) - .build()); + private static final int COMPARATOR_DELAY_TICKS = 3; + private static final boolean DEBUG = true; - private final Setting debug = sgDefault.add(new BoolSetting.Builder() - .name("debug") - .description( - "Useless. Just prints info about every chest it locates in your render distance, will spam chat a lot.") - .defaultValue(false) - .build()); + private final SettingGroup sgDefault = settings.getDefaultGroup(); + private final SettingGroup sgAutoSearch = settings.createGroup("Auto-Search"); private final Setting playSound = sgDefault.add(new BoolSetting.Builder() .name("play-sound") @@ -49,135 +47,353 @@ public class TanukiEgapFinder extends Module { .defaultValue(false) .build()); - private boolean check; - private boolean lock; - private int stage = 0; - private int checkDelay; - private int comparatorHold = 0; - private BlockPos chest; - private BlockPos prevChest; + private final Setting autoSearch = sgAutoSearch.add(new BoolSetting.Builder() + .name("auto-search") + .description("Teleports you to a new area to be scanned when the current area is already completed.") + .defaultValue(false) + .build()); - public TanukiEgapFinder() { - super(NumbyHack.CATEGORY, "egap-finder", - "Finds Egaps in a SP world and creates a file called \"egap-coords.txt\"."); + private final Setting renderDistance = sgAutoSearch.add(new IntSetting.Builder() + .name("render-distance") + .description("The render distance in your settings, this is used to calculate the position to tp to.") + .defaultValue(16) + .min(5) + .sliderMax(32) + .visible(autoSearch::get) + .build()); + + private final Setting noChestTimeout = sgAutoSearch.add(new DoubleSetting.Builder() + .name("no-chest-timeout") + .description("Time in seconds where no chest is found and the area is deemed as looted.") + .defaultValue(2.5) + .min(1) + .sliderMax(10) + .visible(autoSearch::get) + .decimalPlaces(1) + .build()); + + private final Setting chunkStableTime = sgAutoSearch.add(new DoubleSetting.Builder() + .name("chunk-stable-time") + .description("Time in seconds no new chunks are loading before moving on.") + .defaultValue(2.5) + .min(1) + .sliderMax(10) + .visible(autoSearch::get) + .decimalPlaces(1) + .build()); + + private final Setting teleportCooldown = sgAutoSearch.add(new DoubleSetting.Builder() + .name("teleport-cooldown") + .description("Minimum time in seconds to wait after teleporting before moving again.") + .defaultValue(5) + .min(1) + .sliderMax(20) + .visible(autoSearch::get) + .decimalPlaces(1) + .build()); + + private enum ProcessState { + IDLE, + PLACE_COMPARATOR, + PLACE_LEAVES, + CHECK_FOR_EGAP, + VERIFY_RESULT } - private static void writeToFile(String coords) { - try (FileWriter fw = new FileWriter("egap-coords.txt", true); - BufferedWriter bw = new BufferedWriter(fw); - PrintWriter out = new PrintWriter(bw)) { - out.println(coords); - } catch (IOException exception) { - exception.printStackTrace(); - } + private ProcessState currentState = ProcessState.IDLE; + private boolean isProcessingChest = false; + private int ticksWithoutChest = 0; + private int comparatorDelayCounter = 0; + private BlockPos currentChestPos = null; + private BlockPos lastCheckedChestPos = null; + private SpiralTraversal spiralTraversal; + private int ticksSinceLastChunkData = 0; + private int ticksSinceTeleport = 0; + + public TanukiEgapFinder() { + super(NumbyHack.CATEGORY, "egap-finder", + "Finds Enchanted Golden Apples in chests and logs coordinates to " + OUTPUT_FILE_NAME + + "-yourworldseed.txt"); } @Override public void onActivate() { - stage = 0; - checkDelay = 0; - lock = true; - if (debug.get()) - ChatUtils.info("STARTING"); + BlockPos playerPos = mc.player.getBlockPos(); + spiralTraversal = new SpiralTraversal(playerPos.getX(), playerPos.getZ()); + resetState(); } - @Override - public void onDeactivate() { - if (debug.get()) - ChatUtils.info("STOPPING"); + @EventHandler + private void onChunkData(ChunkDataEvent event) { + ticksSinceLastChunkData = 0; } @EventHandler private void onTick(TickEvent.Pre event) { - check = false; - for (BlockEntity blockEntity : Utils.blockEntities()) { - if (blockEntity instanceof ChestBlockEntity) { - if (blockEntity.isRemoved()) - continue; - chest = blockEntity.getPos(); + if (mc.world == null || mc.player == null) { + debug("Tick skipped: world or player is null"); + return; + } + + ticksSinceLastChunkData++; + ticksSinceTeleport++; + + BlockPos foundChest = findNearestChest(); - check = true; - lock = false; + if (foundChest != null) { + if (currentChestPos == null || !currentChestPos.equals(foundChest)) { + debug("Found new chest at: " + formatPos(foundChest)); } + currentChestPos = foundChest; + ticksWithoutChest = 0; + isProcessingChest = false; + } else { + ticksWithoutChest++; } - if (!check) { - checkDelay++; - } else { - checkDelay = 0; + if (autoSearch.get() + && ticksWithoutChest >= noChestTimeout.get() * 20 + && ticksSinceLastChunkData >= chunkStableTime.get() * 20 + && ticksSinceTeleport >= teleportCooldown.get() * 20) { + debug("Chunks stable (" + ticksSinceLastChunkData + + " ticks) and minimum wait satisfied, moving to new area"); + tpToNewSearchArea(); + resetState(); + ticksWithoutChest = 0; + ticksSinceLastChunkData = 0; + ticksSinceTeleport = 0; + return; } - if (checkDelay >= 2) { - lock = true; - checkDelay = 0; - stage = 1; + + if (isProcessingChest || currentChestPos == null) { + return; } - if (!lock) { - if (stage == 0) { - stage = 1; + processCurrentState(); + } + + private void tpToNewSearchArea() { + XZPos positionToTeleportTo = spiralTraversal.next(); + String command = "/tp %d ~ %d"; + + ChatUtils.info(Formatting.GREEN + + String.format("Teleporting to new search area with the center at (%d, ~, %d)!", + positionToTeleportTo.x(), positionToTeleportTo.z())); + + ChatUtils.sendPlayerMsg(String.format(command, positionToTeleportTo.x(), positionToTeleportTo.z())); + ticksSinceTeleport = 0; + } + + private BlockPos findNearestChest() { + for (BlockEntity blockEntity : Utils.blockEntities()) { + if (blockEntity instanceof ChestBlockEntity && !blockEntity.isRemoved()) { + return blockEntity.getPos(); } - switch (stage) { - case 1: { - int adjacent = chest.getX() - 1; - Block block = mc.world.getBlockState(chest.add(-1, 0, 0)).getBlock(); - if (block != Blocks.COMPARATOR) { - ; - ChatUtils.sendPlayerMsg("/setblock " + adjacent + " " + chest.getY() + " " + chest.getZ() - + " minecraft:comparator[facing=east]"); - } - stage++; - break; - } - case 2: { - int xAdjacent = chest.getX() - 1; - int yAdjacent = chest.getY() + 1; - Block block = mc.world.getBlockState(chest.add(-1, +1, 0)).getBlock(); - if (block != Blocks.ACACIA_LEAVES) { - ChatUtils.sendPlayerMsg( - "/setblock " + xAdjacent + " " + yAdjacent + " " + chest.getZ() - + " minecraft:acacia_leaves"); - } - comparatorHold++; - if (comparatorHold == 3) { - stage++; - comparatorHold = 0; - } - break; - } - case 3: { - Block block = mc.world.getBlockState(chest).getBlock(); - if (block == Blocks.CHEST) { - ChatUtils.sendPlayerMsg( - "/execute if data block " + chest.getX() + " " + chest.getY() + " " + chest.getZ() - + " Items[{id:\"minecraft:enchanted_golden_apple\"}] as @p run setblock " - + chest.getX() + " " - + chest.getY() + " " + chest.getZ() + " minecraft:diamond_block"); - } - prevChest = chest; - stage++; - break; - } - case 4: { - Block diamondPos = mc.world.getBlockState(prevChest).getBlock(); - if (diamondPos == Blocks.DIAMOND_BLOCK) { - if (playSound.get()) - mc.player.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f); - ChatUtils.info((!coords.get() ? Formatting.GREEN + "Found an egap! Wrote coords to file." - : Formatting.GREEN + "Found an egap! Wrote coords to file. " + prevChest.getX() + " " - + prevChest.getY() - + " " + prevChest.getZ())); - writeToFile(prevChest.getX() + " " + prevChest.getY() + " " + prevChest.getZ()); - } else - ChatUtils.sendPlayerMsg( - "/setblock " + chest.getX() + " " + chest.getY() + " " + chest.getZ() - + " minecraft:air"); - stage++; - break; - } + } + + return null; + } + + private void processCurrentState() { + debug("Processing state: " + currentState); + + switch (currentState) { + case IDLE: + currentState = ProcessState.PLACE_COMPARATOR; + break; + + case PLACE_COMPARATOR: + placeComparator(); + currentState = ProcessState.PLACE_LEAVES; + break; + + case PLACE_LEAVES: + placeLeavesAndWaitForComparatorUpdate(); + break; + + case CHECK_FOR_EGAP: + checkForEgap(); + currentState = ProcessState.VERIFY_RESULT; + break; + + case VERIFY_RESULT: + verifyAndCleanup(); + currentState = ProcessState.IDLE; + break; + } + } + + private void placeComparator() { + BlockPos comparatorPos = currentChestPos.add(-1, 0, 0); + Block existingBlock = mc.world.getBlockState(comparatorPos).getBlock(); + + if (existingBlock != Blocks.COMPARATOR) { + String command = String.format("/setblock %d %d %d minecraft:comparator[facing=east]", + comparatorPos.getX(), comparatorPos.getY(), comparatorPos.getZ()); + ChatUtils.sendPlayerMsg(command); + } + } + + private void placeLeavesAndWaitForComparatorUpdate() { + BlockPos leavesPos = currentChestPos.add(-1, 1, 0); + Block existingBlock = mc.world.getBlockState(leavesPos).getBlock(); + + if (existingBlock != Blocks.ACACIA_LEAVES) { + String command = String.format("/setblock %d %d %d minecraft:acacia_leaves", + leavesPos.getX(), leavesPos.getY(), leavesPos.getZ()); + ChatUtils.sendPlayerMsg(command); + } + + comparatorDelayCounter++; + + // Wait for comparator to update before checking + if (comparatorDelayCounter >= COMPARATOR_DELAY_TICKS) { + currentState = ProcessState.CHECK_FOR_EGAP; + comparatorDelayCounter = 0; + } + } + + private void checkForEgap() { + Block block = mc.world.getBlockState(currentChestPos).getBlock(); + + if (block != Blocks.CHEST) { + return; + } + + String command = String.format( + "/execute if data block %d %d %d Items[{id:\"minecraft:enchanted_golden_apple\"}] " + + "as @p run setblock %d %d %d minecraft:diamond_block", + currentChestPos.getX(), currentChestPos.getY(), currentChestPos.getZ(), + currentChestPos.getX(), currentChestPos.getY(), currentChestPos.getZ()); + + ChatUtils.sendPlayerMsg(command); + + lastCheckedChestPos = currentChestPos; + } + + private void verifyAndCleanup() { + if (lastCheckedChestPos == null) { + return; + } + + Block resultBlock = mc.world.getBlockState(lastCheckedChestPos).getBlock(); + + if (resultBlock == Blocks.DIAMOND_BLOCK) { + handleEgapFound(lastCheckedChestPos); + } else { + String command = String.format("/setblock %d %d %d minecraft:air", + lastCheckedChestPos.getX(), lastCheckedChestPos.getY(), lastCheckedChestPos.getZ()); + ChatUtils.sendPlayerMsg(command); + } + } + + private void handleEgapFound(BlockPos pos) { + String coords = String.format("%d %d %d", pos.getX(), pos.getY(), pos.getZ()); + + if (playSound.get() && mc.player != null) { + mc.player.playSound(SoundEvents.ENTITY_EXPERIENCE_ORB_PICKUP, 1.0f, 1.0f); + } + + ChatUtils.info(Formatting.GREEN + "Found an egap (" + coords + ")!"); + + if (!writeCoordinatesToFile(coords)) { + ChatUtils.error("Failed to write coordinates to file!"); + } else { + } + } + + private boolean writeCoordinatesToFile(String coords) { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(getOutputFileName(), true))) { + writer.write(coords); + writer.newLine(); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + private void resetState() { + isProcessingChest = true; + currentState = ProcessState.IDLE; + comparatorDelayCounter = 0; + currentChestPos = null; + lastCheckedChestPos = null; + } + + private void debug(String message) { + if (DEBUG) { + System.out.println("[EgapFinder] " + message); + } + } + + private String formatPos(BlockPos pos) { + return String.format("(%d, %d, %d)", pos.getX(), pos.getY(), pos.getZ()); + } + + private class SpiralTraversal { + private final Set visited = new HashSet<>(); + private final Set queued = new HashSet<>(); + private final Queue queue = new LinkedList<>(); + + public SpiralTraversal(int originX, int originZ) { + XZPos origin = new XZPos(originX, originZ); + + visited.add(origin); + updateQueue(origin); + } + + public XZPos next() { + XZPos next = queue.remove(); + + queued.remove(next); + visited.add(next); + updateQueue(next); + + return next; + } + + private void updateQueue(XZPos position) { + int blockOffset = 2 * 16 * renderDistance.get(); + Set neighbors = new HashSet<>(); + + neighbors.add(new XZPos(position.x + blockOffset, position.z)); + neighbors.add(new XZPos(position.x, position.z - blockOffset)); + neighbors.add(new XZPos(position.x - blockOffset, position.z)); + neighbors.add(new XZPos(position.x, position.z + blockOffset)); + + for (XZPos neighbor : neighbors) { + if (visited.contains(neighbor) || queued.contains(neighbor)) + continue; + + queue.add(neighbor); + queued.add(neighbor); } - if (stage == 5) { - stage = 1; + } + } + + record XZPos(int x, int z) { + } + + private String getOutputFileName() { + if (mc.world == null) { + return OUTPUT_FILE_NAME + ".txt"; + } + + long seed = getWorldSeed(); + + // If negative turn - into n + String seedStr = seed < 0 ? "n" + Math.abs(seed) : String.valueOf(seed); + return String.format("%s-%s.txt", OUTPUT_FILE_NAME, seedStr); + } + + private Long getWorldSeed() { + if (mc.getServer() != null) { + var worldProperties = mc.getServer().getSaveProperties(); + if (worldProperties != null) { + return worldProperties.getGeneratorOptions().getSeed(); } } + return null; } }