aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/me/xmrvizzy
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/me/xmrvizzy')
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/SkyblockerMod.java14
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/config/SkyblockerConfig.java20
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java19
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/mixin/InGameHudMixin.java20
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/skyblock/StatusBarTracker.java18
-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
-rw-r--r--src/main/java/me/xmrvizzy/skyblocker/utils/RenderHelper.java198
10 files changed, 1473 insertions, 111 deletions
diff --git a/src/main/java/me/xmrvizzy/skyblocker/SkyblockerMod.java b/src/main/java/me/xmrvizzy/skyblocker/SkyblockerMod.java
index c9705a3b..f15427c1 100644
--- a/src/main/java/me/xmrvizzy/skyblocker/SkyblockerMod.java
+++ b/src/main/java/me/xmrvizzy/skyblocker/SkyblockerMod.java
@@ -11,12 +11,9 @@ import me.xmrvizzy.skyblocker.skyblock.dungeon.DungeonBlaze;
import me.xmrvizzy.skyblocker.skyblock.dungeon.DungeonMap;
import me.xmrvizzy.skyblocker.skyblock.dungeon.LividColor;
import me.xmrvizzy.skyblocker.skyblock.dungeon.TicTacToe;
+import me.xmrvizzy.skyblocker.skyblock.dungeon.secrets.DungeonSecrets;
import me.xmrvizzy.skyblocker.skyblock.dwarven.DwarvenHud;
-import me.xmrvizzy.skyblocker.skyblock.item.CustomArmorDyeColors;
-import me.xmrvizzy.skyblocker.skyblock.item.CustomArmorTrims;
-import me.xmrvizzy.skyblocker.skyblock.item.CustomItemNames;
-import me.xmrvizzy.skyblocker.skyblock.item.PriceInfoTooltip;
-import me.xmrvizzy.skyblocker.skyblock.item.WikiLookup;
+import me.xmrvizzy.skyblocker.skyblock.item.*;
import me.xmrvizzy.skyblocker.skyblock.itemlist.ItemRegistry;
import me.xmrvizzy.skyblocker.skyblock.quicknav.QuickNav;
import me.xmrvizzy.skyblocker.skyblock.rift.TheRift;
@@ -24,7 +21,10 @@ import me.xmrvizzy.skyblocker.skyblock.shortcut.Shortcuts;
import me.xmrvizzy.skyblocker.skyblock.tabhud.TabHud;
import me.xmrvizzy.skyblocker.skyblock.tabhud.screenbuilder.ScreenMaster;
import me.xmrvizzy.skyblocker.skyblock.tabhud.util.PlayerListMgr;
-import me.xmrvizzy.skyblocker.utils.*;
+import me.xmrvizzy.skyblocker.utils.MessageScheduler;
+import me.xmrvizzy.skyblocker.utils.NEURepo;
+import me.xmrvizzy.skyblocker.utils.Scheduler;
+import me.xmrvizzy.skyblocker.utils.Utils;
import me.xmrvizzy.skyblocker.utils.culling.OcclusionCulling;
import me.xmrvizzy.skyblocker.utils.title.TitleContainer;
import net.fabricmc.api.ClientModInitializer;
@@ -89,6 +89,7 @@ public class SkyblockerMod implements ClientModInitializer {
FishingHelper.init();
TabHud.init();
DungeonMap.init();
+ DungeonSecrets.init();
TheRift.init();
TitleContainer.init();
ScreenMaster.init();
@@ -99,6 +100,7 @@ public class SkyblockerMod implements ClientModInitializer {
CustomArmorTrims.init();
TicTacToe.init();
containerSolverManager.init();
+ statusBarTracker.init();
scheduler.scheduleCyclic(Utils::update, 20);
scheduler.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 100);
scheduler.scheduleCyclic(DungeonBlaze::update, 4);
diff --git a/src/main/java/me/xmrvizzy/skyblocker/config/SkyblockerConfig.java b/src/main/java/me/xmrvizzy/skyblocker/config/SkyblockerConfig.java
index e6961819..6ddbdf65 100644
--- a/src/main/java/me/xmrvizzy/skyblocker/config/SkyblockerConfig.java
+++ b/src/main/java/me/xmrvizzy/skyblocker/config/SkyblockerConfig.java
@@ -407,6 +407,8 @@ public class SkyblockerConfig implements ConfigData {
}
public static class Dungeons {
+ @ConfigEntry.Gui.CollapsibleObject
+ public SecretWaypoints secretWaypoints = new SecretWaypoints();
@ConfigEntry.Gui.Tooltip()
public boolean croesusHelper = true;
public boolean enableMap = true;
@@ -427,6 +429,24 @@ public class SkyblockerConfig implements ConfigData {
public Terminals terminals = new Terminals();
}
+ public static class SecretWaypoints {
+
+ public boolean enableSecretWaypoints = true;
+ @ConfigEntry.Gui.Tooltip()
+ public boolean noInitSecretWaypoints = false;
+ public boolean enableEntranceWaypoints = true;
+ public boolean enableSuperboomWaypoints = true;
+ public boolean enableChestWaypoints = true;
+ public boolean enableItemWaypoints = true;
+ public boolean enableBatWaypoints = true;
+ public boolean enableWitherWaypoints = true;
+ public boolean enableLeverWaypoints = true;
+ public boolean enableFairySoulWaypoints = true;
+ public boolean enableStonkWaypoints = true;
+ @ConfigEntry.Gui.Tooltip()
+ public boolean enableDefaultWaypoints = true;
+ }
+
public static class LividColor {
@ConfigEntry.Gui.Tooltip()
public boolean enableLividColor = true;
diff --git a/src/main/java/me/xmrvizzy/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/me/xmrvizzy/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java
index 2d109524..f52e2f7f 100644
--- a/src/main/java/me/xmrvizzy/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java
+++ b/src/main/java/me/xmrvizzy/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java
@@ -1,23 +1,40 @@
package me.xmrvizzy.skyblocker.mixin;
import com.llamalad7.mixinextras.injector.WrapWithCondition;
-
+import com.llamalad7.mixinextras.sugar.Local;
import dev.cbyrne.betterinject.annotations.Inject;
import me.xmrvizzy.skyblocker.skyblock.FishingHelper;
+import me.xmrvizzy.skyblocker.skyblock.dungeon.secrets.DungeonSecrets;
import me.xmrvizzy.skyblocker.utils.Utils;
+import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayNetworkHandler;
+import net.minecraft.entity.ItemEntity;
+import net.minecraft.entity.LivingEntity;
import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket;
import org.slf4j.Logger;
+import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.ModifyVariable;
@Mixin(ClientPlayNetworkHandler.class)
public abstract class ClientPlayNetworkHandlerMixin {
+ @Shadow
+ @Final
+ private MinecraftClient client;
+
@Inject(method = "onPlaySound", at = @At("RETURN"))
private void skyblocker$onPlaySound(PlaySoundS2CPacket packet) {
FishingHelper.onSound(packet);
}
+ @ModifyVariable(method = "onItemPickupAnimation", at = @At(value = "STORE", ordinal = 0))
+ private ItemEntity skyblocker$onItemPickup(ItemEntity itemEntity, @Local LivingEntity collector) {
+ DungeonSecrets.onItemPickup(itemEntity, collector, collector == client.player);
+ return itemEntity;
+ }
+
@WrapWithCondition(method = "onEntityPassengersSet", at = @At(value = "INVOKE", target = "Lorg/slf4j/Logger;warn(Ljava/lang/String;)V", remap = false))
private boolean skyblocker$cancelEntityPassengersWarning(Logger instance, String msg) {
return !Utils.isOnHypixel();
diff --git a/src/main/java/me/xmrvizzy/skyblocker/mixin/InGameHudMixin.java b/src/main/java/me/xmrvizzy/skyblocker/mixin/InGameHudMixin.java
index 4cda73aa..bc3df266 100644
--- a/src/main/java/me/xmrvizzy/skyblocker/mixin/InGameHudMixin.java
+++ b/src/main/java/me/xmrvizzy/skyblocker/mixin/InGameHudMixin.java
@@ -5,14 +5,12 @@ import me.xmrvizzy.skyblocker.SkyblockerMod;
import me.xmrvizzy.skyblocker.config.SkyblockerConfig;
import me.xmrvizzy.skyblocker.skyblock.FancyStatusBars;
import me.xmrvizzy.skyblocker.skyblock.HotbarSlotLock;
-import me.xmrvizzy.skyblocker.skyblock.StatusBarTracker;
import me.xmrvizzy.skyblocker.skyblock.dungeon.DungeonMap;
import me.xmrvizzy.skyblocker.utils.Utils;
import net.fabricmc.api.EnvType;
import net.fabricmc.api.Environment;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
-import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -27,8 +25,6 @@ public abstract class InGameHudMixin {
@Unique
private static final Identifier SLOT_LOCK = new Identifier(SkyblockerMod.NAMESPACE, "textures/gui/slot_lock.png");
@Unique
- private final StatusBarTracker statusBarTracker = SkyblockerMod.getInstance().statusBarTracker;
- @Unique
private final FancyStatusBars statusBars = new FancyStatusBars();
@Shadow
@@ -36,22 +32,6 @@ public abstract class InGameHudMixin {
@Shadow
private int scaledWidth;
- @Shadow
- public abstract void setOverlayMessage(Text message, boolean tinted);
-
- @Inject(method = "setOverlayMessage(Lnet/minecraft/text/Text;Z)V", at = @At("HEAD"), cancellable = true)
- private void skyblocker$onSetOverlayMessage(Text message, boolean tinted, CallbackInfo ci) {
- if (!Utils.isOnSkyblock() || !SkyblockerConfig.get().general.bars.enableBars || Utils.isInTheRift())
- return;
- String msg = message.getString();
- String res = statusBarTracker.update(msg, SkyblockerConfig.get().messages.hideMana);
- if (!msg.equals(res)) {
- if (res != null)
- setOverlayMessage(Text.of(res), tinted);
- ci.cancel();
- }
- }
-
@Inject(method = "renderHotbar", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/hud/InGameHud;renderHotbarItem(Lnet/minecraft/client/gui/DrawContext;IIFLnet/minecraft/entity/player/PlayerEntity;Lnet/minecraft/item/ItemStack;I)V", ordinal = 0))
public void skyblocker$renderHotbarItemLock(float tickDelta, DrawContext context, CallbackInfo ci, @Local(ordinal = 4, name = "m") int index, @Local(ordinal = 5, name = "n") int x, @Local(ordinal = 6, name = "o") int y) {
if (Utils.isOnSkyblock() && HotbarSlotLock.isLocked(index)) {
diff --git a/src/main/java/me/xmrvizzy/skyblocker/skyblock/StatusBarTracker.java b/src/main/java/me/xmrvizzy/skyblocker/skyblock/StatusBarTracker.java
index 96165ce8..aeee9978 100644
--- a/src/main/java/me/xmrvizzy/skyblocker/skyblock/StatusBarTracker.java
+++ b/src/main/java/me/xmrvizzy/skyblocker/skyblock/StatusBarTracker.java
@@ -1,7 +1,11 @@
package me.xmrvizzy.skyblocker.skyblock;
+import me.xmrvizzy.skyblocker.config.SkyblockerConfig;
+import me.xmrvizzy.skyblocker.utils.Utils;
+import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
+import net.minecraft.text.Text;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -16,6 +20,10 @@ public class StatusBarTracker {
private Resource mana = new Resource(100, 100, 0);
private int defense = 0;
+ public void init() {
+ ClientReceiveMessageEvents.MODIFY_GAME.register(this::onOverlayMessage);
+ }
+
public Resource getHealth() {
return this.health;
}
@@ -57,6 +65,13 @@ public class StatusBarTracker {
return str;
}
+ private Text onOverlayMessage(Text text, boolean overlay) {
+ if (!overlay || !Utils.isOnSkyblock() || !SkyblockerConfig.get().general.bars.enableBars || Utils.isInTheRift()) {
+ return text;
+ }
+ return Text.of(update(text.getString(), SkyblockerConfig.get().messages.hideMana));
+ }
+
public String update(String actionBar, boolean filterManaUse) {
var sb = new StringBuilder();
Matcher matcher = STATUS_HEALTH.matcher(actionBar);
@@ -89,5 +104,6 @@ public class StatusBarTracker {
return res.isEmpty() ? null : res;
}
- public record Resource(int value, int max, int overflow) {}
+ public record Resource(int value, int max, int overflow) {
+ }
}
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 net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal;
+
+public class DungeonSecrets {
+ protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonSecrets.class);
+ private static final String DUNGEONS_PATH = "dungeons";
+ /**
+ * Maps the block identifier string to a custom numeric block id used in dungeon rooms data.
+ *
+ * @implNote Not using {@link net.minecraft.registry.Registry#getId(Object) Registry#getId(Block)} and {@link net.minecraft.block.Blocks Blocks} since this is also used by {@link me.xmrvizzy.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft.
+ */
+ @SuppressWarnings("JavadocReference")
+ protected static final Object2ByteMap<String> NUMERIC_ID = new Object2ByteOpenHashMap<>(Map.ofEntries(
+ Map.entry("minecraft:stone", (byte) 1),
+ Map.entry("minecraft:diorite", (byte) 2),
+ Map.entry("minecraft:polished_diorite", (byte) 3),
+ Map.entry("minecraft:andesite", (byte) 4),
+ Map.entry("minecraft:polished_andesite", (byte) 5),
+ Map.entry("minecraft:grass_block", (byte) 6),
+ Map.entry("minecraft:dirt", (byte) 7),
+ Map.entry("minecraft:coarse_dirt", (byte) 8),
+ Map.entry("minecraft:cobblestone", (byte) 9),
+ Map.entry("minecraft:bedrock", (byte) 10),
+ Map.entry("minecraft:oak_leaves", (byte) 11),
+ Map.entry("minecraft:gray_wool", (byte) 12),
+ Map.entry("minecraft:double_stone_slab", (byte) 13),
+ Map.entry("minecraft:mossy_cobblestone", (byte) 14),
+ Map.entry("minecraft:clay", (byte) 15),
+ Map.entry("minecraft:stone_bricks", (byte) 16),
+ Map.entry("minecraft:mossy_stone_bricks", (byte) 17),
+ Map.entry("minecraft:chiseled_stone_bricks", (byte) 18),
+ Map.entry("minecraft:gray_terracotta", (byte) 19),
+ Map.entry("minecraft:cyan_terracotta", (byte) 20),
+ Map.entry("minecraft:black_terracotta", (byte) 21)
+ ));
+ /**
+ * Block data for dungeon rooms. See {@link me.xmrvizzy.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU} for format details and how it's generated.
+ * All access to this map must check {@link #isRoomsLoaded()} to prevent concurrent modification.
+ */
+ @SuppressWarnings("JavadocReference")
+ protected static final HashMap<String, Map<String, Map<String, int[]>>> ROOMS_DATA = new HashMap<>();
+ @NotNull
+ private static final Map<Vector2ic, Room> rooms = new HashMap<>();
+ private static final Map<String, JsonElement> roomsJson = new HashMap<>();
+ private static final Map<String, JsonElement> waypointsJson = new HashMap<>();
+ @Nullable
+ private static CompletableFuture<Void> roomsLoaded;
+ /**
+ * The map position of the top left corner of the entrance room.
+ */
+ @Nullable
+ private static Vector2ic mapEntrancePos;
+ /**
+ * The size of a room on the map.
+ */
+ private static int mapRoomSize;
+ /**
+ * The physical position of the northwest corner of the entrance room.
+ */
+ @Nullable
+ private static Vector2ic physicalEntrancePos;
+ private static Room currentRoom;
+
+ public static boolean isRoomsLoaded() {
+ return roomsLoaded != null && roomsLoaded.isDone();
+ }
+
+ @SuppressWarnings("unused")
+ public static JsonObject getRoomMetadata(String room) {
+ return roomsJson.get(room).getAsJsonObject();
+ }
+
+ public static JsonArray getRoomWaypoints(String room) {
+ return waypointsJson.get(room).getAsJsonArray();
+ }
+
+ /**
+ * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}.
+ * Use {@link #isRoomsLoaded()} to check for completion of loading.
+ */
+ public static void init() {
+ if (SkyblockerConfig.get().locations.dungeons.secretWaypoints.noInitSecretWaypoints) {
+ return;
+ }
+ // Execute with MinecraftClient as executor since we need to wait for MinecraftClient#resourceManager to be set
+ CompletableFuture.runAsync(DungeonSecrets::load, MinecraftClient.getInstance()).exceptionally(e -> {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets", e);
+ return null;
+ });
+ SkyblockerMod.getInstance().scheduler.scheduleCyclic(DungeonSecrets::update, 10);
+ WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonSecrets::render);
+ ClientReceiveMessageEvents.GAME.register(DungeonSecrets::onChatMessage);
+ ClientReceiveMessageEvents.GAME_CANCELED.register(DungeonSecrets::onChatMessage);
+ UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> onUseBlock(world, hitResult));
+ ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets")
+ .then(literal("markAsFound").then(markSecretsCommand(true)))
+ .then(literal("markAsMissing").then(markSecretsCommand(false)))))));
+ ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset()));
+ }
+
+ private static void load() {
+ long startTime = System.currentTimeMillis();
+ List<CompletableFuture<Void>> dungeonFutures = new ArrayList<>();
+ for (Map.Entry<Identifier, Resource> resourceEntry : MinecraftClient.getInstance().getResourceManager().findResources(DUNGEONS_PATH, id -> id.getPath().endsWith(".skeleton")).entrySet()) {
+ String[] path = resourceEntry.getKey().getPath().split("/");
+ if (path.length != 4) {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets, invalid resource identifier {}", resourceEntry.getKey());
+ break;
+ }
+ String dungeon = path[1];
+ String roomShape = path[2];
+ String room = path[3].substring(0, path[3].length() - ".skeleton".length());
+ ROOMS_DATA.computeIfAbsent(dungeon, dungeonKey -> new HashMap<>());
+ ROOMS_DATA.get(dungeon).computeIfAbsent(roomShape, roomShapeKey -> new HashMap<>());
+ dungeonFutures.add(CompletableFuture.supplyAsync(() -> readRoom(resourceEntry.getValue())).thenAcceptAsync(rooms -> {
+ Map<String, int[]> roomsMap = ROOMS_DATA.get(dungeon).get(roomShape);
+ synchronized (roomsMap) {
+ roomsMap.put(room, rooms);
+ }
+ LOGGER.debug("[Skyblocker] Loaded dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room);
+ }).exceptionally(e -> {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room, e);
+ return null;
+ }));
+ }
+ dungeonFutures.add(CompletableFuture.runAsync(() -> {
+ try (BufferedReader roomsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/dungeonrooms.json")); BufferedReader waypointsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/secretlocations.json"))) {
+ SkyblockerMod.GSON.fromJson(roomsReader, JsonObject.class).asMap().forEach((room, jsonElement) -> roomsJson.put(room.toLowerCase(), jsonElement));
+ SkyblockerMod.GSON.fromJson(waypointsReader, JsonObject.class).asMap().forEach((room, jsonElement) -> waypointsJson.put(room.toLowerCase(), jsonElement));
+ LOGGER.debug("[Skyblocker] Loaded dungeon secrets json");
+ } catch (Exception e) {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets json", e);
+ }
+ }));
+ roomsLoaded = CompletableFuture.allOf(dungeonFutures.toArray(CompletableFuture[]::new)).thenRun(() -> LOGGER.info("[Skyblocker] Loaded dungeon secrets for {} dungeon(s), {} room shapes, and {} rooms total in {} ms", ROOMS_DATA.size(), ROOMS_DATA.values().stream().mapToInt(Map::size).sum(), ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).mapToInt(Map::size).sum(), System.currentTimeMillis() - startTime)).exceptionally(e -> {
+ LOGGER.error("[Skyblocker] Failed to load dungeon secrets", e);
+ return null;
+ });
+ LOGGER.info("[Skyblocker] Started loading dungeon secrets in (blocked main thread for) {} ms", System.currentTimeMillis() - startTime);
+ }
+
+ private static int[] readRoom(Resource resource) throws RuntimeException {
+ try (ObjectInputStream in = new ObjectInputStream(new InflaterInputStream(resource.getInputStream()))) {
+ return (int[]) in.readObject();
+ } catch (IOException | ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static ArgumentBuilder<FabricClientCommandSource, RequiredArgumentBuilder<FabricClientCommandSource, Integer>> markSecretsCommand(boolean found) {
+ return argument("secret", IntegerArgumentType.integer()).executes(context -> {
+ int secretIndex = IntegerArgumentType.getInteger(context, "secret");
+ if (markSecrets(secretIndex, found)) {
+ context.getSource().sendFeedback(Text.translatable(found ? "skyblocker.dungeons.secrets.ma