aboutsummaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorolim <bobq4582@gmail.com>2024-06-19 13:39:56 +0100
committerolim <bobq4582@gmail.com>2024-07-15 12:36:19 +0100
commit8fa21f0ec183da2e626cbe0ad2f95226308c9b9a (patch)
tree5bd370ecb0b91a97788bea0dbf03bd1dd1a775e1 /src/main/java
parent7b2efe7c86908ad87df36f296d057b70e7be1427 (diff)
downloadSkyblocker-8fa21f0ec183da2e626cbe0ad2f95226308c9b9a.tar.gz
Skyblocker-8fa21f0ec183da2e626cbe0ad2f95226308c9b9a.tar.bz2
Skyblocker-8fa21f0ec183da2e626cbe0ad2f95226308c9b9a.zip
add wishing compass solver
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/de/hysky/skyblocker/SkyblockerMod.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java2
-rw-r--r--src/main/java/de/hysky/skyblocker/mixins/PlayerListHudMixin.java6
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java1
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dwarven/WishingCompassSolver.java357
5 files changed, 367 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index 8dd1419d..5cc96b42 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -136,6 +136,7 @@ public class SkyblockerMod implements ClientModInitializer {
FarmingHud.init();
LowerSensitivity.init();
CrystalsLocationsManager.init();
+ WishingCompassSolver.init();
MetalDetector.init();
ChatMessageListener.init();
Shortcuts.init();
diff --git a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
index 7bbbac81..c0cd8b7b 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/ClientPlayNetworkHandlerMixin.java
@@ -9,6 +9,7 @@ import de.hysky.skyblocker.skyblock.chocolatefactory.EggFinder;
import de.hysky.skyblocker.skyblock.crimson.dojo.DojoManager;
import de.hysky.skyblocker.skyblock.dungeon.DungeonScore;
import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager;
+import de.hysky.skyblocker.skyblock.dwarven.WishingCompassSolver;
import de.hysky.skyblocker.skyblock.end.BeaconHighlighter;
import de.hysky.skyblocker.skyblock.end.EnderNodes;
import de.hysky.skyblocker.skyblock.end.TheEnd;
@@ -100,6 +101,7 @@ public abstract class ClientPlayNetworkHandlerMixin {
MythologicalRitual.onParticle(packet);
DojoManager.onParticle(packet);
EnderNodes.onParticle(packet);
+ WishingCompassSolver.onParticle(packet);
}
@ModifyExpressionValue(method = "onEntityStatus", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/packet/s2c/play/EntityStatusS2CPacket;getEntity(Lnet/minecraft/world/World;)Lnet/minecraft/entity/Entity;"))
diff --git a/src/main/java/de/hysky/skyblocker/mixins/PlayerListHudMixin.java b/src/main/java/de/hysky/skyblocker/mixins/PlayerListHudMixin.java
index a96a7727..038d4e4f 100644
--- a/src/main/java/de/hysky/skyblocker/mixins/PlayerListHudMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixins/PlayerListHudMixin.java
@@ -12,6 +12,7 @@ import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.PlayerListHud;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
@@ -54,4 +55,9 @@ public class PlayerListHudMixin {
}
}
+ @Inject(at = @At("HEAD"), method = "setFooter")
+ public void skblocker$updateFooter(@Nullable Text footer, CallbackInfo info) {
+ PlayerListMgr.updateFooter(footer);
+ }
+
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java
index 0fc0c64f..964b6cce 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/MiningLocationLabel.java
@@ -163,6 +163,7 @@ public record MiningLocationLabel(Category category, Vec3d centerPos) implements
* enum for the different waypoints used int the crystals hud each with a {@link CrystalHollowsLocationsCategory#name} and associated {@link CrystalHollowsLocationsCategory#color}
*/
enum CrystalHollowsLocationsCategory implements Category {
+ UNKNOWN("Unknown", Color.WHITE, null), //used when a location is known but what's at the location is not known
JUNGLE_TEMPLE("Jungle Temple", new Color(DyeColor.PURPLE.getSignColor()), "[NPC] Kalhuiki Door Guardian:"),
MINES_OF_DIVAN("Mines of Divan", Color.GREEN, " Jade Crystal"),
GOBLIN_QUEENS_DEN("Goblin Queen's Den", new Color(DyeColor.ORANGE.getSignColor()), " Amber Crystal"),
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dwarven/WishingCompassSolver.java b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/WishingCompassSolver.java
new file mode 100644
index 00000000..48ceeb74
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dwarven/WishingCompassSolver.java
@@ -0,0 +1,357 @@
+package de.hysky.skyblocker.skyblock.dwarven;
+
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
+import net.fabricmc.fabric.api.event.player.UseBlockCallback;
+import net.fabricmc.fabric.api.event.player.UseItemCallback;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.item.ItemStack;
+import net.minecraft.network.packet.s2c.play.ParticleS2CPacket;
+import net.minecraft.particle.ParticleTypes;
+import net.minecraft.text.Text;
+import net.minecraft.util.*;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+public class WishingCompassSolver {
+ private static final MinecraftClient CLIENT = MinecraftClient.getInstance();
+
+ enum SolverStates {
+ NOT_STARTED,
+ PROCESSING_FIRST_USE,
+ WAITING_FOR_SECOND,
+ PROCESSING_SECOND_USE,
+ }
+
+ enum ZONE {
+ CRYSTAL_NUCLEUS,
+ JUNGLE,
+ MITHRIL_DEPOSITS,
+ GOBLIN_HOLDOUT,
+ PRECURSOR_REMNANTS,
+ MAGMA_FIELDS,
+ }
+
+ private static final HashMap<ZONE, Box> ZONE_BOUNDING_BOXES = Util.make(new HashMap<>(), map -> {
+ map.put(ZONE.CRYSTAL_NUCLEUS, new Box(462, 63, 461, 564, 181, 565));
+ map.put(ZONE.JUNGLE, new Box(201, 63, 201, 513, 189, 513));
+ map.put(ZONE.MITHRIL_DEPOSITS, new Box(512, 63, 201, 824, 189, 513));
+ map.put(ZONE.GOBLIN_HOLDOUT, new Box(201, 63, 512, 513, 189, 824));
+ map.put(ZONE.PRECURSOR_REMNANTS, new Box(512, 63, 512, 824, 189, 824));
+ map.put(ZONE.MAGMA_FIELDS, new Box(201, 30, 201, 824, 64, 824));
+ });
+ private static final Vec3d JUNGLE_TEMPLE_DOOR_OFFSET = new Vec3d(-57, 36, -21);
+ /**
+ * how many particles to use to get direction of a line
+ */
+ private static final long PARTICLES_PER_LINE = 25;
+ /**
+ * the amount of milliseconds to wait for the next particle until assumed failed
+ */
+ private static final long PARTICLES_MAX_DELAY = 500;
+ /**
+ * the distance the player has to be from where they used the first compass to where they use the second
+ */
+ private static final long DISTANCE_BETWEEN_USES = 32;
+
+ private static SolverStates currentState = SolverStates.NOT_STARTED;
+ private static Vec3d startPosOne = Vec3d.ZERO;
+ private static Vec3d startPosTwo = Vec3d.ZERO;
+ private static Vec3d directionOne = Vec3d.ZERO;
+ private static Vec3d directionTwo = Vec3d.ZERO;
+ private static long particleUsedCountOne = 0;
+ private static long particleUsedCountTwo = 0;
+ private static long particleLastUpdate = System.currentTimeMillis();
+
+
+ public static void init() {
+ UseItemCallback.EVENT.register(WishingCompassSolver::onItemInteract);
+ UseBlockCallback.EVENT.register(WishingCompassSolver::onBlockInteract);
+ ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset());
+ }
+
+ private static void reset() {
+ currentState = SolverStates.NOT_STARTED;
+ startPosOne = Vec3d.ZERO;
+ startPosTwo = Vec3d.ZERO;
+ directionOne = Vec3d.ZERO;
+ directionTwo = Vec3d.ZERO;
+ particleUsedCountOne = 0;
+ particleUsedCountTwo = 0;
+ particleLastUpdate = System.currentTimeMillis();
+ }
+
+ private static boolean isKingsScentPresent() {
+ String footer = PlayerListMgr.getFooter();
+ if (footer == null) {
+ return false;
+ }
+ return footer.contains("King's Scent I");
+ }
+
+ private static boolean isKeyInInventory() {
+ if (CLIENT.player == null) {
+ return false;
+ }
+ for (ItemStack item : CLIENT.player.getInventory().main) {
+ if (item != null && Objects.equals(item.getSkyblockId(), "JUNGLE_KEY")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static ZONE getZoneOfLocation(Vec3d location) {
+ for (Map.Entry<ZONE, Box> zone : ZONE_BOUNDING_BOXES.entrySet()) {
+ if (zone.getValue().contains(location)) {
+ return zone.getKey();
+ }
+ }
+
+ //default to nucleus if somehow not in another zone
+ return ZONE.CRYSTAL_NUCLEUS;
+ }
+
+ private static Boolean isZoneComplete(ZONE zone) {
+ if (CLIENT.getNetworkHandler() == null || CLIENT.player == null) {
+ return false;
+ }
+ //creates cleaned stream of all the entry's in tab list
+ Stream<PlayerListEntry> playerListStream = CLIENT.getNetworkHandler().getPlayerList().stream();
+ Stream<String> displayNameStream = playerListStream.map(PlayerListEntry::getDisplayName).filter(Objects::nonNull).map(Text::getString).map(String::strip);
+
+ //make sure the data is in tab and if not tell the user
+ if (displayNameStream.noneMatch(entry -> entry.equals("Crystals:"))) {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Enable crystals in /tab so the compass can know what has been found")), false);
+ return false;
+ }
+
+ //return if the crystal for a zone is found
+ playerListStream = CLIENT.getNetworkHandler().getPlayerList().stream();
+ displayNameStream = playerListStream.map(PlayerListEntry::getDisplayName).filter(Objects::nonNull).map(Text::getString).map(String::strip);
+ return switch (zone) {
+ case JUNGLE -> displayNameStream.noneMatch(entry -> entry.equals("Amethyst: ✖ Not Found"));
+ case MITHRIL_DEPOSITS -> displayNameStream.noneMatch(entry -> entry.equals("Jade: ✖ Not Found"));
+ case GOBLIN_HOLDOUT -> displayNameStream.noneMatch(entry -> entry.equals("Amber: ✖ Not Found"));
+ case PRECURSOR_REMNANTS -> displayNameStream.noneMatch(entry -> entry.equals("Sapphire: ✖ Not Found"));
+ case MAGMA_FIELDS -> displayNameStream.noneMatch(entry -> entry.equals("Topaz: ✖ Not Found"));
+ default -> false;
+ };
+ }
+
+ private static MiningLocationLabel.CrystalHollowsLocationsCategory getTargetLocation(ZONE startingZone) {
+ //if the zone is complete return null
+ if (isZoneComplete(startingZone)) {
+ return MiningLocationLabel.CrystalHollowsLocationsCategory.UNKNOWN;
+ }
+ return switch (startingZone) {
+ case JUNGLE ->
+ isKeyInInventory() ? MiningLocationLabel.CrystalHollowsLocationsCategory.JUNGLE_TEMPLE : MiningLocationLabel.CrystalHollowsLocationsCategory.ODAWA;
+ case MITHRIL_DEPOSITS -> MiningLocationLabel.CrystalHollowsLocationsCategory.MINES_OF_DIVAN;
+ case GOBLIN_HOLDOUT ->
+ isKingsScentPresent() ? MiningLocationLabel.CrystalHollowsLocationsCategory.GOBLIN_QUEENS_DEN : MiningLocationLabel.CrystalHollowsLocationsCategory.KING_YOLKAR;
+ case PRECURSOR_REMNANTS -> MiningLocationLabel.CrystalHollowsLocationsCategory.LOST_PRECURSOR_CITY;
+ case MAGMA_FIELDS -> MiningLocationLabel.CrystalHollowsLocationsCategory.KHAZAD_DUM;
+ default -> MiningLocationLabel.CrystalHollowsLocationsCategory.UNKNOWN;
+ };
+ }
+
+
+ public static void onParticle(ParticleS2CPacket packet) {
+ if (!Utils.isInCrystalHollows() || !ParticleTypes.HAPPY_VILLAGER.equals(packet.getParameters().getType())) {
+ return;
+ }
+ //get location of particle
+ Vec3d particlePos = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
+ //update particle used time
+ particleLastUpdate = System.currentTimeMillis();
+
+ switch (currentState) {
+ case PROCESSING_FIRST_USE -> {
+ Vec3d particleDirection = particlePos.subtract(startPosOne).normalize();
+ //move direction to fit with particle
+ directionOne = directionOne.add(particleDirection.multiply((double) 1 / PARTICLES_PER_LINE));
+ particleUsedCountOne += 1;
+ //if used enough particle go to next state
+ if (particleUsedCountOne >= PARTICLES_PER_LINE) {
+ currentState = SolverStates.WAITING_FOR_SECOND;
+ if (CLIENT.player != null) {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Wishing compass used. Move to another location and use another compass to triangulate target").formatted(Formatting.GREEN)), false);
+ }
+ }
+ }
+ case PROCESSING_SECOND_USE -> {
+ Vec3d particleDirection = particlePos.subtract(startPosTwo).normalize();
+ //move direction to fit with particle
+ directionTwo = directionTwo.add(particleDirection.multiply((double) 1 / PARTICLES_PER_LINE));
+ particleUsedCountTwo += 1;
+ //if used enough particle go to next state
+ if (particleUsedCountTwo >= PARTICLES_PER_LINE) {
+ processSolution();
+ }
+ }
+ }
+ }
+
+ private static void processSolution() {
+ if (CLIENT.player == null) {
+ reset();
+ return;
+ }
+ Vec3d targetLocation = solve(startPosOne, startPosTwo, directionOne, directionTwo);
+ if (targetLocation == null) {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Something went wrong. lines do not cross. please try again").formatted(Formatting.RED)), false);
+ } else {
+ //send message to player with location and name
+ MiningLocationLabel.CrystalHollowsLocationsCategory location = getTargetLocation(getZoneOfLocation(startPosOne));
+ //offset the jungle location to its doors
+ if (location == MiningLocationLabel.CrystalHollowsLocationsCategory.JUNGLE_TEMPLE) {
+ targetLocation = targetLocation.add(JUNGLE_TEMPLE_DOOR_OFFSET);
+ }
+
+ CLIENT.player.sendMessage(Constants.PREFIX.get()
+ .append(Text.literal("Wishing compass solver found ").formatted(Formatting.GREEN))
+ .append(Text.literal(location.getName()).withColor(location.getColor()))
+ .append(Text.literal(": " + (int) targetLocation.getX() + " " + (int) targetLocation.getY() + " " + (int) targetLocation.getZ())),
+ false);
+ }
+
+ //reset ready for another go
+ reset();
+ }
+
+ /**
+ * using the stating locations and line direction solve for where the location must be
+ */
+ protected static Vec3d solve(Vec3d startPosOne, Vec3d startPosTwo, Vec3d directionOne, Vec3d directionTwo) {
+ Vec3d crossProduct = directionOne.crossProduct(directionTwo);
+ if (crossProduct.equals(Vec3d.ZERO)) {
+ //lines are parallel or coincident
+ return null;
+ }
+ // Calculate the difference vector between startPosTwo and startPosOne
+ Vec3d diff = startPosTwo.subtract(startPosOne);
+ // projecting 'diff' onto the plane defined by 'directionTwo' and 'crossProduct'. then scaling by the inverse squared length of 'crossProduct'
+ double intersectionScalar = diff.dotProduct(directionTwo.crossProduct(crossProduct)) / crossProduct.lengthSquared();
+ // if intersectionScalar is a negative number the lines are meeting in the opposite direction and giving incorrect cords
+ if (intersectionScalar < 0) {
+ return null;
+ }
+ //get final target location
+ return startPosOne.add(directionOne.multiply(intersectionScalar));
+ }
+
+ private static ActionResult onBlockInteract(PlayerEntity playerEntity, World world, Hand hand, BlockHitResult blockHitResult) {
+ if (CLIENT.player == null) {
+ return null;
+ }
+ ItemStack stack = CLIENT.player.getStackInHand(hand);
+ //make sure the user is in the crystal hollows and holding the wishing compass
+ if (!Utils.isInCrystalHollows() || !Objects.equals(stack.getSkyblockId(), "WISHING_COMPASS")) {
+ return ActionResult.PASS;
+ }
+ if (useCompass()) {
+ return ActionResult.FAIL;
+ }
+
+ return ActionResult.PASS;
+ }
+
+ private static TypedActionResult<ItemStack> onItemInteract(PlayerEntity playerEntity, World world, Hand hand) {
+ if (CLIENT.player == null) {
+ return null;
+ }
+ ItemStack stack = CLIENT.player.getStackInHand(hand);
+ //make sure the user is in the crystal hollows and holding the wishing compass
+ if (!Utils.isInCrystalHollows() || !Objects.equals(stack.getSkyblockId(), "WISHING_COMPASS")) {
+ return TypedActionResult.pass(stack);
+ }
+ if (useCompass()) {
+ return TypedActionResult.fail(stack);
+ }
+
+ return TypedActionResult.pass(stack);
+ }
+
+ /**
+ * Computes what to do next when a compass is used.
+ *
+ * @return if the use event should be canceled
+ */
+ private static boolean useCompass() {
+ if (CLIENT.player == null) {
+ return true;
+ }
+ Vec3d playerPos = CLIENT.player.getEyePos();
+ ZONE currentZone = getZoneOfLocation(playerPos);
+
+ switch (currentState) {
+ case NOT_STARTED -> {
+ //do not start if the player is in nucleus as this does not work well
+ if (currentZone == ZONE.CRYSTAL_NUCLEUS) {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Use compass outside of nucleus for better results")), false);
+ return true;
+ }
+ startNewState(SolverStates.PROCESSING_FIRST_USE);
+ }
+
+ case WAITING_FOR_SECOND -> {
+ //only continue if the player is far enough away from the first position to get a better reading
+ if (startPosOne.distanceTo(playerPos) < DISTANCE_BETWEEN_USES) {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Move further away from the first use before using again")), false);
+ return true;
+ } else {
+ //make sure the player is in the same zone as they used to first or restart
+ if (currentZone != getZoneOfLocation(startPosOne)) {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Changed zone. Restarting...")), false);
+ startNewState(SolverStates.PROCESSING_FIRST_USE);
+ } else {
+ startNewState(SolverStates.PROCESSING_SECOND_USE);
+ }
+ }
+ }
+
+ case PROCESSING_FIRST_USE, PROCESSING_SECOND_USE -> {
+ //if still looking for particles for line tell the user to wait
+ //else tell the use something went wrong and its starting again
+ if (System.currentTimeMillis() - particleLastUpdate < PARTICLES_MAX_DELAY) {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Wait a little before using another wishing compass").formatted(Formatting.RED)), false);
+ return true;
+ } else {
+ CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("Could not detect last use. Restarting...").formatted(Formatting.RED)), false);
+ startNewState(SolverStates.PROCESSING_FIRST_USE);
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static void startNewState(SolverStates newState) {
+ if (CLIENT.player == null) {
+ return;
+ }
+ Vec3d playerPos = CLIENT.player.getEyePos();
+
+ if (newState == SolverStates.PROCESSING_FIRST_USE) {
+ currentState = SolverStates.PROCESSING_FIRST_USE;
+ startPosOne = playerPos;
+ particleLastUpdate = System.currentTimeMillis();
+ } else if (newState == SolverStates.PROCESSING_SECOND_USE) {
+ currentState = SolverStates.PROCESSING_SECOND_USE;
+ startPosTwo = playerPos;
+ particleLastUpdate = System.currentTimeMillis();
+ }
+ }
+}