From 73fad7064fb47d9d79ccdb3bab5988df1b9eea6f Mon Sep 17 00:00:00 2001 From: CraftyOldMiner <85420839+CraftyOldMiner@users.noreply.github.com> Date: Mon, 7 Mar 2022 21:44:17 -0600 Subject: Wishing Compass Solver, Metal Detector Solver improvements, add /neudiag, other misc changes (#90) --- .../miscfeatures/CrystalWishingCompassSolver.java | 329 +++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java (limited to 'src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java') diff --git a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java new file mode 100644 index 00000000..25f39c3a --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java @@ -0,0 +1,329 @@ +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.options.customtypes.NEUDebugFlag; +import io.github.moulberry.notenoughupdates.util.NEUDebugLogger; +import io.github.moulberry.notenoughupdates.util.SBInfo; +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.minecraftforge.event.entity.player.PlayerInteractEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.fml.common.Loader; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; + +public class CrystalWishingCompassSolver { + private static final CrystalWishingCompassSolver INSTANCE = new CrystalWishingCompassSolver(); + public static CrystalWishingCompassSolver getInstance() { + return INSTANCE; + } + + 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; + } + + private void resetForNewTarget() { + NEUDebugLogger.log(NEUDebugFlag.WISHING,"Resetting for new target"); + resetForNewCompass(); + firstCompassLine = null; + secondCompassLine = null; + solutionIntersectionLine = null; + prevPlayerPos = null; + solution = null; + } + + @SubscribeEvent + public void onWorldLoad(WorldEvent.Unload event) { + resetForNewTarget(); + isSkytilsPresent = Loader.isModLoaded("skytils"); + } + + @SubscribeEvent + public void onPlayerInteract(PlayerInteractEvent event) { + if (!NotEnoughUpdates.INSTANCE.config.mining.wishingCompassSolver || + SBInfo.getInstance().getLocation() == null || + !SBInfo.getInstance().getLocation().equals("crystal_hollows") || + event.entityPlayer != mc.thePlayer || + (event.action != PlayerInteractEvent.Action.RIGHT_CLICK_AIR && + event.action != PlayerInteractEvent.Action.RIGHT_CLICK_BLOCK) + ) { + return; + } + + ItemStack heldItem = event.entityPlayer.getHeldItem(); + if (heldItem == null || heldItem.getItem() != Items.skull) { + return; + } + + String heldInternalName = NotEnoughUpdates.INSTANCE.manager.getInternalNameForItem(heldItem); + if (heldInternalName == null || !heldInternalName.equals("WISHING_COMPASS")) { + return; + } + + try { + if (isSolved()) { + resetForNewTarget(); + } + + // 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; + } + + 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(); + } + } + + /* + * Processes particles if the wishing compass was used within the last 5 seconds. + * + * The first and the last particles are used to create a line for each wishing compass + * use that is then used to calculate the target. + * + * Once two lines have been calculated, the shortest line between the two is calculated + * with the midpoint on that line being the wishing compass target. The accuracy of this + * seems to be very high. + * + * The target location varies based on various criteria, including, but not limited to: + * Topaz Crystal (Khazad-dรปm) Magma Fields + * Odawa (Jungle Village) Jungle w/no Jungle Key in inventory + * Amethyst Crystal (Jungle Temple) Jungle w/Jungle Key in inventory + * Sapphire Crystal (Lost Precursor City) Precursor Remnants + * Jade Crystal (Mines of Divan) Mithril Deposits + * King Yolkar Goblin Holdout without "King's Scent I" effect + * Goblin Queen Goblin Holdout with "King's Scent I" effect + * Crystal Nucleus All Crystals found and none placed + * per-area structure missing, or because Hypixel. + * Always within 1 block of X=513 Y=106 Z=551. + */ + public void onSpawnParticle( + EnumParticleTypes particleType, + double x, + double y, + double z + ) { + if (!NotEnoughUpdates.INSTANCE.config.mining.wishingCompassSolver || + particleType != EnumParticleTypes.VILLAGER_HAPPY || + !SBInfo.getInstance().getLocation().equals("crystal_hollows") || + isSolved() || + System.currentTimeMillis() - compassUsedMillis > 5000) { + return; + } + + try { + Vec3 particleVec = new Vec3(x, y, z); + if (firstParticle == null) { + firstParticle = particleVec; + return; + } + + double distanceFromFirst = particleVec.distanceTo(firstParticle); + if (distanceFromFirst > MAX_COMPASS_PARTICLE_SPREAD) { + return; + } + + if (distanceFromFirst >= lastParticleDistanceFromFirst) { + lastParticleDistanceFromFirst = distanceFromFirst; + lastParticle = particleVec; + return; + } + + // 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; + } + + 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; + } + + solution = solutionIntersectionLine.getMidpoint(); + + if (solution.distanceTo(firstParticle) < 8) { + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "[NEU] WARNING: Solution is likely incorrect.")); + logDiagnosticData(false); + return; + } + + showSolution(); + } catch (Exception e) { + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Exception while calculating wishing compass solution - see log for details")); + e.printStackTrace(); + } + } + + private boolean isSolved() { + return solution != null; + } + + 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); + + if (NUCLEUS_BB.isVecInside(solution)) { + description += "Crystal Nucleus (" + coordsText + ")"; + } else { + description += coordsText; + } + + ChatComponentText message = new ChatComponentText(EnumChatFormatting.YELLOW + description); + + 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); + } + + Minecraft.getMinecraft().thePlayer.addChatMessage(message); + } + + 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")); + return; + } + + if (!NotEnoughUpdates.INSTANCE.config.mining.wishingCompassSolver) + { + mc.thePlayer.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "[NEU] Wishing Compass Solver is not enabled.")); + return; + } + + if (!outputAlways && !NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.contains(NEUDebugFlag.WISHING)) { + return; + } + + boolean originalDebugFlag = !NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.add(NEUDebugFlag.WISHING); + + StringBuilder diagsMessage = new StringBuilder(); + + diagsMessage.append("\n"); + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Skytils Present: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(isSkytilsPresent); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Compass Used Millis: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(compassUsedMillis); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Compass Used Position: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((prevPlayerPos == null) ? "" : prevPlayerPos.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("First Particle: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((firstParticle == null) ? "" : firstParticle.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Last Particle: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((lastParticle == null) ? "" : lastParticle.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Last Particle Distance From First: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(lastParticleDistanceFromFirst); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("First Compass Line: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((firstCompassLine == null) ? "" : firstCompassLine.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Second Compass Line: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((secondCompassLine == null) ? "" : secondCompassLine.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Intersection Line: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((secondCompassLine == null) ? "" : solutionIntersectionLine); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Solution: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((solution == null) ? "" : solution.toString()); + diagsMessage.append("\n"); + + NEUDebugLogger.log(NEUDebugFlag.WISHING, diagsMessage.toString()); + + if (!originalDebugFlag) { + NotEnoughUpdates.INSTANCE.config.hidden.debugFlags.remove(NEUDebugFlag.WISHING); + } + } +} -- cgit From e202e4adf979073921455083f5e737bc4fcf5f14 Mon Sep 17 00:00:00 2001 From: CraftyOldMiner <85420839+CraftyOldMiner@users.noreply.github.com> Date: Thu, 24 Mar 2022 14:55:20 -0500 Subject: Refactor Hollows solvers, add tests, add Vec3Comparable, fix bugs (#95) --- .../miscfeatures/CrystalWishingCompassSolver.java | 942 +++++++++++++++++---- 1 file changed, 800 insertions(+), 142 deletions(-) (limited to 'src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java') 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 getAsCrystalEnumSet(); + } + public CrystalEnumSetSupplier foundCrystals = this::getFoundCrystals; + + private SolverState solverState; + private Compass firstCompass; + private Compass secondCompass; + private Line solutionIntersectionLine; + private EnumSet possibleTargets; + private Vec3Comparable solution; + private Vec3Comparable originalSolution; + private EnumSet solutionPossibleTargets; + + public SolverState getSolverState() { + return solverState; + } + + public Vec3i getSolutionCoords() { + return new Vec3i(solution.xCoord, solution.yCoord, solution.zCoord); + } + + public EnumSet 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 getFoundCrystals() { + EnumSet foundCrystals = EnumSet.noneOf(Crystal.class); + NEUConfig.HiddenProfileSpecific perProfileConfig = NotEnoughUpdates.INSTANCE.config.getProfileSpecific(); + if (perProfileConfig == null) return foundCrystals; + HashMap 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 getSolutionTargets( + EnumSet possibleTargets, + Vec3Comparable solution) { + EnumSet 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 calculatePossibleTargets(BlockPos playerPos) { + boolean targetsBasedOnZoneWithoutCrystal = false; + EnumSet candidateTargets = EnumSet.allOf(CompassTarget.class); + EnumSet 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(""); + 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(""); + 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) ? "" : prevPlayerPos.toString()); + diagsMessage.append((solutionIntersectionLine == null) ? "" : 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) ? "" : 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) ? "" : 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 ? "" : 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) ? "" : 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) ? "" : 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) ? "" : solutionIntersectionLine); + diagsMessage.append((solution == null) ? "" : solution.toString()); diagsMessage.append("\n"); diagsMessage.append(EnumChatFormatting.AQUA); - diagsMessage.append("Solution: "); + diagsMessage.append("Solution Targets: "); diagsMessage.append(EnumChatFormatting.WHITE); - diagsMessage.append((solution == null) ? "" : solution.toString()); + diagsMessage.append((solutionPossibleTargets == null) ? "" : 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 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) ? "" : 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) ? "" : 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; + } } } } -- cgit From 5e7a8a02fe836c1e635faf067fbe8913549bc0e0 Mon Sep 17 00:00:00 2001 From: Walker Selby Date: Sun, 27 Mar 2022 12:13:08 -0500 Subject: Add NBT Data to Profile Viewer Tabs and XP Bars (#94) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed what hypxiel broke in dungeons * Added Longswords to Tools Tab (Itemlist) * Set max cata level to 70 * ups * why is it now E * YEP the cap is 99 not 70 * long cata xp BatChest + remove crash check because hypixel profile is null and i havent fixed that yet * Added checks for chat messages for dungeon win overlay - i think ive got them all but 4Shrug * Added an option to alert you if you put something for too much onto ah (default 50%) + Lowest bin alert triggers if lowest bin isnt found * IMPORTANT * fix dungeon page in /pv * show minions from coop in /pv * (BREAKING CHANGE) remove whitespace in changelog * fix crash with minion check * initial draft * tooltip * important changes * progress related things progress bar for community goals total collected points and personal goals display * only show bingo tab on Bingo profiles * make goals display gold when completed * Added MM7 button to dungeon preset * wart go not stonks * fix percents and added stack size * use code nopo in fortnite shop โ€” Today at 17:29 The capitalisation * config option for always showing bingo page * ๐Ÿ™‚ * movble pv tabs * 2.1.md ๐Ÿ™‚ * oops * der Kabel * import * Added powder amount to level up perk * Made it only show if the perk isnt maxed / level 0 * fix /pv crash when pet is not in repo * 2.1.md ๐Ÿ™‚ * web editor my beloved * Add NBT Data to Profile Viewer Tabs and XP Bars Add NBT Data Support for Resources in Tabs Add NBT Data Support for Resources in XP Bars Optimize renderTabs function Optimize mouseClicked Function Optimize ProfileViewerPage Enum * Update Style Co-authored-by: NopoTheGamer <40329022+NopoTheGamer@users.noreply.github.com> * Update GuiProfileViewer Rebased on pvbingo Updated formatting with IntelliJ reformatting tool (hopefully that matches now, let me know if you want me to try again but I think I did it right) * dungeons and pv bingo tab (#93) * add custom skull loading back (#96) * Refactor Hollows solvers, add tests, add Vec3Comparable, fix bugs (#95) * Rebase on master * Fix conflicts and rebase on master Add changes to change log * Oops Co-authored-by: nopo Co-authored-by: jani270 Co-authored-by: Lulonaut Co-authored-by: NopoTheGamer <40329022+NopoTheGamer@users.noreply.github.com> Co-authored-by: Lulonaut <67191924+Lulonaut@users.noreply.github.com> Co-authored-by: CraftyOldMiner <85420839+CraftyOldMiner@users.noreply.github.com> --- .../miscfeatures/CrystalWishingCompassSolver.java | 942 ++++----------------- 1 file changed, 142 insertions(+), 800 deletions(-) (limited to 'src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java') 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 9a950e7f..25f39c3a 100644 --- a/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java +++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java @@ -2,67 +2,27 @@ 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.Vec3i; -import net.minecraftforge.client.ClientCommandHandler; +import net.minecraft.util.Vec3; 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; @@ -71,72 +31,40 @@ public class CrystalWishingCompassSolver { private static final Minecraft mc = Minecraft.getMinecraft(); private static boolean isSkytilsPresent = false; - // 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 getAsCrystalEnumSet(); - } - public CrystalEnumSetSupplier foundCrystals = this::getFoundCrystals; - - private SolverState solverState; - private Compass firstCompass; - private Compass secondCompass; - private Line solutionIntersectionLine; - private EnumSet