aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets
diff options
context:
space:
mode:
authormsg-programs <msgdoesstuff@gmail.com>2023-10-31 21:03:42 +0100
committermsg-programs <msgdoesstuff@gmail.com>2023-10-31 21:03:42 +0100
commitd560c4611e603fa9e72ff6842bc14518d7bdbd63 (patch)
tree36eb1e24443bc9d9fbba51e14f12e5aacfac935c /src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets
parent1df4ef827d8a2e2fcc3767c1f5bf961f16b7fa19 (diff)
parent5bb91104d3275283d7479f0b35c1b18be470d632 (diff)
downloadSkyblocker-d560c4611e603fa9e72ff6842bc14518d7bdbd63.tar.gz
Skyblocker-d560c4611e603fa9e72ff6842bc14518d7bdbd63.tar.bz2
Skyblocker-d560c4611e603fa9e72ff6842bc14518d7bdbd63.zip
Merge branch 'master' of https://github.com/SkyblockerMod/Skyblocker into cleanup-2
# Conflicts: # src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java # src/main/java/de/hysky/skyblocker/utils/Http.java # src/main/java/de/hysky/skyblocker/utils/render/title/TitleContainerConfigScreen.java Pull changes from upstream master
Diffstat (limited to 'src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets')
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java3
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java238
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java127
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java144
-rw-r--r--src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java174
5 files changed, 601 insertions, 85 deletions
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java
index 259cc3f3..73d4a452 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java
@@ -7,7 +7,6 @@ import net.minecraft.block.MapColor;
import net.minecraft.item.map.MapIcon;
import net.minecraft.item.map.MapState;
import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.math.Vec3i;
import org.jetbrains.annotations.NotNull;
@@ -173,7 +172,7 @@ public class DungeonMapUtils {
@NotNull
public static Vector2ic getPhysicalRoomPos(double x, double z) {
Vector2i physicalPos = new Vector2i(x + 8.5, z + 8.5, RoundingMode.TRUNCATE);
- return physicalPos.sub(MathHelper.floorMod(physicalPos.x(), 32), MathHelper.floorMod(physicalPos.y(), 32)).sub(8, 8);
+ return physicalPos.sub(Math.floorMod(physicalPos.x(), 32), Math.floorMod(physicalPos.y(), 32)).sub(8, 8);
}
public static Vector2ic[] getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic... mapPositions) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java
index c2358689..eda08cf6 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java
@@ -1,5 +1,7 @@
package de.hysky.skyblocker.skyblock.dungeon.secrets;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
@@ -7,15 +9,19 @@ import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.IntegerArgumentType;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
-import it.unimi.dsi.fastutil.objects.Object2ByteMap;
-import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap;
-import it.unimi.dsi.fastutil.objects.ObjectIntPair;
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.serialization.JsonOps;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.Utils;
import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import it.unimi.dsi.fastutil.objects.Object2ByteMap;
+import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectIntPair;
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.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
@@ -23,6 +29,9 @@ import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.command.argument.BlockPosArgumentType;
+import net.minecraft.command.argument.PosArgument;
+import net.minecraft.command.argument.TextArgumentType;
import net.minecraft.entity.ItemEntity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.AmbientEntity;
@@ -32,11 +41,15 @@ import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.item.map.MapState;
import net.minecraft.resource.Resource;
+import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Identifier;
import net.minecraft.util.hit.BlockHitResult;
+import net.minecraft.util.hit.HitResult;
+import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
+import net.minecraft.util.math.Vec3i;
import net.minecraft.world.World;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
@@ -46,10 +59,14 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
+import java.util.stream.Stream;
import java.util.zip.InflaterInputStream;
import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
@@ -58,6 +75,7 @@ import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.lit
public class DungeonSecrets {
protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonSecrets.class);
private static final String DUNGEONS_PATH = "dungeons";
+ private static final Path CUSTOM_WAYPOINTS_DIR = SkyblockerMod.CONFIG_DIR.resolve("custom_secret_waypoints.json");
/**
* Maps the block identifier string to a custom numeric block id used in dungeon rooms data.
*
@@ -97,6 +115,10 @@ public class DungeonSecrets {
private static final Map<Vector2ic, Room> rooms = new HashMap<>();
private static final Map<String, JsonElement> roomsJson = new HashMap<>();
private static final Map<String, JsonElement> waypointsJson = new HashMap<>();
+ /**
+ * The map of dungeon room names to custom waypoints relative to the room.
+ */
+ private static final Table<String, BlockPos, SecretWaypoint> customWaypoints = HashBasedTable.create();
@Nullable
private static CompletableFuture<Void> roomsLoaded;
/**
@@ -119,6 +141,10 @@ public class DungeonSecrets {
return roomsLoaded != null && roomsLoaded.isDone();
}
+ public static Stream<Room> getRoomsStream() {
+ return rooms.values().stream();
+ }
+
@SuppressWarnings("unused")
public static JsonObject getRoomMetadata(String room) {
return roomsJson.get(room).getAsJsonObject();
@@ -129,6 +155,38 @@ public class DungeonSecrets {
}
/**
+ * @see #customWaypoints
+ */
+ public static Map<BlockPos, SecretWaypoint> getCustomWaypoints(String room) {
+ return customWaypoints.row(room);
+ }
+
+ /**
+ * @see #customWaypoints
+ */
+ @SuppressWarnings("UnusedReturnValue")
+ public static SecretWaypoint addCustomWaypoint(String room, SecretWaypoint waypoint) {
+ return customWaypoints.put(room, waypoint.pos, waypoint);
+ }
+
+ /**
+ * @see #customWaypoints
+ */
+ public static void addCustomWaypoints(String room, Collection<SecretWaypoint> waypoints) {
+ for (SecretWaypoint waypoint : waypoints) {
+ addCustomWaypoint(room, waypoint);
+ }
+ }
+
+ /**
+ * @see #customWaypoints
+ */
+ @Nullable
+ public static SecretWaypoint removeCustomWaypoint(String room, BlockPos pos) {
+ return customWaypoints.remove(room, pos);
+ }
+
+ /**
* Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}.
* Use {@link #isRoomsLoaded()} to check for completion of loading.
*/
@@ -138,9 +196,10 @@ public class DungeonSecrets {
}
// Execute with MinecraftClient as executor since we need to wait for MinecraftClient#resourceManager to be set
CompletableFuture.runAsync(DungeonSecrets::load, MinecraftClient.getInstance()).exceptionally(e -> {
- LOGGER.error("[Skyblocker] Failed to load dungeon secrets", e);
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e);
return null;
});
+ ClientLifecycleEvents.CLIENT_STOPPING.register(DungeonSecrets::saveCustomWaypoints);
Scheduler.INSTANCE.scheduleCyclic(DungeonSecrets::update, 10);
WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonSecrets::render);
ClientReceiveMessageEvents.GAME.register(DungeonSecrets::onChatMessage);
@@ -148,7 +207,14 @@ public class DungeonSecrets {
UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> onUseBlock(world, hitResult));
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets")
.then(literal("markAsFound").then(markSecretsCommand(true)))
- .then(literal("markAsMissing").then(markSecretsCommand(false)))))));
+ .then(literal("markAsMissing").then(markSecretsCommand(false)))
+ .then(literal("getRelativePos").executes(DungeonSecrets::getRelativePos))
+ .then(literal("getRelativeTargetPos").executes(DungeonSecrets::getRelativeTargetPos))
+ .then(literal("addWaypoint").then(addCustomWaypointCommand(false)))
+ .then(literal("addWaypointRelatively").then(addCustomWaypointCommand(true)))
+ .then(literal("removeWaypoint").then(removeCustomWaypointCommand(false)))
+ .then(literal("removeWaypointRelatively").then(removeCustomWaypointCommand(true)))
+ ))));
ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset()));
}
@@ -158,7 +224,7 @@ public class DungeonSecrets {
for (Map.Entry<Identifier, Resource> resourceEntry : MinecraftClient.getInstance().getResourceManager().findResources(DUNGEONS_PATH, id -> id.getPath().endsWith(".skeleton")).entrySet()) {
String[] path = resourceEntry.getKey().getPath().split("/");
if (path.length != 4) {
- LOGGER.error("[Skyblocker] Failed to load dungeon secrets, invalid resource identifier {}", resourceEntry.getKey());
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets, invalid resource identifier {}", resourceEntry.getKey());
break;
}
String dungeon = path[1];
@@ -171,9 +237,9 @@ public class DungeonSecrets {
synchronized (roomsMap) {
roomsMap.put(room, rooms);
}
- LOGGER.debug("[Skyblocker] Loaded dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room);
+ LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room);
}).exceptionally(e -> {
- LOGGER.error("[Skyblocker] Failed to load dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room, e);
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room, e);
return null;
}));
}
@@ -181,16 +247,39 @@ public class DungeonSecrets {
try (BufferedReader roomsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/dungeonrooms.json")); BufferedReader waypointsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/secretlocations.json"))) {
loadJson(roomsReader, roomsJson);
loadJson(waypointsReader, waypointsJson);
- LOGGER.debug("[Skyblocker] Loaded dungeon secrets json");
+ LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secret waypoints json");
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secret waypoints json", e);
+ }
+ }));
+ dungeonFutures.add(CompletableFuture.runAsync(() -> {
+ try (BufferedReader customWaypointsReader = Files.newBufferedReader(CUSTOM_WAYPOINTS_DIR)) {
+ SkyblockerMod.GSON.fromJson(customWaypointsReader, JsonObject.class).asMap().forEach((room, waypointsJson) ->
+ addCustomWaypoints(room, SecretWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, waypointsJson).resultOrPartial(LOGGER::error).orElseThrow())
+ );
+ LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded custom dungeon secret waypoints");
} catch (Exception e) {
- LOGGER.error("[Skyblocker] Failed to load dungeon secrets json", e);
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load custom dungeon secret waypoints", e);
}
}));
- roomsLoaded = CompletableFuture.allOf(dungeonFutures.toArray(CompletableFuture[]::new)).thenRun(() -> LOGGER.info("[Skyblocker] Loaded dungeon secrets for {} dungeon(s), {} room shapes, and {} rooms total in {} ms", ROOMS_DATA.size(), ROOMS_DATA.values().stream().mapToInt(Map::size).sum(), ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).mapToInt(Map::size).sum(), System.currentTimeMillis() - startTime)).exceptionally(e -> {
- LOGGER.error("[Skyblocker] Failed to load dungeon secrets", e);
+ roomsLoaded = CompletableFuture.allOf(dungeonFutures.toArray(CompletableFuture[]::new)).thenRun(() -> LOGGER.info("[Skyblocker Dungeon Secrets] Loaded dungeon secrets for {} dungeon(s), {} room shapes, {} rooms, and {} custom secret waypoints total in {} ms", ROOMS_DATA.size(), ROOMS_DATA.values().stream().mapToInt(Map::size).sum(), ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).mapToInt(Map::size).sum(), customWaypoints.size(), System.currentTimeMillis() - startTime)).exceptionally(e -> {
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e);
return null;
});
- LOGGER.info("[Skyblocker] Started loading dungeon secrets in (blocked main thread for) {} ms", System.currentTimeMillis() - startTime);
+ LOGGER.info("[Skyblocker Dungeon Secrets] Started loading dungeon secrets in (blocked main thread for) {} ms", System.currentTimeMillis() - startTime);
+ }
+
+ private static void saveCustomWaypoints(MinecraftClient client) {
+ try (BufferedWriter writer = Files.newBufferedWriter(CUSTOM_WAYPOINTS_DIR)) {
+ JsonObject customWaypointsJson = new JsonObject();
+ customWaypoints.rowMap().forEach((room, waypoints) ->
+ customWaypointsJson.add(room, SecretWaypoint.LIST_CODEC.encodeStart(JsonOps.INSTANCE, new ArrayList<>(waypoints.values())).resultOrPartial(LOGGER::error).orElseThrow())
+ );
+ SkyblockerMod.GSON.toJson(customWaypointsJson, writer);
+ LOGGER.info("[Skyblocker Dungeon Secrets] Saved custom dungeon secret waypoints");
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to save custom dungeon secret waypoints", e);
+ }
}
private static int[] readRoom(Resource resource) throws RuntimeException {
@@ -203,25 +292,110 @@ public class DungeonSecrets {
/**
* Loads the json from the given {@link BufferedReader} into the given {@link Map}.
+ *
* @param reader the reader to read the json from
- * @param map the map to load into
+ * @param map the map to load into
*/
private static void loadJson(BufferedReader reader, Map<String, JsonElement> map) {
SkyblockerMod.GSON.fromJson(reader, JsonObject.class).asMap().forEach((room, jsonElement) -> map.put(room.toLowerCase().replaceAll(" ", "-"), jsonElement));
}
private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, Integer>> markSecretsCommand(boolean found) {
- return argument("secret", IntegerArgumentType.integer()).executes(context -> {
- int secretIndex = IntegerArgumentType.getInteger(context, "secret");
+ return argument("secretIndex", IntegerArgumentType.integer()).executes(context -> {
+ int secretIndex = IntegerArgumentType.getInteger(context, "secretIndex");
if (markSecrets(secretIndex, found)) {
- context.getSource().sendFeedback(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFound" : "skyblocker.dungeons.secrets.markSecretMissing", secretIndex));
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFound" : "skyblocker.dungeons.secrets.markSecretMissing", secretIndex)));
} else {
- context.getSource().sendError(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFoundUnable" : "skyblocker.dungeons.secrets.markSecretMissingUnable", secretIndex));
+ context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFoundUnable" : "skyblocker.dungeons.secrets.markSecretMissingUnable", secretIndex)));
}
return Command.SINGLE_SUCCESS;
});
}
+ private static int getRelativePos(CommandContext<FabricClientCommandSource> context) {
+ return getRelativePos(context.getSource(), context.getSource().getPlayer().getBlockPos());
+ }
+
+ private static int getRelativeTargetPos(CommandContext<FabricClientCommandSource> context) {
+ if (MinecraftClient.getInstance().crosshairTarget instanceof BlockHitResult blockHitResult && blockHitResult.getType() == HitResult.Type.BLOCK) {
+ return getRelativePos(context.getSource(), blockHitResult.getBlockPos());
+ } else {
+ context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.noTarget")));
+ }
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static int getRelativePos(FabricClientCommandSource source, BlockPos pos) {
+ Room room = getRoomAtPhysical(pos);
+ if (isRoomMatched(room)) {
+ BlockPos relativePos = currentRoom.actualToRelative(pos);
+ source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), relativePos.getX(), relativePos.getY(), relativePos.getZ())));
+ } else {
+ source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched")));
+ }
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, PosArgument>> addCustomWaypointCommand(boolean relative) {
+ return argument("pos", BlockPosArgumentType.blockPos())
+ .then(argument("secretIndex", IntegerArgumentType.integer())
+ .then(argument("category", SecretWaypoint.Category.CategoryArgumentType.category())
+ .then(argument("name", TextArgumentType.text()).executes(context -> {
+ // TODO Less hacky way with custom ClientBlockPosArgumentType
+ BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null));
+ return relative ? addCustomWaypointRelative(context, pos) : addCustomWaypoint(context, pos);
+ }))
+ )
+ );
+ }
+
+ private static int addCustomWaypoint(CommandContext<FabricClientCommandSource> context, BlockPos pos) {
+ Room room = getRoomAtPhysical(pos);
+ if (isRoomMatched(room)) {
+ room.addCustomWaypoint(context, room.actualToRelative(pos));
+ } else {
+ context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched")));
+ }
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static int addCustomWaypointRelative(CommandContext<FabricClientCommandSource> context, BlockPos pos) {
+ if (isCurrentRoomMatched()) {
+ currentRoom.addCustomWaypoint(context, pos);
+ } else {
+ context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched")));
+ }
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, PosArgument>> removeCustomWaypointCommand(boolean relative) {
+ return argument("pos", BlockPosArgumentType.blockPos())
+ .executes(context -> {
+ // TODO Less hacky way with custom ClientBlockPosArgumentType
+ BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null));
+ return relative ? removeCustomWaypointRelative(context, pos) : removeCustomWaypoint(context, pos);
+ });
+ }
+
+ private static int removeCustomWaypoint(CommandContext<FabricClientCommandSource> context, BlockPos pos) {
+ Room room = getRoomAtPhysical(pos);
+ if (isRoomMatched(room)) {
+ room.removeCustomWaypoint(context, room.actualToRelative(pos));
+ } else {
+ context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched")));
+ }
+ return Command.SINGLE_SUCCESS;
+ }
+
+ private static int removeCustomWaypointRelative(CommandContext<FabricClientCommandSource> context, BlockPos pos) {
+ if (isCurrentRoomMatched()) {
+ currentRoom.removeCustomWaypoint(context, pos);
+ } else {
+ context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched")));
+ }
+ return Command.SINGLE_SUCCESS;
+ }
+
/**
* Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod.
* <p></p>
@@ -282,7 +456,7 @@ public class DungeonSecrets {
}
mapEntrancePos = mapEntrancePosAndSize.left();
mapRoomSize = mapEntrancePosAndSize.rightInt();
- LOGGER.info("[Skyblocker] Started dungeon with map room size {}, map entrance pos {}, player pos {}, and physical entrance pos {}", mapRoomSize, mapEntrancePos, client.player.getPos(), physicalEntrancePos);
+ LOGGER.info("[Skyblocker Dungeon Secrets] Started dungeon with map room size {}, map entrance pos {}, player pos {}, and physical entrance pos {}", mapRoomSize, mapEntrancePos, client.player.getPos(), physicalEntrancePos);
}
Vector2ic physicalPos = DungeonMapUtils.getPhysicalRoomPos(client.player.getPos());
@@ -320,7 +494,7 @@ public class DungeonSecrets {
}
return newRoom;
} catch (IllegalArgumentException e) {
- LOGGER.error("[Skyblocker] Failed to create room", e);
+ LOGGER.error("[Skyblocker Dungeon Secrets] Failed to create room", e);
}
return null;
}
@@ -339,9 +513,16 @@ public class DungeonSecrets {
* Used to detect when all secrets in a room are found.
*/
private static void onChatMessage(Text text, boolean overlay) {
+ String message = text.getString();
+
if (overlay && isCurrentRoomMatched()) {
- currentRoom.onChatMessage(text.getString());
+ currentRoom.onChatMessage(message);
}
+
+ if (message.equals("[BOSS] Bonzo: Gratz for making it this far, but I'm basically unbeatable.") || message.equals("[BOSS] Scarf: This is where the journey ends for you, Adventurers.")
+ || message.equals("[BOSS] The Professor: I was burdened with terrible news recently...") || message.equals("[BOSS] Thorn: Welcome Adventurers! I am Thorn, the Spirit! And host of the Vegan Trials!")
+ || message.equals("[BOSS] Livid: Welcome, you've arrived right on time. I am Livid, the Master of Shadows.") || message.equals("[BOSS] Sadan: So you made it all the way here... Now you wish to defy me? Sadan?!")
+ || message.equals("[BOSS] Maxor: WELL! WELL! WELL! LOOK WHO'S HERE!")) reset();
}
/**
@@ -410,6 +591,19 @@ public class DungeonSecrets {
}
/**
+ * Gets the room at the given physical position.
+ *
+ * @param pos the physical position
+ * @return the room at the given physical position, or null if there is no room at the given physical position
+ * @see #rooms
+ * @see DungeonMapUtils#getPhysicalRoomPos(Vec3i)
+ */
+ @Nullable
+ private static Room getRoomAtPhysical(Vec3i pos) {
+ return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos));
+ }
+
+ /**
* Calls {@link #isRoomMatched(Room)} on {@link #currentRoom}.
*
* @return {@code true} if {@link #currentRoom} is not null and {@link #isRoomMatched(Room)}
@@ -439,7 +633,7 @@ public class DungeonSecrets {
}
/**
- * Resets fields when leaving a dungeon.
+ * Resets fields when leaving a dungeon or entering boss.
*/
private static void reset() {
mapEntrancePos = null;
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java
index dd7dc91e..ecfcf496 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java
@@ -1,14 +1,17 @@
package de.hysky.skyblocker.skyblock.dungeon.secrets;
import com.google.common.collect.HashBasedTable;
-import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Table;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
+import com.mojang.brigadier.arguments.IntegerArgumentType;
+import com.mojang.brigadier.context.CommandContext;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.scheduler.Scheduler;
import it.unimi.dsi.fastutil.ints.IntRBTreeSet;
import it.unimi.dsi.fastutil.ints.IntSortedSet;
import it.unimi.dsi.fastutil.ints.IntSortedSets;
-import de.hysky.skyblocker.utils.scheduler.Scheduler;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.fabricmc.fabric.api.util.TriState;
import net.minecraft.block.BlockState;
@@ -21,13 +24,14 @@ import net.minecraft.entity.ItemEntity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.mob.AmbientEntity;
import net.minecraft.registry.Registries;
+import net.minecraft.text.Text;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.BlockPos;
-import net.minecraft.util.math.MathHelper;
import net.minecraft.world.World;
import org.apache.commons.lang3.tuple.MutableTriple;
import org.apache.commons.lang3.tuple.Triple;
import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
import org.joml.Vector2i;
import org.joml.Vector2ic;
@@ -72,6 +76,9 @@ public class Room {
*/
private TriState matched = TriState.DEFAULT;
private Table<Integer, BlockPos, SecretWaypoint> secretWaypoints;
+ private String name;
+ private Direction direction;
+ private Vector2ic physicalCornerPos;
public Room(@NotNull Type type, @NotNull Vector2ic... physicalPositions) {
this.type = type;
@@ -92,6 +99,13 @@ public class Room {
return matched == TriState.TRUE;
}
+ /**
+ * Not null if {@link #isMatched()}.
+ */
+ public String getName() {
+ return name;
+ }
+
@Override
public String toString() {
return "Room{type=" + type + ", shape=" + shape + ", matched=" + matched + ", segments=" + Arrays.toString(segments.toArray()) + "}";
@@ -145,6 +159,79 @@ public class Room {
}
/**
+ * @see #addCustomWaypoint(int, SecretWaypoint.Category, Text, BlockPos)
+ */
+ protected void addCustomWaypoint(CommandContext<FabricClientCommandSource> context, BlockPos pos) {
+ int secretIndex = IntegerArgumentType.getInteger(context, "secretIndex");
+ SecretWaypoint.Category category = SecretWaypoint.Category.CategoryArgumentType.getCategory(context, "category");
+ Text waypointName = context.getArgument("name", Text.class);
+ addCustomWaypoint(secretIndex, category, waypointName, pos);
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.customWaypointAdded", pos.getX(), pos.getY(), pos.getZ(), name, secretIndex, category, waypointName)));
+ }
+
+ /**
+ * Adds a custom waypoint relative to this room to {@link DungeonSecrets#customWaypoints} and all existing instances of this room.
+ *
+ * @param secretIndex the index of the secret waypoint
+ * @param category the category of the secret waypoint
+ * @param waypointName the name of the secret waypoint
+ * @param pos the position of the secret waypoint relative to this room
+ */
+ @SuppressWarnings("JavadocReference")
+ private void addCustomWaypoint(int secretIndex, SecretWaypoint.Category category, Text waypointName, BlockPos pos) {
+ SecretWaypoint waypoint = new SecretWaypoint(secretIndex, category, waypointName, pos);
+ DungeonSecrets.addCustomWaypoint(name, waypoint);
+ DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.addCustomWaypoint(waypoint));
+ }
+
+ /**
+ * Adds a custom waypoint relative to this room to this instance of the room.
+ *
+ * @param relativeWaypoint the secret waypoint relative to this room to add
+ */
+ private void addCustomWaypoint(SecretWaypoint relativeWaypoint) {
+ SecretWaypoint actualWaypoint = relativeWaypoint.relativeToActual(this);
+ secretWaypoints.put(actualWaypoint.secretIndex, actualWaypoint.pos, actualWaypoint);
+ }
+
+ /**
+ * @see #removeCustomWaypoint(BlockPos)
+ */
+ protected void removeCustomWaypoint(CommandContext<FabricClientCommandSource> context, BlockPos pos) {
+ SecretWaypoint waypoint = removeCustomWaypoint(pos);
+ if (waypoint != null) {
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.customWaypointRemoved", pos.getX(), pos.getY(), pos.getZ(), name, waypoint.secretIndex, waypoint.category, waypoint.name)));
+ } else {
+ context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.customWaypointNotFound", pos.getX(), pos.getY(), pos.getZ(), name)));
+ }
+ }
+
+ /**
+ * Removes a custom waypoint relative to this room from {@link DungeonSecrets#customWaypoints} and all existing instances of this room.
+ * @param pos the position of the secret waypoint relative to this room
+ * @return the removed secret waypoint or {@code null} if there was no secret waypoint at the given position
+ */
+ @SuppressWarnings("JavadocReference")
+ @Nullable
+ private SecretWaypoint removeCustomWaypoint(BlockPos pos) {
+ SecretWaypoint waypoint = DungeonSecrets.removeCustomWaypoint(name, pos);
+ if (waypoint != null) {
+ DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.removeCustomWaypoint(waypoint.secretIndex, pos));
+ }
+ return waypoint;
+ }
+
+ /**
+ * Removes a custom waypoint relative to this room from this instance of the room.
+ * @param secretIndex the index of the secret waypoint
+ * @param relativePos the position of the secret waypoint relative to this room
+ */
+ private void removeCustomWaypoint(int secretIndex, BlockPos relativePos) {
+ BlockPos actualPos = relativeToActual(relativePos);
+ secretWaypoints.remove(secretIndex, actualPos);
+ }
+
+ /**
* Updates the room.
* <p></p>
* This method returns immediately if any of the following conditions are met:
@@ -186,8 +273,8 @@ public class Room {
if (pos.getY() < 66 || pos.getY() > 73) {
return true;
}
- int x = MathHelper.floorMod(pos.getX() - 8, 32);
- int z = MathHelper.floorMod(pos.getZ() - 8, 32);
+ int x = Math.floorMod(pos.getX() - 8, 32);
+ int z = Math.floorMod(pos.getZ() - 8, 32);
return (x < 13 || x > 17 || z > 2 && z < 28) && (z < 13 || z > 17 || x > 2 && x < 28);
}
@@ -217,7 +304,7 @@ public class Room {
* </ul>
* <li> If there are exactly one room matching: </li>
* <ul>
- * <li> Call {@link #roomMatched(String, Direction, Vector2ic)}. </li>
+ * <li> Call {@link #roomMatched()}. </li>
* <li> Discard the no longer needed fields to save memory. </li>
* <li> Return {@code true} </li>
* </ul>
@@ -256,7 +343,10 @@ public class Room {
// If one room matches, load the secrets for that room and discard the no longer needed fields.
for (Triple<Direction, Vector2ic, List<String>> directionRooms : possibleRooms) {
if (directionRooms.getRight().size() == 1) {
- roomMatched(directionRooms.getRight().get(0), directionRooms.getLeft(), directionRooms.getMiddle());
+ name = directionRooms.getRight().get(0);
+ direction = directionRooms.getLeft();
+ physicalCornerPos = directionRooms.getMiddle();
+ roomMatched();
discard();
return true;
}
@@ -286,17 +376,18 @@ public class Room {
* @param directionRooms the direction, position, and name of the room
*/
@SuppressWarnings("JavadocReference")
- private void roomMatched(String name, Direction direction, Vector2ic physicalCornerPos) {
- Table<Integer, BlockPos, SecretWaypoint> secretWaypointsMutable = HashBasedTable.create();
+ private void roomMatched() {
+ secretWaypoints = HashBasedTable.create();
for (JsonElement waypointElement : DungeonSecrets.getRoomWaypoints(name)) {
JsonObject waypoint = waypointElement.getAsJsonObject();
String secretName = waypoint.get("secretName").getAsString();
int secretIndex = Integer.parseInt(secretName.substring(0, Character.isDigit(secretName.charAt(1)) ? 2 : 1));
BlockPos pos = DungeonMapUtils.relativeToActual(direction, physicalCornerPos, waypoint);
- secretWaypointsMutable.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos));
+ secretWaypoints.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos));
}
- secretWaypoints = ImmutableTable.copyOf(secretWaypointsMutable);
+ DungeonSecrets.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint);
matched = TriState.TRUE;
+
DungeonSecrets.LOGGER.info("[Skyblocker] Room {} matched after checking {} block(s)", name, checkedBlocks.size());
}
@@ -323,6 +414,20 @@ public class Room {
}
/**
+ * Fails if !{@link #isMatched()}
+ */
+ protected BlockPos actualToRelative(BlockPos pos) {
+ return DungeonMapUtils.actualToRelative(direction, physicalCornerPos, pos);
+ }
+
+ /**
+ * Fails if !{@link #isMatched()}
+ */
+ protected BlockPos relativeToActual(BlockPos pos) {
+ return DungeonMapUtils.relativeToActual(direction, physicalCornerPos, pos);
+ }
+
+ /**
* Calls {@link SecretWaypoint#render(WorldRenderContext)} on {@link #secretWaypoints all secret waypoints}.
*/
protected void render(WorldRenderContext context) {
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java
index d2a31ea3..0c2d1b34 100644
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java
@@ -1,38 +1,61 @@
package de.hysky.skyblocker.skyblock.dungeon.secrets;
import com.google.gson.JsonObject;
-
+import com.mojang.brigadier.context.CommandContext;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.JsonOps;
+import com.mojang.serialization.codecs.RecordCodecBuilder;
import de.hysky.skyblocker.config.SkyblockerConfig;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.utils.render.RenderHelper;
+import de.hysky.skyblocker.utils.waypoint.Waypoint;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.argument.EnumArgumentType;
import net.minecraft.entity.Entity;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
+import net.minecraft.util.StringIdentifiable;
+import net.minecraft.util.dynamic.Codecs;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3d;
+import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.function.ToDoubleFunction;
-public class SecretWaypoint {
+public class SecretWaypoint extends Waypoint {
+ public static final Codec<SecretWaypoint> CODEC = RecordCodecBuilder.create(instance -> instance.group(
+ Codec.INT.fieldOf("secretIndex").forGetter(secretWaypoint -> secretWaypoint.secretIndex),
+ Category.CODEC.fieldOf("category").forGetter(secretWaypoint -> secretWaypoint.category),
+ Codecs.TEXT.fieldOf("name").forGetter(secretWaypoint -> secretWaypoint.name),
+ BlockPos.CODEC.fieldOf("pos").forGetter(secretWaypoint -> secretWaypoint.pos)
+ ).apply(instance, SecretWaypoint::new));
+ public static final Codec<List<SecretWaypoint>> LIST_CODEC = CODEC.listOf();
static final List<String> SECRET_ITEMS = List.of("Decoy", "Defuse Kit", "Dungeon Chest Key", "Healing VIII", "Inflatable Jerry", "Spirit Leap", "Training Weights", "Trap", "Treasure Talisman");
+ private static final SkyblockerConfig.SecretWaypoints CONFIG = SkyblockerConfigManager.get().locations.dungeons.secretWaypoints;
+ private static final Supplier<Type> TYPE_SUPPLIER = () -> CONFIG.waypointType;
final int secretIndex;
final Category category;
- private final Text name;
- private final BlockPos pos;
+ final Text name;
private final Vec3d centerPos;
- private boolean missing;
SecretWaypoint(int secretIndex, JsonObject waypoint, String name, BlockPos pos) {
+ this(secretIndex, Category.get(waypoint), name, pos);
+ }
+
+ SecretWaypoint(int secretIndex, Category category, String name, BlockPos pos) {
+ this(secretIndex, category, Text.of(name), pos);
+ }
+
+ SecretWaypoint(int secretIndex, Category category, Text name, BlockPos pos) {
+ super(pos, TYPE_SUPPLIER, category.colorComponents);
this.secretIndex = secretIndex;
- this.category = Category.get(waypoint);
- this.name = Text.of(name);
- this.pos = pos;
+ this.category = category;
+ this.name = name;
this.centerPos = pos.toCenterPos();
- this.missing = true;
}
static ToDoubleFunction<SecretWaypoint> getSquaredDistanceToFunction(Entity entity) {
@@ -43,8 +66,9 @@ public class SecretWaypoint {
return secretWaypoint -> entity.squaredDistanceTo(secretWaypoint.centerPos) <= 36D;
}
- boolean shouldRender() {
- return category.isEnabled() && missing;
+ @Override
+ public boolean shouldRender() {
+ return super.shouldRender() && category.isEnabled();
}
boolean needsInteraction() {
@@ -63,40 +87,47 @@ public class SecretWaypoint {
return category.isBat();
}
- void setFound() {
- this.missing = false;
- }
-
- void setMissing() {
- this.missing = true;
- }
-
/**
* Renders the secret waypoint, including a filled cube, a beacon beam, the name, and the distance from the player.
*/
- void render(WorldRenderContext context) {
- RenderHelper.renderFilledThroughWallsWithBeaconBeam(context, pos, category.colorComponents, 0.5F);
- Vec3d posUp = centerPos.add(0, 1, 0);
- RenderHelper.renderText(context, name, posUp, true);
- double distance = context.camera().getPos().distanceTo(centerPos);
- RenderHelper.renderText(context, Text.literal(Math.round(distance) + "m").formatted(Formatting.YELLOW), posUp, 1, MinecraftClient.getInstance().textRenderer.fontHeight + 1, true);
+ @Override
+ public void render(WorldRenderContext context) {
+ //TODO In the future, shrink the box for wither essence and items so its more realistic
+ super.render(context);
+
+ if (CONFIG.showSecretText) {
+ Vec3d posUp = centerPos.add(0, 1, 0);
+ RenderHelper.renderText(context, name, posUp, true);
+ double distance = context.camera().getPos().distanceTo(centerPos);
+ RenderHelper.renderText(context, Text.literal(Math.round(distance) + "m").formatted(Formatting.YELLOW), posUp, 1, MinecraftClient.getInstance().textRenderer.fontHeight + 1, true);
+ }
+ }
+
+ @NotNull
+ SecretWaypoint relativeToActual(Room room) {
+ return new SecretWaypoint(secretIndex, category, name, room.relativeToActual(pos));
}
- enum Category {
- ENTRANCE(secretWaypoints -> secretWaypoints.enableEntranceWaypoints, 0, 255, 0),
- SUPERBOOM(secretWaypoints -> secretWaypoints.enableSuperboomWaypoints, 255, 0, 0),
- CHEST(secretWaypoints -> secretWaypoints.enableChestWaypoints, 2, 213, 250),
- ITEM(secretWaypoints -> secretWaypoints.enableItemWaypoints, 2, 64, 250),
- BAT(secretWaypoints -> secretWaypoints.enableBatWaypoints, 142, 66, 0),
- WITHER(secretWaypoints -> secretWaypoints.enableWitherWaypoints, 30, 30, 30),
- LEVER(secretWaypoints -> secretWaypoints.enableLeverWaypoints, 250, 217, 2),
- FAIRYSOUL(secretWaypoints -> secretWaypoints.enableFairySoulWaypoints, 255, 85, 255),
- STONK(secretWaypoints -> secretWaypoints.enableStonkWaypoints, 146, 52, 235),
- DEFAULT(secretWaypoints -> secretWaypoints.enableDefaultWaypoints, 190, 255, 252);
+ enum Category implements StringIdentifiable {
+ ENTRANCE("entrance", secretWaypoints -> secretWaypoints.enableEntranceWaypoints, 0, 255, 0),
+ SUPERBOOM("superboom", secretWaypoints -> secretWaypoints.enableSuperboomWaypoints, 255, 0, 0),
+ CHEST("chest", secretWaypoints -> secretWaypoints.enableChestWaypoints, 2, 213, 250),
+ ITEM("item", secretWaypoints -> secretWaypoints.enableItemWaypoints, 2, 64, 250),
+ BAT("bat", secretWaypoints -> secretWaypoints.enableBatWaypoints, 142, 66, 0),
+ WITHER("wither", secretWaypoints -> secretWaypoints.enableWitherWaypoints, 30, 30, 30),
+ LEVER("lever", secretWaypoints -> secretWaypoints.enableLeverWaypoints, 250, 217, 2),
+ FAIRYSOUL("fairysoul", secretWaypoints -> secretWaypoints.enableFairySoulWaypoints, 255, 85, 255),
+ STONK("stonk", secretWaypoints -> secretWaypoints.enableStonkWaypoints, 146, 52, 235),
+ AOTV("aotv", secretWaypoints -> secretWaypoints.enableAotvWaypoints, 252, 98, 3),
+ PEARL("pearl", secretWaypoints -> secretWaypoints.enablePearlWaypoints, 57, 117, 125),
+ DEFAULT("default", secretWaypoints -> secretWaypoints.enableDefaultWaypoints, 190, 255, 252);
+ private static final Codec<Category> CODEC = StringIdentifiable.createCodec(Category::values);
+ private final String name;
private final Predicate<SkyblockerConfig.SecretWaypoints> enabledPredicate;
private final float[] colorComponents;
- Category(Predicate<SkyblockerConfig.SecretWaypoints> enabledPredicate, int... intColorComponents) {
+ Category(String name, Predicate<SkyblockerConfig.SecretWaypoints> enabledPredicate, int... intColorComponents) {
+ this.name = name;
this.enabledPredicate = enabledPredicate;
colorComponents = new float[intColorComponents.length];
for (int i = 0; i < intColorComponents.length; i++) {
@@ -104,19 +135,8 @@ public class SecretWaypoint {
}
}
- private static Category get(JsonObject categoryJson) {
- return switch (categoryJson.get("category").getAsString()) {
- case "entrance" -> Category.ENTRANCE;
- case "superboom" -> Category.SUPERBOOM;
- case "chest" -> Category.CHEST;
- case "item" -> Category.ITEM;
- case "bat" -> Category.BAT;
- case "wither" -> Category.WITHER;
- case "lever" -> Category.LEVER;
- case "fairysoul" -> Category.FAIRYSOUL;
- case "stonk" -> Category.STONK;
- default -> Category.DEFAULT;
- };
+ private static Category get(JsonObject waypointJson) {
+ return CODEC.parse(JsonOps.INSTANCE, waypointJson.get("category")).resultOrPartial(DungeonSecrets.LOGGER::error).orElseThrow();
}
boolean needsInteraction() {
@@ -138,5 +158,29 @@ public class SecretWaypoint {
boolean isEnabled() {
return enabledPredicate.test(SkyblockerConfigManager.get().locations.dungeons.secretWaypoints);
}
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public String asString() {
+ return name;
+ }
+
+ static class CategoryArgumentType extends EnumArgumentType<Category> {
+ public CategoryArgumentType() {
+ super(Category.CODEC, Category::values);
+ }
+
+ public static CategoryArgumentType category() {
+ return new CategoryArgumentType();
+ }
+
+ public static <S> Category getCategory(CommandContext<S> context, String name) {
+ return context.getArgument(name, Category.class);
+ }
+ }
}
}
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java
new file mode 100644
index 00000000..0690952e
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretsTracker.java
@@ -0,0 +1,174 @@
+package de.hysky.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
+import de.hysky.skyblocker.skyblock.tabhud.widget.DungeonPlayerWidget;
+import de.hysky.skyblocker.utils.ApiUtils;
+import de.hysky.skyblocker.utils.Constants;
+import de.hysky.skyblocker.utils.Http;
+import de.hysky.skyblocker.utils.Http.ApiResponse;
+import de.hysky.skyblocker.utils.Utils;
+import it.unimi.dsi.fastutil.ints.IntIntPair;
+import it.unimi.dsi.fastutil.objects.Object2IntMap.Entry;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.HoverEvent;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Tracks the amount of secrets players get every run
+ */
+public class SecretsTracker {
+ private static final Logger LOGGER = LoggerFactory.getLogger(SecretsTracker.class);
+ private static final Pattern TEAM_SCORE_PATTERN = Pattern.compile(" +Team Score: [0-9]+ \\([A-z+]+\\)");
+
+ private static volatile TrackedRun currentRun = null;
+ private static volatile TrackedRun lastRun = null;
+ private static volatile long lastRunEnded = 0L;
+
+ public static void init() {
+ ClientReceiveMessageEvents.GAME.register(SecretsTracker::onMessage);
+ }
+
+ //If -1 is somehow encountered, it would be very rare, so I just disregard its possibility for now
+ //people would probably recognize if it was inaccurate so yeah
+ private static void calculate(RunPhase phase) {
+ switch (phase) {
+ case START -> CompletableFuture.runAsync(() -> {
+ TrackedRun newlyStartedRun = new TrackedRun();
+
+ //Initialize players in new run
+ for (int i = 0; i < 5; i++) {
+ String playerName = getPlayerNameAt(i + 1);
+
+ //The player name will be blank if there isn't a player at that index
+ if (!playerName.isEmpty()) {
+
+ //If the player was a part of the last run (and didn't have -1 secret count) and that run ended less than 5 mins ago then copy the secrets over
+ if (lastRun != null && System.currentTimeMillis() <= lastRunEnded + 300_000 && lastRun.secretCounts().getOrDefault(playerName, -1) != -1) {
+ newlyStartedRun.secretCounts().put(playerName, lastRun.secretCounts().getInt(playerName));
+ } else {
+ newlyStartedRun.secretCounts().put(playerName, getPlayerSecrets(playerName).leftInt());
+ }
+ }
+ }
+
+ currentRun = newlyStartedRun;
+ });
+
+ case END -> CompletableFuture.runAsync(() -> {
+ //In case the game crashes from something
+ if (currentRun != null) {
+ Object2ObjectOpenHashMap<String, IntIntPair> secretsFound = new Object2ObjectOpenHashMap<>();
+
+ //Update secret counts
+ for (Entry<String> entry : currentRun.secretCounts().object2IntEntrySet()) {
+ String playerName = entry.getKey();
+ int startingSecrets = entry.getIntValue();
+ IntIntPair secretsNow = getPlayerSecrets(playerName);
+ int secretsPlayerFound = secretsNow.leftInt() - startingSecrets;
+
+ secretsFound.put(playerName, IntIntPair.of(secretsPlayerFound, secretsNow.rightInt()));
+ entry.setValue(secretsNow.leftInt());
+ }
+
+ //Print the results all in one go, so its clean and less of a chance of it being broken up
+ for (Map.Entry<String, IntIntPair> entry : secretsFound.entrySet()) {
+ sendResultMessage(entry.getKey(), entry.getValue().leftInt(), entry.getValue().rightInt(), true);
+ }
+
+ //Swap the current and last run as well as mark the run end time
+ lastRunEnded = System.currentTimeMillis();
+ lastRun = currentRun;
+ currentRun = null;
+ } else {
+ sendResultMessage(null, -1, -1, false);
+ }
+ });
+ }
+ }
+
+ private static void sendResultMessage(String player, int secrets, int cacheAge, boolean success) {
+ PlayerEntity playerEntity = MinecraftClient.getInstance().player;
+ if (playerEntity != null) {
+ if (success) {
+ playerEntity.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secretsTracker.feedback", Text.literal(player).styled(Constants.WITH_COLOR.apply(0xf57542)), "ยง7" + secrets, getCacheText(cacheAge))));
+ } else {
+ playerEntity.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secretsTracker.failFeedback")));
+ }
+ }
+ }
+
+ private static Text getCacheText(int cacheAge) {
+ return Text.literal("\u2139").styled(style -> style.withColor(cacheAge == -1 ? 0x218bff : 0xeac864).withHoverEvent(
+ new HoverEvent(HoverEvent.Action.SHOW_TEXT, cacheAge == -1 ? Text.translatable("skyblocker.api.cache.MISS") : Text.translatable("skyblocker.api.cache.HIT", cacheAge))));
+ }
+
+ private static void onMessage(Text text, boolean overlay) {
+ if (Utils.isInDungeons() && SkyblockerConfigManager.get().locations.dungeons.playerSecretsTracker) {
+ String message = Formatting.strip(text.getString());
+
+ try {
+ if (message.equals("[NPC] Mort: Here, I found this map when I first entered the dungeon.")) calculate(RunPhase.START);
+ if (TEAM_SCORE_PATTERN.matcher(message).matches()) calculate(RunPhase.END);
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Encountered an unknown error while trying to track player secrets!", e);
+ }
+ }
+ }
+
+ private static String getPlayerNameAt(int index) {
+ Matcher matcher = PlayerListMgr.regexAt(1 + (index - 1) * 4, DungeonPlayerWidget.PLAYER_PATTERN);
+
+ return matcher != null ? matcher.group("name") : "";
+ }
+
+ private static IntIntPair getPlayerSecrets(String name) {
+ String uuid = ApiUtils.name2Uuid(name);
+
+ if (!uuid.isEmpty()) {
+ try (ApiResponse response = Http.sendHypixelRequest("player", "?uuid=" + uuid)) {
+ return IntIntPair.of(getSecretCountFromAchievements(JsonParser.parseString(response.content()).getAsJsonObject()), response.age());
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Encountered an error while trying to fetch {} secret count!", name + "'s", e);
+ }
+ }
+
+ return IntIntPair.of(-1, -1);
+ }
+
+ /**
+ * Gets a player's secret count from their hypixel achievements
+ */
+ private static int getSecretCountFromAchievements(JsonObject playerJson) {
+ JsonObject player = playerJson.get("player").getAsJsonObject();
+ JsonObject achievements = (player.has("achievements")) ? player.get("achievements").getAsJsonObject() : null;
+ return (achievements != null && achievements.has("skyblock_treasure_hunter")) ? achievements.get("skyblock_treasure_hunter").getAsInt() : 0;
+ }
+
+ /**
+ * This will either reflect the value at the start or the end depending on when this is called
+ */
+ private record TrackedRun(Object2IntOpenHashMap<String> secretCounts) {
+ private TrackedRun() {
+ this(new Object2IntOpenHashMap<>());
+ }
+ }
+
+ private enum RunPhase {
+ START, END
+ }
+}