From d23c1acb7e416b67d40982e6e50968e1c23cf799 Mon Sep 17 00:00:00 2001
From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>
Date: Fri, 10 Nov 2023 22:55:56 -0500
Subject: Fix room matching and secret index parsing bug

---
 .../skyblocker/skyblock/dungeon/secrets/Room.java  | 114 +++++++++++++--------
 .../skyblocker/dungeons/secretlocations.json       |   4 +-
 2 files changed, 71 insertions(+), 47 deletions(-)

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 ecfcf496..c9b32be9 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
@@ -41,6 +41,7 @@ import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public class Room {
+    private static final Pattern SECRET_INDEX = Pattern.compile("^(\\d+)");
     private static final Pattern SECRETS = Pattern.compile("§7(\\d{1,2})/(\\d{1,2}) Secrets");
     @NotNull
     private final Type type;
@@ -70,11 +71,12 @@ public class Room {
     private int doubleCheckBlocks;
     /**
      * Represents the matching state of the room with the following possible values:
-     * <li>{@link TriState#DEFAULT} means that the room has not been checked, is being processed, or does not {@link Type#needsScanning() need to be processed}.
-     * <li>{@link TriState#FALSE} means that the room has been checked and there is no match.
-     * <li>{@link TriState#TRUE} means that the room has been checked and there is a match.
+     * <li>{@link MatchState#MATCHING} means that the room has not been checked, is being processed, or does not {@link Type#needsScanning() need to be processed}.</li>
+     * <li>{@link MatchState#DOUBLE_CHECKING} means that the room has a unique match and is being double checked.</li>
+     * <li>{@link MatchState#MATCHED} means that the room has a unique match ans has been double checked.</li>
+     * <li>{@link MatchState#FAILED} means that the room has been checked and there is no match.</li>
      */
-    private TriState matched = TriState.DEFAULT;
+    private MatchState matchState = MatchState.MATCHING;
     private Table<Integer, BlockPos, SecretWaypoint> secretWaypoints;
     private String name;
     private Direction direction;
@@ -96,7 +98,7 @@ public class Room {
     }
 
     public boolean isMatched() {
-        return matched == TriState.TRUE;
+        return matchState == MatchState.DOUBLE_CHECKING || matchState == MatchState.MATCHED;
     }
 
     /**
@@ -108,7 +110,7 @@ public class Room {
 
     @Override
     public String toString() {
-        return "Room{type=" + type + ", shape=" + shape + ", matched=" + matched + ", segments=" + Arrays.toString(segments.toArray()) + "}";
+        return "Room{type=" + type + ", shape=" + shape + ", matchState=" + matchState + ", segments=" + Arrays.toString(segments.toArray()) + "}";
     }
 
     @NotNull
@@ -208,6 +210,7 @@ public class Room {
 
     /**
      * 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
      */
@@ -223,6 +226,7 @@ public class Room {
 
     /**
      * 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
      */
@@ -237,7 +241,7 @@ public class Room {
      * This method returns immediately if any of the following conditions are met:
      * <ul>
      *     <li> The room does not need to be scanned and matched. (When the room is not of type {@link Type.ROOM}, {@link Type.PUZZLE}, or {@link Type.TRAP}. See {@link Type#needsScanning()}) </li>
-     *     <li> The room has been matched or failed to match and is on cooldown. See {@link #matched}. </li>
+     *     <li> The room has been matched or failed to match and is on cooldown. See {@link #matchState}. </li>
      *     <li> {@link #findRoom The previous update} has not completed. </li>
      * </ul>
      * Then this method tries to match this room through:
@@ -251,7 +255,7 @@ public class Room {
     @SuppressWarnings("JavadocReference")
     protected void update() {
         // Logical AND has higher precedence than logical OR
-        if (!type.needsScanning() || matched != TriState.DEFAULT || !DungeonSecrets.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) {
+        if (!type.needsScanning() || matchState != MatchState.MATCHING && matchState != MatchState.DOUBLE_CHECKING || !DungeonSecrets.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) {
             return;
         }
         MinecraftClient client = MinecraftClient.getInstance();
@@ -266,6 +270,9 @@ public class Room {
                     break;
                 }
             }
+        }).exceptionally(e -> {
+            DungeonSecrets.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown error while matching room {}", this, e);
+            return null;
         });
     }
 
@@ -297,16 +304,24 @@ public class Room {
      *     </ul>
      *     <li> If there are no matching rooms left: </li>
      *     <ul>
-     *         <li> Terminate matching by setting {@link #matched} to {@link TriState#FALSE}. </li>
+     *         <li> Terminate matching by setting {@link #matchState} to {@link TriState#FALSE}. </li>
      *         <li> Schedule another matching attempt in 50 ticks (2.5 seconds). </li>
      *         <li> Reset {@link #possibleRooms} and {@link #checkedBlocks} with {@link #reset()}. </li>
      *         <li> Return {@code true} </li>
      *     </ul>
      *     <li> If there are exactly one room matching: </li>
      *     <ul>
-     *         <li> Call {@link #roomMatched()}. </li>
-     *         <li> Discard the no longer needed fields to save memory. </li>
-     *         <li> Return {@code true} </li>
+     *         <li> If {@link #matchState} is {@link MatchState#MATCHING}: </li>
+     *         <ul>
+     *             <li> Call {@link #roomMatched()}. </li>
+     *             <li> Return {@code false}. </li>
+     *         </ul>
+     *         <li> If {@link #matchState} is {@link MatchState#DOUBLE_CHECKING}: </li>
+     *         <ul>
+     *             <li> Set the match state to {@link MatchState#MATCHED}. </li>
+     *             <li> Discard the no longer needed fields to save memory. </li>
+     *             <li> Return {@code true}. </li>
+     *         </ul>
      *     </ul>
      *     <li> Return {@code false} </li>
      * </ul>
@@ -320,40 +335,39 @@ public class Room {
         if (id == 0) {
             return false;
         }
-        for (MutableTriple<Direction, Vector2ic, List<String>> directionRooms : possibleRooms) {
+        possibleRooms.removeIf(directionRooms -> {
             int block = posIdToInt(DungeonMapUtils.actualToRelative(directionRooms.getLeft(), directionRooms.getMiddle(), pos), id);
-            List<String> possibleDirectionRooms = new ArrayList<>();
-            for (String room : directionRooms.getRight()) {
-                if (Arrays.binarySearch(roomsData.get(room), block) >= 0) {
-                    possibleDirectionRooms.add(room);
-                }
-            }
-            directionRooms.setRight(possibleDirectionRooms);
-        }
+            directionRooms.getRight().removeIf(room -> Arrays.binarySearch(roomsData.get(room), block) < 0);
+            return directionRooms.getRight().isEmpty();
+        });
 
         int matchingRoomsSize = possibleRooms.stream().map(Triple::getRight).mapToInt(Collection::size).sum();
         if (matchingRoomsSize == 0) {
             // If no rooms match, reset the fields and scan again after 50 ticks.
-            matched = TriState.FALSE;
-            DungeonSecrets.LOGGER.warn("[Skyblocker] No dungeon room matches after checking {} block(s)", checkedBlocks.size());
-            Scheduler.INSTANCE.schedule(() -> matched = TriState.DEFAULT, 50);
+            DungeonSecrets.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks + 1);
+            Scheduler.INSTANCE.schedule(() -> matchState = MatchState.MATCHING, 50);
             reset();
             return true;
-        } else if (matchingRoomsSize == 1 && ++doubleCheckBlocks >= 10) {
-            // 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) {
-                    name = directionRooms.getRight().get(0);
-                    direction = directionRooms.getLeft();
-                    physicalCornerPos = directionRooms.getMiddle();
-                    roomMatched();
-                    discard();
-                    return true;
-                }
+        } else if (matchingRoomsSize == 1) {
+            if (matchState == MatchState.MATCHING) {
+                // If one room matches, load the secrets for that room and set state to double-checking.
+                assert possibleRooms.size() == 1;
+                Triple<Direction, Vector2ic, List<String>> directionRoom = possibleRooms.get(0);
+                assert directionRoom.getRight().size() == 1;
+                name = directionRoom.getRight().get(0);
+                direction = directionRoom.getLeft();
+                physicalCornerPos = directionRoom.getMiddle();
+                roomMatched();
+                return false;
+            } else if (matchState == MatchState.DOUBLE_CHECKING && ++doubleCheckBlocks >= 10) {
+                // If double-checked, set state to matched and discard the no longer needed fields.
+                DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks);
+                discard();
+                return true;
             }
-            return false; // This should never happen, we just checked that there is one possible room, and the return true in the loop should activate
+            return false;
         } else {
-            DungeonSecrets.LOGGER.debug("[Skyblocker] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size());
+            DungeonSecrets.LOGGER.debug("[Skyblocker Dungeon Secrets] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size());
             return false;
         }
     }
@@ -371,7 +385,7 @@ public class Room {
 
     /**
      * Loads the secret waypoints for the room from {@link DungeonSecrets#waypointsJson} once it has been matched
-     * and sets {@link #matched} to {@link TriState#TRUE}.
+     * and sets {@link #matchState} to {@link MatchState#DOUBLE_CHECKING}.
      *
      * @param directionRooms the direction, position, and name of the room
      */
@@ -381,25 +395,30 @@ public class Room {
         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));
+            Matcher secretIndexMatcher = SECRET_INDEX.matcher(secretName);
+            int secretIndex = secretIndexMatcher.find() ? Integer.parseInt(secretIndexMatcher.group(1)) : 0;
             BlockPos pos = DungeonMapUtils.relativeToActual(direction, physicalCornerPos, waypoint);
             secretWaypoints.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos));
         }
         DungeonSecrets.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint);
-        matched = TriState.TRUE;
-
-        DungeonSecrets.LOGGER.info("[Skyblocker] Room {} matched after checking {} block(s)", name, checkedBlocks.size());
+        matchState = MatchState.DOUBLE_CHECKING;
+        DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s), starting double checking", name, checkedBlocks.size());
     }
 
     /**
      * Resets fields for another round of matching after room matching fails.
      */
     private void reset() {
+        matchState = MatchState.FAILED;
         IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::x).toArray()));
         IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::y).toArray()));
         possibleRooms = getPossibleRooms(segmentsX, segmentsY);
         checkedBlocks = new HashSet<>();
         doubleCheckBlocks = 0;
+        secretWaypoints = null;
+        name = null;
+        direction = null;
+        physicalCornerPos = null;
     }
 
     /**
@@ -407,6 +426,7 @@ public class Room {
      * These fields are no longer needed and are discarded to save memory.
      */
     private void discard() {
+        matchState = MatchState.MATCHED;
         roomsData = null;
         possibleRooms = null;
         checkedBlocks = null;
@@ -473,7 +493,7 @@ public class Room {
         BlockState state = world.getBlockState(hitResult.getBlockPos());
         if (state.isOf(Blocks.CHEST) || state.isOf(Blocks.PLAYER_HEAD) || state.isOf(Blocks.PLAYER_WALL_HEAD)) {
             secretWaypoints.column(hitResult.getBlockPos()).values().stream().filter(SecretWaypoint::needsInteraction).findAny()
-                    .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker] Detected {} interaction, setting secret #{} as found", secretWaypoint.category, secretWaypoint.secretIndex));
+                    .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker Dungeon Secrets] Detected {} interaction, setting secret #{} as found", secretWaypoint.category, secretWaypoint.secretIndex));
         } else if (state.isOf(Blocks.LEVER)) {
             secretWaypoints.column(hitResult.getBlockPos()).values().stream().filter(SecretWaypoint::isLever).forEach(SecretWaypoint::setFound);
         }
@@ -491,7 +511,7 @@ public class Room {
             return;
         }
         secretWaypoints.values().stream().filter(SecretWaypoint::needsItemPickup).min(Comparator.comparingDouble(SecretWaypoint.getSquaredDistanceToFunction(collector))).filter(SecretWaypoint.getRangePredicate(collector))
-                .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker] Detected {} picked up a {} from a {} secret, setting secret #{} as found", collector.getName().getString(), itemEntity.getName().getString(), secretWaypoint.category, secretWaypoint.secretIndex));
+                .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker Dungeon Secrets] Detected {} picked up a {} from a {} secret, setting secret #{} as found", collector.getName().getString(), itemEntity.getName().getString(), secretWaypoint.category, secretWaypoint.secretIndex));
     }
 
     /**
@@ -502,7 +522,7 @@ public class Room {
      */
     protected void onBatRemoved(AmbientEntity bat) {
         secretWaypoints.values().stream().filter(SecretWaypoint::isBat).min(Comparator.comparingDouble(SecretWaypoint.getSquaredDistanceToFunction(bat)))
-                .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker] Detected {} killed for a {} secret, setting secret #{} as found", bat.getName().getString(), secretWaypoint.category, secretWaypoint.secretIndex));
+                .ifPresent(secretWaypoint -> onSecretFound(secretWaypoint, "[Skyblocker Dungeon Secrets] Detected {} killed for a {} secret, setting secret #{} as found", bat.getName().getString(), secretWaypoint.category, secretWaypoint.secretIndex));
     }
 
     /**
@@ -575,4 +595,8 @@ public class Room {
     public enum Direction {
         NW, NE, SW, SE
     }
+
+    public enum MatchState {
+        MATCHING, DOUBLE_CHECKING, MATCHED, FAILED
+    }
 }
diff --git a/src/main/resources/assets/skyblocker/dungeons/secretlocations.json b/src/main/resources/assets/skyblocker/dungeons/secretlocations.json
index a0a97c67..02e19632 100644
--- a/src/main/resources/assets/skyblocker/dungeons/secretlocations.json
+++ b/src/main/resources/assets/skyblocker/dungeons/secretlocations.json
@@ -2173,7 +2173,7 @@
     },
     {
       "secretName":"3 - Lever 2",
-      "category":"Lever",
+      "category":"lever",
       "x":31,
       "y":53,
       "z":24
@@ -2375,7 +2375,7 @@
     },
     {
       "secretName":"2 - Item",
-      "category":"Item",
+      "category":"item",
       "x":27,
       "y":56,
       "z":19
-- 
cgit 


From d07ad6a62ae84e361b3723017fc173f616f49d0e Mon Sep 17 00:00:00 2001
From: Aaron <51387595+AzureAaron@users.noreply.github.com>
Date: Sat, 11 Nov 2023 02:31:36 -0500
Subject: Refactor MobGlow

---
 .../java/de/hysky/skyblocker/entity/MobGlow.java   | 67 ++++++++++++++++++++++
 .../hysky/skyblocker/mixin/WorldRendererMixin.java |  2 +-
 .../hysky/skyblocker/skyblock/dungeon/MobGlow.java | 66 ---------------------
 3 files changed, 68 insertions(+), 67 deletions(-)
 create mode 100644 src/main/java/de/hysky/skyblocker/entity/MobGlow.java
 delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/MobGlow.java

diff --git a/src/main/java/de/hysky/skyblocker/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/entity/MobGlow.java
new file mode 100644
index 00000000..f60a1bfe
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/entity/MobGlow.java
@@ -0,0 +1,67 @@
+package de.hysky.skyblocker.entity;
+
+import java.util.List;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.dungeon.LividColor;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.entity.passive.BatEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.util.math.Box;
+import net.minecraft.world.World;
+
+public class MobGlow {
+	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)) {
+			String name = entity.getName().getString();
+
+			// Minibosses
+			if (entity instanceof PlayerEntity) {
+				switch (name) {
+					case "Lost Adventurer", "Shadow Assassin", "Diamond Guy": return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
+					case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
+					"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid": return LividColor.shouldGlow(name);
+				}
+			}
+
+			// Regular Mobs
+			if (!(entity instanceof ArmorStandEntity)) {
+				List<ArmorStandEntity> armorStands = getArmorStands(entity.getWorld(), box);
+
+				if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
+			}
+
+			// Bats
+			return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow && entity instanceof BatEntity;
+		}
+
+		return false;
+	}
+
+	private static List<ArmorStandEntity> getArmorStands(World world, Box box) {
+        return world.getEntitiesByClass(ArmorStandEntity.class, box.expand(0, 2, 0), EntityPredicates.NOT_MOUNTED);
+	}
+
+	public static int getGlowColor(Entity entity) {
+		String name = entity.getName().getString();
+
+		if (entity instanceof PlayerEntity) {
+			return switch (name) {
+				case "Lost Adventurer" -> 0xfee15c;
+				case "Shadow Assassin" -> 0x5b2cb2;
+				case "Diamond Guy" -> 0x57c2f7;
+				case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
+				"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid" -> LividColor.getGlowColor(name);
+				default -> 0xf57738;
+			};
+		}
+
+		return 0xf57738;
+	}
+}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java b/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
index 78c61416..03c8f93f 100644
--- a/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
@@ -9,7 +9,7 @@ import com.llamalad7.mixinextras.sugar.Local;
 import com.llamalad7.mixinextras.sugar.Share;
 import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
 
-import de.hysky.skyblocker.skyblock.dungeon.MobGlow;
+import de.hysky.skyblocker.entity.MobGlow;
 import net.minecraft.client.render.WorldRenderer;
 import net.minecraft.entity.Entity;
 
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/MobGlow.java
deleted file mode 100644
index 523b7a98..00000000
--- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/MobGlow.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package de.hysky.skyblocker.skyblock.dungeon;
-
-import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.utils.Utils;
-import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
-import net.minecraft.entity.Entity;
-import net.minecraft.entity.decoration.ArmorStandEntity;
-import net.minecraft.entity.passive.BatEntity;
-import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.predicate.entity.EntityPredicates;
-import net.minecraft.util.math.Box;
-import net.minecraft.world.World;
-
-import java.util.List;
-
-public class MobGlow {
-	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)) {
-			String name = entity.getName().getString();
-
-			// Minibosses
-			if (entity instanceof PlayerEntity) {
-				switch (name) {
-					case "Lost Adventurer", "Shadow Assassin", "Diamond Guy": return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
-					case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
-					"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid": return LividColor.shouldGlow(name);
-				}
-			}
-
-			// Regular Mobs
-			if (!(entity instanceof ArmorStandEntity)) {
-				List<ArmorStandEntity> armorStands = getArmorStands(entity.getWorld(), box);
-
-				if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
-			}
-
-			// Bats
-			return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow && entity instanceof BatEntity;
-		}
-
-		return false;
-	}
-
-	private static List<ArmorStandEntity> getArmorStands(World world, Box box) {
-        return world.getEntitiesByClass(ArmorStandEntity.class, box.expand(0, 2, 0), EntityPredicates.NOT_MOUNTED);
-	}
-
-	public static int getGlowColor(Entity entity) {
-		String name = entity.getName().getString();
-
-		if (entity instanceof PlayerEntity) {
-			return switch (name) {
-				case "Lost Adventurer" -> 0xfee15c;
-				case "Shadow Assassin" -> 0x5b2cb2;
-				case "Diamond Guy" -> 0x57c2f7;
-				case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
-				"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid" -> LividColor.getGlowColor(name);
-				default -> 0xf57738;
-			};
-		}
-
-		return 0xf57738;
-	}
-}
-- 
cgit 


From 3873256a79589ccff9d100b51152f4d1fd508c24 Mon Sep 17 00:00:00 2001
From: Aaron <51387595+AzureAaron@users.noreply.github.com>
Date: Sat, 11 Nov 2023 03:23:01 -0500
Subject: Blobbercyst Glow

---
 .../java/de/hysky/skyblocker/SkyblockerMod.java    |  2 +
 .../hysky/skyblocker/config/SkyblockerConfig.java  |  3 ++
 .../config/categories/LocationsCategory.java       |  8 ++++
 src/main/java/de/hysky/skyblocker/debug/Debug.java | 13 +++++++
 .../hysky/skyblocker/debug/DumpPlayersCommand.java | 34 +++++++++++++++++
 .../java/de/hysky/skyblocker/entity/MobGlow.java   | 44 +++++++++++++++-------
 .../resources/assets/skyblocker/lang/en_us.json    |  2 +
 7 files changed, 92 insertions(+), 14 deletions(-)
 create mode 100644 src/main/java/de/hysky/skyblocker/debug/Debug.java
 create mode 100644 src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java

diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
index b398e9b6..65404c6c 100644
--- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
+++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java
@@ -3,6 +3,7 @@ package de.hysky.skyblocker;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.debug.Debug;
 import de.hysky.skyblocker.skyblock.*;
 import de.hysky.skyblocker.skyblock.diana.MythologicalRitual;
 import de.hysky.skyblocker.skyblock.dungeon.*;
@@ -113,6 +114,7 @@ public class SkyblockerMod implements ClientModInitializer {
         MuseumItemCache.init();
         SecretsTracker.init();
         ApiUtils.init();
+        Debug.init();
         containerSolverManager.init();
         statusBarTracker.init();
         Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
index 699f91ef..974c451d 100644
--- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
+++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java
@@ -759,6 +759,9 @@ public class SkyblockerConfig {
 		@SerialEntry
 		public boolean mirrorverseWaypoints = true;
 
+		@SerialEntry
+		public boolean blobbercystGlow = true;
+
 		@SerialEntry
 		public boolean enigmaSoulWaypoints = false;
 
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java
index 55629a66..0b388d16 100644
--- a/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java
+++ b/src/main/java/de/hysky/skyblocker/config/categories/LocationsCategory.java
@@ -46,6 +46,14 @@ public class LocationsCategory {
 										newValue -> config.locations.rift.mirrorverseWaypoints = newValue)
 								.controller(ConfigUtils::createBooleanController)
 								.build())
+						.option(Option.<Boolean>createBuilder()
+								.name(Text.translatable("text.autoconfig.skyblocker.option.locations.rift.blobbercystGlow"))
+								.description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.rift.blobbercystGlow.@Tooltip")))
+								.binding(defaults.locations.rift.blobbercystGlow,
+										() -> config.locations.rift.blobbercystGlow,
+										newValue -> config.locations.rift.blobbercystGlow = newValue)
+								.controller(ConfigUtils::createBooleanController)
+								.build())
 						.option(Option.<Boolean>createBuilder()
 								.name(Text.translatable("text.autoconfig.skyblocker.option.locations.rift.enigmaSoulWaypoints"))
 								.description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.rift.enigmaSoulWaypoints.@Tooltip")))
diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java
new file mode 100644
index 00000000..5991f72c
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java
@@ -0,0 +1,13 @@
+package de.hysky.skyblocker.debug;
+
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+
+public class Debug {
+	private static final boolean DEBUG_ENABLED = Boolean.parseBoolean(System.getProperty("skyblocker.debug", "false"));
+
+	public static void init() {
+		if (DEBUG_ENABLED) {
+			ClientCommandRegistrationCallback.EVENT.register(DumpPlayersCommand::register);
+		}
+	}
+}
diff --git a/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java b/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java
new file mode 100644
index 00000000..598672ef
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java
@@ -0,0 +1,34 @@
+package de.hysky.skyblocker.debug;
+
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+import com.mojang.brigadier.Command;
+import com.mojang.brigadier.CommandDispatcher;
+
+import de.hysky.skyblocker.SkyblockerMod;
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.command.CommandRegistryAccess;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.text.Text;
+
+public class DumpPlayersCommand {
+	
+	static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
+		dispatcher.register(literal(SkyblockerMod.NAMESPACE)
+				.then(literal("debug")
+						.then(literal("dumpPlayers")
+								.executes(context -> {
+									FabricClientCommandSource source = context.getSource();
+									MinecraftClient client = source.getClient();
+									
+									client.world.getEntities().forEach(e -> {
+										if (e instanceof PlayerEntity player) {
+											source.sendFeedback(Text.of("\"" + player.getName().getString() + "\""));
+										}
+									});
+									
+									return Command.SINGLE_SUCCESS;
+								}))));
+	}
+}
diff --git a/src/main/java/de/hysky/skyblocker/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/entity/MobGlow.java
index f60a1bfe..0c7dd3f4 100644
--- a/src/main/java/de/hysky/skyblocker/entity/MobGlow.java
+++ b/src/main/java/de/hysky/skyblocker/entity/MobGlow.java
@@ -11,6 +11,7 @@ import net.minecraft.entity.decoration.ArmorStandEntity;
 import net.minecraft.entity.passive.BatEntity;
 import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.util.Formatting;
 import net.minecraft.util.math.Box;
 import net.minecraft.world.World;
 
@@ -18,27 +19,41 @@ public class MobGlow {
 	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)) {
+		if (!entity.isInvisible() && OcclusionCulling.isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
 			String name = entity.getName().getString();
 
-			// Minibosses
-			if (entity instanceof PlayerEntity) {
-				switch (name) {
-					case "Lost Adventurer", "Shadow Assassin", "Diamond Guy": return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
-					case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
-					"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid": return LividColor.shouldGlow(name);
+			// Dungeons
+			if (Utils.isInDungeons()) {
+
+				// Minibosses
+				if (entity instanceof PlayerEntity) {
+					switch (name) {
+						case "Lost Adventurer", "Shadow Assassin", "Diamond Guy": return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
+						case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
+						"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid": return LividColor.shouldGlow(name);
+					}
 				}
-			}
 
-			// Regular Mobs
-			if (!(entity instanceof ArmorStandEntity)) {
-				List<ArmorStandEntity> armorStands = getArmorStands(entity.getWorld(), box);
+				// Regular Mobs
+				if (!(entity instanceof ArmorStandEntity)) {
+					List<ArmorStandEntity> armorStands = getArmorStands(entity.getWorld(), box);
+
+					if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
+				}
 
-				if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
+				// Bats
+				return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow && entity instanceof BatEntity;
 			}
 
-			// Bats
-			return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow && entity instanceof BatEntity;
+			// Rift
+			if (Utils.isInTheRift()) {
+				if (entity instanceof PlayerEntity) {
+					switch (name) {
+						// They have a space in their name for some reason...
+						case "Blobbercyst ": return SkyblockerConfigManager.get().locations.rift.blobbercystGlow;
+					}
+				}
+			}
 		}
 
 		return false;
@@ -58,6 +73,7 @@ public class MobGlow {
 				case "Diamond Guy" -> 0x57c2f7;
 				case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
 				"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid" -> LividColor.getGlowColor(name);
+				case "Blobbercyst " -> Formatting.GREEN.getColorValue();
 				default -> 0xf57738;
 			};
 		}
diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json
index 376e2bdf..d1a6e0bd 100644
--- a/src/main/resources/assets/skyblocker/lang/en_us.json
+++ b/src/main/resources/assets/skyblocker/lang/en_us.json
@@ -228,6 +228,8 @@
 
   "text.autoconfig.skyblocker.option.locations.rift": "The Rift",
   "text.autoconfig.skyblocker.option.locations.rift.mirrorverseWaypoints": "Enable Mirrorverse Waypoints",
+  "text.autoconfig.skyblocker.option.locations.rift.blobbercystGlow": "Blobbercyst Glow",
+  "text.autoconfig.skyblocker.option.locations.rift.blobbercystGlow.@Tooltip": "Applies the glowing effect to the Blobbercysts from the BACTE fight.",
   "text.autoconfig.skyblocker.option.locations.rift.enigmaSoulWaypoints": "Enable Enigma Soul Waypoints",
   "text.autoconfig.skyblocker.option.locations.rift.enigmaSoulWaypoints.@Tooltip": "Note: Many enigma souls have a small task you must complete in order to get it, so its recommended to also watch a YouTube video when finding them.",
   "text.autoconfig.skyblocker.option.locations.rift.highlightFoundEnigmaSouls": "Highlight Found Enigma Souls",
-- 
cgit 


From a9aa80a1764054eda7fef37417aed868130ad890 Mon Sep 17 00:00:00 2001
From: Aaron <51387595+AzureAaron@users.noreply.github.com>
Date: Sat, 11 Nov 2023 03:25:13 -0500
Subject: Repackage MobGlow

I forgot to put the new entity package under skyblock
---
 .../java/de/hysky/skyblocker/entity/MobGlow.java   | 83 ----------------------
 .../hysky/skyblocker/mixin/WorldRendererMixin.java |  2 +-
 .../hysky/skyblocker/skyblock/entity/MobGlow.java  | 83 ++++++++++++++++++++++
 3 files changed, 84 insertions(+), 84 deletions(-)
 delete mode 100644 src/main/java/de/hysky/skyblocker/entity/MobGlow.java
 create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java

diff --git a/src/main/java/de/hysky/skyblocker/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/entity/MobGlow.java
deleted file mode 100644
index 0c7dd3f4..00000000
--- a/src/main/java/de/hysky/skyblocker/entity/MobGlow.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package de.hysky.skyblocker.entity;
-
-import java.util.List;
-
-import de.hysky.skyblocker.config.SkyblockerConfigManager;
-import de.hysky.skyblocker.skyblock.dungeon.LividColor;
-import de.hysky.skyblocker.utils.Utils;
-import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
-import net.minecraft.entity.Entity;
-import net.minecraft.entity.decoration.ArmorStandEntity;
-import net.minecraft.entity.passive.BatEntity;
-import net.minecraft.entity.player.PlayerEntity;
-import net.minecraft.predicate.entity.EntityPredicates;
-import net.minecraft.util.Formatting;
-import net.minecraft.util.math.Box;
-import net.minecraft.world.World;
-
-public class MobGlow {
-	public static boolean shouldMobGlow(Entity entity) {
-		Box box = entity.getBoundingBox();
-
-		if (!entity.isInvisible() && OcclusionCulling.isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
-			String name = entity.getName().getString();
-
-			// Dungeons
-			if (Utils.isInDungeons()) {
-
-				// Minibosses
-				if (entity instanceof PlayerEntity) {
-					switch (name) {
-						case "Lost Adventurer", "Shadow Assassin", "Diamond Guy": return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
-						case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
-						"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid": return LividColor.shouldGlow(name);
-					}
-				}
-
-				// Regular Mobs
-				if (!(entity instanceof ArmorStandEntity)) {
-					List<ArmorStandEntity> armorStands = getArmorStands(entity.getWorld(), box);
-
-					if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
-				}
-
-				// Bats
-				return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow && entity instanceof BatEntity;
-			}
-
-			// Rift
-			if (Utils.isInTheRift()) {
-				if (entity instanceof PlayerEntity) {
-					switch (name) {
-						// They have a space in their name for some reason...
-						case "Blobbercyst ": return SkyblockerConfigManager.get().locations.rift.blobbercystGlow;
-					}
-				}
-			}
-		}
-
-		return false;
-	}
-
-	private static List<ArmorStandEntity> getArmorStands(World world, Box box) {
-        return world.getEntitiesByClass(ArmorStandEntity.class, box.expand(0, 2, 0), EntityPredicates.NOT_MOUNTED);
-	}
-
-	public static int getGlowColor(Entity entity) {
-		String name = entity.getName().getString();
-
-		if (entity instanceof PlayerEntity) {
-			return switch (name) {
-				case "Lost Adventurer" -> 0xfee15c;
-				case "Shadow Assassin" -> 0x5b2cb2;
-				case "Diamond Guy" -> 0x57c2f7;
-				case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
-				"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid" -> LividColor.getGlowColor(name);
-				case "Blobbercyst " -> Formatting.GREEN.getColorValue();
-				default -> 0xf57738;
-			};
-		}
-
-		return 0xf57738;
-	}
-}
diff --git a/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java b/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
index 03c8f93f..42601546 100644
--- a/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
+++ b/src/main/java/de/hysky/skyblocker/mixin/WorldRendererMixin.java
@@ -9,7 +9,7 @@ import com.llamalad7.mixinextras.sugar.Local;
 import com.llamalad7.mixinextras.sugar.Share;
 import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
 
-import de.hysky.skyblocker.entity.MobGlow;
+import de.hysky.skyblocker.skyblock.entity.MobGlow;
 import net.minecraft.client.render.WorldRenderer;
 import net.minecraft.entity.Entity;
 
diff --git a/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
new file mode 100644
index 00000000..5e0995e6
--- /dev/null
+++ b/src/main/java/de/hysky/skyblocker/skyblock/entity/MobGlow.java
@@ -0,0 +1,83 @@
+package de.hysky.skyblocker.skyblock.entity;
+
+import java.util.List;
+
+import de.hysky.skyblocker.config.SkyblockerConfigManager;
+import de.hysky.skyblocker.skyblock.dungeon.LividColor;
+import de.hysky.skyblocker.utils.Utils;
+import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
+import net.minecraft.entity.Entity;
+import net.minecraft.entity.decoration.ArmorStandEntity;
+import net.minecraft.entity.passive.BatEntity;
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.predicate.entity.EntityPredicates;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.Box;
+import net.minecraft.world.World;
+
+public class MobGlow {
+	public static boolean shouldMobGlow(Entity entity) {
+		Box box = entity.getBoundingBox();
+
+		if (!entity.isInvisible() && OcclusionCulling.isVisible(box.minX, box.minY, box.minZ, box.maxX, box.maxY, box.maxZ)) {
+			String name = entity.getName().getString();
+
+			// Dungeons
+			if (Utils.isInDungeons()) {
+
+				// Minibosses
+				if (entity instanceof PlayerEntity) {
+					switch (name) {
+						case "Lost Adventurer", "Shadow Assassin", "Diamond Guy": return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
+						case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
+						"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid": return LividColor.shouldGlow(name);
+					}
+				}
+
+				// Regular Mobs
+				if (!(entity instanceof ArmorStandEntity)) {
+					List<ArmorStandEntity> armorStands = getArmorStands(entity.getWorld(), box);
+
+					if (!armorStands.isEmpty() && armorStands.get(0).getName().getString().contains("✯")) return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow;
+				}
+
+				// Bats
+				return SkyblockerConfigManager.get().locations.dungeons.starredMobGlow && entity instanceof BatEntity;
+			}
+
+			// Rift
+			if (Utils.isInTheRift()) {
+				if (entity instanceof PlayerEntity) {
+					switch (name) {
+						// They have a space in their name for some reason...
+						case "Blobbercyst ": return SkyblockerConfigManager.get().locations.rift.blobbercystGlow;
+					}
+				}
+			}
+		}
+
+		return false;
+	}
+
+	private static List<ArmorStandEntity> getArmorStands(World world, Box box) {
+        return world.getEntitiesByClass(ArmorStandEntity.class, box.expand(0, 2, 0), EntityPredicates.NOT_MOUNTED);
+	}
+
+	public static int getGlowColor(Entity entity) {
+		String name = entity.getName().getString();
+
+		if (entity instanceof PlayerEntity) {
+			return switch (name) {
+				case "Lost Adventurer" -> 0xfee15c;
+				case "Shadow Assassin" -> 0x5b2cb2;
+				case "Diamond Guy" -> 0x57c2f7;
+				case "Arcade Livid", "Crossed Livid", "Doctor Livid", "Frog Livid", "Hockey Livid",
+				"Purple Livid", "Scream Livid", "Smile Livid", "Vendetta Livid" -> LividColor.getGlowColor(name);
+				case "Blobbercyst " -> Formatting.GREEN.getColorValue();
+				default -> 0xf57738;
+			};
+		}
+
+		return 0xf57738;
+	}
+}
-- 
cgit 


From 2abfcece13208818ae86dd83f44a257daba23506 Mon Sep 17 00:00:00 2001
From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>
Date: Sun, 12 Nov 2023 01:01:30 -0500
Subject: Fix room matching

---
 .../skyblock/dungeon/secrets/DungeonSecrets.java   |  4 ++--
 .../skyblocker/skyblock/dungeon/secrets/Room.java  | 25 ++++++++++++----------
 .../skyblock/dungeon/secrets/SecretWaypoint.java   |  2 +-
 .../skyblocker/dungeons/secretlocations.json       |  2 +-
 4 files changed, 18 insertions(+), 15 deletions(-)

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 eda08cf6..ee517eb8 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
@@ -255,7 +255,7 @@ public class DungeonSecrets {
         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())
+                        addCustomWaypoints(room, SecretWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, waypointsJson).resultOrPartial(LOGGER::error).orElseGet(ArrayList::new))
                 );
                 LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded custom dungeon secret waypoints");
             } catch (Exception e) {
@@ -273,7 +273,7 @@ public class DungeonSecrets {
         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())
+                    customWaypointsJson.add(room, SecretWaypoint.LIST_CODEC.encodeStart(JsonOps.INSTANCE, new ArrayList<>(waypoints.values())).resultOrPartial(LOGGER::error).orElseGet(JsonArray::new))
             );
             SkyblockerMod.GSON.toJson(customWaypointsJson, writer);
             LOGGER.info("[Skyblocker Dungeon Secrets] Saved custom dungeon secret waypoints");
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 c9b32be9..9b95f146 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
@@ -110,7 +110,7 @@ public class Room {
 
     @Override
     public String toString() {
-        return "Room{type=" + type + ", shape=" + shape + ", matchState=" + matchState + ", segments=" + Arrays.toString(segments.toArray()) + "}";
+        return "Room{type=%s, segments=%s, shape=%s, matchState=%s, name=%s, direction=%s, physicalCornerPos=%s}".formatted(type, Arrays.toString(segments.toArray()), shape, matchState, name, direction, physicalCornerPos);
     }
 
     @NotNull
@@ -271,7 +271,7 @@ public class Room {
                 }
             }
         }).exceptionally(e -> {
-            DungeonSecrets.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown error while matching room {}", this, e);
+            DungeonSecrets.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown exception while matching room {}", this, e);
             return null;
         });
     }
@@ -335,28 +335,32 @@ public class Room {
         if (id == 0) {
             return false;
         }
-        possibleRooms.removeIf(directionRooms -> {
+        for (MutableTriple<Direction, Vector2ic, List<String>> directionRooms : possibleRooms) {
             int block = posIdToInt(DungeonMapUtils.actualToRelative(directionRooms.getLeft(), directionRooms.getMiddle(), pos), id);
-            directionRooms.getRight().removeIf(room -> Arrays.binarySearch(roomsData.get(room), block) < 0);
-            return directionRooms.getRight().isEmpty();
-        });
+            List<String> possibleDirectionRooms = new ArrayList<>();
+            for (String room : directionRooms.getRight()) {
+                if (Arrays.binarySearch(roomsData.get(room), block) >= 0) {
+                    possibleDirectionRooms.add(room);
+                }
+            }
+            directionRooms.setRight(possibleDirectionRooms);
+        }
 
         int matchingRoomsSize = possibleRooms.stream().map(Triple::getRight).mapToInt(Collection::size).sum();
         if (matchingRoomsSize == 0) {
             // If no rooms match, reset the fields and scan again after 50 ticks.
-            DungeonSecrets.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks + 1);
+            DungeonSecrets.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks);
             Scheduler.INSTANCE.schedule(() -> matchState = MatchState.MATCHING, 50);
             reset();
             return true;
         } else if (matchingRoomsSize == 1) {
             if (matchState == MatchState.MATCHING) {
                 // If one room matches, load the secrets for that room and set state to double-checking.
-                assert possibleRooms.size() == 1;
-                Triple<Direction, Vector2ic, List<String>> directionRoom = possibleRooms.get(0);
-                assert directionRoom.getRight().size() == 1;
+                Triple<Direction, Vector2ic, List<String>> directionRoom = possibleRooms.stream().filter(directionRooms -> directionRooms.getRight().size() == 1).findAny().orElseThrow();
                 name = directionRoom.getRight().get(0);
                 direction = directionRoom.getLeft();
                 physicalCornerPos = directionRoom.getMiddle();
+                DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s), starting double checking", name, checkedBlocks.size());
                 roomMatched();
                 return false;
             } else if (matchState == MatchState.DOUBLE_CHECKING && ++doubleCheckBlocks >= 10) {
@@ -402,7 +406,6 @@ public class Room {
         }
         DungeonSecrets.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint);
         matchState = MatchState.DOUBLE_CHECKING;
-        DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s), starting double checking", name, checkedBlocks.size());
     }
 
     /**
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 0c2d1b34..fdfa88c3 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
@@ -136,7 +136,7 @@ public class SecretWaypoint extends Waypoint {
         }
 
         private static Category get(JsonObject waypointJson) {
-            return CODEC.parse(JsonOps.INSTANCE, waypointJson.get("category")).resultOrPartial(DungeonSecrets.LOGGER::error).orElseThrow();
+            return CODEC.parse(JsonOps.INSTANCE, waypointJson.get("category")).resultOrPartial(DungeonSecrets.LOGGER::error).orElse(Category.DEFAULT);
         }
 
         boolean needsInteraction() {
diff --git a/src/main/resources/assets/skyblocker/dungeons/secretlocations.json b/src/main/resources/assets/skyblocker/dungeons/secretlocations.json
index 02e19632..0f22f597 100644
--- a/src/main/resources/assets/skyblocker/dungeons/secretlocations.json
+++ b/src/main/resources/assets/skyblocker/dungeons/secretlocations.json
@@ -3936,7 +3936,7 @@
     },
     {
       "secretName":"4/5/6 - Entrance 3",
-      "category":"",
+      "category":"entrance",
       "x":31,
       "y":142,
       "z":39
-- 
cgit 


From 35d02596389b2516d62fbd1298b5b90e69b57fa5 Mon Sep 17 00:00:00 2001
From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>
Date: Sun, 12 Nov 2023 17:42:00 -0500
Subject: Add secret waypoint tests

---
 .../skyblock/dungeon/secrets/SecretWaypoint.java   | 13 ++--
 .../dungeon/secrets/SecretWaypointTest.java        | 79 ++++++++++++++++++++++
 2 files changed, 87 insertions(+), 5 deletions(-)
 create mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java

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 fdfa88c3..43f624f6 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
@@ -20,6 +20,8 @@ import net.minecraft.util.dynamic.Codecs;
 import net.minecraft.util.math.BlockPos;
 import net.minecraft.util.math.Vec3d;
 import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.List;
 import java.util.function.Predicate;
@@ -27,6 +29,7 @@ import java.util.function.Supplier;
 import java.util.function.ToDoubleFunction;
 
 public class SecretWaypoint extends Waypoint {
+    protected static final Logger LOGGER = LoggerFactory.getLogger(SecretWaypoint.class);
     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),
@@ -35,8 +38,8 @@ public class SecretWaypoint extends Waypoint {
     ).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;
+    private static final Supplier<SkyblockerConfig.SecretWaypoints> CONFIG = () -> SkyblockerConfigManager.get().locations.dungeons.secretWaypoints;
+    private static final Supplier<Type> TYPE_SUPPLIER = () -> CONFIG.get().waypointType;
     final int secretIndex;
     final Category category;
     final Text name;
@@ -95,7 +98,7 @@ public class SecretWaypoint extends Waypoint {
         //TODO In the future, shrink the box for wither essence and items so its more realistic
         super.render(context);
 
-        if (CONFIG.showSecretText) {
+        if (CONFIG.get().showSecretText) {
             Vec3d posUp = centerPos.add(0, 1, 0);
             RenderHelper.renderText(context, name, posUp, true);
             double distance = context.camera().getPos().distanceTo(centerPos);
@@ -135,8 +138,8 @@ public class SecretWaypoint extends Waypoint {
             }
         }
 
-        private static Category get(JsonObject waypointJson) {
-            return CODEC.parse(JsonOps.INSTANCE, waypointJson.get("category")).resultOrPartial(DungeonSecrets.LOGGER::error).orElse(Category.DEFAULT);
+        static Category get(JsonObject waypointJson) {
+            return CODEC.parse(JsonOps.INSTANCE, waypointJson.get("category")).resultOrPartial(LOGGER::error).orElse(Category.DEFAULT);
         }
 
         boolean needsInteraction() {
diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java
new file mode 100644
index 00000000..0870e744
--- /dev/null
+++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/SecretWaypointTest.java
@@ -0,0 +1,79 @@
+package de.hysky.skyblocker.skyblock.dungeon.secrets;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.mojang.serialization.JsonOps;
+import net.minecraft.util.math.BlockPos;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+public class SecretWaypointTest {
+    private final Gson gson = new Gson();
+
+    @Test
+    void testCodecSerialize() {
+        SecretWaypoint waypoint = new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN);
+        JsonElement json = SecretWaypoint.CODEC.encodeStart(JsonOps.INSTANCE, waypoint).result().orElseThrow();
+        String expectedJson = "{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]}";
+
+        Assertions.assertEquals(expectedJson, json.toString());
+    }
+
+    @Test
+    void testCodecDeserialize() {
+        String json = "{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]}";
+        SecretWaypoint waypoint = SecretWaypoint.CODEC.parse(JsonOps.INSTANCE, gson.fromJson(json, JsonElement.class)).result().orElseThrow();
+        SecretWaypoint expectedWaypoint = new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN);
+
+        equal(expectedWaypoint, waypoint);
+    }
+
+    @Test
+    void testListCodecSerialize() {
+        List<SecretWaypoint> waypoints = List.of(new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN), new SecretWaypoint(1, SecretWaypoint.Category.CHEST, "name", new BlockPos(-1, 0, 1)));
+        JsonElement json = SecretWaypoint.LIST_CODEC.encodeStart(JsonOps.INSTANCE, waypoints).result().orElseThrow();
+        String expectedJson = "[{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]},{\"secretIndex\":1,\"category\":\"chest\",\"name\":{\"text\":\"name\"},\"pos\":[-1,0,1]}]";
+
+        Assertions.assertEquals(expectedJson, json.toString());
+    }
+
+    @Test
+    void testListCodecDeserialize() {
+        String json = "[{\"secretIndex\":0,\"category\":\"default\",\"name\":{\"text\":\"name\"},\"pos\":[0,0,0]},{\"secretIndex\":1,\"category\":\"chest\",\"name\":{\"text\":\"name\"},\"pos\":[-1,0,1]}]";
+        List<SecretWaypoint> waypoints = SecretWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, gson.fromJson(json, JsonElement.class)).result().orElseThrow();
+        List<SecretWaypoint> expectedWaypoints = List.of(new SecretWaypoint(0, SecretWaypoint.Category.DEFAULT, "name", BlockPos.ORIGIN), new SecretWaypoint(1, SecretWaypoint.Category.CHEST, "name", new BlockPos(-1, 0, 1)));
+
+        Assertions.assertEquals(expectedWaypoints.size(), waypoints.size());
+        for (int i = 0; i < expectedWaypoints.size(); i++) {
+            SecretWaypoint expectedWaypoint = expectedWaypoints.get(i);
+            SecretWaypoint waypoint = waypoints.get(i);
+            equal(expectedWaypoint, waypoint);
+        }
+    }
+
+    @Test
+    void testGetCategory() {
+        JsonObject waypointJson = new JsonObject();
+        waypointJson.addProperty("category", "chest");
+        SecretWaypoint.Category category = SecretWaypoint.Category.get(waypointJson);
+        Assertions.assertEquals(SecretWaypoint.Category.CHEST, category);
+    }
+
+    @Test
+    void testGetCategoryDefault() {
+        JsonObject waypointJson = new JsonObject();
+        waypointJson.addProperty("category", "");
+        SecretWaypoint.Category category = SecretWaypoint.Category.get(waypointJson);
+        Assertions.assertEquals(SecretWaypoint.Category.DEFAULT, category);
+    }
+
+    private static void equal(SecretWaypoint expectedWaypoint, SecretWaypoint waypoint) {
+        Assertions.assertEquals(expectedWaypoint.secretIndex, waypoint.secretIndex);
+        Assertions.assertEquals(expectedWaypoint.category, waypoint.category);
+        Assertions.assertEquals(expectedWaypoint.name, waypoint.name);
+        Assertions.assertEquals(expectedWaypoint.pos, waypoint.pos);
+    }
+}
-- 
cgit 


From 47d75bf898b72622fe755b010042c5409d91331d Mon Sep 17 00:00:00 2001
From: Kevin <92656833+kevinthegreat1@users.noreply.github.com>
Date: Mon, 13 Nov 2023 14:48:58 -0500
Subject: Fix item nbt matching (#412)

---
 .../de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java    | 3 +--
 .../de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java  | 6 +++++-
 2 files changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java
index b52d6ff5..218eb8d1 100644
--- a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java
+++ b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockEmiRecipe.java
@@ -4,7 +4,6 @@ import de.hysky.skyblocker.skyblock.itemlist.SkyblockCraftingRecipe;
 import de.hysky.skyblocker.utils.ItemUtils;
 import dev.emi.emi.api.recipe.EmiCraftingRecipe;
 import dev.emi.emi.api.recipe.EmiRecipeCategory;
-import dev.emi.emi.api.stack.Comparison;
 import dev.emi.emi.api.stack.EmiIngredient;
 import dev.emi.emi.api.stack.EmiStack;
 import dev.emi.emi.api.widget.WidgetHolder;
@@ -16,7 +15,7 @@ public class SkyblockEmiRecipe extends EmiCraftingRecipe {
     private final String craftText;
 
     public SkyblockEmiRecipe(SkyblockCraftingRecipe recipe) {
-        super(recipe.getGrid().stream().map(EmiStack::of).map(EmiIngredient.class::cast).toList(), EmiStack.of(recipe.getResult()).comparison(Comparison.compareNbt()), Identifier.of("skyblock", ItemUtils.getItemId(recipe.getResult()).toLowerCase().replace(';', '_') + "_" + recipe.getResult().getCount()));
+        super(recipe.getGrid().stream().map(EmiStack::of).map(EmiIngredient.class::cast).toList(), EmiStack.of(recipe.getResult()), Identifier.of("skyblock", ItemUtils.getItemId(recipe.getResult()).toLowerCase().replace(';', '_') + "_" + recipe.getResult().getCount()));
         this.craftText = recipe.getCraftText();
     }
 
diff --git a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
index 6ed6a32a..8dfc5dc9 100644
--- a/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
+++ b/src/main/java/de/hysky/skyblocker/compatibility/emi/SkyblockerEMIPlugin.java
@@ -7,6 +7,7 @@ import dev.emi.emi.api.EmiPlugin;
 import dev.emi.emi.api.EmiRegistry;
 import dev.emi.emi.api.recipe.EmiRecipeCategory;
 import dev.emi.emi.api.render.EmiTexture;
+import dev.emi.emi.api.stack.Comparison;
 import dev.emi.emi.api.stack.EmiStack;
 import net.minecraft.item.Items;
 import net.minecraft.util.Identifier;
@@ -21,7 +22,10 @@ public class SkyblockerEMIPlugin implements EmiPlugin {
 
     @Override
     public void register(EmiRegistry registry) {
-        ItemRepository.getItemsStream().map(EmiStack::of).forEach(registry::addEmiStack);
+        ItemRepository.getItemsStream().map(EmiStack::of).forEach(emiStack -> {
+            registry.addEmiStack(emiStack);
+            registry.setDefaultComparison(emiStack, Comparison.compareNbt());
+        });
         registry.addCategory(SKYBLOCK);
         registry.addWorkstation(SKYBLOCK, EmiStack.of(Items.CRAFTING_TABLE));
         ItemRepository.getRecipesStream().map(SkyblockEmiRecipe::new).forEach(registry::addRecipe);
-- 
cgit 


From fb249dcac9581945d35a89c417bb54afe4ed7c32 Mon Sep 17 00:00:00 2001
From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>
Date: Mon, 13 Nov 2023 19:29:54 -0500
Subject: Refactor debug

---
 src/main/java/de/hysky/skyblocker/debug/Debug.java        |  3 ++-
 .../de/hysky/skyblocker/debug/DumpPlayersCommand.java     | 15 ++++++---------
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/src/main/java/de/hysky/skyblocker/debug/Debug.java b/src/main/java/de/hysky/skyblocker/debug/Debug.java
index 5991f72c..1fc22d2a 100644
--- a/src/main/java/de/hysky/skyblocker/debug/Debug.java
+++ b/src/main/java/de/hysky/skyblocker/debug/Debug.java
@@ -1,12 +1,13 @@
 package de.hysky.skyblocker.debug;
 
 import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
+import net.fabricmc.loader.api.FabricLoader;
 
 public class Debug {
 	private static final boolean DEBUG_ENABLED = Boolean.parseBoolean(System.getProperty("skyblocker.debug", "false"));
 
 	public static void init() {
-		if (DEBUG_ENABLED) {
+		if (DEBUG_ENABLED || FabricLoader.getInstance().isDevelopmentEnvironment()) {
 			ClientCommandRegistrationCallback.EVENT.register(DumpPlayersCommand::register);
 		}
 	}
diff --git a/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java b/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java
index 598672ef..5f6e0362 100644
--- a/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java
+++ b/src/main/java/de/hysky/skyblocker/debug/DumpPlayersCommand.java
@@ -1,17 +1,15 @@
 package de.hysky.skyblocker.debug;
 
-import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
-
 import com.mojang.brigadier.Command;
 import com.mojang.brigadier.CommandDispatcher;
-
 import de.hysky.skyblocker.SkyblockerMod;
 import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource;
-import net.minecraft.client.MinecraftClient;
 import net.minecraft.command.CommandRegistryAccess;
 import net.minecraft.entity.player.PlayerEntity;
 import net.minecraft.text.Text;
 
+import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
 public class DumpPlayersCommand {
 	
 	static void register(CommandDispatcher<FabricClientCommandSource> dispatcher, CommandRegistryAccess registryAccess) {
@@ -20,14 +18,13 @@ public class DumpPlayersCommand {
 						.then(literal("dumpPlayers")
 								.executes(context -> {
 									FabricClientCommandSource source = context.getSource();
-									MinecraftClient client = source.getClient();
-									
-									client.world.getEntities().forEach(e -> {
+
+									source.getWorld().getEntities().forEach(e -> {
 										if (e instanceof PlayerEntity player) {
-											source.sendFeedback(Text.of("\"" + player.getName().getString() + "\""));
+											source.sendFeedback(Text.of("'" + player.getName().getString() + "'"));
 										}
 									});
-									
+
 									return Command.SINGLE_SUCCESS;
 								}))));
 	}
-- 
cgit