aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon
diff options
context:
space:
mode:
authorKevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>2023-08-31 20:21:30 -0400
committerKevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>2023-08-31 20:40:44 -0400
commitaa2abcf644bf58adb3330e87d870ce56a09ce3f5 (patch)
treef9e9114f9b0031ce61dcfba05a030453f540d6b1 /src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon
parentfea919794608128b6c436f1a64d1be38e935241d (diff)
parent1370097922f19815e14bdfd8c9e606cde2bc8f39 (diff)
downloadSkyblocker-aa2abcf644bf58adb3330e87d870ce56a09ce3f5.tar.gz
Skyblocker-aa2abcf644bf58adb3330e87d870ce56a09ce3f5.tar.bz2
Skyblocker-aa2abcf644bf58adb3330e87d870ce56a09ce3f5.zip
Merge branch 'master' into utils-cleanup
# Conflicts: # src/main/java/me/xmrvizzy/skyblocker/SkyblockerMod.java # src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/DungeonBlaze.java # src/main/java/me/xmrvizzy/skyblocker/skyblock/dwarven/DwarvenHud.java # src/main/java/me/xmrvizzy/skyblocker/skyblock/dwarven/DwarvenHudConfigScreen.java # src/main/java/me/xmrvizzy/skyblocker/utils/RenderHelper.java
Diffstat (limited to 'src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon')
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/CroesusHelper.java2
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/DungeonBlaze.java139
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/StarredMobGlow.java51
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/TicTacToe.java136
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java275
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java427
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/Room.java461
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/SecretWaypoint.java132
8 files changed, 1575 insertions, 48 deletions
diff --git a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/CroesusHelper.java b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/CroesusHelper.java
index 0e56ece0..1e949cd1 100644
--- a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/CroesusHelper.java
+++ b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/CroesusHelper.java
@@ -25,7 +25,7 @@ public class CroesusHelper extends ContainerSolver {
List<ColorHighlight> highlights = new ArrayList<>();
for (Map.Entry<Integer, ItemStack> entry : slots.entrySet()) {
ItemStack stack = entry.getValue();
- if (stack != null && stack.getNbt() != null && stack.getNbt().toString().contains("No more Chests to open!")) {
+ if (stack != null && stack.getNbt() != null && stack.getNbt().toString().contains("Opened Chest:")) {
highlights.add(ColorHighlight.gray(entry.getKey()));
}
}
diff --git a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/DungeonBlaze.java b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/DungeonBlaze.java
index ef94d9ec..09bef802 100644
--- a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/DungeonBlaze.java
+++ b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/DungeonBlaze.java
@@ -8,8 +8,10 @@ import me.xmrvizzy.skyblocker.utils.render.RenderHelper;
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.client.network.ClientPlayerEntity;
import net.minecraft.client.world.ClientWorld;
-import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.predicate.entity.EntityPredicates;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import org.slf4j.Logger;
@@ -19,89 +21,132 @@ import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
+/**
+ * This class provides functionality to render outlines around Blaze entities
+ */
public class DungeonBlaze {
private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName());
private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F};
- private static final float[] WHITE_COLOR_COMPONENTS = { 1.0f, 1.0f, 1.0f };
- static Entity highestBlaze = null;
- static Entity lowestBlaze = null;
- static Entity nextHighestBlaze = null;
- static Entity nextLowestBlaze = null;
+ private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f};
+
+ private static ArmorStandEntity highestBlaze = null;
+ private static ArmorStandEntity lowestBlaze = null;
+ private static ArmorStandEntity nextHighestBlaze = null;
+ private static ArmorStandEntity nextLowestBlaze = null;
public static void init() {
SkyblockerMod.getInstance().scheduler.scheduleCyclic(DungeonBlaze::update, 10);
WorldRenderEvents.BEFORE_DEBUG_RENDER.register(DungeonBlaze::blazeRenderer);
}
+ /**
+ * Updates the state of Blaze entities and triggers the rendering process if necessary.
+ */
public static void update() {
- if (!SkyblockerConfig.get().locations.dungeons.blazesolver) return;
ClientWorld world = MinecraftClient.getInstance().world;
- if (world == null || !Utils.isInDungeons()) return;
- Iterable<Entity> entities = world.getEntities();
- List<ObjectIntPair<Entity>> blazes = new ArrayList<>();
-
- for (Entity entity : entities) {
- String blazeName = entity.getName().getString();
+ ClientPlayerEntity player = MinecraftClient.getInstance().player;
+ if (world == null || player == null || !Utils.isInDungeons()) return;
+ List<ObjectIntPair<ArmorStandEntity>> blazes = getBlazesInWorld(world, player);
+ sortBlazes(blazes);
+ updateBlazeEntities(blazes);
+ }
+ /**
+ * Retrieves Blaze entities in the world and parses their health information.
+ *
+ * @param world The client world to search for Blaze entities.
+ * @return A list of Blaze entities and their associated health.
+ */
+ private static List<ObjectIntPair<ArmorStandEntity>> getBlazesInWorld(ClientWorld world, ClientPlayerEntity player) {
+ List<ObjectIntPair<ArmorStandEntity>> blazes = new ArrayList<>();
+ for (ArmorStandEntity blaze : world.getEntitiesByClass(ArmorStandEntity.class, player.getBoundingBox().expand(500D), EntityPredicates.NOT_MOUNTED)) {
+ String blazeName = blaze.getName().getString();
if (blazeName.contains("Blaze") && blazeName.contains("/")) {
try {
int health = Integer.parseInt(blazeName.substring(blazeName.indexOf("/") + 1, blazeName.length() - 1));
-
- blazes.add(ObjectIntPair.of(entity, health));
+ blazes.add(ObjectIntPair.of(blaze, health));
} catch (NumberFormatException e) {
- LOGGER.error("[Skyblocker DungeonBlazeSolver] Failed to parse blaze health: " + blazeName, e);
+ handleException(e);
}
}
}
+ return blazes;
+ }
- // Order the blazes in the list from the lowest health to the highest health
+ /**
+ * Sorts the Blaze entities based on their health values.
+ *
+ * @param blazes The list of Blaze entities to be sorted.
+ */
+ private static void sortBlazes(List<ObjectIntPair<ArmorStandEntity>> blazes) {
blazes.sort(Comparator.comparingInt(ObjectIntPair::rightInt));
+ }
- // Ensure that there are blazes in the list
+ /**
+ * Updates information about Blaze entities based on sorted list.
+ *
+ * @param blazes The sorted list of Blaze entities with associated health values.
+ */
+ private static void updateBlazeEntities(List<ObjectIntPair<ArmorStandEntity>> blazes) {
if (!blazes.isEmpty()) {
lowestBlaze = blazes.get(0).left();
-
int highestIndex = blazes.size() - 1;
highestBlaze = blazes.get(highestIndex).left();
-
- // If there's more than 1 blaze
if (blazes.size() > 1) {
- nextLowestBlaze = blazes.get(1).left();
- nextHighestBlaze = blazes.get(highestIndex - 1).left();
+ nextLowestBlaze = blazes.get(1).left();
+ nextHighestBlaze = blazes.get(highestIndex - 1).left();
}
}
-
}
- public static void blazeRenderer(WorldRenderContext context) {
+ /**
+ * Renders outlines for Blaze entities based on health and position.
+ *
+ * @param wrc The WorldRenderContext used for rendering.
+ */
+ public static void blazeRenderer(WorldRenderContext wrc) {
try {
- if (SkyblockerConfig.get().locations.dungeons.blazesolver && highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive()) {
- /* Outline */
+ if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfig.get().locations.dungeons.blazesolver) {
if (highestBlaze.getY() < 69) {
- Box blaze = highestBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
- RenderHelper.renderOutline(context, blaze, GREEN_COLOR_COMPONENTS, 5f);
-
- if (nextHighestBlaze != null && nextHighestBlaze.isAlive() && nextHighestBlaze != highestBlaze) {
- Box nextBlaze = nextHighestBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
- RenderHelper.renderOutline(context, nextBlaze, WHITE_COLOR_COMPONENTS, 5f);
- RenderHelper.renderLinesFromPoints(context, new Vec3d[] { blaze.getCenter(), nextBlaze.getCenter() }, WHITE_COLOR_COMPONENTS, 1f, 5f);
- }
+ renderBlazeOutline(highestBlaze, nextHighestBlaze, wrc);
}
-
- /* Outline */
if (lowestBlaze.getY() > 69) {
- Box blaze = lowestBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
- RenderHelper.renderOutline(context, blaze, GREEN_COLOR_COMPONENTS, 5f);
-
- if (nextLowestBlaze != null && nextLowestBlaze.isAlive() && nextLowestBlaze != lowestBlaze) {
- Box nextBlaze = nextLowestBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
- RenderHelper.renderOutline(context, nextBlaze, WHITE_COLOR_COMPONENTS, 5f);
- RenderHelper.renderLinesFromPoints(context, new Vec3d[] { blaze.getCenter(), nextBlaze.getCenter() }, WHITE_COLOR_COMPONENTS, 1f, 5f);
- }
+ renderBlazeOutline(lowestBlaze, nextLowestBlaze, wrc);
}
}
} catch (Exception e) {
- LOGGER.warn("[Skyblocker DungeonBlazeRenderer] Failed to render blaze boxes", e);
+ handleException(e);
+ }
+ }
+
+ /**
+ * Renders outlines for Blaze entities and connections between them.
+ *
+ * @param blaze The Blaze entity for which to render an outline.
+ * @param nextBlaze The next Blaze entity for connection rendering.
+ * @param wrc The WorldRenderContext used for rendering.
+ */
+ private static void renderBlazeOutline(ArmorStandEntity blaze, ArmorStandEntity nextBlaze, WorldRenderContext wrc) {
+ Box blazeBox = blaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
+ RenderHelper.renderOutline(wrc, blazeBox, GREEN_COLOR_COMPONENTS, 5f);
+
+ if (nextBlaze != null && nextBlaze.isAlive() && nextBlaze != blaze) {
+ Box nextBlazeBox = nextBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0);
+ RenderHelper.renderOutline(wrc, nextBlazeBox, WHITE_COLOR_COMPONENTS, 5f);
+
+ Vec3d blazeCenter = blazeBox.getCenter();
+ Vec3d nextBlazeCenter = nextBlazeBox.getCenter();
+
+ RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f);
}
}
-} \ No newline at end of file
+
+ /**
+ * Handles exceptions by logging and printing stack traces.
+ *
+ * @param e The exception to handle.
+ */
+ private static void handleException(Exception e) {
+ LOGGER.warn("[Skyblocker BlazeRenderer] Encountered an unknown exception", e);
+ }
+}
diff --git a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/StarredMobGlow.java b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/StarredMobGlow.java
new file mode 100644
index 00000000..64a28712
--- /dev/null
+++ b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/StarredMobGlow.java
@@ -0,0 +1,51 @@
+package me.xmrvizzy.skyblocker.skyblock.dungeon;
+
+import java.util.List;
+
+import me.xmrvizzy.skyblocker.utils.Utils;
+import me.xmrvizzy.skyblocker.utils.render.culling.OcclusionCulling;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.util.math.Box;
+
+public class StarredMobGlow {
+
+ public static boolean shouldMobGlow(Entity entity) {
+ Box box = entity.getBoundingBox();
+
+ if (Utils.isInDungeons() && !entity.isInvisible() && OcclusionCulling.isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
+ //Minibosses
+ if (entity instanceof PlayerEntity) {
+ switch (entity.getName().getString()) {
+ case "Lost Adventurer": return true;
+ case "Shadow Assassin": return true;
+ case "Diamond Guy": return true;
+ }
+ }
+
+ //Regular Mobs
+ if (!(entity instanceof ArmorStandEntity)) {
+ Box searchBox = box.expand(0, 2, 0);
+ List<ArmorStandEntity> armorStands = entity.getWorld().getEntitiesByClass(ArmorStandEntity.class, searchBox, EntityPredicates.NOT_MOUNTED);
+
+ if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static int getGlowColor(Entity entity) {
+ if (entity instanceof PlayerEntity) {
+ switch (entity.getName().getString()) {
+ case "Lost Adventurer": return 0xfee15c;
+ case "Shadow Assassin": return 0x5b2cb2;
+ case "Diamond Guy": return 0x57c2f7;
+ }
+ }
+
+ return 0xf57738;
+ }
+}
diff --git a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/TicTacToe.java b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/TicTacToe.java
new file mode 100644
index 00000000..a97b6752
--- /dev/null
+++ b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/TicTacToe.java
@@ -0,0 +1,136 @@
+package me.xmrvizzy.skyblocker.skyblock.dungeon;
+
+import me.xmrvizzy.skyblocker.config.SkyblockerConfig;
+import me.xmrvizzy.skyblocker.utils.Utils;
+import me.xmrvizzy.skyblocker.utils.render.RenderHelper;
+import me.xmrvizzy.skyblocker.utils.tictactoe.TicTacToeUtils;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
+import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
+import net.minecraft.block.Block;
+import net.minecraft.block.Blocks;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.decoration.ItemFrameEntity;
+import net.minecraft.item.FilledMapItem;
+import net.minecraft.item.map.MapState;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.Box;
+import net.minecraft.util.math.Direction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+/**
+ * Thanks to Danker for a reference implementation!
+ */
+public class TicTacToe {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class);
+ private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F};
+ private static Box nextBestMoveToMake = null;
+
+ public static void init() {
+ WorldRenderEvents.BEFORE_DEBUG_RENDER.register(TicTacToe::solutionRenderer);
+ }
+
+ public static void tick() {
+ MinecraftClient client = MinecraftClient.getInstance();
+ ClientWorld world = client.world;
+ ClientPlayerEntity player = client.player;
+
+ nextBestMoveToMake = null;
+
+ if (world == null || player == null || !Utils.isInDungeons()) return;
+
+ //Search within 21 blocks for item frames that contain maps
+ Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21);
+ List<ItemFrameEntity> itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap);
+
+ try {
+ //Only attempt to solve if its the player's turn
+ if (itemFramesThatHoldMaps.size() != 9 && itemFramesThatHoldMaps.size() % 2 == 1) {
+ char[][] board = new char[3][3];
+ BlockPos leftmostRow = null;
+ int sign = 1;
+ char facing = 'X';
+
+ for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) {
+ MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt()));
+
+ if (mapState == null) continue;
+
+ int column = 0, row;
+ sign = 1;
+
+ //Find position of the item frame relative to where it is on the tic tac toe board
+ if (itemFrame.getHorizontalFacing() == Direction.SOUTH || itemFrame.getHorizontalFacing() == Direction.WEST) sign = -1;
+ BlockPos itemFramePos = BlockPos.ofFloored(itemFrame.getX(), itemFrame.getY(), itemFrame.getZ());
+
+ for (int i = 2; i >= 0; i--) {
+ int realI = i * sign;
+ BlockPos blockPos = itemFramePos;
+
+ if (itemFrame.getX() % 0.5 == 0) {
+ blockPos = itemFramePos.add(realI, 0, 0);
+ } else if (itemFrame.getZ() % 0.5 == 0) {
+ blockPos = itemFramePos.add(0, 0, realI);
+ facing = 'Z';
+ }
+
+ Block block = world.getBlockState(blockPos).getBlock();
+ if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) {
+ leftmostRow = blockPos;
+ column = i;
+
+ break;
+ }
+ }
+
+ //Determine the row of the item frame
+ if (itemFrame.getY() == 72.5) {
+ row = 0;
+ } else if (itemFrame.getY() == 71.5) {
+ row = 1;
+ } else if (itemFrame.getY() == 70.5) {
+ row = 2;
+ } else {
+ continue;
+ }
+
+
+ //Get the color of the middle pixel of the map which determines whether its X or O
+ int middleColor = mapState.colors[8256] & 255;
+
+ if (middleColor == 114) {
+ board[row][column] = 'X';
+ } else if (middleColor == 33) {
+ board[row][column] = 'O';
+ }
+
+ int bestMove = TicTacToeUtils.getBestMove(board) - 1;
+
+ if (leftmostRow != null) {
+ double drawX = facing == 'X' ? leftmostRow.getX() - sign * (bestMove % 3) : leftmostRow.getX();
+ double drawY = 72 - (double) (bestMove / 3);
+ double drawZ = facing == 'Z' ? leftmostRow.getZ() - sign * (bestMove % 3) : leftmostRow.getZ();
+
+ nextBestMoveToMake = new Box(drawX, drawY, drawZ, drawX + 1, drawY + 1, drawZ + 1);
+ }
+ }
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while determining a tic tac toe solution!", e);
+ }
+ }
+
+ private static void solutionRenderer(WorldRenderContext context) {
+ try {
+ if (SkyblockerConfig.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) {
+ RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5);
+ }
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while rendering the tic tac toe solution!", e);
+ }
+ }
+}
diff --git a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java
new file mode 100644
index 00000000..b56b2827
--- /dev/null
+++ b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java
@@ -0,0 +1,275 @@
+package me.xmrvizzy.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.JsonObject;
+import it.unimi.dsi.fastutil.ints.IntSortedSet;
+import it.unimi.dsi.fastutil.objects.ObjectIntPair;
+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;
+import org.jetbrains.annotations.Nullable;
+import org.joml.RoundingMode;
+import org.joml.Vector2i;
+import org.joml.Vector2ic;
+
+import java.util.*;
+
+public class DungeonMapUtils {
+ public static final byte BLACK_COLOR = MapColor.BLACK.getRenderColorByte(MapColor.Brightness.LOWEST);
+ public static final byte WHITE_COLOR = MapColor.WHITE.getRenderColorByte(MapColor.Brightness.HIGH);
+
+ public static byte getColor(MapState map, @Nullable Vector2ic pos) {
+ return pos == null ? -1 : getColor(map, pos.x(), pos.y());
+ }
+
+ public static byte getColor(MapState map, int x, int z) {
+ if (x < 0 || z < 0 || x >= 128 || z >= 128) {
+ return -1;
+ }
+ return map.colors[x + (z << 7)];
+ }
+
+ public static boolean isEntranceColor(MapState map, int x, int z) {
+ return getColor(map, x, z) == Room.Type.ENTRANCE.color;
+ }
+
+ public static boolean isEntranceColor(MapState map, @Nullable Vector2ic pos) {
+ return getColor(map, pos) == Room.Type.ENTRANCE.color;
+ }
+
+ @Nullable
+ private static Vector2i getMapPlayerPos(MapState map) {
+ for (MapIcon icon : map.getIcons()) {
+ if (icon.getType() == MapIcon.Type.FRAME) {
+ return new Vector2i((icon.getX() >> 1) + 64, (icon.getZ() >> 1) + 64);
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ public static ObjectIntPair<Vector2ic> getMapEntrancePosAndRoomSize(@NotNull MapState map) {
+ Vector2ic mapPos = getMapPlayerPos(map);
+ if (mapPos == null) {
+ return null;
+ }
+ Queue<Vector2ic> posToCheck = new ArrayDeque<>();
+ Set<Vector2ic> checked = new HashSet<>();
+ posToCheck.add(mapPos);
+ checked.add(mapPos);
+ while ((mapPos = posToCheck.poll()) != null) {
+ if (isEntranceColor(map, mapPos)) {
+ ObjectIntPair<Vector2ic> mapEntranceAndRoomSizePos = getMapEntrancePosAndRoomSizeAt(map, mapPos);
+ if (mapEntranceAndRoomSizePos.rightInt() > 0) {
+ return mapEntranceAndRoomSizePos;
+ }
+ }
+ Vector2ic pos = new Vector2i(mapPos).sub(10, 0);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ pos = new Vector2i(mapPos).sub(0, 10);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ pos = new Vector2i(mapPos).add(10, 0);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ pos = new Vector2i(mapPos).add(0, 10);
+ if (checked.add(pos)) {
+ posToCheck.add(pos);
+ }
+ }
+ return null;
+ }
+
+ private static ObjectIntPair<Vector2ic> getMapEntrancePosAndRoomSizeAt(MapState map, Vector2ic mapPosImmutable) {
+ Vector2i mapPos = new Vector2i(mapPosImmutable);
+ // noinspection StatementWithEmptyBody
+ while (isEntranceColor(map, mapPos.sub(1, 0))) {
+ }
+ mapPos.add(1, 0);
+ //noinspection StatementWithEmptyBody
+ while (isEntranceColor(map, mapPos.sub(0, 1))) {
+ }
+ return ObjectIntPair.of(mapPos.add(0, 1), getMapRoomSize(map, mapPos));
+ }
+
+ public static int getMapRoomSize(MapState map, Vector2ic mapEntrancePos) {
+ int i = -1;
+ //noinspection StatementWithEmptyBody
+ while (isEntranceColor(map, mapEntrancePos.x() + ++i, mapEntrancePos.y())) {
+ }
+ return i > 5 ? i : 0;
+ }
+
+ /**
+ * Gets the map position of the top left corner of the room the player is in.
+ *
+ * @param map the map
+ * @param mapEntrancePos the map position of the top left corner of the entrance
+ * @param mapRoomSize the size of a room on the map
+ * @return the map position of the top left corner of the room the player is in
+ * @implNote {@code mapPos} is shifted by 2 so room borders are evenly split.
+ * {@code mapPos} is then shifted by {@code offset} to align the top left most room at (0, 0)
+ * so subtracting the modulo will give the top left corner of the room shifted by {@code offset}.
+ * Finally, {@code mapPos} is shifted back by {@code offset} to its intended position.
+ */
+ @Nullable
+ public static Vector2ic getMapRoomPos(MapState map, Vector2ic mapEntrancePos, int mapRoomSize) {
+ int mapRoomSizeWithGap = mapRoomSize + 4;
+ Vector2i mapPos = getMapPlayerPos(map);
+ if (mapPos == null) {
+ return null;
+ }
+ Vector2ic offset = new Vector2i(mapEntrancePos.x() % mapRoomSizeWithGap, mapEntrancePos.y() % mapRoomSizeWithGap);
+ return mapPos.add(2, 2).sub(offset).sub(mapPos.x() % mapRoomSizeWithGap, mapPos.y() % mapRoomSizeWithGap).add(offset);
+ }
+
+ /**
+ * Gets the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room.
+ *
+ * @param physicalEntrancePos the physical position of the northwest corner of the entrance room
+ * @param mapEntrancePos the map position of the top left corner of the entrance room
+ * @param mapRoomSize the size of a room on the map
+ * @param physicalPos the physical position of the northwest corner of the room
+ * @return the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room
+ */
+ public static Vector2ic getMapPosFromPhysical(Vector2ic physicalEntrancePos, Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalPos) {
+ return new Vector2i(physicalPos).sub(physicalEntrancePos).div(32).mul(mapRoomSize + 4).add(mapEntrancePos);
+ }
+
+ /**
+ * @see #getPhysicalRoomPos(double, double)
+ */
+ @NotNull
+ public static Vector2ic getPhysicalRoomPos(@NotNull Vec3d pos) {
+ return getPhysicalRoomPos(pos.getX(), pos.getZ());
+ }
+
+ /**
+ * @see #getPhysicalRoomPos(double, double)
+ */
+ @NotNull
+ public static Vector2ic getPhysicalRoomPos(@NotNull Vec3i pos) {
+ return getPhysicalRoomPos(pos.getX(), pos.getZ());
+ }
+
+ /**
+ * Gets the physical position of the northwest corner of the room the given coordinate is in. Hypixel Skyblock Dungeons are aligned to a 32 by 32 blocks grid, allowing corners to be calculated through math.
+ *
+ * @param x the x position of the coordinate to calculate
+ * @param z the z position of the coordinate to calculate
+ * @return the physical position of the northwest corner of the room the player is in
+ * @implNote {@code physicalPos} is shifted by 0.5 so room borders are evenly split.
+ * {@code physicalPos} is further shifted by 8 because Hypixel offset dungeons by 8 blocks in Skyblock 0.12.3.
+ * Subtracting the modulo gives the northwest corner of the room shifted by 8. Finally, {@code physicalPos} is shifted back by 8 to its intended position.
+ */
+ @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);
+ }
+
+ public static Vector2ic[] getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic... mapPositions) {
+ for (int i = 0; i < mapPositions.length; i++) {
+ mapPositions[i] = getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, mapPositions[i]);
+ }
+ return mapPositions;
+ }
+
+ /**
+ * Gets the physical position of the northwest corner of the room corresponding to the map position of the top left corner of a room.
+ *
+ * @param mapEntrancePos the map position of the top left corner of the entrance room
+ * @param mapRoomSize the size of a room on the map
+ * @param physicalEntrancePos the physical position of the northwest corner of the entrance room
+ * @param mapPos the map position of the top left corner of the room
+ * @return the physical position of the northwest corner of the room corresponding to the map position of the top left corner of a room
+ */
+ public static Vector2ic getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic mapPos) {
+ return new Vector2i(mapPos).sub(mapEntrancePos).div(mapRoomSize + 4).mul(32).add(physicalEntrancePos);
+ }
+
+ public static Vector2ic getPhysicalCornerPos(Room.Direction direction, IntSortedSet segmentsX, IntSortedSet segmentsY) {
+ return switch (direction) {
+ case NW -> new Vector2i(segmentsX.firstInt(), segmentsY.firstInt());
+ case NE -> new Vector2i(segmentsX.lastInt() + 30, segmentsY.firstInt());
+ case SW -> new Vector2i(segmentsX.firstInt(), segmentsY.lastInt() + 30);
+ case SE -> new Vector2i(segmentsX.lastInt() + 30, segmentsY.lastInt() + 30);
+ };
+ }
+
+ public static BlockPos actualToRelative(Room.Direction direction, Vector2ic physicalCornerPos, BlockPos pos) {
+ return switch (direction) {
+ case NW -> new BlockPos(pos.getX() - physicalCornerPos.x(), pos.getY(), pos.getZ() - physicalCornerPos.y());
+ case NE -> new BlockPos(pos.getZ() - physicalCornerPos.y(), pos.getY(), -pos.getX() + physicalCornerPos.x());
+ case SW -> new BlockPos(-pos.getZ() + physicalCornerPos.y(), pos.getY(), pos.getX() - physicalCornerPos.x());
+ case SE -> new BlockPos(-pos.getX() + physicalCornerPos.x(), pos.getY(), -pos.getZ() + physicalCornerPos.y());
+ };
+ }
+
+ public static BlockPos relativeToActual(Room.Direction direction, Vector2ic physicalCornerPos, JsonObject posJson) {
+ return relativeToActual(direction, physicalCornerPos, new BlockPos(posJson.get("x").getAsInt(), posJson.get("y").getAsInt(), posJson.get("z").getAsInt()));
+ }
+
+ public static BlockPos relativeToActual(Room.Direction direction, Vector2ic physicalCornerPos, BlockPos pos) {
+ return switch (direction) {
+ case NW -> new BlockPos(pos.getX() + physicalCornerPos.x(), pos.getY(), pos.getZ() + physicalCornerPos.y());
+ case NE -> new BlockPos(-pos.getZ() + physicalCornerPos.x(), pos.getY(), pos.getX() + physicalCornerPos.y());
+ case SW -> new BlockPos(pos.getZ() + physicalCornerPos.x(), pos.getY(), -pos.getX() + physicalCornerPos.y());
+ case SE -> new BlockPos(-pos.getX() + physicalCornerPos.x(), pos.getY(), -pos.getZ() + physicalCornerPos.y());
+ };
+ }
+
+ public static Room.Type getRoomType(MapState map, Vector2ic mapPos) {
+ return switch (getColor(map, mapPos)) {
+ case 30 -> Room.Type.ENTRANCE;
+ case 63 -> Room.Type.ROOM;
+ case 66 -> Room.Type.PUZZLE;
+ case 62 -> Room.Type.TRAP;
+ case 74 -> Room.Type.MINIBOSS;
+ case 82 -> Room.Type.FAIRY;
+ case 18 -> Room.Type.BLOOD;
+ case 85 -> Room.Type.UNKNOWN;
+ default -> null;
+ };
+ }
+
+ public static Vector2ic[] getRoomSegments(MapState map, Vector2ic mapPos, int mapRoomSize, byte color) {
+ Set<Vector2ic> segments = new HashSet<>();
+ Queue<Vector2ic> queue = new ArrayDeque<>();
+ segments.add(mapPos);
+ queue.add(mapPos);
+ while (!queue.isEmpty()) {
+ Vector2ic curMapPos = queue.poll();
+ Vector2i newMapPos = new Vector2i();
+ if (getColor(map, newMapPos.set(curMapPos).sub(1, 0)) == color && !segments.contains(newMapPos.sub(mapRoomSize + 3, 0))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ newMapPos = new Vector2i();
+ }
+ if (getColor(map, newMapPos.set(curMapPos).sub(0, 1)) == color && !segments.contains(newMapPos.sub(0, mapRoomSize + 3))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ newMapPos = new Vector2i();
+ }
+ if (getColor(map, newMapPos.set(curMapPos).add(mapRoomSize, 0)) == color && !segments.contains(newMapPos.add(4, 0))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ newMapPos = new Vector2i();
+ }
+ if (getColor(map, newMapPos.set(curMapPos).add(0, mapRoomSize)) == color && !segments.contains(newMapPos.add(0, 4))) {
+ segments.add(newMapPos);
+ queue.add(newMapPos);
+ }
+ }
+ DungeonSecrets.LOGGER.debug("[Skyblocker] Found dungeon room segments: {}", Arrays.toString(segments.toArray()));
+ return segments.toArray(Vector2ic[]::new);
+ }
+}
diff --git a/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java
new file mode 100644
index 00000000..c916a5e4
--- /dev/null
+++ b/src/main/java/me/xmrvizzy/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java
@@ -0,0 +1,427 @@
+package me.xmrvizzy.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+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 me.xmrvizzy.skyblocker.SkyblockerMod;
+import me.xmrvizzy.skyblocker.config.SkyblockerConfig;
+import me.xmrvizzy.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+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;
+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.entity.ItemEntity;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.item.FilledMapItem;
+import net.minecraft.item.ItemStack;
+import net.minecraft.item.Items;
+import net.minecraft.item.map.MapState;
+import net.minecraft.resource.Resource;
+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.math.Vec3d;
+import net.minecraft.world.World;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.joml.Vector2ic;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.zip.InflaterInputStream;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument;
+import static n