diff options
13 files changed, 2916 insertions, 337 deletions
diff --git a/Update Notes/2.1.md b/Update Notes/2.1.md index 98672187..4e8e7bf8 100644 --- a/Update Notes/2.1.md +++ b/Update Notes/2.1.md @@ -14,7 +14,7 @@ - Added a pet overlay that shows your active pet in your inventory - [Price graph for items on /ah and /bz](https://cdn.discordapp.com/attachments/896407218151366687/926968296929107999/unknown.png) - DeDiamondPro - [Added bingo tab in /pv](https://cdn.discordapp.com/attachments/702456294874808330/951909539383500820/unknown.png) - Lulonaut -- Added wishing compass solver that shows target coordinates using two samples - CraftyOldMiner +- Added wishing compass solver that shows target coordinates, structure, and integrates with Skytils waypoints - CraftyOldMiner - Improved metal detector logic to solve using a single position in most cases using known locations based on Keeper coordinates - CraftyOldMiner - Added support for official Hypixel wiki, can be toggled in /neu misc - DeDiamondPro ### **Minor Changes:** @@ -104,5 +104,6 @@ - Added 4 new textures for the on/off switches in /neu - Code Cleanup - IRONM00N - Renamed Keebler408 to CraftyOldMiner +- Added JUnit unit tests for metal detector and wishing compass solver ### **Previous change log** https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/master/Update%20Notes/2.0-Pre31-Release.md diff --git a/build.gradle.kts b/build.gradle.kts index e47b0f67..e134387e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { annotationProcessor("org.spongepowered:mixin:0.7.11-SNAPSHOT") implementation("com.fasterxml.jackson.core:jackson-core:2.13.1") implementation("info.bliki.wiki:bliki-core:3.1.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") } @@ -76,6 +77,10 @@ tasks.withType(JavaCompile::class) { options.encoding = "UTF-8" } +tasks.named<Test>("test") { + useJUnitPlatform() +} + tasks.withType(Jar::class) { archiveBaseName.set("NotEnoughUpdates") manifest.attributes.run { diff --git a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.java b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.java index 88264538..dab99698 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/commands/dev/DiagCommand.java @@ -17,8 +17,10 @@ public class DiagCommand extends ClientCommandBase { private static final String USAGE_TEXT = EnumChatFormatting.WHITE + "Usage: /neudiag <metal | wishing | debug>\n\n" + - "/neudiag metal Metal Detector Solver diagnostics\n" + - "/neudiag wishing Wishing Compass Solver diagnostics\n" + + "/neudiag metal Metal Detector Solver diagnostics\n" + + " <no sub-command> Show current solution diags\n" + + " center=<off | on> Disable / enable using center\n" + + "/neudiag wishing Wishing Compass Solver diagnostics\n" + "/neudiag debug\n" + " <no sub-command> Show current flags\n" + " <enable | disable> <flag> Enable/disable flag\n"; @@ -37,7 +39,26 @@ public class DiagCommand extends ClientCommandBase { String command = args[0].toLowerCase(); switch (command) { case "metal": - CrystalMetalDetectorSolver.logDiagnosticData(true); + if (args.length == 1) { + CrystalMetalDetectorSolver.logDiagnosticData(true); + return; + } + + String subCommand = args[1].toLowerCase(); + if (subCommand.equals("center=off")) { + CrystalMetalDetectorSolver.setDebugDoNotUseCenter(true); + sender.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "Center coordinates-based solutions disabled")); + } + else if (subCommand.equals("center=on")) { + CrystalMetalDetectorSolver.setDebugDoNotUseCenter(false); + sender.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "Center coordinates-based solutions enabled")); + } else { + showUsage(sender); + return; + } + break; case "wishing": CrystalWishingCompassSolver.getInstance().logDiagnosticData(true); diff --git a/src/main/java/io/github/moulberry/notenoughupdates/core/util/Vec3Comparable.java b/src/main/java/io/github/moulberry/notenoughupdates/core/util/Vec3Comparable.java new file mode 100644 index 00000000..cd144c21 --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/core/util/Vec3Comparable.java @@ -0,0 +1,129 @@ +package io.github.moulberry.notenoughupdates.core.util; + +import net.minecraft.util.BlockPos; +import net.minecraft.util.Vec3; +import net.minecraft.util.Vec3i; + +public class Vec3Comparable extends Vec3 implements Comparable<Vec3Comparable> { + public static final Vec3Comparable NULL_VECTOR = new Vec3Comparable(0.0, 0.0, 0.0); + + public Vec3Comparable(double x, double y, double z) { + super(x, y, z); + } + + public Vec3Comparable(Vec3i sourceVec) { + super(sourceVec); + } + + public Vec3Comparable(Vec3 source) { + super(source.xCoord, source.yCoord, source.zCoord); + } + + public Vec3Comparable(BlockPos source) { + + super(source.getX(), source.getY(), source.getZ()); + } + + public Vec3Comparable(Vec3Comparable source) { + super(source.xCoord, source.yCoord, source.zCoord); + } + + @Override + public Vec3Comparable subtractReverse(Vec3 vec) { + return new Vec3Comparable(super.subtractReverse(vec)); + } + + @Override + public Vec3Comparable normalize() { + return new Vec3Comparable(super.normalize()); + } + + @Override + public Vec3Comparable crossProduct(Vec3 vec) { + return new Vec3Comparable(super.crossProduct(vec)); + } + + @Override + public Vec3Comparable subtract(Vec3 vec) { + return new Vec3Comparable(super.subtract(vec)); + } + + @Override + public Vec3Comparable subtract(double x, double y, double z) { + return new Vec3Comparable(super.subtract(x, y, z)); + } + + @Override + public Vec3Comparable add(Vec3 other) { + return new Vec3Comparable(super.add(other)); + } + + @Override + public Vec3Comparable addVector(double x, double y, double z) { + return new Vec3Comparable(super.addVector(x, y, z)); + } + + @Override + public Vec3Comparable getIntermediateWithXValue(Vec3 vec, double x) { + return new Vec3Comparable(super.getIntermediateWithXValue(vec, x)); + } + + @Override + public Vec3Comparable getIntermediateWithYValue(Vec3 vec, double y) { + return new Vec3Comparable(super.getIntermediateWithYValue(vec, y)); + } + + @Override + public Vec3Comparable getIntermediateWithZValue(Vec3 vec, double z) { + return new Vec3Comparable(super.getIntermediateWithZValue(vec, z)); + } + + @Override + public Vec3Comparable rotatePitch(float pitch) { + return new Vec3Comparable(super.rotatePitch(pitch)); + } + + @Override + public Vec3Comparable rotateYaw(float yaw) { + return new Vec3Comparable(super.rotateYaw(yaw)); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof Vec3Comparable)) { + return false; + } else { + Vec3Comparable vec3c = (Vec3Comparable) other; + return this.xCoord == vec3c.xCoord && this.yCoord == vec3c.yCoord && this.zCoord == vec3c.zCoord; + } + } + + @Override + public int hashCode() { + long bits = 1L; + bits = 31L * bits + doubleToLongBits(xCoord); + bits = 31L * bits + doubleToLongBits(yCoord); + bits = 31L * bits + doubleToLongBits(zCoord); + return (int) (bits ^ (bits >> 32)); + } + + public int compareTo(Vec3Comparable other) { + return this.yCoord == other.yCoord ? + (this.zCoord == other.zCoord ? + (int)(this.xCoord - other.xCoord) + : (int)(this.zCoord - other.zCoord)) + : (int)(this.yCoord - other.yCoord); + } + + public boolean signumEquals(Vec3 other) { + return Math.signum(xCoord) == Math.signum(other.xCoord) && + Math.signum(yCoord) == Math.signum(other.yCoord) && + Math.signum(zCoord) == Math.signum(other.zCoord); + } + + private static long doubleToLongBits(double d) { + return d == 0.0 ? 0L : Double.doubleToLongBits(d); + } +} diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalMetalDetectorSolver.java b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalMetalDetectorSolver.java index 27c334ad..f43ebda2 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalMetalDetectorSolver.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalMetalDetectorSolver.java @@ -1,6 +1,7 @@ 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; @@ -11,7 +12,6 @@ import net.minecraft.util.BlockPos; import net.minecraft.util.ChatComponentText; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.IChatComponent; -import net.minecraft.util.Vec3; import net.minecraft.util.Vec3i; import java.util.Arrays; @@ -22,25 +22,35 @@ import java.util.List; 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 BlockPos prevPlayerPos; - private static double prevDistToTreasure = 0; + private static Vec3Comparable prevPlayerPos; + private static double prevDistToTreasure; private static HashSet<BlockPos> possibleBlocks = new HashSet<>(); - private static final HashMap<BlockPos, Double> evaluatedPlayerPositions = new HashMap<>(); - private static BlockPos blockPosIfLastSolutionInvalid; - private static Boolean chestRecentlyFound = false; - private static long chestLastFoundMillis = 0; + private static final HashMap<Vec3Comparable, Double> evaluatedPlayerPositions = new HashMap<>(); + private static boolean chestRecentlyFound; + private static long chestLastFoundMillis; private static final HashSet<BlockPos> openedChestPositions = new HashSet<>(); // Keeper and Mines of Divan center location info private static Vec3i minesCenter; - private static boolean visitKeeperMessagePrinted = false; - private static String KEEPER_OF_STRING = "Keeper of "; - private static String DIAMOND_STRING = "diamond"; - private static String LAPIS_STRING = "lapis"; - private static String EMERALD_STRING = "emerald"; - private static String GOLD_STRING = "gold"; + 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<String, Vec3i> keeperOffsets = new HashMap<String, Vec3i>() {{ put(DIAMOND_STRING, new Vec3i(33,0,3)); put(LAPIS_STRING, new Vec3i(-33,0,-3)); @@ -94,6 +104,14 @@ public class CrystalMetalDetectorSolver { -9896946827286L // x=-37, y=-21, z=-22 )); + static Predicate<BlockPos> treasureAllowedPredicate = CrystalMetalDetectorSolver::treasureAllowed; + static SolutionState currentState = SolutionState.NOT_STARTED; + static SolutionState previousState = SolutionState.NOT_STARTED; + + public interface Predicate<BlockPos> { + boolean check(BlockPos blockPos); + } + public static void process(IChatComponent message) { if (SBInfo.getInstance().getLocation() == null || !NotEnoughUpdates.INSTANCE.config.mining.metalDetectorEnabled || @@ -102,7 +120,7 @@ public class CrystalMetalDetectorSolver { return; } - locateMinesCenterIfNeeded(); + boolean centerNewlyDiscovered = locateMinesCenterIfNeeded(); double distToTreasure = Double.parseDouble(message .getUnformattedText() @@ -122,20 +140,64 @@ public class CrystalMetalDetectorSolver { chestRecentlyFound = false; } - if (prevDistToTreasure == distToTreasure && - prevPlayerPos.equals(mc.thePlayer.getPosition()) && - !evaluatedPlayerPositions.keySet().contains(mc.thePlayer.getPosition())) { + 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: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + "[NEU] Found solution.")); + if (NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.contains(NEUDebugFlag.METAL) && + (previousState == SolutionState.INVALID || previousState == SolutionState.FAILED)) { + NEUDebugLogger.log(NEUDebugFlag.METAL, + EnumChatFormatting.AQUA + "Solution coordinates: " + + EnumChatFormatting.WHITE + possibleBlocks.iterator().next().toString()); + } + break; + case INVALID: + mc.thePlayer.addChatMessage(new ChatComponentText( + EnumChatFormatting.RED + "[NEU] Previous solution is invalid.")); + logDiagnosticData(false); + resetSolution(false); + break; + case FAILED: + mc.thePlayer.addChatMessage(new ChatComponentText( + EnumChatFormatting.RED + "[NEU] Failed to find a solution.")); + logDiagnosticData(false); + resetSolution(false); + break; + case MULTIPLE_KNOWN: + NEUDebugLogger.log(NEUDebugFlag.METAL, "Multiple known locations identified:"); + // falls through + case MULTIPLE: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] Need another position to find solution. Possible blocks: " + possibleBlocks.size())); + break; + default: + throw new IllegalStateException("Metal detector is in invalid state"); + } + } + } + + static void findPossibleSolutions(double distToTreasure, Vec3Comparable playerPos, boolean centerNewlyDiscovered) { + if (prevDistToTreasure == distToTreasure && prevPlayerPos.equals(playerPos) && + !evaluatedPlayerPositions.containsKey(playerPos)) { + evaluatedPlayerPositions.put(playerPos, distToTreasure); if (possibleBlocks.size() == 0) { - evaluatedPlayerPositions.put(mc.thePlayer.getPosition(), distToTreasure); 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(mc.thePlayer.posX) + xOffset, - y, Math.floor(mc.thePlayer.posZ) + zOffset); - calculatedDist = getPlayerPos().distanceTo(new Vec3(pos).addVector(0D, 1D, 0D)); - if (round(calculatedDist, 1) == distToTreasure && treasureAllowed(pos)) { + 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++; @@ -143,10 +205,11 @@ public class CrystalMetalDetectorSolver { xOffset = 0; calculatedDist = 0; while (calculatedDist < distToTreasure) { - BlockPos pos = new BlockPos(Math.floor(mc.thePlayer.posX) - xOffset, - y, Math.floor(mc.thePlayer.posZ) + zOffset); - calculatedDist = getPlayerPos().distanceTo(new Vec3(pos).addVector(0D, 1D, 0D)); - if (round(calculatedDist, 1) == distToTreasure && treasureAllowed(pos)) { + 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++; @@ -154,48 +217,33 @@ public class CrystalMetalDetectorSolver { } } - checkAndDisplaySolutionState(); + updateSolutionState(); } else if (possibleBlocks.size() != 1) { - evaluatedPlayerPositions.put(mc.thePlayer.getPosition().getImmutable(), distToTreasure); HashSet<BlockPos> temp = new HashSet<>(); for (BlockPos pos : possibleBlocks) { - if (round(getPlayerPos().distanceTo(new Vec3(pos).addVector(0D, 1D, 0D)), 1) == distToTreasure) { + if (round(playerPos.distanceTo(new Vec3Comparable(pos).addVector(0D, 1D, 0D)), 1) == distToTreasure) { temp.add(pos); } } possibleBlocks = temp; - checkAndDisplaySolutionState(); + updateSolutionState(); } else { BlockPos pos = possibleBlocks.iterator().next(); - if (Math.abs(distToTreasure - (getPlayerPos().distanceTo(new Vec3(pos)))) > 5) { - mc.thePlayer.addChatMessage(new ChatComponentText( - EnumChatFormatting.RED + "[NEU] Previous solution is invalid.")); - blockPosIfLastSolutionInvalid = pos.getImmutable(); - logDiagnosticData(false); - resetSolution(false); + if (Math.abs(distToTreasure - (playerPos.distanceTo(new Vec3Comparable(pos)))) > 5) { + currentState = SolutionState.INVALID; } } + } else if (centerNewlyDiscovered && possibleBlocks.size() > 1) { + updateSolutionState(); } - prevPlayerPos = mc.thePlayer.getPosition().getImmutable(); + prevPlayerPos = playerPos; prevDistToTreasure = distToTreasure; } - private static void checkForSingleKnownLocationMatch() { - if (minesCenter == BlockPos.NULL_VECTOR || possibleBlocks.size() < 2) { - return; - } - - HashSet<BlockPos> temp = possibleBlocks.stream() - .filter(block -> knownChestOffsets.contains(block.subtract(minesCenter).toLong())) - .collect(Collectors.toCollection(HashSet::new)); - if (temp.size() == 1) { - possibleBlocks = temp; - NEUDebugLogger.log(NEUDebugFlag.METAL, "Known location identified."); - } else if (temp.size() > 1) { - NEUDebugLogger.log(NEUDebugFlag.METAL, temp.size() + " known locations identified:"); - } + public static void setDebugDoNotUseCenter(boolean val) { + debugDoNotUseCenter = val; } private static String getFriendlyBlockPositions(Collection<BlockPos> positions) { @@ -206,10 +254,13 @@ public class CrystalMetalDetectorSolver { StringBuilder sb = new StringBuilder(); sb.append("\n"); for (BlockPos blockPos : positions) { - sb.append("Absolute: " + blockPos.toString()); + sb.append("Absolute: "); + sb.append(blockPos.toString()); if (minesCenter != Vec3i.NULL_VECTOR) { BlockPos relativeOffset = blockPos.subtract(minesCenter); - sb.append(", Relative: " + relativeOffset.toString() + " (" + relativeOffset.toLong() + ")"); + sb.append(", Relative: "); + sb.append(relativeOffset.toString() ); + sb.append(" (" + relativeOffset.toLong() + ")"); } sb.append("\n"); } @@ -217,22 +268,23 @@ public class CrystalMetalDetectorSolver { return sb.toString(); } - private static String getFriendlyEvaluatedPositions(HashMap<BlockPos, Double> positions) { - if (!NEUDebugLogger.isFlagEnabled(NEUDebugFlag.METAL) || positions.size() == 0) { + private static String getFriendlyEvaluatedPositions() { + if (!NEUDebugLogger.isFlagEnabled(NEUDebugFlag.METAL) || evaluatedPlayerPositions.size() == 0) { return ""; } StringBuilder sb = new StringBuilder(); sb.append("\n"); - for (BlockPos blockPos : positions.keySet()) { - sb.append("Absolute: " + blockPos.toString()); + for (Vec3Comparable vec : evaluatedPlayerPositions.keySet()) { + sb.append("Absolute: " + vec.toString()); if (minesCenter != Vec3i.NULL_VECTOR) { - BlockPos relativeOffset = blockPos.subtract(minesCenter); + BlockPos positionBlockPos = new BlockPos(vec); + BlockPos relativeOffset = positionBlockPos.subtract(minesCenter); sb.append(", Relative: " + relativeOffset.toString() + " (" + relativeOffset.toLong() + ")"); } sb.append(" Distance: "); - sb.append(positions.get(blockPos)); + sb.append(evaluatedPlayerPositions.get(vec)); sb.append("\n"); } @@ -240,101 +292,8 @@ public class CrystalMetalDetectorSolver { return sb.toString(); } - public static void logDiagnosticData(boolean outputAlways) { - if (SBInfo.getInstance().getLocation() == null) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + - "[NEU] This command is not available outside SkyBlock")); - return; - } - - if (!NotEnoughUpdates.INSTANCE.config.mining.metalDetectorEnabled) - { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + - "[NEU] Metal Detector Solver is not enabled.")); - return; - } - - if (!outputAlways && !NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.contains(NEUDebugFlag.METAL)) { - return; - } - - boolean originalDebugFlag = !NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.add(NEUDebugFlag.METAL); - - StringBuilder diagsMessage = new StringBuilder(); - - diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Mines Center: "); - diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((minesCenter == Vec3i.NULL_VECTOR) ? "<NOT DISCOVERED>" : minesCenter.toString()); - diagsMessage.append("\n"); - - diagsMessage.append("\n"); - diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Previous Player Position: "); - diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((prevPlayerPos == null) ? "<NONE>" : prevPlayerPos.toString()); - diagsMessage.append("\n"); - - diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Previous Distance To Treasure: "); - diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((prevDistToTreasure == 0) ? "<NONE>" : prevDistToTreasure); - diagsMessage.append("\n"); - - diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Last Solution Invalid Position: "); - diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((blockPosIfLastSolutionInvalid == null) ? "<NONE>" : blockPosIfLastSolutionInvalid.toString()); - 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(evaluatedPlayerPositions)); - 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<BlockPos> 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("<REQUIRES MINES CENTER>"); - } - - NEUDebugLogger.log(NEUDebugFlag.METAL, diagsMessage.toString()); - - if (!originalDebugFlag) { - NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.remove(NEUDebugFlag.METAL); - } - } - public static void resetSolution(Boolean chestFound) { if (chestFound) { - blockPosIfLastSolutionInvalid = null; prevPlayerPos = null; prevDistToTreasure = 0; if (possibleBlocks.size() == 1) { @@ -345,15 +304,18 @@ public class CrystalMetalDetectorSolver { chestRecentlyFound = chestFound; possibleBlocks.clear(); evaluatedPlayerPositions.clear(); + previousState = currentState; + currentState = SolutionState.NOT_STARTED; } public static void initWorld() { minesCenter = Vec3i.NULL_VECTOR; visitKeeperMessagePrinted = false; - blockPosIfLastSolutionInvalid = null; openedChestPositions.clear(); + chestLastFoundMillis = 0; prevDistToTreasure = 0; prevPlayerPos = null; + currentState = SolutionState.NOT_STARTED; resetSolution(false); } @@ -377,15 +339,14 @@ public class CrystalMetalDetectorSolver { } } - private static void locateMinesCenterIfNeeded() { + private static boolean locateMinesCenterIfNeeded() { if (minesCenter != Vec3i.NULL_VECTOR) { - return; + return false; } List<EntityArmorStand> keeperEntities = mc.theWorld.getEntities(EntityArmorStand.class, (entity) -> { if (!entity.hasCustomName()) return false; - if (entity.getCustomNameTag().contains(KEEPER_OF_STRING)) return true; - return false; + return entity.getCustomNameTag().contains(KEEPER_OF_STRING); }); if (keeperEntities.size() == 0) { @@ -394,7 +355,7 @@ public class CrystalMetalDetectorSolver { "[NEU] Approach a Keeper while holding the metal detector to enable faster treasure hunting.")); visitKeeperMessagePrinted = true; } - return; + return false; } EntityArmorStand keeperEntity = keeperEntities.get(0); @@ -407,6 +368,11 @@ public class CrystalMetalDetectorSolver { EnumChatFormatting.WHITE + minesCenter.toString()); mc.thePlayer.addChatMessage(new ChatComponentText( 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) { @@ -414,42 +380,174 @@ public class CrystalMetalDetectorSolver { return (double) Math.round(value * scale) / scale; } - private static void checkAndDisplaySolutionState() { + private static void updateSolutionState() { + previousState = currentState; + if (possibleBlocks.size() == 0) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[NEU] Failed to find a solution.")); - logDiagnosticData(false); - resetSolution(false); + currentState = SolutionState.FAILED; return; } - checkForSingleKnownLocationMatch(); - if (possibleBlocks.size() > 1) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + - "[NEU] Need another position to find solution. Possible blocks: " + possibleBlocks.size())); - } else { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + "[NEU] Found solution.")); + 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<BlockPos> 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 Vec3 getPlayerPos() { - return new Vec3( + private static Vec3Comparable getPlayerPosAdjustedForEyeHeight() { + return new Vec3Comparable( mc.thePlayer.posX, mc.thePlayer.posY + (mc.thePlayer.getEyeHeight() - mc.thePlayer.getDefaultEyeHeight()), mc.thePlayer.posZ ); } - private static boolean treasureAllowed(BlockPos pos) { - boolean airAbove = mc.theWorld. - getBlockState(pos.add(0, 1, 0)).getBlock().getRegistryName().equals("minecraft:air"); - boolean allowedBlockType = mc.theWorld.getBlockState(pos).getBlock().getRegistryName().equals("minecraft:gold_block") || + 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"); - boolean knownOffset = knownChestOffsets.contains(pos.subtract(minesCenter).toLong()); - return airAbove & (knownOffset | allowedBlockType); + } + + 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)) ? "<NOT DISCOVERED>" : 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) ? "<NONE>" : prevPlayerPos.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Previous Distance To Treasure: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((prevDistToTreasure == 0) ? "<NONE>" : 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<BlockPos> 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("<REQUIRES MINES CENTER>"); + } + + return diagsMessage.toString(); + } + + public static void logDiagnosticData(boolean outputAlways) { + if (!SBInfo.getInstance().checkForSkyblockLocation()) { + return; + } + + if (!NotEnoughUpdates.INSTANCE.config.mining.metalDetectorEnabled) + { + mc.thePlayer.addChatMessage(new ChatComponentText(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()); + } } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java index 25f39c3a..9a950e7f 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java @@ -2,27 +2,67 @@ package io.github.moulberry.notenoughupdates.miscfeatures; import io.github.moulberry.notenoughupdates.NotEnoughUpdates; import io.github.moulberry.notenoughupdates.core.util.Line; +import io.github.moulberry.notenoughupdates.core.util.Vec3Comparable; +import io.github.moulberry.notenoughupdates.options.NEUConfig; 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.Utils; import net.minecraft.client.Minecraft; import net.minecraft.event.ClickEvent; -import net.minecraft.event.HoverEvent; import net.minecraft.init.Items; import net.minecraft.item.ItemStack; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.BlockPos; import net.minecraft.util.ChatComponentText; -import net.minecraft.util.ChatStyle; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.EnumParticleTypes; -import net.minecraft.util.Vec3; +import net.minecraft.util.Vec3i; +import net.minecraftforge.client.ClientCommandHandler; import net.minecraftforge.event.entity.player.PlayerInteractEvent; import net.minecraftforge.event.world.WorldEvent; import net.minecraftforge.fml.common.Loader; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.function.BooleanSupplier; +import java.util.function.LongSupplier; + public class CrystalWishingCompassSolver { + enum SolverState { + NOT_STARTED, + PROCESSING_FIRST_USE, + NEED_SECOND_COMPASS, + PROCESSING_SECOND_USE, + SOLVED, + FAILED_EXCEPTION, + FAILED_TIMEOUT_NO_REPEATING, + FAILED_TIMEOUT_NO_PARTICLES, + FAILED_INTERSECTION_CALCULATION, + FAILED_INVALID_SOLUTION, + } + + enum CompassTarget { + GOBLIN_QUEEN, + GOBLIN_KING, + BAL, + JUNGLE_TEMPLE, + ODAWA, + PRECURSOR_CITY, + MINES_OF_DIVAN, + CRYSTAL_NUCLEUS, + } + + enum Crystal { + AMBER, + AMETHYST, + JADE, + SAPPHIRE, + TOPAZ, + } + private static final CrystalWishingCompassSolver INSTANCE = new CrystalWishingCompassSolver(); public static CrystalWishingCompassSolver getInstance() { return INSTANCE; @@ -31,40 +71,72 @@ public class CrystalWishingCompassSolver { private static final Minecraft mc = Minecraft.getMinecraft(); private static boolean isSkytilsPresent = false; - // Crystal Nucleus unbreakable blocks, area coordinates reported by Hypixel server are slightly different - private static final AxisAlignedBB NUCLEUS_BB = new AxisAlignedBB(463, 63, 460, 563, 181, 564); - private static final double MAX_COMPASS_PARTICLE_SPREAD = 16; - - private static BlockPos prevPlayerPos; - private long compassUsedMillis = 0; - private Vec3 firstParticle = null; - private Vec3 lastParticle = null; - private double lastParticleDistanceFromFirst = 0; - private Line firstCompassLine = null; - private Line secondCompassLine = null; - private Vec3 solution = null; - private Line solutionIntersectionLine = null; - - private void resetForNewCompass() { - compassUsedMillis = 0; - firstParticle = null; - lastParticle = null; - lastParticleDistanceFromFirst = 0; + // NOTE: There is a small set of breakable blocks above the nucleus at Y > 181. While this zone is reported + // as the Crystal Nucleus by Hypixel, for wishing compass purposes it is in the appropriate quadrant. + private static final AxisAlignedBB NUCLEUS_BB = new AxisAlignedBB(462, 63, 461, 564, 181, 565); + private static final AxisAlignedBB HOLLOWS_BB = new AxisAlignedBB(201, 30, 201, 824, 189, 824); + private static final AxisAlignedBB PRECURSOR_REMNANTS_BB = new AxisAlignedBB(513, 64, 513, 824, 189, 824); + private static final AxisAlignedBB MITHRIL_DEPOSITS_BB = new AxisAlignedBB(513, 64, 201, 824, 189, 512); + private static final AxisAlignedBB GOBLIN_HOLDOUT_BB = new AxisAlignedBB(201, 64, 513, 512, 189, 824); + private static final AxisAlignedBB JUNGLE_BB = new AxisAlignedBB(201, 64, 201, 512, 189, 512); + private static final AxisAlignedBB MAGMA_FIELDS_BB = new AxisAlignedBB(201, 30, 201, 824, 63, 824); + private static final double MAX_DISTANCE_BETWEEN_PARTICLES = 0.6; + private static final double MAX_DISTANCE_FROM_USE_TO_FIRST_PARTICLE = 9.0; + + // 64.0 is an arbitrary value but seems to work well + private static final double MINIMUM_DISTANCE_SQ_BETWEEN_COMPASSES = 64.0; + + // All particles typically arrive in < 3500, so 5000 should be enough buffer + public static final long ALL_PARTICLES_MAX_MILLIS = 5000L; + + public LongSupplier currentTimeMillis = System::currentTimeMillis; + public BooleanSupplier kingsScentPresent = this::isKingsScentPresent; + public BooleanSupplier keyInInventory = this::isKeyInInventory; + public interface CrystalEnumSetSupplier { + EnumSet<Crystal> getAsCrystalEnumSet(); + } + public CrystalEnumSetSupplier foundCrystals = this::getFoundCrystals; + + private SolverState solverState; + private Compass firstCompass; + private Compass secondCompass; + private Line solutionIntersectionLine; + private EnumSet<CompassTarget> possibleTargets; + private Vec3Comparable solution; + private Vec3Comparable originalSolution; + private EnumSet<CompassTarget> solutionPossibleTargets; + + public SolverState getSolverState() { + return solverState; + } + + public Vec3i getSolutionCoords() { + return new Vec3i(solution.xCoord, solution.yCoord, solution.zCoord); + } + + public EnumSet<CompassTarget> getPossibleTargets() { + return possibleTargets; } private void resetForNewTarget() { NEUDebugLogger.log(NEUDebugFlag.WISHING,"Resetting for new target"); - resetForNewCompass(); - firstCompassLine = null; - secondCompassLine = null; + solverState = SolverState.NOT_STARTED; + firstCompass = null; + secondCompass = null; solutionIntersectionLine = null; - prevPlayerPos = null; + possibleTargets = null; solution = null; + originalSolution = null; + solutionPossibleTargets = null; + } + + public void initWorld() { + resetForNewTarget(); } @SubscribeEvent public void onWorldLoad(WorldEvent.Unload event) { - resetForNewTarget(); + initWorld(); isSkytilsPresent = Loader.isModLoaded("skytils"); } @@ -90,28 +162,101 @@ public class CrystalWishingCompassSolver { return; } - try { - if (isSolved()) { - resetForNewTarget(); - } + BlockPos playerPos = mc.thePlayer.getPosition().getImmutable(); - // 64.0 is an arbitrary value but seems to work well - if (prevPlayerPos != null && prevPlayerPos.distanceSq(mc.thePlayer.getPosition()) < 64.0) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + - "[NEU] Move a little further before using the wishing compass again.")); - event.setCanceled(true); - return; + try { + HandleCompassResult result = handleCompassUse(playerPos); + switch (result) { + case SUCCESS: + return; + case STILL_PROCESSING_PRIOR_USE: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] Wait a little longer before using the wishing compass again.")); + event.setCanceled(true); + break; + case LOCATION_TOO_CLOSE: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] Move a little further before using the wishing compass again.")); + event.setCanceled(true); + break; + case POSSIBLE_TARGETS_CHANGED: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] Possible wishing compass targets have changed. Solver has been reset.")); + event.setCanceled(true); + break; + case NO_PARTICLES_FOR_PREVIOUS_COMPASS: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] No particles detected for prior compass use. Need another position to solve.")); + break; + case PLAYER_IN_NUCLEUS: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] Wishing compass must be used outside the nucleus for accurate results.")); + event.setCanceled(true); + break; + default: + throw new IllegalStateException("Unexpected wishing compass solver state: \n" + getDiagnosticMessage()); } - - prevPlayerPos = mc.thePlayer.getPosition().getImmutable(); - compassUsedMillis = System.currentTimeMillis(); } catch (Exception e) { mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[NEU] Error processing wishing compass action - see log for details")); e.printStackTrace(); + event.setCanceled(true); + solverState = SolverState.FAILED_EXCEPTION; } } + public HandleCompassResult handleCompassUse(BlockPos playerPos) { + long lastCompassUsedMillis = 0; + switch (solverState) { + case PROCESSING_SECOND_USE: + if (secondCompass != null) { + lastCompassUsedMillis = secondCompass.whenUsedMillis; + } + case PROCESSING_FIRST_USE: + if (lastCompassUsedMillis == 0 && firstCompass != null) { + lastCompassUsedMillis = firstCompass.whenUsedMillis; + } + if (lastCompassUsedMillis != 0 && + (currentTimeMillis.getAsLong() > lastCompassUsedMillis + ALL_PARTICLES_MAX_MILLIS)) { + return HandleCompassResult.NO_PARTICLES_FOR_PREVIOUS_COMPASS; + } + + return HandleCompassResult.STILL_PROCESSING_PRIOR_USE; + case SOLVED: + case FAILED_EXCEPTION: + case FAILED_TIMEOUT_NO_REPEATING: + case FAILED_TIMEOUT_NO_PARTICLES: + case FAILED_INTERSECTION_CALCULATION: + case FAILED_INVALID_SOLUTION: + resetForNewTarget(); + // falls through, NOT_STARTED is the state when resetForNewTarget returns + case NOT_STARTED: + if (NUCLEUS_BB.isVecInside(new Vec3Comparable(playerPos.getX(), playerPos.getY(), playerPos.getZ()))) { + return HandleCompassResult.PLAYER_IN_NUCLEUS; + } + + firstCompass = new Compass(playerPos, currentTimeMillis.getAsLong()); + solverState = SolverState.PROCESSING_FIRST_USE; + possibleTargets = calculatePossibleTargets(playerPos); + return HandleCompassResult.SUCCESS; + case NEED_SECOND_COMPASS: + if (firstCompass.whereUsed.distanceSq(playerPos) < MINIMUM_DISTANCE_SQ_BETWEEN_COMPASSES) { + return HandleCompassResult.LOCATION_TOO_CLOSE; + } + + if (!possibleTargets.equals(calculatePossibleTargets(playerPos))) { + resetForNewTarget(); + return HandleCompassResult.POSSIBLE_TARGETS_CHANGED; + } + + secondCompass = new Compass(playerPos, currentTimeMillis.getAsLong()); + solverState = SolverState.PROCESSING_SECOND_USE; + return HandleCompassResult.SUCCESS; + } + + throw new IllegalStateException("Unexpected compass state" ); + } + /* * Processes particles if the wishing compass was used within the last 5 seconds. * @@ -142,188 +287,701 @@ public class CrystalWishingCompassSolver { ) { if (!NotEnoughUpdates.INSTANCE.config.mining.wishingCompassSolver || particleType != EnumParticleTypes.VILLAGER_HAPPY || - !SBInfo.getInstance().getLocation().equals("crystal_hollows") || - isSolved() || - System.currentTimeMillis() - compassUsedMillis > 5000) { + !SBInfo.getInstance().getLocation().equals("crystal_hollows")) { return; } try { - Vec3 particleVec = new Vec3(x, y, z); - if (firstParticle == null) { - firstParticle = particleVec; - return; + SolverState originalSolverState = solverState; + solveUsingParticle(x, y, z, currentTimeMillis.getAsLong()); + if (solverState != originalSolverState) { + switch (solverState) { + case SOLVED: + showSolution(); + break; + case FAILED_EXCEPTION: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Unable to determine wishing compass target.")); + logDiagnosticData(false); + break; + case FAILED_TIMEOUT_NO_REPEATING: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Timed out waiting for repeat set of compass particles.")); + logDiagnosticData(false); + break; + case FAILED_TIMEOUT_NO_PARTICLES: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Timed out waiting for compass particles.")); + logDiagnosticData(false); + break; + case FAILED_INTERSECTION_CALCULATION: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Unable to determine intersection of wishing compasses.")); + logDiagnosticData(false); + break; + case FAILED_INVALID_SOLUTION: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Failed to find solution.")); + logDiagnosticData(false); + break; + case NEED_SECOND_COMPASS: + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] Need another position to determine wishing compass target.")); + break; + } } + } catch (Exception e) { + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Exception while calculating wishing compass solution - see log for details")); + e.printStackTrace(); + } + } - double distanceFromFirst = particleVec.distanceTo(firstParticle); - if (distanceFromFirst > MAX_COMPASS_PARTICLE_SPREAD) { + /** + * + * @param x Particle x coordinate + * @param y Particle y coordinate + * @param z Particle z coordinate + */ + public void solveUsingParticle(double x, double y, double z, long currentTimeMillis) { + Compass currentCompass; + switch (solverState) { + case PROCESSING_FIRST_USE: + currentCompass = firstCompass; + break; + case PROCESSING_SECOND_USE: + currentCompass = secondCompass; + break; + default: return; - } + } - if (distanceFromFirst >= lastParticleDistanceFromFirst) { - lastParticleDistanceFromFirst = distanceFromFirst; - lastParticle = particleVec; + currentCompass.processParticle(x, y, z, currentTimeMillis); + switch (currentCompass.compassState) { + case FAILED_TIMEOUT_NO_PARTICLES: + solverState = SolverState.FAILED_TIMEOUT_NO_PARTICLES; + return; + case FAILED_TIMEOUT_NO_REPEATING: + solverState = SolverState.FAILED_TIMEOUT_NO_REPEATING; + return; + case WAITING_FOR_FIRST_PARTICLE: + case COMPUTING_LAST_PARTICLE: return; + case COMPLETED: + if (solverState == SolverState.NEED_SECOND_COMPASS) { + return; + } + if (solverState == SolverState.PROCESSING_FIRST_USE) { + solverState = SolverState.NEED_SECOND_COMPASS; + return; + } + break; + } + + // First and Second compasses have completed + solutionIntersectionLine = firstCompass.line.getIntersectionLineSegment(secondCompass.line); + + if (solutionIntersectionLine == null) { + solverState = SolverState.FAILED_INTERSECTION_CALCULATION; + return; + } + + solution = new Vec3Comparable(solutionIntersectionLine.getMidpoint()); + + Vec3Comparable firstDirection = firstCompass.getDirection(); + Vec3Comparable firstSolutionDirection = firstCompass.getDirectionTo(solution); + Vec3Comparable secondDirection = secondCompass.getDirection(); + Vec3Comparable secondSolutionDirection = secondCompass.getDirectionTo(solution); + if (!firstDirection.signumEquals(firstSolutionDirection) || + !secondDirection.signumEquals(secondSolutionDirection) || + !HOLLOWS_BB.isVecInside(solution)) { + solverState = SolverState.FAILED_INVALID_SOLUTION; + return; + } + + solutionPossibleTargets = getSolutionTargets(possibleTargets, solution); + + // Adjust the Jungle Temple solution coordinates + if (solutionPossibleTargets.size() == 1 && + solutionPossibleTargets.contains(CompassTarget.JUNGLE_TEMPLE)) { + originalSolution = solution; + solution = solution.addVector(-57, 36, -21); + } + + solverState = SolverState.SOLVED; + } + + private boolean isKeyInInventory() + { + for (ItemStack item : mc.thePlayer.inventory.mainInventory){ + if (item != null && item.getDisplayName().contains("Jungle Key")) { + return true; } + } + return false; + } - // We get here when the second repetition of particles begins. - // Since the second repetition overlaps with the last few particles - // of the first repetition, the last particle we capture isn't truly the last. - // But that's OK since subsequent particles will be on the same line. - Line line = new Line(firstParticle, lastParticle); - if (firstCompassLine == null) { - firstCompassLine = line; - resetForNewCompass(); - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + - "[NEU] Need another position to determine wishing compass target.")); - return; + private boolean isKingsScentPresent() + { + return SBInfo.getInstance().footer.getUnformattedText().contains("King's Scent I"); + } + + private EnumSet<Crystal> getFoundCrystals() { + EnumSet<Crystal> foundCrystals = EnumSet.noneOf(Crystal.class); + NEUConfig.HiddenProfileSpecific perProfileConfig = NotEnoughUpdates.INSTANCE.config.getProfileSpecific(); + if (perProfileConfig == null) return foundCrystals; + HashMap<String, Integer> crystals = perProfileConfig.crystals; + for (String crystalName : crystals.keySet()) { + Integer crystalState = crystals.get(crystalName); + if (crystalState != null && crystalState > 0) { + foundCrystals.add(Crystal.valueOf(crystalName.toUpperCase())); } + } - secondCompassLine = line; - solutionIntersectionLine = firstCompassLine.getIntersectionLineSegment(secondCompassLine); - if (solutionIntersectionLine == null) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + - "[NEU] Unable to determine wishing compass target.")); - logDiagnosticData(false); - return; + return foundCrystals; + } + + // Returns candidates based on seen Y coordinates and quadrants that + // are not adjacent to the solution's quadrant. If the solution is + // the nucleus then a copy of the original possible targets is + // returned. + // + // NOTE: Adjacent quadrant filtering could be improved based on + // structure sizes in the future to only allow a certain + // distance into the adjacent quadrant. + // + // |----------|------------| + // | Jungle | Mithril | + // | | Deposits | + // |----------|----------- | + // | Goblin | Precursor | + // | Holdout | Deposits | + // |----------|------------| + static public EnumSet<CompassTarget> getSolutionTargets( + EnumSet<CompassTarget> possibleTargets, + Vec3Comparable solution) { + EnumSet<CompassTarget> solutionPossibleTargets; + solutionPossibleTargets = possibleTargets.clone(); + + if (NUCLEUS_BB.isVecInside(solution)) { + return solutionPossibleTargets; + } + + solutionPossibleTargets.remove(CompassTarget.CRYSTAL_NUCLEUS); + + // Eliminate non-adjacent zones first + if (MITHRIL_DEPOSITS_BB.isVecInside(solution)) { + solutionPossibleTargets.remove(CompassTarget.GOBLIN_KING); + solutionPossibleTargets.remove(CompassTarget.GOBLIN_QUEEN); + } else if (PRECURSOR_REMNANTS_BB.isVecInside(solution)) { + solutionPossibleTargets.remove(CompassTarget.ODAWA); + solutionPossibleTargets.remove(CompassTarget.JUNGLE_TEMPLE); + } else if (GOBLIN_HOLDOUT_BB.isVecInside(solution)) { + solutionPossibleTargets.remove(CompassTarget.MINES_OF_DIVAN); + } else if (JUNGLE_BB.isVecInside(solution)) { + solutionPossibleTargets.remove(CompassTarget.PRECURSOR_CITY); + } + + // If there's only 1 possible target then don't remove based + // on Y coordinates since assumptions about Y coordinates could + // be wrong. + if (solutionPossibleTargets.size() > 1) { + // Y coordinates are 43-70 from 11 samples + if (solutionPossibleTargets.contains(CompassTarget.BAL) && + solution.yCoord > 72) { + solutionPossibleTargets.remove(CompassTarget.BAL); } - solution = solutionIntersectionLine.getMidpoint(); + // Y coordinates are 93-157 from 10 samples, may be able to filter + // more based on the offset of the King within the structure + if (solutionPossibleTargets.contains(CompassTarget.GOBLIN_KING) && + solution.yCoord < 64) { + solutionPossibleTargets.remove(CompassTarget.GOBLIN_KING); + } - if (solution.distanceTo(firstParticle) < 8) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + - "[NEU] WARNING: Solution is likely incorrect.")); - logDiagnosticData(false); - return; + // Y coordinates are 129-139 from 10 samples + if (solutionPossibleTargets.contains(CompassTarget.GOBLIN_QUEEN) && + (solution.yCoord < 127 || solution.yCoord > 141)) { + solutionPossibleTargets.remove(CompassTarget.GOBLIN_QUEEN); } - showSolution(); - } catch (Exception e) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + - "[NEU] Exception while calculating wishing compass solution - see log for details")); - e.printStackTrace(); + // Y coordinates are 72-80 from 10 samples + if (solutionPossibleTargets.contains(CompassTarget.JUNGLE_TEMPLE) && + (solution.yCoord < 70 || solution.yCoord > 82)) { + solutionPossibleTargets.remove(CompassTarget.JUNGLE_TEMPLE); + } + + // Y coordinates are 110-128 from 3 samples, not enough data to use + if (solutionPossibleTargets.contains(CompassTarget.ODAWA) && + solution.yCoord < 64) { + solutionPossibleTargets.remove(CompassTarget.ODAWA); + } + + // Y coordinates are 122-129 from 8 samples + if (solutionPossibleTargets.contains(CompassTarget.PRECURSOR_CITY) && + (solution.yCoord < 119 || solution.yCoord > 132)) { + solutionPossibleTargets.remove(CompassTarget.PRECURSOR_CITY); + } + + // Y coordinates are 98-102 from 15 samples + if (solutionPossibleTargets.contains(CompassTarget.MINES_OF_DIVAN) && + (solution.yCoord < 96 || solution.yCoord > 104)) { + solutionPossibleTargets.remove(CompassTarget.MINES_OF_DIVAN); + } } + + return solutionPossibleTargets; } - private boolean isSolved() { - return solution != null; + private EnumSet<CompassTarget> calculatePossibleTargets(BlockPos playerPos) { + boolean targetsBasedOnZoneWithoutCrystal = false; + EnumSet<CompassTarget> candidateTargets = EnumSet.allOf(CompassTarget.class); + EnumSet<Crystal> foundCrystals = this.foundCrystals.getAsCrystalEnumSet(); + Vec3Comparable playerPosVec = new Vec3Comparable(playerPos); + + // If the current zone's crystal hasn't been found then remove all non-nucleus candidates other + // than the ones in the current zone. The one exception is that the king is kept when in the jungle + // since the compass can point to the king if odawa is missing (which often happens). + // The nucleus is kept since it can be returned if the structure for the current zone is missing. + if (GOBLIN_HOLDOUT_BB.isVecInside(playerPosVec) && !foundCrystals.contains(Crystal.AMBER)) { + candidateTargets.clear(); + candidateTargets.add(CompassTarget.CRYSTAL_NUCLEUS); + candidateTargets.add(CompassTarget.GOBLIN_KING); + candidateTargets.add(CompassTarget.GOBLIN_QUEEN); + targetsBasedOnZoneWithoutCrystal = true; + } + + if (JUNGLE_BB.isVecInside(playerPosVec) && !foundCrystals.contains(Crystal.AMETHYST)) { + candidateTargets.clear(); + candidateTargets.add(CompassTarget.CRYSTAL_NUCLEUS); + candidateTargets.add(CompassTarget.ODAWA); + candidateTargets.add(CompassTarget.JUNGLE_TEMPLE); + if (!keyInInventory.getAsBoolean() && !kingsScentPresent.getAsBoolean()) { + // If Odawa is missing then the king may be returned + candidateTargets.add(CompassTarget.GOBLIN_KING); + } + targetsBasedOnZoneWithoutCrystal = true; + } + + if (MITHRIL_DEPOSITS_BB.isVecInside(playerPosVec) && !foundCrystals.contains(Crystal.JADE)) { + candidateTargets.clear(); + candidateTargets.add(CompassTarget.CRYSTAL_NUCLEUS); + candidateTargets.add(CompassTarget.MINES_OF_DIVAN); + targetsBasedOnZoneWithoutCrystal = true; + } + + if (PRECURSOR_REMNANTS_BB.isVecInside(playerPosVec) && !foundCrystals.contains(Crystal.SAPPHIRE)) { + candidateTargets.clear(); + candidateTargets.add(CompassTarget.CRYSTAL_NUCLEUS); + candidateTargets.add(CompassTarget.PRECURSOR_CITY); + targetsBasedOnZoneWithoutCrystal = true; + } + + if (MAGMA_FIELDS_BB.isVecInside(playerPosVec) && !foundCrystals.contains(Crystal.TOPAZ)) { + candidateTargets.clear(); + candidateTargets.add(CompassTarget.CRYSTAL_NUCLEUS); + candidateTargets.add(CompassTarget.BAL); + targetsBasedOnZoneWithoutCrystal = true; + } + + if (!targetsBasedOnZoneWithoutCrystal) { + // Filter out crystal-based targets outside the current zone + if (foundCrystals.contains(Crystal.AMBER)) { + candidateTargets.remove(CompassTarget.GOBLIN_KING); + candidateTargets.remove(CompassTarget.GOBLIN_QUEEN); + } + + if (foundCrystals.contains(Crystal.AMETHYST)) { + candidateTargets.remove(CompassTarget.ODAWA); + candidateTargets.remove(CompassTarget.JUNGLE_TEMPLE); + } + + if (foundCrystals.contains(Crystal.JADE)) { + candidateTargets.remove(CompassTarget.MINES_OF_DIVAN); + } + + if (foundCrystals.contains(Crystal.TOPAZ)) { + candidateTargets.remove(CompassTarget.BAL); + } + + if (foundCrystals.contains(Crystal.SAPPHIRE)) { + candidateTargets.remove(CompassTarget.PRECURSOR_CITY); + } + } + + candidateTargets.remove(kingsScentPresent.getAsBoolean() ? CompassTarget.GOBLIN_KING : CompassTarget.GOBLIN_QUEEN); + candidateTargets.remove(keyInInventory.getAsBoolean() ? CompassTarget.ODAWA : CompassTarget.JUNGLE_TEMPLE); + + return candidateTargets; } - private void showSolution() { - if (solution == null) return; - String description = "[NEU] Wishing compass target: "; - String coordsText = String.format("%.0f %.0f %.0f", - solution.xCoord, - solution.yCoord, - solution.zCoord); + private String getFriendlyNameForCompassTarget(CompassTarget compassTarget) { + switch (compassTarget) { + case BAL: return EnumChatFormatting.RED + "Bal"; + case ODAWA: return EnumChatFormatting.GREEN + "Odawa"; + case JUNGLE_TEMPLE: return EnumChatFormatting.AQUA + "the " + + EnumChatFormatting.GREEN + "Jungle Temple"; + case GOBLIN_KING: return EnumChatFormatting.GOLD + "King Yolkar"; + case GOBLIN_QUEEN: return EnumChatFormatting.AQUA + "the " + + EnumChatFormatting.YELLOW + "Goblin Queen"; + case PRECURSOR_CITY: return EnumChatFormatting.AQUA + "the " + + EnumChatFormatting.WHITE + "Precursor City"; + case MINES_OF_DIVAN: return EnumChatFormatting.AQUA + "the " + + EnumChatFormatting.BLUE + "Mines of Divan"; + default: return EnumChatFormatting.WHITE + "an undetermined location"; + } + } - if (NUCLEUS_BB.isVecInside(solution)) { - description += "Crystal Nucleus (" + coordsText + ")"; - } else { - description += coordsText; + private String getNameForCompassTarget(CompassTarget compassTarget) { + boolean useSkytilsNames = (NotEnoughUpdates.INSTANCE.config.mining.wishingCompassWaypointNameType == 1); + switch (compassTarget) { + case BAL: return useSkytilsNames ? "internal_bal" : "Bal"; + case ODAWA: return "Odawa"; + case JUNGLE_TEMPLE: return useSkytilsNames ? "internal_temple" : "Temple"; + case GOBLIN_KING: return useSkytilsNames ? "internal_king" : "King"; + case GOBLIN_QUEEN: return useSkytilsNames ? "internal_den" : "Queen"; + case PRECURSOR_CITY: return useSkytilsNames ? "internal_city" : "City"; + case MINES_OF_DIVAN: return useSkytilsNames ? "internal_mines" : "Mines"; + default: return "WishingTarget"; } + } - ChatComponentText message = new ChatComponentText(EnumChatFormatting.YELLOW + description); + private String getSolutionCoordsText() { + return solution == null ? "" : + String.format("%.0f %.0f %.0f", solution.xCoord, solution.yCoord, solution.zCoord); + } - if (isSkytilsPresent) { - ChatStyle clickEvent = new ChatStyle().setChatClickEvent( - new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, "/sthw add WishingTarget " + coordsText)); - clickEvent.setChatHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, new ChatComponentText( - EnumChatFormatting.YELLOW + - "Add Skytils hollows waypoint"))); - message.setChatStyle(clickEvent); + private String getWishingCompassDestinationsMessage() { + StringBuilder sb = new StringBuilder(); + sb.append(EnumChatFormatting.YELLOW); + sb.append("[NEU] "); + sb.append(EnumChatFormatting.AQUA); + sb.append("Wishing compass points to "); + int index = 1; + for (CompassTarget target : solutionPossibleTargets) { + if (index > 1) { + sb.append(EnumChatFormatting.AQUA); + if (index == solutionPossibleTargets.size()) { + sb.append(" or "); + } else { + sb.append(", "); + } + } + sb.append(getFriendlyNameForCompassTarget(target)); + index++; } - Minecraft.getMinecraft().thePlayer.addChatMessage(message); + sb.append(EnumChatFormatting.AQUA); + sb.append(" ("); + sb.append(getSolutionCoordsText()); + sb.append(")"); + return sb.toString(); } - public void logDiagnosticData(boolean outputAlways) { - if (SBInfo.getInstance().getLocation() == null) { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + - "[NEU] This command is not available outside SkyBlock")); + private void showSolution() { + if (solution == null) return; + + if (NUCLEUS_BB.isVecInside(solution)) { + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + "[NEU] " + + EnumChatFormatting.AQUA + "Wishing compass target is the Crystal Nucleus")); return; } - if (!NotEnoughUpdates.INSTANCE.config.mining.wishingCompassSolver) - { - mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + - "[NEU] Wishing Compass Solver is not enabled.")); + String destinationMessage = getWishingCompassDestinationsMessage(); + + if (!isSkytilsPresent) { + mc.thePlayer.addChatMessage(new ChatComponentText(destinationMessage)); return; } - if (!outputAlways && !NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.contains(NEUDebugFlag.WISHING)) { - return; + String targetNameForSkytils = solutionPossibleTargets.size() == 1 ? + getNameForCompassTarget(solutionPossibleTargets.iterator().next()) : + "WishingTarget"; + String skytilsCommand = String.format("/sthw add %s %s", targetNameForSkytils, getSolutionCoordsText()); + if (NotEnoughUpdates.INSTANCE.config.mining.wishingCompassAutocreateKnownWaypoints && + solutionPossibleTargets.size() == 1) { + mc.thePlayer.addChatMessage(new ChatComponentText(destinationMessage)); + int commandResult = ClientCommandHandler.instance.executeCommand(mc.thePlayer, skytilsCommand); + if (commandResult == 1) + { + return; + } + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[NEU] Failed to automatically run /sthw")); } - boolean originalDebugFlag = !NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.add(NEUDebugFlag.WISHING); + destinationMessage += EnumChatFormatting.YELLOW + " [Add Skytils Waypoint]"; + ChatComponentText chatMessage = new ChatComponentText(destinationMessage); + chatMessage.setChatStyle(Utils.createClickStyle(ClickEvent.Action.RUN_COMMAND, + skytilsCommand, + EnumChatFormatting.YELLOW + "Set waypoint for wishing target\n")); + mc.thePlayer.addChatMessage(chatMessage); + } + private String getDiagnosticMessage() { StringBuilder diagsMessage = new StringBuilder(); - diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Skytils Present: "); + diagsMessage.append("Solver State: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append(isSkytilsPresent); + diagsMessage.append(solverState.name()); diagsMessage.append("\n"); - diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Compass Used Millis: "); - diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append(compassUsedMillis); - diagsMessage.append("\n"); + if (firstCompass == null) { + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("First Compass: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append("<NONE>"); + diagsMessage.append("\n"); + } else { + firstCompass.appendCompassDiagnostics(diagsMessage, "First Compass"); + } + + if (secondCompass == null) { + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Second Compass: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append("<NONE>"); + diagsMessage.append("\n"); + } else { + secondCompass.appendCompassDiagnostics(diagsMessage, "Second Compass"); + } diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Compass Used Position: "); + diagsMessage.append("Intersection Line: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((prevPlayerPos == null) ? "<NONE>" : prevPlayerPos.toString()); + diagsMessage.append((solutionIntersectionLine == null) ? "<NONE>" : solutionIntersectionLine); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("First Particle: "); + diagsMessage.append("Jungle Key in Inventory: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((firstParticle == null) ? "<NONE>" : firstParticle.toString()); + diagsMessage.append(isKeyInInventory()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Last Particle: "); + diagsMessage.append("King's Scent Present: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((lastParticle == null) ? "<NONE>" : lastParticle.toString()); + diagsMessage.append(isKingsScentPresent()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Last Particle Distance From First: "); + diagsMessage.append("First Compass Targets: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append(lastParticleDistanceFromFirst); + diagsMessage.append(possibleTargets == null ? "<NONE>" : possibleTargets.toString()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("First Compass Line: "); + diagsMessage.append("Current Calculated Targets: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((firstCompassLine == null) ? "<NONE>" : firstCompassLine.toString()); + diagsMessage.append(calculatePossibleTargets(mc.thePlayer.getPosition())); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Second Compass Line: "); + diagsMessage.append("Found Crystals: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((secondCompassLine == null) ? "<NONE>" : secondCompassLine.toString()); + diagsMessage.append(getFoundCrystals()); diagsMessage.append("\n"); + if (originalSolution != null) { + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Original Solution: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(originalSolution); + diagsMessage.append("\n"); + } + diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Intersection Line: "); + diagsMessage.append("Solution: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((secondCompassLine == null) ? "<NONE>" : solutionIntersectionLine); + diagsMessage.append((solution == null) ? "<NONE>" : solution.toString()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Solution: "); + diagsMessage.append("Solution Targets: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((solution == null) ? "<NONE>" : solution.toString()); + diagsMessage.append((solutionPossibleTargets == null) ? "<NONE>" : solutionPossibleTargets.toString()); diagsMessage.append("\n"); - NEUDebugLogger.log(NEUDebugFlag.WISHING, diagsMessage.toString()); + return diagsMessage.toString(); + } + + public void logDiagnosticData(boolean outputAlways) { + if (!SBInfo.getInstance().checkForSkyblockLocation()) { + return; + } - if (!originalDebugFlag) { - NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.remove(NEUDebugFlag.WISHING); + if (!NotEnoughUpdates.INSTANCE.config.mining.wishingCompassSolver) + { + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Wishing Compass Solver is not enabled.")); + return; + } + + boolean wishingDebugFlagSet = NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.contains(NEUDebugFlag.WISHING); + if (outputAlways || wishingDebugFlagSet) { + NEUDebugLogger.logAlways(getDiagnosticMessage()); + } + } + + enum CompassState { + WAITING_FOR_FIRST_PARTICLE, + COMPUTING_LAST_PARTICLE, + COMPLETED, + FAILED_TIMEOUT_NO_REPEATING, + FAILED_TIMEOUT_NO_PARTICLES, + } + + enum HandleCompassResult { + SUCCESS, + LOCATION_TOO_CLOSE, + STILL_PROCESSING_PRIOR_USE, + POSSIBLE_TARGETS_CHANGED, + NO_PARTICLES_FOR_PREVIOUS_COMPASS, + PLAYER_IN_NUCLEUS + } + + static class Compass { + public CompassState compassState; + public Line line = null; + + private final BlockPos whereUsed; + private final long whenUsedMillis; + private Vec3Comparable firstParticle = null; + private Vec3Comparable previousParticle = null; + private Vec3Comparable lastParticle = null; + private final ArrayList<ProcessedParticle> processedParticles; + + Compass(BlockPos whereUsed, long whenUsedMillis) { + this.whereUsed = whereUsed; + this.whenUsedMillis = whenUsedMillis; + compassState = CompassState.WAITING_FOR_FIRST_PARTICLE; + processedParticles = new ArrayList<>(); + } + + public Vec3Comparable getDirection() { + if (firstParticle == null || lastParticle == null) { + return null; + } + + return new Vec3Comparable(firstParticle.subtractReverse(lastParticle).normalize()); + } + + public Vec3Comparable getDirectionTo(Vec3Comparable target) { + if (firstParticle == null || target == null) { + return null; + } + + return new Vec3Comparable(firstParticle.subtractReverse(target).normalize()); + } + + public double particleSpread() { + if (firstParticle == null || lastParticle == null) { + return 0.0; + } + return firstParticle.distanceTo(lastParticle); + } + + public void processParticle(double x, double y, double z, long particleTimeMillis) { + if (compassState == CompassState.FAILED_TIMEOUT_NO_REPEATING || + compassState == CompassState.FAILED_TIMEOUT_NO_PARTICLES || + compassState == CompassState.COMPLETED) { + throw new UnsupportedOperationException("processParticle should not be called in a failed or completed state"); + } + + if (particleTimeMillis - this.whenUsedMillis > ALL_PARTICLES_MAX_MILLIS) { + // Assume we have failed if we're still trying to process particles + compassState = CompassState.FAILED_TIMEOUT_NO_REPEATING; + return; + } + + Vec3Comparable currentParticle = new Vec3Comparable(x, y, z); + if (compassState == CompassState.WAITING_FOR_FIRST_PARTICLE) { + if (currentParticle.distanceTo(new Vec3Comparable(whereUsed)) < MAX_DISTANCE_FROM_USE_TO_FIRST_PARTICLE) { + processedParticles.add(new ProcessedParticle(currentParticle, particleTimeMillis)); + firstParticle = currentParticle; + previousParticle = currentParticle; + compassState = CompassState.COMPUTING_LAST_PARTICLE; + } + return; + } + + // State is COMPUTING_LAST_PARTICLE, keep updating the previousParticle until + // the first particle in the second sequence is seen. + if (currentParticle.distanceTo(previousParticle) <= MAX_DISTANCE_BETWEEN_PARTICLES) { + processedParticles.add(new ProcessedParticle(currentParticle, particleTimeMillis)); + previousParticle = currentParticle; + return; + } + + if (currentParticle.distanceTo(firstParticle) > MAX_DISTANCE_BETWEEN_PARTICLES) { + return; + } + + // It's a repeating particle + processedParticles.add(new ProcessedParticle(currentParticle, particleTimeMillis)); + lastParticle = previousParticle; + line = new Line(firstParticle, lastParticle); + compassState = CompassState.COMPLETED; + } + + public void appendCompassDiagnostics(StringBuilder diagsMessage, String compassName) { + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Compass State: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(compassState.name()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append(compassName); + diagsMessage.append(" Used Millis: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(whenUsedMillis); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append(compassName); + diagsMessage.append(" Used Position: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((whereUsed == null) ? "<NONE>" : whereUsed.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append(compassName); + diagsMessage.append(" All Seen Particles: \n"); + diagsMessage.append(EnumChatFormatting.WHITE); + for (ProcessedParticle particle : processedParticles) { + diagsMessage.append(particle.toString()); + diagsMessage.append("\n"); + } + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append(compassName); + diagsMessage.append(" Particle Spread: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(particleSpread()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append(compassName); + diagsMessage.append(" Compass Line: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((line == null) ? "<NONE>" : line.toString()); + diagsMessage.append("\n"); + } + + static class ProcessedParticle { + Vec3Comparable coords; + long particleTimeMillis; + + ProcessedParticle(Vec3Comparable coords, long particleTimeMillis) { + this.coords = coords; + this.particleTimeMillis = particleTimeMillis; + } + + @Override + public String toString() { + return coords.toString() + " " + particleTimeMillis; + } } } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/Mining.java b/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/Mining.java index 6cfb8c04..f793baf0 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/Mining.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/options/seperateSections/Mining.java @@ -641,6 +641,42 @@ public class Mining { @ConfigAccordionId(id = 6) public int crystalHollowNoneColor = 12; + @ConfigOption( + name = "Wishing Compass Solver", + desc = "" + ) + @ConfigEditorAccordion(id = 7) + public boolean wishingCompassSolverAccordion = false; + + @Expose + @ConfigOption( + name = "Enable Solver", + desc = "Show wishing compass target coordinates based on two samples" + ) + @ConfigAccordionId(id = 7) + @ConfigEditorBoolean + public boolean wishingCompassSolver = true; + + @Expose + @ConfigOption( + name = "Skytils Waypoints", + desc = "Automatically create Skytils waypoints for well-known targets" + ) + @ConfigAccordionId(id = 7) + @ConfigEditorBoolean + public boolean wishingCompassAutocreateKnownWaypoints = false; + + @Expose + @ConfigOption( + name = "Waypoint Type", + desc = "Skytils Waypoint name type. Skytils Built-in will be overwritten by Skytils when the waypoint is nearby." + ) + @ConfigAccordionId(id = 7) + @ConfigEditorDropdown( + values = {"NEU", "Skytils"} + ) + public int wishingCompassWaypointNameType = 0; + @Expose @ConfigOption( name = "Puzzler Solver", @@ -665,19 +701,11 @@ public class Mining { @ConfigEditorBoolean public boolean titaniumAlertMustBeVisible = false; - @Expose - @ConfigOption( - name = "Wishing Compass Solver", - desc = "Show wishing compass target coordinates based on two samples" - ) - @ConfigEditorBoolean - public boolean wishingCompassSolver = true; - @ConfigOption( name = "Custom Textures", desc = "" ) - @ConfigEditorAccordion(id = 7) + @ConfigEditorAccordion(id = 8) public boolean texturesAccordion = false; @Expose @@ -685,7 +713,7 @@ public class Mining { name = "Dwarven Mines Textures", desc = "Allows texture packs to retexture blocks in the Dwarven Mines. If you don't have a texture pack that does this, you should leave this off" ) - @ConfigAccordionId(id = 7) + @ConfigAccordionId(id = 8) @ConfigEditorBoolean public boolean dwarvenTextures = false; @Expose @@ -693,7 +721,7 @@ public class Mining { name = "Crystal Hollows Textures", desc = "Allows texture packs to retexture blocks in the Crystal Hollows. If you don't have a texture pack that does this, you should leave this off" ) - @ConfigAccordionId(id = 7) + @ConfigAccordionId(id = 8) @ConfigEditorBoolean public boolean crystalHollowTextures = false; @@ -702,7 +730,7 @@ public class Mining { name = "Replace Gemstone sounds", desc = "Replace the break sounds of crystals in the Crystal Hollows. Requires a texture pack with this feature" ) - @ConfigAccordionId(id = 7) + @ConfigAccordionId(id = 8) @ConfigEditorBoolean public boolean gemstoneSounds = false; @@ -711,7 +739,7 @@ public class Mining { name = "Replace Mithril sounds", desc = "Replace the break sounds of mithril and titanium in the Dwarven mines. Requires a texture pack with this feature" ) - @ConfigAccordionId(id = 7) + @ConfigAccordionId(id = 8) @ConfigEditorBoolean public boolean mithrilSounds = false; diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java b/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java index d662a872..827a3e97 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/NEUDebugLogger.java @@ -13,6 +13,8 @@ import java.util.function.Consumer; public class NEUDebugLogger { private static final Minecraft mc = Minecraft.getMinecraft(); public static Consumer<String> logMethod = NEUDebugLogger::chatLogger; + // Used to prevent accessing NEUConfig in unit tests + public static boolean allFlagsEnabled = false; private static void chatLogger(String message) { mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + "[NEU DEBUG] " + message)); @@ -23,7 +25,13 @@ public class NEUDebugLogger { } public static void log(NEUDebugFlag flag, String message) { - if (logMethod != null && isFlagEnabled(flag)) { + if (logMethod != null && (allFlagsEnabled || isFlagEnabled(flag))) { + logAlways(message); + } + } + + public static void logAlways(String message) { + if (logMethod != null) { logMethod.accept(message); } } diff --git a/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java b/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java index 95a3539a..f1e40671 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/util/SBInfo.java @@ -18,6 +18,8 @@ import net.minecraft.scoreboard.Score; import net.minecraft.scoreboard.ScoreObjective; import net.minecraft.scoreboard.ScorePlayerTeam; import net.minecraft.scoreboard.Scoreboard; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.IChatComponent; import net.minecraftforge.client.event.ClientChatReceivedEvent; import net.minecraftforge.client.event.GuiOpenEvent; @@ -57,7 +59,7 @@ public class SBInfo { public String slayer = ""; public boolean stranded = false; - public String mode = ""; + public String mode = null; public Date currentTimeDate = null; @@ -120,6 +122,16 @@ public class SBInfo { } } + public boolean checkForSkyblockLocation() { + if (!NotEnoughUpdates.INSTANCE.hasSkyblockScoreboard() || getLocation() == null) { + Minecraft.getMinecraft().thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] This command is not available outside SkyBlock")); + return false; + } + + return true; + } + private static final Pattern PROFILE_PATTERN = Pattern.compile("(?<type>(♲ Ironman)|(☀ Stranded)|()) *Profile: (?<name>[^ ]+)"); diff --git a/src/test/java/io/github/moulberry/notenoughupdates/core/util/Vec3ComparableTest.java b/src/test/java/io/github/moulberry/notenoughupdates/core/util/Vec3ComparableTest.java new file mode 100644 index 00000000..c0b451e3 --- /dev/null +++ b/src/test/java/io/github/moulberry/notenoughupdates/core/util/Vec3ComparableTest.java @@ -0,0 +1,193 @@ +package io.github.moulberry.notenoughupdates.core.util; + +import net.minecraft.util.BlockPos; +import net.minecraft.util.Vec3; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Vec3ComparableTest { + @Test + void equals_false_when_null() { + // Arrange + Vec3Comparable vec3c = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act + boolean areEqual = vec3c.equals(null); + + // Assert + assertFalse(areEqual); + } + + @Test + void equals_true_when_same_object() { + // Arrange + Vec3Comparable vec3c = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act + boolean areEqual = vec3c.equals(vec3c); + + // Assert + assertTrue(areEqual); + } + + @Test + void equals_true_when_same_value() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3Comparable vec3c2 = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act + boolean areEqual = vec3c1.equals(vec3c2); + + // Assert + assertTrue(areEqual); + } + + @Test + void equals_false_when_vec3_equals() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3 vec3c2 = new Vec3(1.0, 2.0, 3.0); + + // Act + boolean areEqual = vec3c1.equals(vec3c2); + + // Assert + assertFalse(areEqual); + } + + @Test + void equals_false_when_different_object_type() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(1.0, 2.0, 3.0); + BlockPos blockPos = new BlockPos(1.0, 2.0, 3.0); + + // Act + boolean areEqual = vec3c1.equals(blockPos); + + // Assert + assertFalse(areEqual); + } + + @Test + void equals_false_when_different_value() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3Comparable vec3c2 = new Vec3Comparable(3.0, 2.0, 1.0); + + // Act + boolean areEqual = vec3c1.equals(vec3c2); + + // Assert + assertFalse(areEqual); + } + + @Test + void hashCode_same_when_same_value() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3Comparable vec3c2 = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act + double vec3c1Hash = vec3c1.hashCode(); + double vec3c2Hash = vec3c2.hashCode(); + + // Assert + assertEquals(vec3c1Hash, vec3c2Hash); + } + + @Test + void hashCode_different_when_different_value() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3Comparable vec3c2 = new Vec3Comparable(3.0, 2.0, 1.0); + + // Act + double vec3c1Hash = vec3c1.hashCode(); + double vec3c2Hash = vec3c2.hashCode(); + + // Assert + assertNotEquals(vec3c1Hash, vec3c2Hash); + } + + @Test + void compareTo_zero_when_equal() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3Comparable vec3c2 = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act + int result = vec3c1.compareTo(vec3c2); + + // Assert + assertEquals(0, result); + } + + @Test + void compareTo_negative_when_lower() { + // Arrange + Vec3Comparable vec3c1 = new Vec3Comparable(0.0, 2.0, 3.0); + Vec3Comparable vec3c2 = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act + int result = vec3c1.compareTo(vec3c2); + + // Assert + assertTrue(result < 0); + } + + @Test + void compareTo_positive_when_x_y_or_z_is_higher() { + // Arrange + Vec3Comparable vec3c1x = new Vec3Comparable(2.0, 2.0, 3.0); + Vec3Comparable vec3c2x = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3Comparable vec3c1y = new Vec3Comparable(1.0, 3.0, 3.0); + Vec3Comparable vec3c2y = new Vec3Comparable(1.0, 2.0, 3.0); + Vec3Comparable vec3c1z = new Vec3Comparable(1.0, 2.0, 4.0); + Vec3Comparable vec3c2z = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act + int resultX = vec3c1x.compareTo(vec3c2x); + int resultY = vec3c1y.compareTo(vec3c2y); + int resultZ = vec3c1z.compareTo(vec3c2z); + + // Assert + assertTrue(resultX > 0); + assertTrue(resultY > 0); + assertTrue(resultZ > 0); + } + + @Test + void compareTo_throws_on_null() { + // Arrange + Vec3Comparable vec3c = new Vec3Comparable(1.0, 2.0, 3.0); + + // Act & Assert + Assertions.assertThrows(NullPointerException.class, () -> { + vec3c.compareTo(null); + }); + } + + @Test + void signumEquals_is_true_when_all_signums_match() { + Vec3Comparable first = new Vec3Comparable(-1.0, 1.0, 0); + Vec3 second = new Vec3(-1.0, 1.0, 0); + Assertions.assertTrue(first.signumEquals(second)); + } + + @Test + void signumEquals_is_false_when_any_signum_differs() { + Vec3Comparable first = new Vec3Comparable(-1.0, 1.0, 0); + Vec3 second = new Vec3(-1.0, 1.0, 1.0); + Vec3 third = new Vec3(-1.0, -1.0, 0); + Vec3 fourth = new Vec3(1.0, 1.0, 1.0); + Assertions.assertFalse(first.signumEquals(second)); + Assertions.assertFalse(first.signumEquals(third)); + Assertions.assertFalse(first.signumEquals(fourth)); + } +} diff --git a/src/test/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalMetalDetectorSolverTest.java b/src/test/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalMetalDetectorSolverTest.java new file mode 100644 index 00000000..58233410 --- /dev/null +++ b/src/test/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalMetalDetectorSolverTest.java @@ -0,0 +1,204 @@ +package io.github.moulberry.notenoughupdates.miscfeatures; + +import io.github.moulberry.notenoughupdates.core.util.Vec3Comparable; +import io.github.moulberry.notenoughupdates.miscfeatures.CrystalMetalDetectorSolver.SolutionState; +import io.github.moulberry.notenoughupdates.util.NEUDebugLogger; +import net.minecraft.util.BlockPos; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Assertions; + +import java.util.ArrayList; + +class CrystalMetalDetectorSolverTest { + class Location { + double distance; + Vec3Comparable playerPosition; + SolutionState expectedState; + boolean centerKnown; + + public Location(double distance, + Vec3Comparable playerPosition, + SolutionState expectedState, + boolean centerKnown) { + this.distance = distance; + this.playerPosition = playerPosition; + this.expectedState = expectedState; + this.centerKnown = centerKnown; + } + } + + class Solution { + ArrayList<Location> locations = new ArrayList<>(); + BlockPos center = BlockPos.ORIGIN; + BlockPos expectedSolution = BlockPos.ORIGIN; + } + + @BeforeEach + void setUp() { + CrystalMetalDetectorSolver.initWorld(); + CrystalMetalDetectorSolver.treasureAllowedPredicate = blockPos -> true; + NEUDebugLogger.logMethod = CrystalMetalDetectorSolverTest::neuDebugLog; + NEUDebugLogger.allFlagsEnabled = true; + } + + private void findPossibleSolutionsTwice(Location loc, boolean centerNewlyDiscovered) { + // Each location has to be received twice to be valid + CrystalMetalDetectorSolver.findPossibleSolutions(loc.distance, loc.playerPosition, centerNewlyDiscovered); + CrystalMetalDetectorSolver.findPossibleSolutions(loc.distance, loc.playerPosition, false); + } + + private void checkSolution(Solution solution) { + boolean centerSet = false; + int index = 0; + for (Location loc : solution.locations) { + if (loc.centerKnown && !centerSet && !solution.center.equals(BlockPos.ORIGIN)) { + CrystalMetalDetectorSolver.setMinesCenter(solution.center); + centerSet = true; + findPossibleSolutionsTwice(loc, true); + } else { + findPossibleSolutionsTwice(loc, false); + } + Assertions.assertEquals(loc.expectedState, + CrystalMetalDetectorSolver.currentState, + "Location index " + index); + index++; + } + + Assertions.assertEquals(solution.expectedSolution, CrystalMetalDetectorSolver.getSolution()); + } + + @Test + void findPossibleSolutions_single_location_sample_is_ignored() { + Location location = new Location(37.3, + new Vec3Comparable(779.1057116115207, 70.5, 502.2997937667801), + SolutionState.MULTIPLE, + false); + + CrystalMetalDetectorSolver.findPossibleSolutions(location.distance, location.playerPosition, false); + Assertions.assertEquals(SolutionState.NOT_STARTED, CrystalMetalDetectorSolver.previousState, + "Previous state"); + Assertions.assertEquals(SolutionState.NOT_STARTED, CrystalMetalDetectorSolver.currentState, + "Current state"); + } + + @Test + void findPossibleSolutions_currentState_becomes_previousState() { + Location location = new Location(37.3, + new Vec3Comparable(779.1057116115207, 70.5, 502.2997937667801), + SolutionState.MULTIPLE, + false); + + findPossibleSolutionsTwice(location, false); + Assertions.assertEquals(SolutionState.NOT_STARTED, CrystalMetalDetectorSolver.previousState, + "Previous state"); + Assertions.assertEquals(location.expectedState, CrystalMetalDetectorSolver.currentState, + "Current state"); + } + + @Test + void findPossibleSolutions_state_is_invalid_when_solution_and_distance_mismatch() { + Solution solution = new Solution(); + solution.center = new BlockPos(736, 88, 547); + solution.expectedSolution = new BlockPos(722, 67, 590); + solution.locations.add(new Location(67.5, + new Vec3Comparable(757.8235166144441, 68.0, 532.8037800566217), + SolutionState.FOUND_KNOWN, + true)); + // slightly different player position with invalid distance + solution.locations.add(new Location(4.0, + new Vec3Comparable(757.8235166144441, 69.0, 532.8037800566217), + SolutionState.INVALID, + true)); + checkSolution(solution); + } + + @Test + void findPossibleSolutions_state_is_failed_when_second_location_eliminates_all_blocks() { + Solution solution = new Solution(); + solution.center = new BlockPos(736, 88, 547); + solution.locations.add(new Location(29.4, + new Vec3Comparable(721.5979761606153, 68.0, 590.9056839507032), + SolutionState.MULTIPLE_KNOWN, + true)); + solution.locations.add(new Location(4.0, // actual distance should be 38.2 + new Vec3Comparable(711.858759313838, 67.0, 590.3583935310772), + SolutionState.FAILED, + true)); + checkSolution(solution); + } + + @Test + void findPossibleSolutions_state_is_found_known_when_center_found_after_location() { + Solution solution = new Solution(); + solution.center = new BlockPos(736, 88, 547); + solution.locations.add(new Location(67.5, + new Vec3Comparable(757.8235166144441, 68.0, 532.8037800566217), + SolutionState.MULTIPLE, + false)); + checkSolution(solution); + + solution.locations.get(0).centerKnown = true; + solution.locations.get(0).expectedState = SolutionState.FOUND_KNOWN; + solution.expectedSolution = new BlockPos(722, 67, 590); + checkSolution(solution); + } + + @Test + void findPossibleSolutions_state_is_found_when_single_known_location() { + Solution solution = new Solution(); + solution.center = new BlockPos(736, 88, 547); + solution.expectedSolution = new BlockPos(722, 67, 590); + solution.locations.add(new Location(67.5, + new Vec3Comparable(757.8235166144441, 68.0, 532.8037800566217), + SolutionState.FOUND_KNOWN, + true)); + checkSolution(solution); + } + + @Test + void findPossibleSolutions_states_are_correct_using_multiple_locations_with_unknown_center() { + Solution solution = new Solution(); + solution.locations.add(new Location(37.3, + new Vec3Comparable(779.1057116115207, 70.5, 502.2997937667801), + SolutionState.MULTIPLE, + false)); + solution.locations.add(new Location(34.8, + new Vec3Comparable(782.6999999880791, 71.0, 508.69999998807907), + SolutionState.FOUND, + false)); + solution.expectedSolution = new BlockPos(758, 67, 533); + + checkSolution(solution); + } + + @Test + void findPossibleSolutions_states_are_correct_when_multiple_known_locations_found() { + Solution solution = new Solution(); + + // First, validate that the solution doesn't work without the center + solution.locations.add(new Location(29.4, + new Vec3Comparable(721.5979761606153, 68.0, 590.9056839507032), + SolutionState.MULTIPLE, + false)); + solution.locations.add(new Location(38.2, + new Vec3Comparable(711.858759313838, 67.0, 590.3583935310772), + SolutionState.MULTIPLE, + false)); + checkSolution(solution); + + // Now validate that the solution works with the center + CrystalMetalDetectorSolver.resetSolution(false); + solution.locations.get(0).expectedState = SolutionState.MULTIPLE_KNOWN; + solution.locations.get(0).centerKnown = true; + solution.locations.get(1).expectedState = SolutionState.FOUND_KNOWN; + solution.locations.get(1).centerKnown = true; + solution.expectedSolution = new BlockPos(748, 66, 578); + solution.center = new BlockPos(736, 88, 547); + checkSolution(solution); + } + + private static void neuDebugLog(String message) { + System.out.println(message); + } +} diff --git a/src/test/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolverTest.java b/src/test/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolverTest.java new file mode 100644 index 00000000..ed39a505 --- /dev/null +++ b/src/test/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolverTest.java @@ -0,0 +1,1206 @@ +package io.github.moulberry.notenoughupdates.miscfeatures; + +import io.github.moulberry.notenoughupdates.core.util.Vec3Comparable; +import io.github.moulberry.notenoughupdates.miscfeatures.CrystalWishingCompassSolver.CompassTarget; +import io.github.moulberry.notenoughupdates.miscfeatures.CrystalWishingCompassSolver.HandleCompassResult; +import io.github.moulberry.notenoughupdates.miscfeatures.CrystalWishingCompassSolver.SolverState; +import io.github.moulberry.notenoughupdates.util.NEUDebugLogger; +import net.minecraft.util.BlockPos; +import net.minecraft.util.Vec3; +import net.minecraft.util.Vec3i; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; + +import static io.github.moulberry.notenoughupdates.miscfeatures.CrystalWishingCompassSolver.*; + +class CrystalWishingCompassSolverTest { + private static final CrystalWishingCompassSolver solver = getInstance(); + long systemTimeMillis; + private final long DELAY_AFTER_FIRST_COMPASS_LAST_PARTICLE = 500L; + + private final CompassUse minesOfDivanCompassUse1 = new CompassUse( + 1647528732979L, + new BlockPos(754, 137, 239), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(754.358459, 138.536407, 239.200928), 137), + new ParticleSpawn(new Vec3Comparable(754.315735, 138.444351, 239.690521), 45), + new ParticleSpawn(new Vec3Comparable(754.272278, 138.352051, 240.180008), 51), + new ParticleSpawn(new Vec3Comparable(754.228760, 138.259750, 240.669479), 49), + new ParticleSpawn(new Vec3Comparable(754.185303, 138.167435, 241.158966), 57), + new ParticleSpawn(new Vec3Comparable(754.141846, 138.075134, 241.648438), 50), + new ParticleSpawn(new Vec3Comparable(754.098328, 137.982819, 242.137909), 51), + new ParticleSpawn(new Vec3Comparable(754.054871, 137.890518, 242.627396), 57), + new ParticleSpawn(new Vec3Comparable(754.011353, 137.798203, 243.116867), 44), + new ParticleSpawn(new Vec3Comparable(753.967896, 137.705887, 243.606354), 59), + new ParticleSpawn(new Vec3Comparable(753.924438, 137.613586, 244.095825), 35), + new ParticleSpawn(new Vec3Comparable(753.880920, 137.521271, 244.585297), 48), + new ParticleSpawn(new Vec3Comparable(753.837463, 137.428970, 245.074783), 70), + new ParticleSpawn(new Vec3Comparable(753.793945, 137.336655, 245.564255), 33), + new ParticleSpawn(new Vec3Comparable(753.750488, 137.244354, 246.053741), 55), + new ParticleSpawn(new Vec3Comparable(753.707031, 137.152039, 246.543213), 42), + new ParticleSpawn(new Vec3Comparable(753.663513, 137.059738, 247.032700), 56), + new ParticleSpawn(new Vec3Comparable(753.620056, 136.967422, 247.522171), 48), + new ParticleSpawn(new Vec3Comparable(753.576538, 136.875122, 248.011642), 56), + new ParticleSpawn(new Vec3Comparable(754.333618, 138.527710, 239.197800), 55) + )), + HandleCompassResult.SUCCESS, + SolverState.NEED_SECOND_COMPASS); + + private final CompassUse minesOfDivanCompassUse2 = new CompassUse( + DELAY_AFTER_FIRST_COMPASS_LAST_PARTICLE, + new BlockPos(760, 134, 266), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(759.686951, 135.524994, 266.190704), 129), + new ParticleSpawn(new Vec3Comparable(759.625183, 135.427887, 266.677277), 69), + new ParticleSpawn(new Vec3Comparable(759.561707, 135.330704, 267.163635), 31), + new ParticleSpawn(new Vec3Comparable(759.498230, 135.233536, 267.649963), 115), + new ParticleSpawn(new Vec3Comparable(759.434753, 135.136368, 268.136322), 0), + new ParticleSpawn(new Vec3Comparable(759.371277, 135.039200, 268.622650), 46), + new ParticleSpawn(new Vec3Comparable(759.307800, 134.942017, 269.109009), 49), + new ParticleSpawn(new Vec3Comparable(759.244324, 134.844849, 269.595337), 59), + new ParticleSpawn(new Vec3Comparable(759.180847, 134.747681, 270.081696), 45), + new ParticleSpawn(new Vec3Comparable(759.117371, 134.650513, 270.568024), 39), + new ParticleSpawn(new Vec3Comparable(759.053894, 134.553329, 271.054352), 67), + new ParticleSpawn(new Vec3Comparable(758.990356, 134.456161, 271.540710), 49), + new ParticleSpawn(new Vec3Comparable(758.926880, 134.358994, 272.027039), 32), + new ParticleSpawn(new Vec3Comparable(758.863403, 134.261826, 272.513397), 61), + new ParticleSpawn(new Vec3Comparable(758.799927, 134.164642, 272.999725), 44), + new ParticleSpawn(new Vec3Comparable(758.736450, 134.067474, 273.486084), 48), + new ParticleSpawn(new Vec3Comparable(758.672974, 133.970306, 273.972412), 57), + new ParticleSpawn(new Vec3Comparable(758.609497, 133.873138, 274.458740), 55), + new ParticleSpawn(new Vec3Comparable(758.546021, 133.775955, 274.945099), 59), + new ParticleSpawn(new Vec3Comparable(758.482544, 133.678787, 275.431427), 38), + new ParticleSpawn(new Vec3Comparable(759.636658, 135.522827, 266.186371), 0) + )), + HandleCompassResult.SUCCESS, + SolverState.SOLVED); + + Vec3i minesOfDivanSolution = new Vec3i(735, 98, 451); + + private final CompassUse goblinHoldoutCompassUse1 = new CompassUse( + 1647776326763L, + new BlockPos(454, 87, 776), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(454.171722, 88.616852, 775.807190), 188), + new ParticleSpawn(new Vec3Comparable(454.010315, 88.613464, 775.333984), 44), + new ParticleSpawn(new Vec3Comparable(453.849243, 88.610069, 774.860657), 61), + new ParticleSpawn(new Vec3Comparable(453.688141, 88.606674, 774.387329), 51), + new ParticleSpawn(new Vec3Comparable(453.527069, 88.603271, 773.914001), 40), + new ParticleSpawn(new Vec3Comparable(453.365997, 88.599876, 773.440674), 57), + new ParticleSpawn(new Vec3Comparable(453.204926, 88.596481, 772.967346), 45), + new ParticleSpawn(new Vec3Comparable(453.043854, 88.593086, 772.494019), 49), + new ParticleSpawn(new Vec3Comparable(452.882782, 88.589691, 772.020691), 46), + new ParticleSpawn(new Vec3Comparable(452.721710, 88.586288, 771.547302), 65), + new ParticleSpawn(new Vec3Comparable(452.560638, 88.582893, 771.073975), 43), + new ParticleSpawn(new Vec3Comparable(452.399567, 88.579498, 770.600647), 50), + new ParticleSpawn(new Vec3Comparable(452.238495, 88.576103, 770.127319), 48), + new ParticleSpawn(new Vec3Comparable(452.077423, 88.572701, 769.653992), 47), + new ParticleSpawn(new Vec3Comparable(451.916351, 88.569305, 769.180664), 60), + new ParticleSpawn(new Vec3Comparable(451.755280, 88.565910, 768.707336), 40), + new ParticleSpawn(new Vec3Comparable(451.594208, 88.562515, 768.234009), 69), + new ParticleSpawn(new Vec3Comparable(451.433136, 88.559120, 767.760681), 40), + new ParticleSpawn(new Vec3Comparable(451.272064, 88.555717, 767.287354), 42), + new ParticleSpawn(new Vec3Comparable(454.183441, 88.616600, 775.803040), 54) + )), + HandleCompassResult.SUCCESS, + SolverState.NEED_SECOND_COMPASS); + + private final CompassUse goblinHoldoutCompassUse2 = new CompassUse( + DELAY_AFTER_FIRST_COMPASS_LAST_PARTICLE, + new BlockPos(439, 85, 777), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(439.068848, 86.624870, 776.043701), 136), + new ParticleSpawn(new Vec3Comparable(438.936066, 86.625786, 775.561646), 46), + new ParticleSpawn(new Vec3Comparable(438.804352, 86.626595, 775.079346), 65), + new ParticleSpawn(new Vec3Comparable(438.672699, 86.627396, 774.596985), 40), + new ParticleSpawn(new Vec3Comparable(438.541016, 86.628197, 774.114624), 51), + new ParticleSpawn(new Vec3Comparable(438.409332, 86.628998, 773.632263), 50), + new ParticleSpawn(new Vec3Comparable(438.277679, 86.629799, 773.149902), 50), + new ParticleSpawn(new Vec3Comparable(438.145996, 86.630608, 772.667603), 56), + new ParticleSpawn(new Vec3Comparable(438.014343, 86.631409, 772.185242), 40), + new ParticleSpawn(new Vec3Comparable(437.882660, 86.632210, 771.702881), 65), + new ParticleSpawn(new Vec3Comparable(437.751007, 86.633011, 771.220520), 45), + new ParticleSpawn(new Vec3Comparable(437.619324, 86.633812, 770.738159), 42), + new ParticleSpawn(new Vec3Comparable(437.487671, 86.634613, 770.255798), 60), + new ParticleSpawn(new Vec3Comparable(437.355988, 86.635414, 769.773499), 51), + new ParticleSpawn(new Vec3Comparable(437.224335, 86.636215, 769.291138), 44), + new ParticleSpawn(new Vec3Comparable(437.092651, 86.637024, 768.808777), 56), + new ParticleSpawn(new Vec3Comparable(436.960999, 86.637825, 768.326416), 56), + new ParticleSpawn(new Vec3Comparable(436.829315, 86.638626, 767.844055), 40), + new ParticleSpawn(new Vec3Comparable(436.697632, 86.639427, 767.361694), 50), + new ParticleSpawn(new Vec3Comparable(436.565979, 86.640228, 766.879395), 46), + new ParticleSpawn(new Vec3Comparable(439.108551, 86.620811, 776.031067), 0) + )), + HandleCompassResult.SUCCESS, + SolverState.SOLVED); + + Vec3i goblinHoldoutKingSolution = new Vec3i(377, 87, 550); + Vec3i goblinHoldoutQueenSolution = new Vec3i(322, 139, 769); + + private final CompassUse precursorCityCompassUse1 = new CompassUse( + 1647744920365L, + new BlockPos(570, 120, 565), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(570.428955, 121.630745, 565.674500), 192), + new ParticleSpawn(new Vec3Comparable(570.572998, 121.642563, 566.153137), 52), + new ParticleSpawn(new Vec3Comparable(570.714233, 121.654442, 566.632629), 45), + new ParticleSpawn(new Vec3Comparable(570.855286, 121.666321, 567.112183), 51), + new ParticleSpawn(new Vec3Comparable(570.996338, 121.678200, 567.591736), 0), + new ParticleSpawn(new Vec3Comparable(571.137390, 121.690079, 568.071289), 111), + new ParticleSpawn(new Vec3Comparable(571.278442, 121.701958, 568.550781), 38), + new ParticleSpawn(new Vec3Comparable(571.419495, 121.713844, 569.030334), 51), + new ParticleSpawn(new Vec3Comparable(571.560547, 121.725723, 569.509888), 49), + new ParticleSpawn(new Vec3Comparable(571.701599, 121.737602, 569.989441), 0), + new ParticleSpawn(new Vec3Comparable(571.842651, 121.749481, 570.468994), 101), + new ParticleSpawn(new Vec3Comparable(571.983704, 121.761360, 570.948547), 53), + new ParticleSpawn(new Vec3Comparable(572.124756, 121.773239, 571.428101), 47), + new ParticleSpawn(new Vec3Comparable(572.265747, 121.785118, 571.907654), 49), + new ParticleSpawn(new Vec3Comparable(572.406799, 121.796997, 572.387207), 49), + new ParticleSpawn(new Vec3Comparable(572.547852, 121.808876, 572.866699), 51), + new ParticleSpawn(new Vec3Comparable(572.688904, 121.820755, 573.346252), 57), + new ParticleSpawn(new Vec3Comparable(572.829956, 121.832634, 573.825806), 42), + new ParticleSpawn(new Vec3Comparable(572.971008, 121.844513, 574.305359), 50), + new ParticleSpawn(new Vec3Comparable(573.112061, 121.856392, 574.784912), 52), + new ParticleSpawn(new Vec3Comparable(570.372192, 121.631874, 565.694946), 0) + )), + HandleCompassResult.SUCCESS, + SolverState.NEED_SECOND_COMPASS); + + private final CompassUse precursorCityCompassUse2 = new CompassUse( + DELAY_AFTER_FIRST_COMPASS_LAST_PARTICLE, + new BlockPos(591, 136, 579), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(590.847961, 137.589584, 579.776672), 192), + new ParticleSpawn(new Vec3Comparable(590.918945, 137.528259, 580.267761), 50), + new ParticleSpawn(new Vec3Comparable(590.985229, 137.465118, 580.759338), 56), + new ParticleSpawn(new Vec3Comparable(591.051147, 137.401855, 581.250916), 47), + new ParticleSpawn(new Vec3Comparable(591.117126, 137.338593, 581.742493), 47), + new ParticleSpawn(new Vec3Comparable(591.183044, 137.275330, 582.234070), 49), + new ParticleSpawn(new Vec3Comparable(591.249023, 137.212067, 582.725647), 60), + new ParticleSpawn(new Vec3Comparable(591.314941, 137.148804, 583.217224), 55), + new ParticleSpawn(new Vec3Comparable(591.380920, 137.085541, 583.708801), 47), + new ParticleSpawn(new Vec3Comparable(591.446838, 137.022263, 584.200378), 50), + new ParticleSpawn(new Vec3Comparable(591.512817, 136.959000, 584.691956), 39), + new ParticleSpawn(new Vec3Comparable(591.578735, 136.895737, 585.183533), 53), + new ParticleSpawn(new Vec3Comparable(591.644714, 136.832474, 585.675110), 53), + new ParticleSpawn(new Vec3Comparable(591.710632, 136.769211, 586.166687), 45), + new ParticleSpawn(new Vec3Comparable(591.776611, 136.705948, 586.658264), 79), + new ParticleSpawn(new Vec3Comparable(591.842529, 136.642685, 587.149841), 20), + new ParticleSpawn(new Vec3Comparable(591.908508, 136.579407, 587.641418), 62), + new ParticleSpawn(new Vec3Comparable(591.974426, 136.516144, 588.132996), 48), + new ParticleSpawn(new Vec3Comparable(592.040344, 136.452881, 588.624573), 40), + new ParticleSpawn(new Vec3Comparable(592.106323, 136.389618, 589.116150), 51), + new ParticleSpawn(new Vec3Comparable(590.766357, 137.556885, 579.791565), 0) + )), + HandleCompassResult.SUCCESS, + SolverState.SOLVED); + + Vec3i precursorCitySolution = new Vec3i(604, 124, 681); + + private final CompassUse jungleCompassUse1 = new CompassUse( + 1647744980313L, + new BlockPos(454, 122, 459), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(453.954895, 122.958122, 458.687866), 141), + new ParticleSpawn(new Vec3Comparable(453.515991, 122.760010, 458.553314), 59), + new ParticleSpawn(new Vec3Comparable(453.078156, 122.560112, 458.417877), 41), + new ParticleSpawn(new Vec3Comparable(452.640381, 122.360123, 458.282349), 50), + new ParticleSpawn(new Vec3Comparable(452.202606, 122.160133, 458.146851), 66), + new ParticleSpawn(new Vec3Comparable(451.764832, 121.960136, 458.011353), 35), + new ParticleSpawn(new Vec3Comparable(451.327057, 121.760147, 457.875854), 49), + new ParticleSpawn(new Vec3Comparable(450.889313, 121.560150, 457.740356), 50), + new ParticleSpawn(new Vec3Comparable(450.451538, 121.360161, 457.604858), 49), + new ParticleSpawn(new Vec3Comparable(450.013763, 121.160172, 457.469330), 51), + new ParticleSpawn(new Vec3Comparable(449.575989, 120.960175, 457.333832), 59), + new ParticleSpawn(new Vec3Comparable(449.138214, 120.760185, 457.198334), 41), + new ParticleSpawn(new Vec3Comparable(448.700439, 120.560196, 457.062836), 55), + new ParticleSpawn(new Vec3Comparable(448.262695, 120.360199, 456.927338), 50), + new ParticleSpawn(new Vec3Comparable(447.824921, 120.160210, 456.791840), 49), + new ParticleSpawn(new Vec3Comparable(447.387146, 119.960213, 456.656311), 53), + new ParticleSpawn(new Vec3Comparable(446.949371, 119.760223, 456.520813), 43), + new ParticleSpawn(new Vec3Comparable(446.511597, 119.560234, 456.385315), 51), + new ParticleSpawn(new Vec3Comparable(446.073853, 119.360237, 456.249817), 49), + new ParticleSpawn(new Vec3Comparable(445.636078, 119.160248, 456.114319), 56), + new ParticleSpawn(new Vec3Comparable(453.975647, 122.920158, 458.668488), 0) + )), + HandleCompassResult.SUCCESS, + SolverState.NEED_SECOND_COMPASS); + + private final CompassUse jungleCompassUse2 = new CompassUse( + DELAY_AFTER_FIRST_COMPASS_LAST_PARTICLE, + new BlockPos(438, 126, 468), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(437.701721, 127.395279, 467.455048), 139), + new ParticleSpawn(new Vec3Comparable(437.297852, 127.161415, 467.275604), 35), + new ParticleSpawn(new Vec3Comparable(436.895813, 126.927208, 467.092529), 68), + new ParticleSpawn(new Vec3Comparable(436.493896, 126.692986, 466.909241), 41), + new ParticleSpawn(new Vec3Comparable(436.091980, 126.458763, 466.725952), 54), + new ParticleSpawn(new Vec3Comparable(435.690033, 126.224533, 466.542664), 39), + new ParticleSpawn(new Vec3Comparable(435.288116, 125.990311, 466.359375), 52), + new ParticleSpawn(new Vec3Comparable(434.886200, 125.756088, 466.176086), 66), + new ParticleSpawn(new Vec3Comparable(434.484283, 125.521866, 465.992767), 40), + new ParticleSpawn(new Vec3Comparable(434.082367, 125.287636, 465.809479), 41), + new ParticleSpawn(new Vec3Comparable(433.680420, 125.053413, 465.626190), 50), + new ParticleSpawn(new Vec3Comparable(433.278503, 124.819191, 465.442902), 59), + new ParticleSpawn(new Vec3Comparable(432.876587, 124.584969, 465.259613), 54), + new ParticleSpawn(new Vec3Comparable(432.474670, 124.350746, 465.076294), 38), + new ParticleSpawn(new Vec3Comparable(432.072723, 124.116516, 464.893005), 63), + new ParticleSpawn(new Vec3Comparable(431.670807, 123.882294, 464.709717), 36), + new ParticleSpawn(new Vec3Comparable(431.268890, 123.648071, 464.526428), 64), + new ParticleSpawn(new Vec3Comparable(430.866974, 123.413849, 464.343140), 48), + new ParticleSpawn(new Vec3Comparable(430.465057, 123.179619, 464.159821), 53), + new ParticleSpawn(new Vec3Comparable(430.063110, 122.945396, 463.976532), 46), + new ParticleSpawn(new Vec3Comparable(437.732666, 127.385803, 467.381592), 1) + )), + HandleCompassResult.SUCCESS, + SolverState.SOLVED); + + Vec3i jungleSolution = new Vec3i(343, 72, 424); + Vec3i jungleSolutionTempleDoor = new Vec3i( + jungleSolution.getX() - 57, + jungleSolution.getY() + 36, + jungleSolution.getZ() -21); + + private final CompassUse magmaCompassUse1 = new CompassUse( + 1647745029814L, + new BlockPos(462, 58, 550), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(462.226898, 59.614380, 550.032654), 160), + new ParticleSpawn(new Vec3Comparable(462.693848, 59.609089, 549.853943), 47), + new ParticleSpawn(new Vec3Comparable(463.160706, 59.603809, 549.674988), 48), + new ParticleSpawn(new Vec3Comparable(463.627533, 59.598526, 549.496033), 136), + new ParticleSpawn(new Vec3Comparable(464.094391, 59.593246, 549.317017), 0), + new ParticleSpawn(new Vec3Comparable(464.561218, 59.587963, 549.138062), 0), + new ParticleSpawn(new Vec3Comparable(465.028076, 59.582684, 548.959106), 53), + new ParticleSpawn(new Vec3Comparable(465.494904, 59.577400, 548.780090), 48), + new ParticleSpawn(new Vec3Comparable(465.961761, 59.572117, 548.601135), 55), + new ParticleSpawn(new Vec3Comparable(466.428589, 59.566837, 548.422180), 47), + new ParticleSpawn(new Vec3Comparable(466.895416, 59.561554, 548.243164), 46), + new ParticleSpawn(new Vec3Comparable(467.362274, 59.556274, 548.064209), 53), + new ParticleSpawn(new Vec3Comparable(467.829102, 59.550991, 547.885254), 50), + new ParticleSpawn(new Vec3Comparable(468.295959, 59.545712, 547.706238), 54), + new ParticleSpawn(new Vec3Comparable(468.762787, 59.540428, 547.527283), 52), + new ParticleSpawn(new Vec3Comparable(469.229645, 59.535145, 547.348328), 105), + new ParticleSpawn(new Vec3Comparable(469.696472, 59.529865, 547.169312), 1), + new ParticleSpawn(new Vec3Comparable(470.163300, 59.524582, 546.990356), 51), + new ParticleSpawn(new Vec3Comparable(470.630157, 59.519302, 546.811401), 40), + new ParticleSpawn(new Vec3Comparable(471.096985, 59.514019, 546.632385), 49), + new ParticleSpawn(new Vec3Comparable(462.221954, 59.614719, 550.019165), 0) + )), + HandleCompassResult.SUCCESS, + SolverState.NEED_SECOND_COMPASS); + + private final CompassUse magmaCompassUse2 = new CompassUse( + DELAY_AFTER_FIRST_COMPASS_LAST_PARTICLE, + new BlockPos(449, 53, 556), + new ArrayList<>(Arrays.asList( + new ParticleSpawn(new Vec3Comparable(449.120911, 54.624340, 556.108948), 204), + new ParticleSpawn(new Vec3Comparable(449.587433, 54.627399, 555.929138), 102), + new ParticleSpawn(new Vec3Comparable(450.053741, 54.630432, 555.748657), 0), + new ParticleSpawn(new Vec3Comparable(450.520020, 54.633465, 555.568237), 62), + new ParticleSpawn(new Vec3Comparable(450.986298, 54.636497, 555.387756), 38), + new ParticleSpawn(new Vec3Comparable(451.452606, 54.639530, 555.207275), 48), + new ParticleSpawn(new Vec3Comparable(451.918884, 54.642563, 555.026794), 63), + new ParticleSpawn(new Vec3Comparable(452.385162, 54.645596, 554.846375), 52), + new ParticleSpawn(new Vec3Comparable(452.851471, 54.648628, 554.665894), 35), + new ParticleSpawn(new Vec3Comparable(453.317749, 54.651661, 554.485413), 53), + new ParticleSpawn(new Vec3Comparable(453.784027, 54.654694, 554.304993), 54), + new ParticleSpawn(new Vec3Comparable(454.250305, 54.657726, 554.124512), 50), + new ParticleSpawn(new Vec3Comparable(454.716614, 54.660759, 553.944031), 55), + new ParticleSpawn(new Vec3Comparable(455.182892, 54.663792, 553.763550), 49), + new ParticleSpawn(new Vec3Comparable(455.649170, 54.666824, 553.583130), 41), + new ParticleSpawn(new Vec3Comparable(456.115479, 54.669857, 553.402649), 48), + new ParticleSpawn(new Vec3Comparable(456.581757, 54.672890, 553.222168), 54), + new ParticleSpawn(new Vec3Comparable(457.048035, 54.675922, 553.041687), 45), + new ParticleSpawn(new Vec3Comparable(457.514313, 54.678959, 552.861267), 55), + new ParticleSpawn(new Vec3Comparable(449.110443, 54.623035, 556.079163), 49) + )), + HandleCompassResult.SUCCESS, + SolverState.SOLVED); + + Vec3i magmaSolution = new Vec3i(737, 56, 444); + + Vec3Comparable kingMinesOrNucleusCoordsInRemnants = new Vec3Comparable(604, 100, 681); + Vec3Comparable odawaSolution = new Vec3Comparable(349, 110, 390); + + private final CompassUse nucleusCompass = new CompassUse( + 1647745029814L, + new BlockPos(512, 106, 512), + null, + HandleCompassResult.PLAYER_IN_NUCLEUS, + SolverState.NOT_STARTED); + + private void resetSolverState() { + solver.initWorld(); + systemTimeMillis = 0; + solver.currentTimeMillis = () -> (systemTimeMillis); + // These must be overridden for all test cases or an exception will be thrown when + // data that is only present when running in the context of Minecraft is accessed. + solver.keyInInventory = () -> false; + solver.kingsScentPresent = () -> false; + solver.foundCrystals = () -> EnumSet.noneOf(Crystal.class); + } + + @BeforeEach + void setUp() { + NEUDebugLogger.logMethod = CrystalWishingCompassSolverTest::neuDebugLog; + NEUDebugLogger.allFlagsEnabled = true; + resetSolverState(); + } + + private void checkSolution(Solution solution) { + int index = 0; + for (CompassUse compassUse : solution.compassUses) { + systemTimeMillis += compassUse.timeIncrementMillis; + HandleCompassResult handleCompassResult = solver.handleCompassUse(compassUse.playerPos); + Assertions.assertEquals(compassUse.expectedHandleCompassUseResult, + handleCompassResult, + "CompassUse index " + index); + + for (ParticleSpawn particle : compassUse.particles) { + systemTimeMillis += particle.timeIncrementMillis; + solver.solveUsingParticle( + particle.spawnLocation.xCoord, + particle.spawnLocation.yCoord, + particle.spawnLocation.zCoord, + systemTimeMillis); + } + + Assertions.assertEquals(compassUse.expectedSolverState, + solver.getSolverState(), + "CompassUse index " + index); + if (compassUse.expectedSolverState == SolverState.SOLVED) { + Assertions.assertEquals(solution.expectedSolutionCoords, + solver.getSolutionCoords()); + } + + index++; + } + } + + @Test + void first_compass_without_particles_sets_solver_state_to_processing_first_use() { + // Arrange + CompassUse compassUse = new CompassUse(minesOfDivanCompassUse1); + compassUse.particles.clear(); + compassUse.expectedSolverState = SolverState.PROCESSING_FIRST_USE; + + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(compassUse)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void new_compass_resets_processing_first_use_state_after_timeout() { + // Arrange + CompassUse processingFirstUseCompassUse = new CompassUse(minesOfDivanCompassUse1); + processingFirstUseCompassUse.particles.clear(); + processingFirstUseCompassUse.expectedSolverState = SolverState.PROCESSING_FIRST_USE; + Solution processingFirstUseSolution = new Solution( + new ArrayList<>(Collections.singletonList(processingFirstUseCompassUse)), + Vec3i.NULL_VECTOR); + checkSolution(processingFirstUseSolution); + Assertions.assertEquals(SolverState.PROCESSING_FIRST_USE, solver.getSolverState()); + + CompassUse resetStateCompassUse = new CompassUse(jungleCompassUse1); + resetStateCompassUse.timeIncrementMillis = ALL_PARTICLES_MAX_MILLIS + 1; + resetStateCompassUse.expectedHandleCompassUseResult = HandleCompassResult.NO_PARTICLES_FOR_PREVIOUS_COMPASS; + resetStateCompassUse.expectedSolverState = SolverState.FAILED_TIMEOUT_NO_REPEATING; + Solution goodSolution = new Solution( + new ArrayList<>(Collections.singletonList(resetStateCompassUse)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(goodSolution); + } + + @Test + void first_compass_with_repeating_particles_sets_state_to_need_second_compass() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(minesOfDivanCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void first_compass_in_nucleus_sets_state_to_player_in_nucleus() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(nucleusCompass)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void use_while_handling_previous_returns_still_processing_first_use() { + // Arrange + CompassUse compassUse1 = new CompassUse( + 1647528732979L, + new BlockPos(754, 137, 239), + new ArrayList<>(Collections.singletonList( + new ParticleSpawn(new Vec3Comparable(754.358459, 138.536407, 239.200928), 137) + )), + HandleCompassResult.SUCCESS, + SolverState.PROCESSING_FIRST_USE); + + // STILL_PROCESSING_FIRST_USE is expected instead of LOCATION_TOO_CLOSE since the solver + // isn't ready for the second compass use, which includes the location check + CompassUse compassUse2 = new CompassUse(compassUse1); + compassUse2.expectedHandleCompassUseResult = HandleCompassResult.STILL_PROCESSING_PRIOR_USE; + compassUse2.timeIncrementMillis = 500; + + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(compassUse1, compassUse2)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void missing_repeating_particles_sets_state_to_failed_timeout_no_repeating() { + CompassUse compassUse = new CompassUse(minesOfDivanCompassUse1); + compassUse.particles.remove(compassUse.particles.size()-1); + compassUse.particles.get(compassUse.particles.size()-1).timeIncrementMillis += ALL_PARTICLES_MAX_MILLIS; + compassUse.expectedSolverState = SolverState.FAILED_TIMEOUT_NO_REPEATING; + + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(compassUse)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void compasses_too_close_returns_location_too_close_and_solver_state_is_still_need_second_compass() { + // Arrange + CompassUse secondCompassUse = new CompassUse( + DELAY_AFTER_FIRST_COMPASS_LAST_PARTICLE, + minesOfDivanCompassUse1.playerPos.add(2, 2, 2), + null, + HandleCompassResult.LOCATION_TOO_CLOSE, + SolverState.NEED_SECOND_COMPASS + ); + + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(minesOfDivanCompassUse1, secondCompassUse)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void second_compass_sets_solver_state_to_processing_second_use() { + // Arrange + CompassUse secondCompassUse = new CompassUse(minesOfDivanCompassUse2); + secondCompassUse.expectedSolverState = SolverState.PROCESSING_SECOND_USE; + secondCompassUse.particles.clear(); + + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(minesOfDivanCompassUse1, minesOfDivanCompassUse2)), + minesOfDivanSolution + ); + + // Act & Assert + checkSolution(solution); + } + + @Test + void second_compass_with_repeating_particles_sets_state_to_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(minesOfDivanCompassUse1, minesOfDivanCompassUse2)), + minesOfDivanSolution + ); + + // Act & Assert + checkSolution(solution); + } + + @Test + void particles_from_first_compass_are_ignored_by_second_compass() { + // Arrange + CompassUse compassUse2 = new CompassUse(minesOfDivanCompassUse2); + compassUse2.particles.add(0, minesOfDivanCompassUse1.particles.get(0)); + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(minesOfDivanCompassUse1, compassUse2)), + minesOfDivanSolution + ); + + // Act & Assert + checkSolution(solution); + } + + private void execInvalidParticlesInvalidSolution() { + // Arrange + CompassUse compassUse2 = new CompassUse(minesOfDivanCompassUse2); + + // reverse the direction of the particles, moving the repeat particle + // to "new" end + compassUse2.particles.remove(compassUse2.particles.size()-1); + Collections.reverse(compassUse2.particles); + // add a new repeat particle + compassUse2.particles.add(new ParticleSpawn(compassUse2.particles.get(0))); + + // Adjust the player position + compassUse2.playerPos = new BlockPos(compassUse2.particles.get(0).spawnLocation); + compassUse2.expectedSolverState = SolverState.FAILED_INVALID_SOLUTION; + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(minesOfDivanCompassUse1, compassUse2)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void second_compass_with_inverted_particles_sets_state_to_invalid_solution() { + // Arrange, Act, and Assert + execInvalidParticlesInvalidSolution(); + } + + @Test + void solution_outside_hollows_sets_state_to_invalid_solution() { + // Arrange + CompassUse compassUse1 = new CompassUse(minesOfDivanCompassUse1); + CompassUse compassUse2 = new CompassUse(minesOfDivanCompassUse2); + Vec3 offset = new Vec3(0.0, 200.0, 0.0); + + compassUse1.playerPos = compassUse1.playerPos.add(offset.xCoord, offset.yCoord, offset.zCoord); + for (ParticleSpawn particle : compassUse1.particles) { + particle.spawnLocation = particle.spawnLocation.add(offset); + } + + compassUse2.playerPos = compassUse2.playerPos.add(offset.xCoord, offset.yCoord, offset.zCoord); + for (ParticleSpawn particle : compassUse2.particles) { + particle.spawnLocation = particle.spawnLocation.add(offset); + } + compassUse2.expectedSolverState = SolverState.FAILED_INVALID_SOLUTION; + + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(compassUse1, compassUse2)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void second_solution_can_be_solved_after_state_is_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(minesOfDivanCompassUse1, minesOfDivanCompassUse2)), + minesOfDivanSolution + ); + checkSolution(solution); + + Solution solution2 = new Solution( + new ArrayList<>(Arrays.asList(precursorCityCompassUse1, precursorCityCompassUse2)), + precursorCitySolution + ); + + // Act & Assert + checkSolution(solution2); + } + + @Test + void second_solution_can_be_solved_after_state_is_failed() { + // Arrange + execInvalidParticlesInvalidSolution(); + Assertions.assertEquals(solver.getSolverState(), SolverState.FAILED_INVALID_SOLUTION); + + Solution solution2 = new Solution( + new ArrayList<>(Arrays.asList(precursorCityCompassUse1, precursorCityCompassUse2)), + precursorCitySolution + ); + + // Act & Assert + checkSolution(solution2); + } + + @Test + void distant_particles_are_ignored() { + // Arrange + CompassUse compassUse = new CompassUse(minesOfDivanCompassUse1); + compassUse.particles.get(2).spawnLocation.addVector(100.0, 100.0, 100.0); + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(minesOfDivanCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act & Assert + checkSolution(solution); + } + + @Test + void possible_targets_includes_queen_and_excludes_king_when_kings_scent_is_present() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(goblinHoldoutCompassUse1)), + Vec3i.NULL_VECTOR); + solver.kingsScentPresent = () -> true; + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertTrue(targets.contains(CompassTarget.GOBLIN_QUEEN)); + Assertions.assertFalse(targets.contains(CompassTarget.GOBLIN_KING)); + } + + @Test + void possible_targets_excludes_king_and_includes_queen_when_kings_scent_is_not_present() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(goblinHoldoutCompassUse1)), + Vec3i.NULL_VECTOR); + solver.kingsScentPresent = () -> false; + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertFalse(targets.contains(CompassTarget.GOBLIN_QUEEN)); + Assertions.assertTrue(targets.contains(CompassTarget.GOBLIN_KING)); + } + + @Test + void possible_targets_excludes_odawa_and_includes_temple_when_key_in_inventory() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(jungleCompassUse1)), + Vec3i.NULL_VECTOR); + solver.keyInInventory = () -> true; + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertFalse(targets.contains(CompassTarget.ODAWA)); + Assertions.assertTrue(targets.contains(CompassTarget.JUNGLE_TEMPLE)); + } + + @Test + void possible_targets_includes_odawa_and_excludes_temple_when_key_not_in_inventory() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(jungleCompassUse1)), + Vec3i.NULL_VECTOR); + solver.keyInInventory = () -> false; + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertTrue(targets.contains(CompassTarget.ODAWA)); + Assertions.assertFalse(targets.contains(CompassTarget.JUNGLE_TEMPLE)); + } + + @Test + void possible_targets_only_contains_city_and_nucleus_when_in_remnants_without_sapphire_crystal() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(precursorCityCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertTrue(targets.contains(CompassTarget.PRECURSOR_CITY)); + Assertions.assertTrue(targets.contains(CompassTarget.CRYSTAL_NUCLEUS)); + Assertions.assertEquals(2, targets.size()); + } + + @Test + void possible_targets_only_contains_mines_and_nucleus_when_in_deposits_without_jade_crystal() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(minesOfDivanCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertTrue(targets.contains(CompassTarget.MINES_OF_DIVAN)); + Assertions.assertTrue(targets.contains(CompassTarget.CRYSTAL_NUCLEUS)); + Assertions.assertEquals(2, targets.size()); + } + + @Test + void possible_targets_only_contains_king_or_queen_and_nucleus_when_in_holdout_without_crystal() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(goblinHoldoutCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertTrue(targets.contains(CompassTarget.GOBLIN_KING) || + targets.contains(CompassTarget.GOBLIN_QUEEN)); + Assertions.assertTrue(targets.contains(CompassTarget.CRYSTAL_NUCLEUS)); + Assertions.assertEquals(2, targets.size()); + } + + @Test + void possible_targets_only_contains_king_and_odawa_and_nucleus_when_in_jungle_without_crystal_or_key() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(jungleCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertTrue(targets.contains(CompassTarget.GOBLIN_KING)); + Assertions.assertTrue(targets.contains(CompassTarget.ODAWA)); + Assertions.assertTrue(targets.contains(CompassTarget.CRYSTAL_NUCLEUS)); + Assertions.assertEquals(3, targets.size()); + } + + @Test + void possible_targets_only_contains_bal_and_nucleus_when_in_magma_fields_without_crystal() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(magmaCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + EnumSet<CompassTarget> targets = solver.getPossibleTargets(); + + // Assert + Assertions.assertTrue(targets.contains(CompassTarget.BAL)); + Assertions.assertTrue(targets.contains(CompassTarget.CRYSTAL_NUCLEUS)); + Assertions.assertEquals(2, targets.size()); + } + + private void CheckExcludedTargetsForCrystals( + CompassUse compassUseToExecute, + ArrayList<CompassTarget> excludedTargets, + EnumSet<Crystal> foundCrystals) { + // Arrange + EnumSet<CompassTarget> targets; + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(compassUseToExecute)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + targets = solver.getPossibleTargets(); + boolean targetFound = false; + for (CompassTarget target : excludedTargets) { + if (targets.contains(target)) { + targetFound = true; + break; + } + } + Assertions.assertTrue(targetFound); + + resetSolverState(); + solver.foundCrystals = () -> foundCrystals; + checkSolution(solution); + targets = solver.getPossibleTargets(); + + // Assert + for (CompassTarget target : excludedTargets) { + Assertions.assertFalse(targets.contains(target)); + } + } + + @Test + void possible_targets_excludes_king_and_queen_when_amber_crystal_found() { + ArrayList<CompassTarget> excludedTargets = new ArrayList<>(Arrays.asList( + CompassTarget.GOBLIN_KING, + CompassTarget.GOBLIN_QUEEN + )); + CheckExcludedTargetsForCrystals(goblinHoldoutCompassUse1, excludedTargets, EnumSet.of(Crystal.AMBER)); + } + + @Test + void possible_targets_excludes_odawa_and_temple_when_amethyst_crystal_found() { + ArrayList<CompassTarget> excludedTargets = new ArrayList<>(Arrays.asList( + CompassTarget.ODAWA, + CompassTarget.JUNGLE_TEMPLE)); + CheckExcludedTargetsForCrystals(jungleCompassUse1, excludedTargets, EnumSet.of(Crystal.AMETHYST)); + } + + @Test + void possible_targets_excludes_mines_when_jade_crystal_found() { + ArrayList<CompassTarget> excludedTargets = new ArrayList<>(Collections.singletonList( + CompassTarget.MINES_OF_DIVAN)); + CheckExcludedTargetsForCrystals(minesOfDivanCompassUse1, excludedTargets, EnumSet.of(Crystal.JADE)); + } + + @Test + void possible_targets_excludes_city_when_sapphire_crystal_found() { + ArrayList<CompassTarget> excludedTargets = new ArrayList<>(Collections.singletonList( + CompassTarget.PRECURSOR_CITY)); + CheckExcludedTargetsForCrystals(precursorCityCompassUse1, excludedTargets, EnumSet.of(Crystal.SAPPHIRE)); + } + + @Test + void possible_targets_excludes_bal_when_topaz_crystal_found() { + ArrayList<CompassTarget> excludedTargets = new ArrayList<>(Collections.singletonList( + CompassTarget.BAL)); + CheckExcludedTargetsForCrystals(magmaCompassUse1, excludedTargets, EnumSet.of(Crystal.TOPAZ)); + } + + @Test + void solver_resets_when_possible_targets_change_based_on_found_crystals() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(minesOfDivanCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + systemTimeMillis += minesOfDivanCompassUse2.timeIncrementMillis; + solver.foundCrystals = () -> EnumSet.of(Crystal.JADE); + HandleCompassResult handleCompassResult = solver.handleCompassUse(minesOfDivanCompassUse2.playerPos); + + // Assert + Assertions.assertEquals(HandleCompassResult.POSSIBLE_TARGETS_CHANGED, handleCompassResult); + Assertions.assertEquals(SolverState.NOT_STARTED, solver.getSolverState()); + } + + @Test + void solver_resets_when_possible_targets_change_based_on_location() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Collections.singletonList(minesOfDivanCompassUse1)), + Vec3i.NULL_VECTOR); + + // Act + checkSolution(solution); + systemTimeMillis += minesOfDivanCompassUse2.timeIncrementMillis; + BlockPos newLocation = minesOfDivanCompassUse2.playerPos.add(-400, 0 ,0); + HandleCompassResult handleCompassResult = solver.handleCompassUse(newLocation); + + // Assert + Assertions.assertEquals(HandleCompassResult.POSSIBLE_TARGETS_CHANGED, handleCompassResult); + Assertions.assertEquals(SolverState.NOT_STARTED, solver.getSolverState()); + } + + @Test + void mines_of_divan_solution_is_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(minesOfDivanCompassUse1, minesOfDivanCompassUse2)), + minesOfDivanSolution + ); + + // Act & Assert + checkSolution(solution); + } + + @Test + void jungle_temple_solution_with_key_in_inventory_is_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(jungleCompassUse1, jungleCompassUse2)), + jungleSolutionTempleDoor + ); + solver.keyInInventory = () -> true; + + // Act & Assert + checkSolution(solution); + } + + @Test + void jungle_temple_solution_is_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(jungleCompassUse1, jungleCompassUse2)), + jungleSolution + ); + + // Act & Assert + checkSolution(solution); + } + + @Test + void precursor_city_solution_is_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(precursorCityCompassUse1, precursorCityCompassUse2)), + precursorCitySolution + ); + + // Act & Assert + checkSolution(solution); + } + + @Test + void goblin_king_solution_is_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(goblinHoldoutCompassUse1, goblinHoldoutCompassUse2)), + goblinHoldoutKingSolution + ); + + // Act & Assert + checkSolution(solution); + } + + @Test + void bal_solution_is_solved() { + // Arrange + Solution solution = new Solution( + new ArrayList<>(Arrays.asList(magmaCompassUse1, magmaCompassUse2)), + magmaSolution + ); + + // Act & Assert + checkSolution(solution); + } + + EnumSet<CompassTarget> GetSolutionTargetsHelper( + EnumSet<CompassTarget> possibleTargets, + Vec3Comparable solutionCoords, + int expectedSolutionCount) { + EnumSet<CompassTarget> solutionTargets = + CrystalWishingCompassSolver.getSolutionTargets(possibleTargets, solutionCoords); + Assertions.assertEquals(expectedSolutionCount, solutionTargets.size()); + return solutionTargets; + } + + @Test + void solutionPossibleTargets_removes_nucleus_when_coords_not_in_nucleus() { + // Arrange & Act + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + EnumSet.allOf(CompassTarget.class), + new Vec3Comparable(minesOfDivanSolution), + 2); + + //Assert + Assertions.assertFalse(solutionTargets.contains(CompassTarget.CRYSTAL_NUCLEUS)); + } + + @Test + void solutionPossibleTargets_includes_adjacent_zones() { + // Arrange & Act + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + EnumSet.allOf(CompassTarget.class), + kingMinesOrNucleusCoordsInRemnants, + 2); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.GOBLIN_KING)); + Assertions.assertTrue(solutionTargets.contains(CompassTarget.MINES_OF_DIVAN)); + } + + @Test + void solutionPossibleTargets_skips_y_filtering_when_single_possible_target() { + // Arrange & Act + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + EnumSet.of(CompassTarget.GOBLIN_QUEEN), + new Vec3Comparable(goblinHoldoutKingSolution), // Coords not valid for queen + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.GOBLIN_QUEEN)); + } + + @Test + void solutionPossibleTargets_still_filters_non_adjacent_when_single_possible_target() { + // Arrange, Act and Assert + GetSolutionTargetsHelper( + EnumSet.of(CompassTarget.ODAWA), + kingMinesOrNucleusCoordsInRemnants, + 0); + } + + @Test + void solutionPossibleTargets_includes_king_based_on_y_coordinate() { + // Arrange & Act + EnumSet<CompassTarget> possibleTargets = EnumSet.allOf(CompassTarget.class); + possibleTargets.remove(CompassTarget.ODAWA); + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + possibleTargets, + new Vec3Comparable(goblinHoldoutKingSolution), + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.GOBLIN_KING)); + } + + @Test + void solutionPossibleTargets_includes_odawa_based_on_y_coordinate() { + // Arrange & Act + // Arrange & Act + EnumSet<CompassTarget> possibleTargets = EnumSet.allOf(CompassTarget.class); + possibleTargets.remove(CompassTarget.GOBLIN_KING); + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + possibleTargets, + new Vec3Comparable(odawaSolution), + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.ODAWA)); + } + + @Test + void solutionPossibleTargets_includes_mines_based_on_y_coordinate() { + // Arrange & Act + EnumSet<CompassTarget> possibleTargets = EnumSet.allOf(CompassTarget.class); + possibleTargets.remove(CompassTarget.ODAWA); + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + possibleTargets, + new Vec3Comparable(minesOfDivanSolution), + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.MINES_OF_DIVAN)); + } + + @Test + void solutionPossibleTargets_includes_temple_based_on_y_coordinate() { + // Arrange & Act + EnumSet<CompassTarget> possibleTargets = EnumSet.allOf(CompassTarget.class); + possibleTargets.remove(CompassTarget.BAL); + possibleTargets.remove(CompassTarget.ODAWA); + possibleTargets.remove(CompassTarget.GOBLIN_KING); + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + possibleTargets, + new Vec3Comparable(jungleSolution), + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.JUNGLE_TEMPLE)); + } + + @Test + void solutionPossibleTargets_includes_queen_based_on_y_coordinate() { + // Arrange & Act + EnumSet<CompassTarget> possibleTargets = EnumSet.allOf(CompassTarget.class); + possibleTargets.remove(CompassTarget.GOBLIN_KING); + possibleTargets.remove(CompassTarget.ODAWA); + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + possibleTargets, + new Vec3Comparable(goblinHoldoutQueenSolution), + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.GOBLIN_QUEEN)); + } + + @Test + void solutionPossibleTargets_includes_city_based_on_y_coordinate() { + // Arrange & Act + EnumSet<CompassTarget> possibleTargets = EnumSet.allOf(CompassTarget.class); + possibleTargets.remove(CompassTarget.GOBLIN_KING); + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + possibleTargets, + new Vec3Comparable(precursorCitySolution), + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.PRECURSOR_CITY)); + } + + @Test + void solutionPossibleTargets_includes_bal_based_on_y_coordinate() { + // Arrange & Act + EnumSet<CompassTarget> solutionTargets = GetSolutionTargetsHelper( + EnumSet.allOf(CompassTarget.class), + new Vec3Comparable(magmaSolution), + 1); + + //Assert + Assertions.assertTrue(solutionTargets.contains(CompassTarget.BAL)); + } + + // Represents a particle spawn, including: + // - Milliseconds to increment the "system time" prior to spawn. + // - The particle spawn location. + static class ParticleSpawn { + long timeIncrementMillis; + Vec3Comparable spawnLocation; + + ParticleSpawn(Vec3Comparable spawnLocation, long timeIncrementMillis) { + this.timeIncrementMillis = timeIncrementMillis; + this.spawnLocation = spawnLocation; + } + + ParticleSpawn(ParticleSpawn source) { + timeIncrementMillis = source.timeIncrementMillis; + spawnLocation = new Vec3Comparable(source.spawnLocation); + } + } + + // Represents a use of the wishing compass, including: + // - Milliseconds to increment the "system time" prior to use. + // - The player's position when the compass is used. + // - The resulting set of particles + // - The expected state of the wishing compass solver after this compass is used + static class CompassUse { + long timeIncrementMillis; + BlockPos playerPos; + ArrayList<ParticleSpawn> particles; + HandleCompassResult expectedHandleCompassUseResult; + SolverState expectedSolverState; + + CompassUse(long timeIncrementMillis, + BlockPos playerPos, + ArrayList<ParticleSpawn> particles, + HandleCompassResult expectedHandleCompassUseResult, + SolverState expectedState) { + this.timeIncrementMillis = timeIncrementMillis; + this.playerPos = playerPos; + this.particles = particles != null ? particles : new ArrayList<>(); + this.expectedHandleCompassUseResult = expectedHandleCompassUseResult; + this.expectedSolverState = expectedState; + } + + CompassUse(CompassUse source) { + this.timeIncrementMillis = source.timeIncrementMillis; + this.playerPos = new BlockPos(source.playerPos); + this.particles = new ArrayList<>(source.particles); + this.expectedHandleCompassUseResult = source.expectedHandleCompassUseResult; + this.expectedSolverState = source.expectedSolverState; + } + } + + static class Solution { + ArrayList<CompassUse> compassUses; + Vec3i expectedSolutionCoords; + + Solution(ArrayList<CompassUse> compassUses, Vec3i expectedSolutionCoords) { + this.compassUses = compassUses; + this.expectedSolutionCoords = new Vec3i(expectedSolutionCoords.getX(), expectedSolutionCoords.getY(), expectedSolutionCoords.getZ()); + } + } + + private static void neuDebugLog(String message) { + System.out.println(message); + } +} diff --git a/src/test/resources/log4j2-test.xml b/src/test/resources/log4j2-test.xml new file mode 100644 index 00000000..25209eae --- /dev/null +++ b/src/test/resources/log4j2-test.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Configuration status="WARN"> + <Appenders> + <Console name="SysOut" target="SYSTEM_OUT"> + <PatternLayout pattern="[%d{HH:mm:ss}] [%t/%level]: %msg{nolookups}%n" /> + </Console> + </Appenders> + <Loggers> + <Root level="info"> + <filters> + <MarkerFilter marker="NETWORK_PACKETS" onMatch="DENY" onMismatch="NEUTRAL" /> + </filters> + <AppenderRef ref="SysOut"/> + </Root> + </Loggers> +</Configuration> |