From 6eb95596a26c0dc7275690b0300cf42944d69ac2 Mon Sep 17 00:00:00 2001 From: Kevin <92656833+kevinthegreat1@users.noreply.github.com> Date: Fri, 24 Nov 2023 12:01:03 -0800 Subject: [PATCH] Add Wither & Blood Door Highlight (#429) --- .../skyblocker/config/SkyblockerConfig.java | 26 ++++++++ .../config/categories/DungeonsCategory.java | 22 +++++++ .../dungeon/secrets/DungeonMapUtils.java | 25 ++++++++ .../dungeon/secrets/DungeonSecrets.java | 47 ++++++++++++-- .../skyblock/dungeon/secrets/Room.java | 63 ++++++++++++++++--- .../skyblocker/utils/render/RenderHelper.java | 12 ++-- .../assets/skyblocker/lang/en_us.json | 6 ++ 7 files changed, 185 insertions(+), 16 deletions(-) diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index 3c8e6739f7..633ea670e8 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -587,6 +587,9 @@ public static class Dungeons { @SerialEntry public SecretWaypoints secretWaypoints = new SecretWaypoints(); + @SerialEntry + public DoorHighlight doorHighlight = new DoorHighlight(); + @SerialEntry public DungeonChestProfit dungeonChestProfit = new DungeonChestProfit(); @@ -683,6 +686,29 @@ public static class SecretWaypoints { public boolean enableDefaultWaypoints = true; } + public static class DoorHighlight { + @SerialEntry + public boolean enableDoorHighlight = true; + + @SerialEntry + public Type doorHighlightType = Type.OUTLINED_HIGHLIGHT; + + public enum Type { + HIGHLIGHT, + OUTLINED_HIGHLIGHT, + OUTLINE; + + @Override + public String toString() { + return switch (this) { + case HIGHLIGHT -> "Highlight"; + case OUTLINED_HIGHLIGHT -> "Outlined Highlight"; + case OUTLINE -> "Outline"; + }; + } + } + } + public static class DungeonChestProfit { @SerialEntry public boolean enableProfitCalculator = true; diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java index 37f268b0ac..2f738ff220 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -147,6 +147,28 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig .build()) .build()) + //Dungeon Door Highlight + .group(OptionGroup.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight")) + .collapsed(true) + .option(Option.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.enableDoorHighlight")) + .binding(defaults.locations.dungeons.doorHighlight.enableDoorHighlight, + () -> config.locations.dungeons.doorHighlight.enableDoorHighlight, + newValue -> config.locations.dungeons.doorHighlight.enableDoorHighlight = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType.@Tooltip"), Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType.secretWaypointsNote"))) + .binding(defaults.locations.dungeons.doorHighlight.doorHighlightType, + () -> config.locations.dungeons.doorHighlight.doorHighlightType, + newValue -> config.locations.dungeons.doorHighlight.doorHighlightType = newValue) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) + .build()) + + //Dungeon Chest Profit .group(OptionGroup.createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit")) .collapsed(true) diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java index 73d4a452fb..01f2c9fcd0 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java @@ -3,12 +3,15 @@ import com.google.gson.JsonObject; import it.unimi.dsi.fastutil.ints.IntSortedSet; import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import net.minecraft.block.BlockState; +import net.minecraft.block.Blocks; 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.Vec3d; import net.minecraft.util.math.Vec3i; +import net.minecraft.world.World; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.RoundingMode; @@ -271,4 +274,26 @@ public static Vector2ic[] getRoomSegments(MapState map, Vector2ic mapPos, int ma DungeonSecrets.LOGGER.debug("[Skyblocker] Found dungeon room segments: {}", Arrays.toString(segments.toArray())); return segments.toArray(Vector2ic[]::new); } + + public static BlockPos getWitherBloodDoorPos(World world, Collection physicalPositions) { + BlockPos.Mutable doorPos = new BlockPos.Mutable(); + for (Vector2ic pos : physicalPositions) { + if (hasWitherOrBloodDoor(world, pos, doorPos)) { + return doorPos; + } + } + return null; + } + + private static boolean hasWitherOrBloodDoor(World world, Vector2ic pos, BlockPos.Mutable doorPos) { + return isWitherOrBloodDoor(world, doorPos.set(pos.x() - 2, 69, pos.y() + 14)) || + isWitherOrBloodDoor(world, doorPos.set(pos.x() + 14, 69, pos.y() - 2)) || + isWitherOrBloodDoor(world, doorPos.set(pos.x() + 14, 69, pos.y() + 30)) || + isWitherOrBloodDoor(world, doorPos.set(pos.x() + 30, 69, pos.y() + 14)); + } + + private static boolean isWitherOrBloodDoor(World world, BlockPos pos) { + BlockState state = world.getBlockState(pos); + return state.isOf(Blocks.COAL_BLOCK) || state.isOf(Blocks.RED_TERRACOTTA); + } } 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 ee517eb883..7f401fdb64 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 @@ -32,6 +32,7 @@ import net.minecraft.command.argument.BlockPosArgumentType; import net.minecraft.command.argument.PosArgument; import net.minecraft.command.argument.TextArgumentType; +import net.minecraft.entity.Entity; import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.mob.AmbientEntity; @@ -66,6 +67,8 @@ import java.nio.file.Path; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Stream; import java.util.zip.InflaterInputStream; @@ -76,6 +79,7 @@ public class DungeonSecrets { protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonSecrets.class); private static final String DUNGEONS_PATH = "dungeons"; private static final Path CUSTOM_WAYPOINTS_DIR = SkyblockerMod.CONFIG_DIR.resolve("custom_secret_waypoints.json"); + private static final Pattern KEY_FOUND = Pattern.compile("^(?:\\[.+] )?(?\\w+) has obtained (?Wither|Blood) Key!$"); /** * Maps the block identifier string to a custom numeric block id used in dungeon rooms data. * @@ -500,25 +504,60 @@ private static Room newRoom(Room.Type type, Vector2ic... physicalPositions) { } /** - * Renders the secret waypoints in {@link #currentRoom} if {@link #isCurrentRoomMatched()}. + * Renders the secret waypoints in {@link #currentRoom} if {@link #shouldProcess()} and {@link #currentRoom} is not null. */ private static void render(WorldRenderContext context) { - if (isCurrentRoomMatched()) { + if (shouldProcess() && currentRoom != null) { currentRoom.render(context); } } /** - * Calls {@link Room#onChatMessage(String)} on {@link #currentRoom} if the message is an overlay message and {@link #isCurrentRoomMatched()}. - * Used to detect when all secrets in a room are found. + * Calls {@link Room#onChatMessage(String)} on {@link #currentRoom} if the message is an overlay message and {@link #isCurrentRoomMatched()} and processes key obtained messages. + *

Used to detect when all secrets in a room are found and detect when a wither or blood door is unlocked. + * To process key obtained messages, this method checks if door highlight is enabled and if the message matches a key obtained message. + * Then, it calls {@link Room#keyFound()} on {@link #currentRoom} if the client's player is the one who obtained the key. + * Otherwise, it calls {@link Room#keyFound()} on the room the player who obtained the key is in. */ private static void onChatMessage(Text text, boolean overlay) { + if (!shouldProcess()) { + return; + } + String message = text.getString(); if (overlay && isCurrentRoomMatched()) { currentRoom.onChatMessage(message); } + // Process key found messages for door highlight + if (SkyblockerConfigManager.get().locations.dungeons.doorHighlight.enableDoorHighlight) { + Matcher matcher = KEY_FOUND.matcher(message); + if (matcher.matches()) { + String name = matcher.group("name"); + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player != null && client.player.getGameProfile().getName().equals(name)) { + if (currentRoom != null) { + currentRoom.keyFound(); + } else { + LOGGER.warn("[Skyblocker Dungeon Door] The current room at the current player {} does not exist", name); + } + } else if (client.world != null) { + Optional posOptional = client.world.getPlayers().stream().filter(player -> player.getGameProfile().getName().equals(name)).findAny().map(Entity::getPos); + if (posOptional.isPresent()) { + Room room = getRoomAtPhysical(posOptional.get()); + if (room != null) { + room.keyFound(); + } else { + LOGGER.warn("[Skyblocker Dungeon Door] Failed to find room at player {} with position {}", name, posOptional.get()); + } + } else { + LOGGER.warn("[Skyblocker Dungeon Door] Failed to find player {}", name); + } + } + } + } + if (message.equals("[BOSS] Bonzo: Gratz for making it this far, but I'm basically unbeatable.") || message.equals("[BOSS] Scarf: This is where the journey ends for you, Adventurers.") || message.equals("[BOSS] The Professor: I was burdened with terrible news recently...") || message.equals("[BOSS] Thorn: Welcome Adventurers! I am Thorn, the Spirit! And host of the Vegan Trials!") || message.equals("[BOSS] Livid: Welcome, you've arrived right on time. I am Livid, the Master of Shadows.") || message.equals("[BOSS] Sadan: So you made it all the way here... Now you wish to defy me? Sadan?!") 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 9b95f14655..7797513fa5 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 @@ -6,7 +6,9 @@ import com.google.gson.JsonObject; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.context.CommandContext; +import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.ints.IntRBTreeSet; import it.unimi.dsi.fastutil.ints.IntSortedSet; @@ -27,6 +29,8 @@ import net.minecraft.text.Text; import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; import net.minecraft.world.World; import org.apache.commons.lang3.tuple.MutableTriple; import org.apache.commons.lang3.tuple.Triple; @@ -43,10 +47,14 @@ 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"); + private static final Vec3d DOOR_SIZE = new Vec3d(3, 4, 3); + private static final float[] RED_COLOR_COMPONENTS = {1, 0, 0}; + private static final float[] GREEN_COLOR_COMPONENTS = {0, 1, 0}; @NotNull private final Type type; @NotNull private final Set segments; + /** * The shape of the room. See {@link #getShape(IntSortedSet, IntSortedSet)}. */ @@ -82,6 +90,12 @@ public class Room { private Direction direction; private Vector2ic physicalCornerPos; + @Nullable + private BlockPos doorPos; + @Nullable + private Box doorBox; + private boolean keyFound; + public Room(@NotNull Type type, @NotNull Vector2ic... physicalPositions) { this.type = type; segments = Set.of(physicalPositions); @@ -238,7 +252,8 @@ private void removeCustomWaypoint(int secretIndex, BlockPos relativePos) { /** * Updates the room. *

- * This method returns immediately if any of the following conditions are met: + * First, this method tries to find a wither door and blood door. + * Then, this method returns immediately if any of the following conditions are met: *
    *
  • 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()})
  • *
  • The room has been matched or failed to match and is on cooldown. See {@link #matchState}.
  • @@ -254,14 +269,27 @@ private void removeCustomWaypoint(int secretIndex, BlockPos relativePos) { */ @SuppressWarnings("JavadocReference") protected void update() { + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + if (world == null) { + return; + } + + // Wither and blood door + if (SkyblockerConfigManager.get().locations.dungeons.doorHighlight.enableDoorHighlight && doorPos == null) { + doorPos = DungeonMapUtils.getWitherBloodDoorPos(world, segments); + if (doorPos != null) { + doorBox = new Box(doorPos.getX(), doorPos.getY(), doorPos.getZ(), doorPos.getX() + DOOR_SIZE.getX(), doorPos.getY() + DOOR_SIZE.getY(), doorPos.getZ() + DOOR_SIZE.getZ()); + } + } + + // Room scanning and matching // Logical AND has higher precedence than logical OR if (!type.needsScanning() || matchState != MatchState.MATCHING && matchState != MatchState.DOUBLE_CHECKING || !DungeonSecrets.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) { return; } - MinecraftClient client = MinecraftClient.getInstance(); ClientPlayerEntity player = client.player; - ClientWorld world = client.world; - if (player == null || world == null) { + if (player == null) { return; } findRoom = CompletableFuture.runAsync(() -> { @@ -451,14 +479,29 @@ protected BlockPos relativeToActual(BlockPos pos) { } /** - * Calls {@link SecretWaypoint#render(WorldRenderContext)} on {@link #secretWaypoints all secret waypoints}. + * Calls {@link SecretWaypoint#render(WorldRenderContext)} on {@link #secretWaypoints all secret waypoints} and renders a highlight around the wither or blood door, if it exists. */ protected void render(WorldRenderContext context) { - for (SecretWaypoint secretWaypoint : secretWaypoints.values()) { - if (secretWaypoint.shouldRender()) { - secretWaypoint.render(context); + if (isMatched()) { + for (SecretWaypoint secretWaypoint : secretWaypoints.values()) { + if (secretWaypoint.shouldRender()) { + secretWaypoint.render(context); + } } } + + if (!SkyblockerConfigManager.get().locations.dungeons.doorHighlight.enableDoorHighlight || doorPos == null) { + return; + } + float[] colorComponents = keyFound ? GREEN_COLOR_COMPONENTS : RED_COLOR_COMPONENTS; + switch (SkyblockerConfigManager.get().locations.dungeons.doorHighlight.doorHighlightType) { + case HIGHLIGHT -> RenderHelper.renderFilled(context, doorPos, DOOR_SIZE, colorComponents, 0.5f, true); + case OUTLINED_HIGHLIGHT -> { + RenderHelper.renderFilled(context, doorPos, DOOR_SIZE, colorComponents, 0.5f, true); + RenderHelper.renderOutline(context, doorBox, colorComponents, 5, true); + } + case OUTLINE -> RenderHelper.renderOutline(context, doorBox, colorComponents, 5, true); + } } /** @@ -550,6 +593,10 @@ protected boolean markSecrets(int secretIndex, boolean found) { } } + protected void keyFound() { + keyFound = true; + } + public enum Type { ENTRANCE(MapColor.DARK_GREEN.getRenderColorByte(MapColor.Brightness.HIGH)), ROOM(MapColor.ORANGE.getRenderColorByte(MapColor.Brightness.LOWEST)), diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java index 43d595a5d2..9ffd3a4314 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java @@ -52,13 +52,17 @@ public static void renderFilledWithBeaconBeam(WorldRenderContext context, BlockP } public static void renderFilled(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha, boolean throughWalls) { + renderFilled(context, Vec3d.of(pos), ONE, colorComponents, alpha, throughWalls); + } + + public static void renderFilled(WorldRenderContext context, BlockPos pos, Vec3d dimensions, float[] colorComponents, float alpha, boolean throughWalls) { if (throughWalls) { - if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) { - renderFilled(context, Vec3d.of(pos), ONE, colorComponents, alpha, true); + if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + dimensions.x, pos.getY() + dimensions.y, pos.getZ() + dimensions.z)) { + renderFilled(context, Vec3d.of(pos), dimensions, colorComponents, alpha, true); } } else { - if (OcclusionCulling.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) { - renderFilled(context, Vec3d.of(pos), ONE, colorComponents, alpha, false); + if (OcclusionCulling.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + dimensions.x, pos.getY() + dimensions.y, pos.getZ() + dimensions.z)) { + renderFilled(context, Vec3d.of(pos), dimensions, colorComponents, alpha, false); } } } diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 4679fa113b..1a52732658 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -183,6 +183,12 @@ "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enablePearlWaypoints.@Tooltip": "With these waypoints, you throw a pearl towards the block and at the same time AOTV up.", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableDefaultWaypoints" : "Enable Default Waypoints", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableDefaultWaypoints.@Tooltip" : "This includes all waypoints that do not belong to a category.", + "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight": "Door Highlight", + "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.enableDoorHighlight": "Enable Door Highlight", + "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.enableDoorHighlight.@Tooltip": "Highlights wither and blood doors red if locked and green if unlocked.", + "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType": "Door Highlight Type", + "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType.@Tooltip": "Highlight: Only displays a highlight.\n\nOutlined Highlight: Displays both a highlight and an outline.\n\nOutline: Only displays an outline.", + "text.autoconfig.skyblocker.option.locations.dungeons.doorHighlight.doorHighlightType.secretWaypointsNote": "\n\n\nNote: Dungeon Secret Waypoints must be enabled for this to work.", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit": "Dungeon Chest Profit Calculator", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.enableProfitCalculator": "Enable Profit Calculator", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.enableProfitCalculator.@Tooltip": "Displays the profit of a dungeon chest in the chest screen's title.\nGreen if there's profit.\nRed if there isn't profit.\nGray if you don't gain or lose anything.\nBlue if calculations were based on incomplete data.",