/* * Copyright (C) 2022 NotEnoughUpdates contributors * * This file is part of NotEnoughUpdates. * * NotEnoughUpdates is free software: you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation, either * version 3 of the License, or (at your option) any later version. * * NotEnoughUpdates is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with NotEnoughUpdates. If not, see . */ package io.github.moulberry.notenoughupdates.miscfeatures; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.core.util.Vec3Comparable; import io.github.moulberry.notenoughupdates.core.util.render.RenderUtils; import io.github.moulberry.notenoughupdates.options.customtypes.NEUDebugFlag; import io.github.moulberry.notenoughupdates.util.NEUDebugLogger; import io.github.moulberry.notenoughupdates.util.SBInfo; import io.github.moulberry.notenoughupdates.util.SpecialColour; import io.github.moulberry.notenoughupdates.util.TitleUtil; import io.github.moulberry.notenoughupdates.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.entity.item.EntityArmorStand; import net.minecraft.util.BlockPos; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.IChatComponent; import net.minecraft.util.Vec3i; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.stream.Collectors; public class CrystalMetalDetectorSolver { enum SolutionState { NOT_STARTED, MULTIPLE, MULTIPLE_KNOWN, FOUND, FOUND_KNOWN, FAILED, INVALID, } private static final Minecraft mc = Minecraft.getMinecraft(); private static Vec3Comparable prevPlayerPos; private static double prevDistToTreasure; private static HashSet possibleBlocks = new HashSet<>(); private static final HashMap evaluatedPlayerPositions = new HashMap<>(); private static boolean chestRecentlyFound; private static long chestLastFoundMillis; private static final HashSet openedChestPositions = new HashSet<>(); // Keeper and Mines of Divan center location info private static Vec3i minesCenter; private static boolean debugDoNotUseCenter = false; private static boolean visitKeeperMessagePrinted; private static final String KEEPER_OF_STRING = "Keeper of "; private static final String DIAMOND_STRING = "diamond"; private static final String LAPIS_STRING = "lapis"; private static final String EMERALD_STRING = "emerald"; private static final String GOLD_STRING = "gold"; private static final HashMap keeperOffsets = new HashMap() {{ put(DIAMOND_STRING, new Vec3i(33, 0, 3)); put(LAPIS_STRING, new Vec3i(-33, 0, -3)); put(EMERALD_STRING, new Vec3i(-3, 0, 33)); put(GOLD_STRING, new Vec3i(3, 0, -33)); }}; // Chest offsets from center private static final HashSet knownChestOffsets = new HashSet<>(Arrays.asList( -10171958951910L, // x=-38, y=-22, z=26 10718829084646L, // x=38, y=-22, z=-26 -10721714765806L, // x=-40, y=-22, z=18 -10996458455018L, // x=-41, y=-20, z=22 -1100920913904L, // x=-5, y=-21, z=16 11268584898530L, // x=40, y=-22, z=-30 -11271269253148L, // x=-42, y=-20, z=-28 -11546281377832L, // x=-43, y=-22, z=-40 11818542038999L, // x=42, y=-19, z=-41 12093285728240L, // x=43, y=-21, z=-16 -1409286164L, // x=-1, y=-22, z=-20 1922736062492L, // x=6, y=-21, z=28 2197613969419L, // x=7, y=-21, z=11 2197613969430L, // x=7, y=-21, z=22 -3024999153708L, // x=-12, y=-21, z=-44 3571936395295L, // x=12, y=-22, z=31 3572003504106L, // x=12, y=-22, z=-22 3572003504135L, // x=12, y=-21, z=7 3572070612949L, // x=12, y=-21, z=-43 -3574822076373L, // x=-14, y=-21, z=43 -3574822076394L, // x=-14, y=-21, z=22 -4399455797228L, // x=-17, y=-21, z=20 -5224156626944L, // x=-20, y=-22, z=0 548346527764L, // x=1, y=-21, z=20 5496081743901L, // x=19, y=-22, z=29 5770959650816L, // x=20, y=-22, z=0 5771093868518L, // x=20, y=-21, z=-26 -6048790347736L, // x=-23, y=-22, z=40 6320849682418L, // x=22, y=-21, z=-14 -6323668254708L, // x=-24, y=-22, z=12 6595593371674L, // x=23, y=-22, z=26 6595660480473L, // x=23, y=-22, z=-39 6870471278619L, // x=24, y=-22, z=27 7145349185553L, // x=25, y=-22, z=17 8244995030996L, // x=29, y=-21, z=-44 -8247679385612L, // x=-31, y=-21, z=-12 -8247679385640L, // x=-31, y=-21, z=-40 8519872937959L, // x=30, y=-21, z=-25 -8522557292584L, // x=-32, y=-21, z=-40 -9622068920278L, // x=-36, y=-20, z=42 -9896946827278L, // x=-37, y=-21, z=-14 -9896946827286L // x=-37, y=-21, z=-22 )); static Predicate treasureAllowedPredicate = CrystalMetalDetectorSolver::treasureAllowed; static SolutionState currentState = SolutionState.NOT_STARTED; static SolutionState previousState = SolutionState.NOT_STARTED; public interface Predicate { boolean check(BlockPos blockPos); } public static void process(IChatComponent message) { if (SBInfo.getInstance().getLocation() == null || !NotEnoughUpdates.INSTANCE.config.mining.metalDetectorEnabled || !SBInfo.getInstance().getLocation().equals("crystal_hollows") || !message.getUnformattedText().contains("TREASURE: ")) { return; } boolean centerNewlyDiscovered = locateMinesCenterIfNeeded(); double distToTreasure = Double.parseDouble(message .getUnformattedText() .split("TREASURE: ")[1].split("m")[0].replaceAll("(?!\\.)\\D", "")); // Delay to keep old chest location from being treated as the new chest location if (chestRecentlyFound) { long currentTimeMillis = System.currentTimeMillis(); if (currentTimeMillis - chestLastFoundMillis < 1000 && distToTreasure < 5.0) return; chestLastFoundMillis = currentTimeMillis; chestRecentlyFound = false; } SolutionState originalState = currentState; int originalCount = possibleBlocks.size(); Vec3Comparable adjustedPlayerPos = getPlayerPosAdjustedForEyeHeight(); findPossibleSolutions(distToTreasure, adjustedPlayerPos, centerNewlyDiscovered); if (currentState != originalState || originalCount != possibleBlocks.size()) { switch (currentState) { case FOUND_KNOWN: NEUDebugLogger.log(NEUDebugFlag.METAL, "Known location identified."); // falls through case FOUND: Utils.addChatMessage(EnumChatFormatting.YELLOW + "[NEU] Found solution."); metalDetectorTitle( "Found Solution", NotEnoughUpdates.INSTANCE.config.mining.metalDetectorTicks, NotEnoughUpdates.INSTANCE.config.mining.metalDetectorFoundColor ); if (NEUDebugFlag.METAL.isSet() && (previousState == SolutionState.INVALID || previousState == SolutionState.FAILED)) { NEUDebugLogger.log( NEUDebugFlag.METAL, EnumChatFormatting.AQUA + "Solution coordinates: " + EnumChatFormatting.WHITE + possibleBlocks.iterator().next().toString() ); } break; case INVALID: Utils.addChatMessage(EnumChatFormatting.RED + "[NEU] Previous solution is invalid."); logDiagnosticData(false); resetSolution(false); break; case FAILED: Utils.addChatMessage(EnumChatFormatting.RED + "[NEU] Failed to find a solution."); metalDetectorTitle( "Failed to find a solution!", NotEnoughUpdates.INSTANCE.config.mining.metalDetectorTicks, NotEnoughUpdates.INSTANCE.config.mining.metalDetectorFailedColor ); logDiagnosticData(false); resetSolution(false); break; case MULTIPLE_KNOWN: NEUDebugLogger.log(NEUDebugFlag.METAL, "Multiple known locations identified:"); // falls through case MULTIPLE: Utils.addChatMessage( EnumChatFormatting.YELLOW + "[NEU] Need another position to find solution. Possible blocks: " + possibleBlocks.size()); metalDetectorTitle( "Need another position!", NotEnoughUpdates.INSTANCE.config.mining.metalDetectorTicks, NotEnoughUpdates.INSTANCE.config.mining.metalDetectorMoveColor ); break; default: throw new IllegalStateException("Metal detector is in invalid state"); } } } private static void metalDetectorTitle(String title, int ticks, String color) { if (NotEnoughUpdates.INSTANCE.config.mining.metalDetectorTitle) { TitleUtil.getInstance().createTitle(title, ticks, SpecialColour.specialToChromaRGB(color)); } } static void findPossibleSolutions(double distToTreasure, Vec3Comparable playerPos, boolean centerNewlyDiscovered) { if (Math.abs(prevDistToTreasure - distToTreasure) < 0.2 && prevPlayerPos.distanceTo(playerPos) <= 0.1 && !evaluatedPlayerPositions.containsKey(playerPos)) { evaluatedPlayerPositions.put(playerPos, distToTreasure); if (possibleBlocks.size() == 0) { for (int zOffset = (int) Math.floor(-distToTreasure); zOffset <= Math.ceil(distToTreasure); zOffset++) { for (int y = 65; y <= 75; y++) { double calculatedDist = 0; int xOffset = 0; while (calculatedDist < distToTreasure) { BlockPos pos = new BlockPos(Math.floor(playerPos.xCoord) + xOffset, y, Math.floor(playerPos.zCoord) + zOffset ); calculatedDist = playerPos.distanceTo(new Vec3Comparable(pos).addVector(0D, 1D, 0D)); if (round(calculatedDist, 1) == distToTreasure && treasureAllowedPredicate.check(pos)) { possibleBlocks.add(pos); } xOffset++; } xOffset = 0; calculatedDist = 0; while (calculatedDist < distToTreasure) { BlockPos pos = new BlockPos(Math.floor(playerPos.xCoord) - xOffset, y, Math.floor(playerPos.zCoord) + zOffset ); calculatedDist = playerPos.distanceTo(new Vec3Comparable(pos).addVector(0D, 1D, 0D)); if (round(calculatedDist, 1) == distToTreasure && treasureAllowedPredicate.check(pos)) { possibleBlocks.add(pos); } xOffset++; } } } updateSolutionState(); } else if (possibleBlocks.size() != 1) { HashSet temp = new HashSet<>(); for (BlockPos pos : possibleBlocks) { if (round(playerPos.distanceTo(new Vec3Comparable(pos).addVector(0D, 1D, 0D)), 1) == distToTreasure) { temp.add(pos); } } possibleBlocks = temp; updateSolutionState(); } else { BlockPos pos = possibleBlocks.iterator().next(); if (Math.abs(distToTreasure - (playerPos.distanceTo(new Vec3Comparable(pos)))) > 5) { currentState = SolutionState.INVALID; } } } else if (centerNewlyDiscovered && possibleBlocks.size() > 1) { updateSolutionState(); } prevPlayerPos = playerPos; prevDistToTreasure = distToTreasure; } public static void setDebugDoNotUseCenter(boolean val) { debugDoNotUseCenter = val; } private static String getFriendlyBlockPositions(Collection positions) { if (!NEUDebugLogger.isFlagEnabled(NEUDebugFlag.METAL) || positions.size() == 0) { return ""; } StringBuilder sb = new StringBuilder(); sb.append("\n"); for (BlockPos blockPos : positions) { sb.append("Absolute: "); sb.append(blockPos.toString()); if (minesCenter != Vec3i.NULL_VECTOR) { BlockPos relativeOffset = blockPos.subtract(minesCenter); sb.append(", Relative: "); sb.append(relativeOffset.toString()); sb.append(" (" + relativeOffset.toLong() + ")"); } sb.append("\n"); } return sb.toString(); } private static String getFriendlyEvaluatedPositions() { if (!NEUDebugLogger.isFlagEnabled(NEUDebugFlag.METAL) || evaluatedPlayerPositions.size() == 0) { return ""; } StringBuilder sb = new StringBuilder(); sb.append("\n"); for (Vec3Comparable vec : evaluatedPlayerPositions.keySet()) { sb.append("Absolute: " + vec.toString()); if (minesCenter != Vec3i.NULL_VECTOR) { BlockPos positionBlockPos = new BlockPos(vec); BlockPos relativeOffset = positionBlockPos.subtract(minesCenter); sb.append(", Relative: " + relativeOffset.toString() + " (" + relativeOffset.toLong() + ")"); } sb.append(" Distance: "); sb.append(evaluatedPlayerPositions.get(vec)); sb.append("\n"); } return sb.toString(); } public static void resetSolution(Boolean chestFound) { if (chestFound) { prevPlayerPos = null; prevDistToTreasure = 0; if (possibleBlocks.size() == 1) { openedChestPositions.add(possibleBlocks.iterator().next().getImmutable()); } } chestRecentlyFound = chestFound; possibleBlocks.clear(); evaluatedPlayerPositions.clear(); previousState = currentState; currentState = SolutionState.NOT_STARTED; } public static void initWorld() { minesCenter = Vec3i.NULL_VECTOR; visitKeeperMessagePrinted = false; openedChestPositions.clear(); chestLastFoundMillis = 0; prevDistToTreasure = 0; prevPlayerPos = null; currentState = SolutionState.NOT_STARTED; resetSolution(false); } public static void render(float partialTicks) { int beaconRGB = 0x1fd8f1; if (SBInfo.getInstance().getLocation() != null && SBInfo.getInstance().getLocation().equals("crystal_hollows") && SBInfo.getInstance().location.equals("Mines of Divan")) { if (possibleBlocks.size() == 1) { BlockPos block = possibleBlocks.iterator().next(); RenderUtils.renderBeaconBeam(block.add(0, 1, 0), beaconRGB, 1.0f, partialTicks); RenderUtils.renderWayPoint("Treasure", possibleBlocks.iterator().next().add(0, 2.5, 0), partialTicks); if (NotEnoughUpdates.INSTANCE.config.mining.metalDetectorLineToSolution) { RenderUtils.renderLineToBlock(block.add(0, 1, 0), 0x1fd8f1, partialTicks); } } else if (possibleBlocks.size() > 1 && NotEnoughUpdates.INSTANCE.config.mining.metalDetectorShowPossible) { for (BlockPos block : possibleBlocks) { RenderUtils.renderBeaconBeam(block.add(0, 1, 0), beaconRGB, 1.0f, partialTicks); RenderUtils.renderWayPoint("Possible Treasure Location", block.add(0, 2.5, 0), partialTicks); } } } } private static boolean locateMinesCenterIfNeeded() { if (minesCenter != Vec3i.NULL_VECTOR) { return false; } List keeperEntities = mc.theWorld.getEntities(EntityArmorStand.class, (entity) -> { if (!entity.hasCustomName()) return false; return entity.getCustomNameTag().contains(KEEPER_OF_STRING); }); if (keeperEntities.size() == 0) { if (!visitKeeperMessagePrinted) { Utils.addChatMessage(EnumChatFormatting.YELLOW + "[NEU] Approach a Keeper while holding the metal detector to enable faster treasure hunting."); visitKeeperMessagePrinted = true; } return false; } EntityArmorStand keeperEntity = keeperEntities.get(0); String keeperName = keeperEntity.getCustomNameTag(); NEUDebugLogger.log(NEUDebugFlag.METAL, "Locating center using Keeper: " + EnumChatFormatting.WHITE + keeperEntity); String keeperType = keeperName.substring(keeperName.indexOf(KEEPER_OF_STRING) + KEEPER_OF_STRING.length()); minesCenter = keeperEntity.getPosition().add(keeperOffsets.get(keeperType.toLowerCase(Locale.ROOT))); NEUDebugLogger.log(NEUDebugFlag.METAL, "Mines center: " + EnumChatFormatting.WHITE + minesCenter.toString()); Utils.addChatMessage( EnumChatFormatting.YELLOW + "[NEU] Faster treasure hunting is now enabled based on Keeper location."); return true; } public static void setMinesCenter(BlockPos center) { minesCenter = center; } private static double round(double value, int precision) { int scale = (int) Math.pow(10, precision); return (double) Math.round(value * scale) / scale; } private static void updateSolutionState() { previousState = currentState; if (possibleBlocks.size() == 0) { currentState = SolutionState.FAILED; return; } if (possibleBlocks.size() == 1) { currentState = SolutionState.FOUND; return; } // Narrow solutions using known locations if the mines center is known if (minesCenter.equals(BlockPos.NULL_VECTOR) || debugDoNotUseCenter) { currentState = SolutionState.MULTIPLE; return; } HashSet temp = possibleBlocks.stream() .filter(block -> knownChestOffsets.contains(block.subtract(minesCenter).toLong())) .collect(Collectors.toCollection(HashSet::new)); if (temp.size() == 0) { currentState = SolutionState.MULTIPLE; return; } if (temp.size() == 1) { possibleBlocks = temp; currentState = SolutionState.FOUND_KNOWN; return; } currentState = SolutionState.MULTIPLE_KNOWN; } public static BlockPos getSolution() { if (CrystalMetalDetectorSolver.possibleBlocks.size() != 1) { return BlockPos.ORIGIN; } return CrystalMetalDetectorSolver.possibleBlocks.stream().iterator().next(); } private static Vec3Comparable getPlayerPosAdjustedForEyeHeight() { return new Vec3Comparable( mc.thePlayer.posX, mc.thePlayer.posY + (mc.thePlayer.getEyeHeight() - mc.thePlayer.getDefaultEyeHeight()), mc.thePlayer.posZ ); } static boolean isKnownOffset(BlockPos pos) { return knownChestOffsets.contains(pos.subtract(minesCenter).toLong()); } static boolean isAllowedBlockType(BlockPos pos) { return mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:gold_block") || mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:prismarine") || mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:chest") || mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:stained_glass") || mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:stained_glass_pane") || mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:wool") || mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:stained_hardened_clay"); } static boolean isAirAbove(BlockPos pos) { return mc.theWorld. getBlockState(pos.add(0, 1, 0)).getBlock().getRegistryName().equals("minecraft:air"); } private static boolean treasureAllowed(BlockPos pos) { boolean airAbove = isAirAbove(pos); boolean allowedBlockType = isAllowedBlockType(pos); return isKnownOffset(pos) || (airAbove && allowedBlockType); } static private String getDiagnosticMessage() { StringBuilder diagsMessage = new StringBuilder(); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Mines Center: "); diagsMessage.append(EnumChatFormatting.WHITE); diagsMessage.append((minesCenter.equals(Vec3i.NULL_VECTOR)) ? "" : minesCenter.toString()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Current Solution State: "); diagsMessage.append(EnumChatFormatting.WHITE); diagsMessage.append(currentState.name()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Previous Solution State: "); diagsMessage.append(EnumChatFormatting.WHITE); diagsMessage.append(previousState.name()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Previous Player Position: "); diagsMessage.append(EnumChatFormatting.WHITE); diagsMessage.append((prevPlayerPos == null) ? "" : prevPlayerPos.toString()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Previous Distance To Treasure: "); diagsMessage.append(EnumChatFormatting.WHITE); diagsMessage.append((prevDistToTreasure == 0) ? "" : prevDistToTreasure); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Current Possible Blocks: "); diagsMessage.append(EnumChatFormatting.WHITE); diagsMessage.append(possibleBlocks.size()); diagsMessage.append(getFriendlyBlockPositions(possibleBlocks)); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Evaluated player positions: "); diagsMessage.append(EnumChatFormatting.WHITE); diagsMessage.append(evaluatedPlayerPositions.size()); diagsMessage.append(getFriendlyEvaluatedPositions()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); diagsMessage.append("Chest locations not on known list:\n"); diagsMessage.append(EnumChatFormatting.WHITE); if (minesCenter != Vec3i.NULL_VECTOR) { HashSet locationsNotOnKnownList = openedChestPositions .stream() .filter(block -> !knownChestOffsets.contains(block.subtract(minesCenter).toLong())) .map(block -> block.subtract(minesCenter)) .collect(Collectors.toCollection(HashSet::new)); if (locationsNotOnKnownList.size() > 0) { for (BlockPos blockPos : locationsNotOnKnownList) { diagsMessage.append(String.format( "%dL,\t\t// x=%d, y=%d, z=%d", blockPos.toLong(), blockPos.getX(), blockPos.getY(), blockPos.getZ() )); } } } else { diagsMessage.append(""); } return diagsMessage.toString(); } public static void logDiagnosticData(boolean outputAlways) { if (!SBInfo.getInstance().checkForSkyblockLocation()) { return; } if (!NotEnoughUpdates.INSTANCE.config.mining.metalDetectorEnabled) { Utils.addChatMessage(EnumChatFormatting.RED + "[NEU] Metal Detector Solver is not enabled."); return; } boolean metalDebugFlagSet = NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.contains(NEUDebugFlag.METAL); if (outputAlways || metalDebugFlagSet) { NEUDebugLogger.logAlways(getDiagnosticMessage()); } } }