aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/waypoint
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/waypoint')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java201
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java213
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java164
3 files changed, 578 insertions, 0 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java
new file mode 100644
index 00000000..f0e94770
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/FairySouls.java
@@ -0,0 +1,201 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.mojang.brigadier.CommandDispatcher;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.NEURepoManager;
+import de.hysky.skyblocker.utils.PosUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.waypoint.ProfileAwareWaypoint;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.math.BlockPos;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class FairySouls {
+ private static final Logger LOGGER = LoggerFactory.getLogger(FairySouls.class);
+ private static final Supplier<Waypoint.Type> TYPE_SUPPLIER = () -> SkyblockerConfigManager.get().general.waypoints.waypointType;
+ private static CompletableFuture<Void> fairySoulsLoaded;
+ private static int maxSouls = 0;
+ private static final Map<String, Map<BlockPos, ProfileAwareWaypoint>> fairySouls = new HashMap<>();
+
+ @SuppressWarnings("UnusedReturnValue")
+ public static CompletableFuture<Void> runAsyncAfterFairySoulsLoad(Runnable runnable) {
+ if (fairySoulsLoaded == null) {
+ LOGGER.error("Fairy Souls have not being initialized yet! Please ensure the Fairy Souls module is initialized before modules calling this method in SkyblockerMod#onInitializeClient. This error can be safely ignore in a test environment.");
+ return CompletableFuture.completedFuture(null);
+ }
+ return fairySoulsLoaded.thenRunAsync(runnable);
+ }
+
+ public static int getFairySoulsSize(@Nullable String location) {
+ return location == null ? maxSouls : fairySouls.get(location).size();
+ }
+
+ public static void init() {
+ loadFairySouls();
+ ClientLifecycleEvents.CLIENT_STOPPING.register(FairySouls::saveFoundFairySouls);
+ ClientCommandRegistrationCallback.EVENT.register(FairySouls::registerCommands);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(FairySouls::render);
+ ClientReceiveMessageEvents.GAME.register(FairySouls::onChatMessage);
+ }
+
+ private static void loadFairySouls() {
+ fairySoulsLoaded = NEURepoManager.runAsyncAfterLoad(() -> {
+ maxSouls = NEURepoManager.NEU_REPO.getConstants().getFairySouls().getMaxSouls();
+ NEURepoManager.NEU_REPO.getConstants().getFairySouls().getSoulLocations().forEach((location, fairiesForLocation) -> fairySouls.put(location, fairiesForLocation.stream().map(coordinate -> new BlockPos(coordinate.getX(), coordinate.getY(), coordinate.getZ())).collect(Collectors.toUnmodifiableMap(pos -> pos, pos -> new ProfileAwareWaypoint(pos, TYPE_SUPPLIER, DyeColor.GREEN.getColorComponents(), DyeColor.RED.getColorComponents())))));
+ LOGGER.debug("[Skyblocker] Loaded {} fairy souls across {} locations", fairySouls.values().stream().mapToInt(Map::size).sum(), fairySouls.size());
+
+ try (BufferedReader reader = Files.newBufferedReader(SkyblockerMod.CONFIG_DIR.resolve("found_fairy_souls.json"))) {
+ for (Map.Entry<String, JsonElement> foundFairiesForProfileJson : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) {
+ for (Map.Entry<String, JsonElement> foundFairiesForLocationJson : foundFairiesForProfileJson.getValue().getAsJsonObject().asMap().entrySet()) {
+ String profile = foundFairiesForLocationJson.getKey();
+ Map<BlockPos, ProfileAwareWaypoint> fairiesForLocation = fairySouls.get(profile);
+ for (JsonElement foundFairy : foundFairiesForLocationJson.getValue().getAsJsonArray().asList()) {
+ fairiesForLocation.get(PosUtils.parsePosString(foundFairy.getAsString())).setFound(profile);
+ }
+ }
+ }
+ LOGGER.debug("[Skyblocker] Loaded found fairy souls");
+ } catch (NoSuchFileException ignored) {
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load found fairy souls", e);
+ }
+ LOGGER.info("[Skyblocker] Loaded {} fairy souls across {} locations", fairySouls.values().stream().mapToInt(Map::size).sum(), fairySouls.size());
+ });
+ }
+
+ private static void saveFoundFairySouls(MinecraftClient client) {
+ Map<String, Map<String, Set<BlockPos>>> foundFairies = new HashMap<>();
+ for (Map.Entry<String, Map<BlockPos, ProfileAwareWaypoint>> fairiesForLocation : fairySouls.entrySet()) {
+ for (ProfileAwareWaypoint fairySoul : fairiesForLocation.getValue().values()) {
+ for (String profile : fairySoul.foundProfiles) {
+ foundFairies.computeIfAbsent(profile, profile_ -> new HashMap<>());
+ foundFairies.get(profile).computeIfAbsent(fairiesForLocation.getKey(), location_ -> new HashSet<>());
+ foundFairies.get(profile).get(fairiesForLocation.getKey()).add(fairySoul.pos);
+ }
+ }
+ }
+
+ try (BufferedWriter writer = Files.newBufferedWriter(SkyblockerMod.CONFIG_DIR.resolve("found_fairy_souls.json"))) {
+ JsonObject foundFairiesJson = new JsonObject();
+ for (Map.Entry<String, Map<String, Set<BlockPos>>> foundFairiesForProfile : foundFairies.entrySet()) {
+ JsonObject foundFairiesForProfileJson = new JsonObject();
+ for (Map.Entry<String, Set<BlockPos>> foundFairiesForLocation : foundFairiesForProfile.getValue().entrySet()) {
+ JsonArray foundFairiesForLocationJson = new JsonArray();
+ for (BlockPos foundFairy : foundFairiesForLocation.getValue()) {
+ foundFairiesForLocationJson.add(PosUtils.getPosString(foundFairy));
+ }
+ foundFairiesForProfileJson.add(foundFairiesForLocation.getKey(), foundFairiesForLocationJson);
+ }
+ foundFairiesJson.add(foundFairiesForProfile.getKey(), foundFairiesForProfileJson);
+ }
+ SkyblockerMod.GSON.toJson(foundFairiesJson, writer);
+ LOGGER.info("[Skyblocker] Saved found fairy souls");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to write found fairy souls to file", e);
+ }
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(literal(SkyblockerMod.NAMESPACE)
+ .then(literal("fairySouls")
+ .then(literal("markAllInCurrentIslandFound").executes(context -> {
+ FairySouls.markAllFairiesOnCurrentIslandFound();
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.fairySouls.markAllFound")));
+ return 1;
+ }))
+ .then(literal("markAllInCurrentIslandMissing").executes(context -> {
+ FairySouls.markAllFairiesOnCurrentIslandMissing();
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.fairySouls.markAllMissing")));
+ return 1;
+ }))));
+ }
+
+ private static void render(WorldRenderContext context) {
+ SkyblockerConfig.FairySouls fairySoulsConfig = SkyblockerConfigManager.get().general.fairySouls;
+
+ if (fairySoulsConfig.enableFairySoulsHelper && fairySoulsLoaded.isDone() && fairySouls.containsKey(Utils.getLocationRaw())) {
+ for (Waypoint fairySoul : fairySouls.get(Utils.getLocationRaw()).values()) {
+ boolean fairySoulNotFound = fairySoul.shouldRender();
+ if (!fairySoulsConfig.highlightFoundSouls && !fairySoulNotFound || fairySoulsConfig.highlightOnlyNearbySouls && fairySoul.pos.getSquaredDistance(context.camera().getPos()) > 2500) {
+ continue;
+ }
+ fairySoul.render(context);
+ }
+ }
+ }
+
+ private static void onChatMessage(Text text, boolean overlay) {
+ String message = text.getString();
+ if (message.equals("You have already found that Fairy Soul!") || message.equals("§d§lSOUL! §fYou found a §dFairy Soul§f!")) {
+ markClosestFairyFound();
+ }
+ }
+
+ private static void markClosestFairyFound() {
+ if (!fairySoulsLoaded.isDone()) return;
+
+ PlayerEntity player = MinecraftClient.getInstance().player;
+ if (player == null) {
+ LOGGER.warn("[Skyblocker] Failed to mark closest fairy soul as found because player is null");
+ return;
+ }
+
+ Map<BlockPos, ProfileAwareWaypoint> fairiesOnCurrentIsland = fairySouls.get(Utils.getLocationRaw());
+ if (fairiesOnCurrentIsland == null) {
+ LOGGER.warn("[Skyblocker] Failed to mark closest fairy soul as found because there are no fairy souls loaded on the current island. NEU repo probably failed to load.");
+ return;
+ }
+
+ fairiesOnCurrentIsland.values().stream()
+ .filter(Waypoint::shouldRender)
+ .min(Comparator.comparingDouble(fairySoul -> fairySoul.pos.getSquaredDistance(player.getPos())))
+ .filter(fairySoul -> fairySoul.pos.getSquaredDistance(player.getPos()) <= 16)
+ .ifPresent(Waypoint::setFound);
+ }
+
+ public static void markAllFairiesOnCurrentIslandFound() {
+ Map<BlockPos, ProfileAwareWaypoint> fairiesForLocation = fairySouls.get(Utils.getLocationRaw());
+ if (fairiesForLocation != null) {
+ fairiesForLocation.values().forEach(ProfileAwareWaypoint::setFound);
+ }
+ }
+
+ public static void markAllFairiesOnCurrentIslandMissing() {
+ Map<BlockPos, ProfileAwareWaypoint> fairiesForLocation = fairySouls.get(Utils.getLocationRaw());
+ if (fairiesForLocation != null) {
+ fairiesForLocation.values().forEach(ProfileAwareWaypoint::setMissing);
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java
new file mode 100644
index 00000000..5b5f4715
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java
@@ -0,0 +1,213 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import com.mojang.brigadier.Command;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.ItemUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.fabricmc.fabric.api.event.player.AttackBlockCallback;
+import net.fabricmc.fabric.api.event.player.UseBlockCallback;
+import net.fabricmc.fabric.api.event.player.UseItemCallback;
+import net.fabricmc.fabric.api.util.TriState;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.argument.BlockPosArgumentType;
+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.ActionResult;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Hand;
+import net.minecraft.util.TypedActionResult;
+import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Direction;
+import net.minecraft.util.math.Vec3d;
+import net.minecraft.world.World;
+import org.apache.commons.math3.stat.regression.SimpleRegression;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class MythologicalRitual {
+ private static final Pattern GRIFFIN_BURROW_DUG = Pattern.compile("(?<message>You dug out a Griffin Burrow!|You finished the Griffin burrow chain!) \\((?<index>\\d)/4\\)");
+ private static final float[] ORANGE_COLOR_COMPONENTS = DyeColor.ORANGE.getColorComponents();
+ private static long lastEchoTime;
+ private static final Map<BlockPos, GriffinBurrow> griffinBurrows = new HashMap<>();
+ @Nullable
+ private static BlockPos lastDugBurrowPos;
+ private static GriffinBurrow previousBurrow = new GriffinBurrow(BlockPos.ORIGIN);
+
+ public static void init() {
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(MythologicalRitual::render);
+ AttackBlockCallback.EVENT.register(MythologicalRitual::onAttackBlock);
+ UseBlockCallback.EVENT.register(MythologicalRitual::onUseBlock);
+ UseItemCallback.EVENT.register(MythologicalRitual::onUseItem);
+ ClientReceiveMessageEvents.GAME.register(MythologicalRitual::onChatMessage);
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("diana")
+ .then(literal("clearGriffinBurrows").executes(context -> {
+ griffinBurrows.clear();
+ return Command.SINGLE_SUCCESS;
+ }))
+ .then(literal("clearGriffinBurrow")
+ .then(argument("pos", BlockPosArgumentType.blockPos()).executes(context -> {
+ griffinBurrows.remove(context.getArgument("pos", BlockPos.class));
+ return Command.SINGLE_SUCCESS;
+ }))
+ )
+ )));
+
+ // Put a root burrow so echo detection works without a previous burrow
+ previousBurrow.confirmed = TriState.DEFAULT;
+ griffinBurrows.put(BlockPos.ORIGIN, previousBurrow);
+ }
+
+ public static void onParticle(ParticleS2CPacket packet) {
+ if (isActive()) {
+ if (ParticleTypes.CRIT.equals(packet.getParameters().getType()) || ParticleTypes.ENCHANT.equals(packet.getParameters().getType())) {
+ BlockPos pos = BlockPos.ofFloored(packet.getX(), packet.getY(), packet.getZ()).down();
+ if (MinecraftClient.getInstance().world == null || !MinecraftClient.getInstance().world.getBlockState(pos).isOf(Blocks.GRASS_BLOCK)) {
+ return;
+ }
+ GriffinBurrow burrow = griffinBurrows.computeIfAbsent(pos, GriffinBurrow::new);
+ if (ParticleTypes.CRIT.equals(packet.getParameters().getType())) burrow.critParticle++;
+ if (ParticleTypes.ENCHANT.equals(packet.getParameters().getType())) burrow.enchantParticle++;
+ if (burrow.critParticle >= 5 && burrow.enchantParticle >= 5 && burrow.confirmed == TriState.FALSE) {
+ griffinBurrows.get(pos).init();
+ }
+ } else if (ParticleTypes.DUST.equals(packet.getParameters().getType())) {
+ BlockPos pos = BlockPos.ofFloored(packet.getX(), packet.getY(), packet.getZ()).down(2);
+ GriffinBurrow burrow = griffinBurrows.get(pos);
+ if (burrow == null) {
+ return;
+ }
+ burrow.regression.addData(packet.getX(), packet.getZ());
+ double slope = burrow.regression.getSlope();
+ if (Double.isNaN(slope)) {
+ return;
+ }
+ Vec3d nextBurrowDirection = new Vec3d(100, 0, slope * 100).normalize().multiply(100);
+ if (burrow.nextBurrowPlane == null) {
+ burrow.nextBurrowPlane = new Vec3d[4];
+ }
+ burrow.nextBurrowPlane[0] = Vec3d.of(pos).add(nextBurrowDirection).subtract(0, 50, 0);
+ burrow.nextBurrowPlane[1] = Vec3d.of(pos).subtract(nextBurrowDirection).subtract(0, 50, 0);
+ burrow.nextBurrowPlane[2] = burrow.nextBurrowPlane[1].add(0, 100, 0);
+ burrow.nextBurrowPlane[3] = burrow.nextBurrowPlane[0].add(0, 100, 0);
+ } else if (ParticleTypes.DRIPPING_LAVA.equals(packet.getParameters().getType()) && packet.getCount() == 2) {
+ if (System.currentTimeMillis() > lastEchoTime + 10_000) {
+ return;
+ }
+ if (previousBurrow.echoBurrowDirection == null) {
+ previousBurrow.echoBurrowDirection = new Vec3d[2];
+ }
+ previousBurrow.echoBurrowDirection[0] = previousBurrow.echoBurrowDirection[1];
+ previousBurrow.echoBurrowDirection[1] = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
+ if (previousBurrow.echoBurrowDirection[0] == null || previousBurrow.echoBurrowDirection[1] == null) {
+ return;
+ }
+ Vec3d echoBurrowDirection = previousBurrow.echoBurrowDirection[1].subtract(previousBurrow.echoBurrowDirection[0]).normalize().multiply(100);
+ if (previousBurrow.echoBurrowPlane == null) {
+ previousBurrow.echoBurrowPlane = new Vec3d[4];
+ }
+ previousBurrow.echoBurrowPlane[0] = previousBurrow.echoBurrowDirection[0].add(echoBurrowDirection).subtract(0, 50, 0);
+ previousBurrow.echoBurrowPlane[1] = previousBurrow.echoBurrowDirection[0].subtract(echoBurrowDirection).subtract(0, 50, 0);
+ previousBurrow.echoBurrowPlane[2] = previousBurrow.echoBurrowPlane[1].add(0, 100, 0);
+ previousBurrow.echoBurrowPlane[3] = previousBurrow.echoBurrowPlane[0].add(0, 100, 0);
+ }
+ }
+ }
+
+ public static void render(WorldRenderContext context) {
+ if (isActive()) {
+ for (GriffinBurrow burrow : griffinBurrows.values()) {
+ if (burrow.shouldRender()) {
+ burrow.render(context);
+ }
+ if (burrow.confirmed != TriState.FALSE) {
+ if (burrow.nextBurrowPlane != null) {
+ RenderHelper.renderQuad(context, burrow.nextBurrowPlane, ORANGE_COLOR_COMPONENTS, 0.25F, true);
+ }
+ if (burrow.echoBurrowPlane != null) {
+ RenderHelper.renderQuad(context, burrow.echoBurrowPlane, ORANGE_COLOR_COMPONENTS, 0.25F, true);
+ }
+ }
+ }
+ }
+ }
+
+ public static ActionResult onAttackBlock(PlayerEntity player, World world, Hand hand, BlockPos pos, Direction direction) {
+ return onInteractBlock(pos);
+ }
+
+ public static ActionResult onUseBlock(PlayerEntity player, World world, Hand hand, BlockHitResult hitResult) {
+ return onInteractBlock(hitResult.getBlockPos());
+ }
+
+ @NotNull
+ private static ActionResult onInteractBlock(BlockPos pos) {
+ if (isActive() && griffinBurrows.containsKey(pos)) {
+ lastDugBurrowPos = pos;
+ }
+ return ActionResult.PASS;
+ }
+
+ public static TypedActionResult<ItemStack> onUseItem(PlayerEntity player, World world, Hand hand) {
+ ItemStack stack = player.getStackInHand(hand);
+ if (isActive() && ItemUtils.getItemId(stack).equals("ANCESTRAL_SPADE")) {
+ lastEchoTime = System.currentTimeMillis();
+ }
+ return TypedActionResult.pass(stack);
+ }
+
+ public static void onChatMessage(Text message, boolean overlay) {
+ if (isActive() && GRIFFIN_BURROW_DUG.matcher(message.getString()).matches()) {
+ previousBurrow.confirmed = TriState.FALSE;
+ previousBurrow = griffinBurrows.get(lastDugBurrowPos);
+ previousBurrow.confirmed = TriState.DEFAULT;
+ }
+ }
+
+ private static boolean isActive() {
+ return SkyblockerConfigManager.get().general.mythologicalRitual.enableMythologicalRitualHelper && Utils.getLocationRaw().equals("hub");
+ }
+
+ private static class GriffinBurrow extends Waypoint {
+ private int critParticle;
+ private int enchantParticle;
+ private TriState confirmed = TriState.FALSE;
+ private final SimpleRegression regression = new SimpleRegression();
+ private Vec3d[] nextBurrowPlane;
+ @Nullable
+ private Vec3d[] echoBurrowDirection;
+ private Vec3d[] echoBurrowPlane;
+
+ private GriffinBurrow(BlockPos pos) {
+ super(pos, Type.WAYPOINT, ORANGE_COLOR_COMPONENTS, 0.25F);
+ }
+
+ private void init() {
+ confirmed = TriState.TRUE;
+ regression.clear();
+ }
+
+ @Override
+ public boolean shouldRender() {
+ return super.shouldRender() && confirmed == TriState.TRUE;
+ }
+ }
+}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java
new file mode 100644
index 00000000..952f2f59
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/Relics.java
@@ -0,0 +1,164 @@
+package de.hysky.skyblocker.skyblock.waypoint;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.mojang.brigadier.CommandDispatcher;
+import de.hysky.skyblocker.SkyblockerMod;
+import de.hysky.skyblocker.config.SkyblockerConfig;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.PosUtils;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.waypoint.ProfileAwareWaypoint;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.Text;
+import net.minecraft.util.DyeColor;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.BlockPos;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Supplier;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class Relics {
+ private static final Logger LOGGER = LoggerFactory.getLogger(Relics.class);
+ private static final Supplier<Waypoint.Type> TYPE_SUPPLIER = () -> SkyblockerConfigManager.get().general.waypoints.waypointType;
+ private static CompletableFuture<Void> relicsLoaded;
+ @SuppressWarnings({"unused", "FieldCanBeLocal"})
+ private static int totalRelics = 0;
+ private static final Map<BlockPos, ProfileAwareWaypoint> relics = new HashMap<>();
+
+ public static void init() {
+ ClientLifecycleEvents.CLIENT_STARTED.register(Relics::loadRelics);
+ ClientLifecycleEvents.CLIENT_STOPPING.register(Relics::saveFoundRelics);
+ ClientCommandRegistrationCallback.EVENT.register(Relics::registerCommands);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(Relics::render);
+ ClientReceiveMessageEvents.GAME.register(Relics::onChatMessage);
+ }
+
+ private static void loadRelics(MinecraftClient client) {
+ relicsLoaded = CompletableFuture.runAsync(() -> {
+ try (BufferedReader reader = client.getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "spidersden/relics.json"))) {
+ for (Map.Entry<String, JsonElement> json : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) {
+ if (json.getKey().equals("total")) {
+ totalRelics = json.getValue().getAsInt();
+ } else if (json.getKey().equals("locations")) {
+ for (JsonElement locationJson : json.getValue().getAsJsonArray().asList()) {
+ JsonObject posData = locationJson.getAsJsonObject();
+ BlockPos pos = new BlockPos(posData.get("x").getAsInt(), posData.get("y").getAsInt(), posData.get("z").getAsInt());
+ relics.put(pos, new ProfileAwareWaypoint(pos, TYPE_SUPPLIER, DyeColor.YELLOW.getColorComponents(), DyeColor.BROWN.getColorComponents()));
+ }
+ }
+ }
+ LOGGER.info("[Skyblocker] Loaded relics locations");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load relics locations", e);
+ }
+
+ try (BufferedReader reader = Files.newBufferedReader(SkyblockerMod.CONFIG_DIR.resolve("found_relics.json"))) {
+ for (Map.Entry<String, JsonElement> profileJson : JsonParser.parseReader(reader).getAsJsonObject().asMap().entrySet()) {
+ for (JsonElement foundRelicsJson : profileJson.getValue().getAsJsonArray().asList()) {
+ relics.get(PosUtils.parsePosString(foundRelicsJson.getAsString())).setFound(profileJson.getKey());
+ }
+ }
+ LOGGER.debug("[Skyblocker] Loaded found relics");
+ } catch (NoSuchFileException ignored) {
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to load found relics", e);
+ }
+ });
+ }
+
+ private static void saveFoundRelics(MinecraftClient client) {
+ Map<String, Set<BlockPos>> foundRelics = new HashMap<>();
+ for (ProfileAwareWaypoint relic : relics.values()) {
+ for (String profile : relic.foundProfiles) {
+ foundRelics.computeIfAbsent(profile, profile_ -> new HashSet<>());
+ foundRelics.get(profile).add(relic.pos);
+ }
+ }
+
+ try (BufferedWriter writer = Files.newBufferedWriter(SkyblockerMod.CONFIG_DIR.resolve("found_relics.json"))) {
+ JsonObject json = new JsonObject();
+ for (Map.Entry<String, Set<BlockPos>> foundRelicsForProfile : foundRelics.entrySet()) {
+ JsonArray foundRelicsJson = new JsonArray();
+ for (BlockPos foundRelic : foundRelicsForProfile.getValue()) {
+ foundRelicsJson.add(PosUtils.getPosString(foundRelic));
+ }
+ json.add(foundRelicsForProfile.getKey(), foundRelicsJson);
+ }
+ SkyblockerMod.GSON.toJson(json, writer);
+ LOGGER.debug("[Skyblocker] Saved found relics");
+ } catch (IOException e) {
+ LOGGER.error("[Skyblocker] Failed to write found relics to file", e);
+ }
+ }
+
+ private static void registerCommands(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+ dispatcher.register(literal(SkyblockerMod.NAMESPACE)
+ .then(literal("relics")
+ .then(literal("markAllFound").executes(context -> {
+ relics.values().forEach(ProfileAwareWaypoint::setFound);
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.relics.markAllFound")));
+ return 1;
+ }))
+ .then(literal("markAllMissing").executes(context -> {
+ relics.values().forEach(ProfileAwareWaypoint::setMissing);
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.relics.markAllMissing")));
+ return 1;
+ }))));
+ }
+
+ private static void render(WorldRenderContext context) {
+ SkyblockerConfig.Relics config = SkyblockerConfigManager.get().locations.spidersDen.relics;
+
+ if (config.enableRelicsHelper && relicsLoaded.isDone() && Utils.getLocationRaw().equals("combat_1")) {
+ for (ProfileAwareWaypoint relic : relics.values()) {
+ boolean isRelicMissing = relic.shouldRender();
+ if (!isRelicMissing && !config.highlightFoundRelics) continue;
+ relic.render(context);
+ }
+ }
+ }
+
+ private static void onChatMessage(Text text, boolean overlay) {
+ String message = text.getString();
+ if (message.equals("You've already found this relic!") || message.startsWith("+10,000 Coins! (") && message.endsWith("/28 Relics)")) {
+ markClosestRelicFound();
+ }
+ }
+
+ private static void markClosestRelicFound() {
+ if (!relicsLoaded.isDone()) return;
+ PlayerEntity player = MinecraftClient.getInstance().player;
+ if (player == null) {
+ LOGGER.warn("[Skyblocker] Failed to mark closest relic as found because player is null");
+ return;
+ }
+ relics.values().stream()
+ .filter(Waypoint::shouldRender)
+ .min(Comparator.comparingDouble(relic -> relic.pos.getSquaredDistance(player.getPos())))
+ .filter(relic -> relic.pos.getSquaredDistance(player.getPos()) <= 16)
+ .ifPresent(Waypoint::setFound);
+ }
+}