diff options
| author | Moulberry <jjenour@student.unimelb.edu.au> | 2022-10-15 16:14:46 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-10-15 16:14:46 +0200 |
| commit | 9dff9de9be425a07691951f7f7e6d43ca2c967bf (patch) | |
| tree | 525c4086b4b8cb9ee2a329dee7a0915ceaaa788a /src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java | |
| parent | 7c6d37b2eb758a13b342b906f0aef88b940bc52a (diff) | |
| parent | df02addf3404f07f245d6e6da8ce3ee8d72bd235 (diff) | |
| download | notenoughupdates-9dff9de9be425a07691951f7f7e6d43ca2c967bf.tar.gz notenoughupdates-9dff9de9be425a07691951f7f7e6d43ca2c967bf.tar.bz2 notenoughupdates-9dff9de9be425a07691951f7f7e6d43ca2c967bf.zip | |
Merge pull request #268 from NotEnoughUpdates/master
2.1 Continued
Diffstat (limited to 'src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java')
| -rw-r--r-- | src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java | 1108 |
1 files changed, 1108 insertions, 0 deletions
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..0ee29b4f --- /dev/null +++ b/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CrystalWishingCompassSolver.java @@ -0,0 +1,1108 @@ +/* + * Copyright (C) 2022 NotEnoughUpdates contributors + * + * This file is part of NotEnoughUpdates. + * + * NotEnoughUpdates is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * NotEnoughUpdates is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with NotEnoughUpdates. If not, see <https://www.gnu.org/licenses/>. + */ + +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.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.EnumChatFormatting; +import net.minecraft.util.EnumParticleTypes; +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.ArrayDeque; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Locale; +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, + } + + enum HollowsZone { + CRYSTAL_NUCLEUS, + JUNGLE, + MITHRIL_DEPOSITS, + GOBLIN_HOLDOUT, + PRECURSOR_REMNANTS, + MAGMA_FIELDS, + } + + 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; + private static final ArrayDeque<ParticleData> seenParticles = new ArrayDeque<>(); + + // 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); + // Bounding box around all breakable blocks in the crystal hollows, appears as bedrock in-game + private static final AxisAlignedBB HOLLOWS_BB = new AxisAlignedBB(201, 30, 201, 824, 189, 824); + + // Zone bounding boxes + private static final AxisAlignedBB PRECURSOR_REMNANTS_BB = new AxisAlignedBB(512, 63, 512, 824, 189, 824); + private static final AxisAlignedBB MITHRIL_DEPOSITS_BB = new AxisAlignedBB(512, 63, 201, 824, 189, 513); + private static final AxisAlignedBB GOBLIN_HOLDOUT_BB = new AxisAlignedBB(201, 63, 512, 513, 189, 824); + private static final AxisAlignedBB JUNGLE_BB = new AxisAlignedBB(201, 63, 201, 513, 189, 513); + private static final AxisAlignedBB MAGMA_FIELDS_BB = new AxisAlignedBB(201, 30, 201, 824, 64, 824); + + // Structure bounding boxes (size + 2 in each dimension to make it an actual bounding box) + private static final AxisAlignedBB PRECURSOR_CITY_BB = new AxisAlignedBB(0, 0, 0, 107, 122, 107); + private static final AxisAlignedBB GOBLIN_KING_BB = new AxisAlignedBB(0, 0, 0, 59, 53, 56); + private static final AxisAlignedBB GOBLIN_QUEEN_BB = new AxisAlignedBB(0, 0, 0, 108, 114, 108); + private static final AxisAlignedBB JUNGLE_TEMPLE_BB = new AxisAlignedBB(0, 0, 0, 108, 120, 108); + private static final AxisAlignedBB ODAWA_BB = new AxisAlignedBB(0, 0, 0, 53, 46, 54); + private static final AxisAlignedBB MINES_OF_DIVAN_BB = new AxisAlignedBB(0, 0, 0, 108, 125, 108); + private static final AxisAlignedBB KHAZAD_DUM_BB = new AxisAlignedBB(0, 0, 0, 110, 46, 108); + + private static final Vec3Comparable JUNGLE_DOOR_OFFSET_FROM_CRYSTAL = new Vec3Comparable(-57, 36, -21); + + 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; + } + + public static HollowsZone getZoneForCoords(BlockPos blockPos) { + return getZoneForCoords(new Vec3Comparable(blockPos)); + } + + public static HollowsZone getZoneForCoords(Vec3Comparable coords) { + if (NUCLEUS_BB.isVecInside(coords)) return HollowsZone.CRYSTAL_NUCLEUS; + if (JUNGLE_BB.isVecInside(coords)) return HollowsZone.JUNGLE; + if (MITHRIL_DEPOSITS_BB.isVecInside(coords)) return HollowsZone.MITHRIL_DEPOSITS; + if (GOBLIN_HOLDOUT_BB.isVecInside(coords)) return HollowsZone.GOBLIN_HOLDOUT; + if (PRECURSOR_REMNANTS_BB.isVecInside(coords)) return HollowsZone.PRECURSOR_REMNANTS; + if (MAGMA_FIELDS_BB.isVecInside(coords)) return HollowsZone.MAGMA_FIELDS; + throw new IllegalArgumentException("Coordinates do not fall in known zone: " + coords.toString()); + } + + private void resetForNewTarget() { + NEUDebugLogger.log(NEUDebugFlag.WISHING, "Resetting for new target"); + solverState = SolverState.NOT_STARTED; + firstCompass = null; + secondCompass = null; + solutionIntersectionLine = null; + possibleTargets = null; + solution = null; + originalSolution = null; + solutionPossibleTargets = null; + } + + public void initWorld() { + resetForNewTarget(); + } + + @SubscribeEvent + public void onWorldLoad(WorldEvent.Unload event) { + initWorld(); + 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; + } + + BlockPos playerPos = mc.thePlayer.getPosition().getImmutable(); + + 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()); + } + } 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()); + seenParticles.clear(); + 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; + } + + HollowsZone firstCompassZone = getZoneForCoords(firstCompass.whereUsed); + HollowsZone playerZone = getZoneForCoords(playerPos); + if (!possibleTargets.equals(calculatePossibleTargets(playerPos)) || + firstCompassZone != playerZone) { + 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. + * + * 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")) { + return; + } + + // Capture particle troubleshooting info for two minutes starting when the first compass is used. + // This list is reset each time the first compass is used from a NOT_STARTED state. + if (firstCompass != null && !solverState.equals(SolverState.SOLVED) && + System.currentTimeMillis() < firstCompass.whenUsedMillis + 2 * 60 * 1000) { + seenParticles.add(new ParticleData(new Vec3Comparable(x, y, z), System.currentTimeMillis())); + } + + try { + 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(); + } + } + + /** + * @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; + } + + 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( + getZoneForCoords(firstCompass.whereUsed), + foundCrystals.getAsCrystalEnumSet(), + possibleTargets, + solution + ); + + // Adjust the Jungle Temple solution coordinates + if (solutionPossibleTargets.size() == 1 && + solutionPossibleTargets.contains(CompassTarget.JUNGLE_TEMPLE)) { + originalSolution = solution; + solution = solution.add(JUNGLE_DOOR_OFFSET_FROM_CRYSTAL); + } + + 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; + } + + 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(Locale.US).replace("İ", "I"))); + } + } + + return foundCrystals; + } + + // Returns candidates based on: + // - Structure Y levels observed in various lobbies. It is assumed + // that structures other than Khazad Dum cannot have any portion + // in the Magma Fields. + // + // - Structure sizes & offsets into other zones that assume at least + // one block must be in the correct zone. + // + // - An assumption that any structure could be missing with a + // special exception for the Jungle Temple since it often conflicts + // with Bal and a lobby with a missing Jungle Temple has not been + // observed. This exception will remove Bal as a target if: + // - Target candidates include both Bal & the Jungle Temple. + // - The Amethyst crystal has not been acquired. + // - The zone that the compass was used in is the Jungle. + // + // - If the solution is the Crystal Nucleus then a copy of the + // passed in possible targets is returned. + // + // |----------|------------| + // | Jungle | Mithril | + // | | Deposits | + // |----------|----------- | + // | Goblin | Precursor | + // | Holdout | Deposits | + // |----------|------------| + static public EnumSet<CompassTarget> getSolutionTargets( + HollowsZone compassUsedZone, + EnumSet<Crystal> foundCrystals, + EnumSet<CompassTarget> possibleTargets, + Vec3Comparable solution + ) { + EnumSet<CompassTarget> solutionPossibleTargets; + solutionPossibleTargets = possibleTargets.clone(); + + HollowsZone solutionZone = getZoneForCoords(solution); + if (solutionZone == HollowsZone.CRYSTAL_NUCLEUS) { + return solutionPossibleTargets; + } + + solutionPossibleTargets.remove(CompassTarget.CRYSTAL_NUCLEUS); + + // Y coordinates are 43-71 from 13 samples + // Y=41/74 is the absolute min/max based on structure size if + // the center of the topaz crystal has to be in magma fields. + if (solutionPossibleTargets.contains(CompassTarget.BAL) && + solution.yCoord > 75) { + solutionPossibleTargets.remove(CompassTarget.BAL); + } + + // Y coordinates are 93-157 from 15 samples. + // Y=83/167 is the absolute min/max based on structure size + if (solutionPossibleTargets.contains(CompassTarget.GOBLIN_KING) && + solution.yCoord < 82 || solution.yCoord > 168) { + solutionPossibleTargets.remove(CompassTarget.GOBLIN_KING); + } + + // Y coordinates are 129-139 from 10 samples + // Y=126/139 is the absolute min/max based on structure size + if (solutionPossibleTargets.contains(CompassTarget.GOBLIN_QUEEN) && + (solution.yCoord < 125 || solution.yCoord > 140)) { + solutionPossibleTargets.remove(CompassTarget.GOBLIN_QUEEN); + } + + // Y coordinates are 72-80 from 10 samples + // Y=73/80 is the absolute min/max based on structure size + if (solutionPossibleTargets.contains(CompassTarget.JUNGLE_TEMPLE) && + (solution.yCoord < 72 || solution.yCoord > 81)) { + solutionPossibleTargets.remove(CompassTarget.JUNGLE_TEMPLE); + } + + // Y coordinates are 87-155 from 7 samples + // Y=74/155 is the absolute min/max solution based on structure size + if (solutionPossibleTargets.contains(CompassTarget.ODAWA) && + (solution.yCoord < 73 || solution.yCoord > 155)) { + solutionPossibleTargets.remove(CompassTarget.ODAWA); + } + + // Y coordinates are 122-129 from 8 samples + // Y=122/129 is the absolute min/max based on structure size + if (solutionPossibleTargets.contains(CompassTarget.PRECURSOR_CITY) && + (solution.yCoord < 121 || solution.yCoord > 130)) { + solutionPossibleTargets.remove(CompassTarget.PRECURSOR_CITY); + } + + // Y coordinates are 98-102 from 15 samples + // Y=98/100 is the absolute min/max based on structure size, + // but 102 has been seen - possibly with earlier code that rounded up + if (solutionPossibleTargets.contains(CompassTarget.MINES_OF_DIVAN) && + (solution.yCoord < 97 || solution.yCoord > 102)) { + solutionPossibleTargets.remove(CompassTarget.MINES_OF_DIVAN); + } + + // Now filter by structure offset + if (solutionPossibleTargets.contains(CompassTarget.GOBLIN_KING) && + (solution.xCoord > GOBLIN_HOLDOUT_BB.maxX + GOBLIN_KING_BB.maxX || + solution.zCoord < GOBLIN_HOLDOUT_BB.minZ - GOBLIN_KING_BB.maxZ)) { + solutionPossibleTargets.remove(CompassTarget.GOBLIN_KING); + } + + if (solutionPossibleTargets.contains(CompassTarget.GOBLIN_QUEEN) && + (solution.xCoord > GOBLIN_HOLDOUT_BB.maxX + GOBLIN_QUEEN_BB.maxX || + solution.zCoord < GOBLIN_HOLDOUT_BB.minZ - GOBLIN_QUEEN_BB.maxZ)) { + solutionPossibleTargets.remove(CompassTarget.GOBLIN_QUEEN); + } + + if (solutionPossibleTargets.contains(CompassTarget.JUNGLE_TEMPLE) && + (solution.xCoord > JUNGLE_BB.maxX + JUNGLE_TEMPLE_BB.maxX || + solution.zCoord > JUNGLE_BB.maxZ + JUNGLE_TEMPLE_BB.maxZ)) { + solutionPossibleTargets.remove(CompassTarget.JUNGLE_TEMPLE); + } + + if (solutionPossibleTargets.contains(CompassTarget.ODAWA) && + (solution.xCoord > JUNGLE_BB.maxX + ODAWA_BB.maxX || + solution.zCoord > JUNGLE_BB.maxZ + ODAWA_BB.maxZ)) { + solutionPossibleTargets.remove(CompassTarget.ODAWA); + } + + if (solutionPossibleTargets.contains(CompassTarget.PRECURSOR_CITY) && + (solution.xCoord < PRECURSOR_REMNANTS_BB.minX - PRECURSOR_CITY_BB.maxX || + solution.zCoord < PRECURSOR_REMNANTS_BB.minZ - PRECURSOR_CITY_BB.maxZ)) { + solutionPossibleTargets.remove(CompassTarget.PRECURSOR_CITY); + } + + if (solutionPossibleTargets.contains(CompassTarget.MINES_OF_DIVAN) && + (solution.xCoord < MITHRIL_DEPOSITS_BB.minX - MINES_OF_DIVAN_BB.maxX || + solution.zCoord > MITHRIL_DEPOSITS_BB.maxZ + MINES_OF_DIVAN_BB.maxZ)) { + solutionPossibleTargets.remove(CompassTarget.MINES_OF_DIVAN); + } + + // Special case the Jungle Temple + if (solutionPossibleTargets.contains(CompassTarget.JUNGLE_TEMPLE) && + solutionPossibleTargets.contains(CompassTarget.BAL) && + !foundCrystals.contains(Crystal.AMETHYST) && + compassUsedZone == HollowsZone.JUNGLE) { + solutionPossibleTargets.remove(CompassTarget.BAL); + } + + return solutionPossibleTargets; + } + + private EnumSet<CompassTarget> calculatePossibleTargets(BlockPos playerPos) { + EnumSet<CompassTarget> candidateTargets = EnumSet.of(CompassTarget.CRYSTAL_NUCLEUS); + EnumSet<Crystal> foundCrystals = this.foundCrystals.getAsCrystalEnumSet(); + + // Add targets based on missing crystals. + // NOTE: + // We used to assume that only the adjacent zone's targets could be returned. That turned + // out to be incorrect (e.g. a compass in the jungle pointed to the Precursor City when + // the king would have been a valid target). Now we assume that any structure could be + // missing (because Hypixel) and depend on the solution coordinates to filter the list. + for (Crystal crystal : Crystal.values()) { + if (foundCrystals.contains(crystal)) { + continue; + } + + switch (crystal) { + case JADE: + candidateTargets.add(CompassTarget.MINES_OF_DIVAN); + break; + case AMBER: + candidateTargets.add( + kingsScentPresent.getAsBoolean() ? CompassTarget.GOBLIN_QUEEN : CompassTarget.GOBLIN_KING); + break; + case TOPAZ: + candidateTargets.add(CompassTarget.BAL); + break; + case AMETHYST: + candidateTargets.add( + keyInInventory.getAsBoolean() ? CompassTarget.JUNGLE_TEMPLE : CompassTarget.ODAWA); + break; + case SAPPHIRE: + candidateTargets.add(CompassTarget.PRECURSOR_CITY); + break; + } + } + + return candidateTargets; + } + + 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"; + } + } + + private String getNameForCompassTarget(CompassTarget compassTarget) { + boolean useSkytilsNames = (NotEnoughUpdates.INSTANCE.config.mining.wishingCompassWaypointNames == 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"; + } + } + + private String getSolutionCoordsText() { + return solution == null ? "" : + String.format("%.0f %.0f %.0f", solution.xCoord, solution.yCoord, solution.zCoord); + } + + 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++; + } + + sb.append(EnumChatFormatting.AQUA); + sb.append(" ("); + sb.append(getSolutionCoordsText()); + sb.append(")"); + return sb.toString(); + } + + 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; + } + + String destinationMessage = getWishingCompassDestinationsMessage(); + + if (!isSkytilsPresent) { + mc.thePlayer.addChatMessage(new ChatComponentText(destinationMessage)); + return; + } + + String targetNameForSkytils = solutionPossibleTargets.size() == 1 ? + getNameForCompassTarget(solutionPossibleTargets.iterator().next()) : + "WishingTarget"; + String skytilsCommand = String.format("/sthw add %s %s", getSolutionCoordsText(), targetNameForSkytils); + 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")); + } + + 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" + )); + mc.thePlayer.addChatMessage(chatMessage); + } + + private String getDiagnosticMessage() { + StringBuilder diagsMessage = new StringBuilder(); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Solver State: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(solverState.name()); + 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("Intersection Line: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((solutionIntersectionLine == null) ? "<NONE>" : solutionIntersectionLine); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Jungle Key in Inventory: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(isKeyInInventory()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("King's Scent Present: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(isKingsScentPresent()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("First Compass Targets: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(possibleTargets == null ? "<NONE>" : possibleTargets.toString()); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Current Calculated Targets: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append(calculatePossibleTargets(mc.thePlayer.getPosition())); + diagsMessage.append("\n"); + + diagsMessage.append(EnumChatFormatting.AQUA); + diagsMessage.append("Found Crystals: "); + diagsMessage.append(EnumChatFormatting.WHITE); + 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("Solution: "); + diagsMessage.append(EnumChatFormatting.WHITE); + diagsMessage.append((solution == null |
