From f50e9a7fa0cb333f126c8d3faab61838fc111327 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Tue, 19 Dec 2023 14:48:24 +0800 Subject: Match puzzle and trap rooms --- .../skyblocker/skyblock/dungeon/secrets/Room.java | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'src/main/java/de/hysky/skyblocker') 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 7797513f..7d717e08 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 @@ -129,12 +129,16 @@ public class Room { @NotNull private Shape getShape(IntSortedSet segmentsX, IntSortedSet segmentsY) { - return switch (segments.size()) { - case 1 -> Shape.ONE_BY_ONE; - case 2 -> Shape.ONE_BY_TWO; - case 3 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.L_SHAPE : Shape.ONE_BY_THREE; - case 4 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.TWO_BY_TWO : Shape.ONE_BY_FOUR; - default -> throw new IllegalArgumentException("There are no matching room shapes with this set of physical positions: " + Arrays.toString(segments.toArray())); + return switch (type) { + case PUZZLE -> Shape.PUZZLE; + case TRAP -> Shape.TRAP; + default -> switch (segments.size()) { + case 1 -> Shape.ONE_BY_ONE; + case 2 -> Shape.ONE_BY_TWO; + case 3 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.L_SHAPE : Shape.ONE_BY_THREE; + case 4 -> segmentsX.size() == 2 && segmentsY.size() == 2 ? Shape.TWO_BY_TWO : Shape.ONE_BY_FOUR; + default -> throw new IllegalArgumentException("There are no matching room shapes with this set of physical positions: " + Arrays.toString(segments.toArray())); + }; }; } @@ -150,7 +154,7 @@ public class Room { @NotNull private Direction[] getPossibleDirections(IntSortedSet segmentsX, IntSortedSet segmentsY) { return switch (shape) { - case ONE_BY_ONE, TWO_BY_TWO -> Direction.values(); + case ONE_BY_ONE, TWO_BY_TWO, PUZZLE, TRAP -> Direction.values(); case ONE_BY_TWO, ONE_BY_THREE, ONE_BY_FOUR -> { if (segmentsX.size() > 1 && segmentsY.size() == 1) { yield new Direction[]{Direction.NW, Direction.SE}; @@ -629,7 +633,9 @@ public class Room { ONE_BY_THREE("1x3"), ONE_BY_FOUR("1x4"), L_SHAPE("L-shape"), - TWO_BY_TWO("2x2"); + TWO_BY_TWO("2x2"), + PUZZLE("puzzle"), + TRAP("trap"); final String shape; Shape(String shape) { -- cgit From a7932307432867082db98956e23535d38fae6084 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:27:23 +0800 Subject: Add support for puzzle detection through room matching --- .../hysky/skyblocker/config/SkyblockerConfig.java | 4 +-- .../config/categories/DungeonsCategory.java | 18 ++++++------- .../de/hysky/skyblocker/events/DungeonEvents.java | 30 ++++++++++++++++++++++ .../skyblock/dungeon/secrets/DungeonSecrets.java | 11 +++----- .../skyblocker/skyblock/dungeon/secrets/Room.java | 10 +++++--- .../resources/assets/skyblocker/lang/en_us.json | 4 +-- .../resources/assets/skyblocker/lang/pt_br.json | 2 -- .../resources/assets/skyblocker/lang/zh_cn.json | 2 -- 8 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/events/DungeonEvents.java (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java index 8604913c..25d0c0bc 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfig.java @@ -644,10 +644,10 @@ public class SkyblockerConfig { public static class SecretWaypoints { @SerialEntry - public boolean enableSecretWaypoints = true; + public boolean enableRoomMatching = true; @SerialEntry - public boolean noInitSecretWaypoints = false; + public boolean enableSecretWaypoints = true; @SerialEntry public Waypoint.Type waypointType = Waypoint.Type.WAYPOINT; 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 3d304487..79e0b00c 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -27,6 +27,15 @@ public class DungeonsCategory { .group(OptionGroup.createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints")) .collapsed(true) + .option(Option.createBuilder() + .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching")) + .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching.@Tooltip"))) + .binding(defaults.locations.dungeons.secretWaypoints.enableRoomMatching, + () -> config.locations.dungeons.secretWaypoints.enableRoomMatching, + newValue -> config.locations.dungeons.secretWaypoints.enableRoomMatching = newValue) + .controller(ConfigUtils::createBooleanController) + .flag(OptionFlag.GAME_RESTART) + .build()) .option(Option.createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints")) .binding(defaults.locations.dungeons.secretWaypoints.enableSecretWaypoints, @@ -34,15 +43,6 @@ public class DungeonsCategory { newValue -> config.locations.dungeons.secretWaypoints.enableSecretWaypoints = newValue) .controller(ConfigUtils::createBooleanController) .build()) - .option(Option.createBuilder() - .name(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints")) - .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip"))) - .binding(defaults.locations.dungeons.secretWaypoints.noInitSecretWaypoints, - () -> config.locations.dungeons.secretWaypoints.noInitSecretWaypoints, - newValue -> config.locations.dungeons.secretWaypoints.noInitSecretWaypoints = newValue) - .controller(ConfigUtils::createBooleanController) - .flag(OptionFlag.GAME_RESTART) - .build()) .option(Option.createBuilder() .name(Text.translatable("text.autoconfig.skyblocker.option.general.waypoints.waypointType")) .description(OptionDescription.of(Text.translatable("text.autoconfig.skyblocker.option.general.waypoints.waypointType.@Tooltip"))) diff --git a/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java new file mode 100644 index 00000000..9efa3607 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java @@ -0,0 +1,30 @@ +package de.hysky.skyblocker.events; + +import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; + +public class DungeonEvents { + public static final Event PUZZLE_MATCHED = EventFactory.createArrayBacked(RoomMatched.class, callbacks -> room -> { + for (RoomMatched callback : callbacks) { + callback.onRoomMatched(room); + } + }); + + public static final Event ROOM_MATCHED = EventFactory.createArrayBacked(RoomMatched.class, callbacks -> room -> { + for (RoomMatched callback : callbacks) { + callback.onRoomMatched(room); + } + if (room.getType() == Room.Type.PUZZLE) { + PUZZLE_MATCHED.invoker().onRoomMatched(room); + } + }); + + @Environment(EnvType.CLIENT) + @FunctionalInterface + public interface RoomMatched { + void onRoomMatched(Room room); + } +} 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 7f401fdb..b9e149c6 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 @@ -195,7 +195,7 @@ public class DungeonSecrets { * Use {@link #isRoomsLoaded()} to check for completion of loading. */ public static void init() { - if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.noInitSecretWaypoints) { + if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching) { return; } // Execute with MinecraftClient as executor since we need to wait for MinecraftClient#resourceManager to be set @@ -426,9 +426,6 @@ public class DungeonSecrets { */ @SuppressWarnings("JavadocReference") private static void update() { - if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints) { - return; - } if (!Utils.isInDungeons()) { if (mapEntrancePos != null) { reset(); @@ -663,12 +660,12 @@ public class DungeonSecrets { } /** - * Checks if the player is in a dungeon and {@link de.hysky.skyblocker.config.SkyblockerConfig.Dungeons#secretWaypoints Secret Waypoints} is enabled. + * Checks if {@link de.hysky.skyblocker.config.SkyblockerConfig.SecretWaypoints#enableRoomMatching room matching} is enabled and the player is in a dungeon. * - * @return whether dungeon secrets should be processed + * @return whether room matching and dungeon secrets should be processed */ private static boolean shouldProcess() { - return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && Utils.isInDungeons(); + return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching && Utils.isInDungeons(); } /** 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 7d717e08..0d3c6a87 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 @@ -7,6 +7,7 @@ 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.events.DungeonEvents; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.scheduler.Scheduler; @@ -381,6 +382,7 @@ public class Room { 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. + matchState = MatchState.FAILED; 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(); @@ -397,7 +399,9 @@ public class Room { 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); + matchState = MatchState.MATCHED; + DungeonEvents.ROOM_MATCHED.invoker().onRoomMatched(this); + DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} confirmed after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks); discard(); return true; } @@ -444,7 +448,6 @@ public class Room { * 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); @@ -461,7 +464,6 @@ 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; @@ -486,7 +488,7 @@ public class Room { * 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) { - if (isMatched()) { + if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && isMatched()) { for (SecretWaypoint secretWaypoint : secretWaypoints.values()) { if (secretWaypoint.shouldRender()) { secretWaypoint.render(context); diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 49c446df..8692c4f8 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -165,9 +165,9 @@ "text.autoconfig.skyblocker.option.locations.spidersDen.relics.highlightFoundRelics": "Highlight found relics", "text.autoconfig.skyblocker.option.locations.dungeons": "Dungeons", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints": "Dungeon Secret Waypoints", + "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching": "Enable Room Matching", + "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableRoomMatching.@Tooltip": "Disabling this option can save around 20 MB of ram, but Secret Waypoint and some puzzle solvers require this option to be enabled.", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints": "Enable Dungeon Secret Waypoints", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints": "Do Not Initialize Secret Waypoints", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip": "This option can save around 20 MB of ram if enabled, but Secret Waypoint will require a restart after turning off this option to work.", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.showSecretText": "Show Secret Text", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableEntranceWaypoints" : "Enable Entrance Waypoints", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSuperboomWaypoints" : "Enable Superboom Waypoints", diff --git a/src/main/resources/assets/skyblocker/lang/pt_br.json b/src/main/resources/assets/skyblocker/lang/pt_br.json index f14b8684..11242adf 100644 --- a/src/main/resources/assets/skyblocker/lang/pt_br.json +++ b/src/main/resources/assets/skyblocker/lang/pt_br.json @@ -239,7 +239,6 @@ "text.autoconfig.skyblocker.option.locations.spidersDen": "Covil da Aranha", "text.autoconfig.skyblocker.option.locations.barn.solveTreasureHunter": "Guia sobre Treasure Hunter", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints": "Ativar marcadores de segredos para dungeon", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints": "Não iniciar marcadores de segredos", "text.autoconfig.skyblocker.option.general.itemInfoDisplay.itemRarityBackgroundStyle.@Tooltip": "Escolha entre um estilo de fundo circular ou quadrado!", "text.autoconfig.skyblocker.option.general.itemInfoDisplay.itemRarityBackgroundStyle": "Estilo de fundo da raridade do item", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints": "Marcadores de segredos da dungeon", @@ -281,6 +280,5 @@ "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeKismet.@Tooltip": "Se ativado, o preço de uma Kismet usada será subtraído no cálculo da margem de lucro", "text.autoconfig.skyblocker.option.locations.dungeons.dungeonChestProfit.includeEssence": "Incluir essência", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.showSecretText": "Exibir texto secreto", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip": "Essa opção pode salvar por volta de 20 MB de ram se ativada, mas os marcadores de segredos vão precisar de um reinício do jogo após desligar essa opção para funcionar.", "text.autoconfig.skyblocker.option.quickNav.button.clickEvent": "Evento ao clicar" } diff --git a/src/main/resources/assets/skyblocker/lang/zh_cn.json b/src/main/resources/assets/skyblocker/lang/zh_cn.json index 83317c00..a268d5a5 100644 --- a/src/main/resources/assets/skyblocker/lang/zh_cn.json +++ b/src/main/resources/assets/skyblocker/lang/zh_cn.json @@ -134,10 +134,8 @@ "text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe": "井字棋谜题助手", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints": "地牢秘密路径点", "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.enableSecretWaypoints": "启用地牢秘密路径点", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints": "不对地牢秘密路径点进行初始化", "text.autoconfig.skyblocker.option.general.tabHud.nameSorting.@Tooltip": "\"Alphabetical\" 以词典序排列玩家, 而 \"Default\" 以Hypixel默认顺序排列", "text.autoconfig.skyblocker.option.locations.dungeons.solveTicTacToe.@Tooltip": "以红色方块标记井字棋的下一步!", - "text.autoconfig.skyblocker.option.locations.dungeons.secretWaypoints.noInitSecretWaypoints.@Tooltip": "此选项可节约20MB左右的内存, 但秘密路径点需要关闭此选项并重启后才能使用。", "text.autoconfig.skyblocker.option.general.quiverWarning.enableQuiverWarningInDungeons": "在地牢内启用箭袋提示", "text.autoconfig.skyblocker.option.general.quiverWarning": "箭袋提示", "text.autoconfig.skyblocker.option.general.quiverWarning.enableQuiverWarning": "启用箭袋提示", -- cgit From b5b1e509d23fbe1e9127110416ea1f467ebb5380 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:48:26 +0800 Subject: Migrate blaze and tic-tac-toe puzzles --- src/main/java/de/hysky/skyblocker/SkyblockerMod.java | 1 - .../hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java | 12 ++++++++++++ .../de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index ad5e442f..5558217f 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -124,7 +124,6 @@ public class SkyblockerMod implements ClientModInitializer { statusBarTracker.init(); Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20); Scheduler.INSTANCE.scheduleCyclic(DiscordRPCManager::updateDataAndPresence, 200); - Scheduler.INSTANCE.scheduleCyclic(TicTacToe::tick, 4); Scheduler.INSTANCE.scheduleCyclic(LividColor::update, 10); Scheduler.INSTANCE.scheduleCyclic(BackpackPreview::tick, 50); Scheduler.INSTANCE.scheduleCyclic(DwarvenHud::update, 40); diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java index f49a2f2e..aabef183 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java @@ -1,10 +1,12 @@ package de.hysky.skyblocker.skyblock.dungeon; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.DungeonEvents; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.ObjectIntPair; +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.minecraft.client.MinecraftClient; @@ -29,20 +31,30 @@ public class DungeonBlaze { private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F}; private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f}; + private static boolean inBlaze; private static ArmorStandEntity highestBlaze = null; private static ArmorStandEntity lowestBlaze = null; private static ArmorStandEntity nextHighestBlaze = null; private static ArmorStandEntity nextLowestBlaze = null; public static void init() { + DungeonEvents.PUZZLE_MATCHED.register(room -> { + if (room.getName().startsWith("blaze-room")) { + inBlaze = true; + } + }); Scheduler.INSTANCE.scheduleCyclic(DungeonBlaze::update, 4); WorldRenderEvents.BEFORE_DEBUG_RENDER.register(DungeonBlaze::blazeRenderer); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> inBlaze = false); } /** * Updates the state of Blaze entities and triggers the rendering process if necessary. */ public static void update() { + if (!inBlaze) { + return; + } ClientWorld world = MinecraftClient.getInstance().world; ClientPlayerEntity player = MinecraftClient.getInstance().player; if (world == null || player == null || !Utils.isInDungeons()) return; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java index 7f249e7d..2bb3e4e0 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java @@ -1,9 +1,12 @@ package de.hysky.skyblocker.skyblock.dungeon; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.DungeonEvents; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.scheduler.Scheduler; import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils; +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.minecraft.block.Block; @@ -28,13 +31,25 @@ import java.util.List; public class TicTacToe { private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class); private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F}; + private static boolean inTicTacToe; private static Box nextBestMoveToMake = null; public static void init() { + DungeonEvents.PUZZLE_MATCHED.register(room -> { + if (room.getName().startsWith("tic-tac-toe")) { + inTicTacToe = true; + } + }); + Scheduler.INSTANCE.scheduleCyclic(TicTacToe::tick, 4); WorldRenderEvents.BEFORE_DEBUG_RENDER.register(TicTacToe::solutionRenderer); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> inTicTacToe = false); } public static void tick() { + if (!inTicTacToe) { + return; + } + MinecraftClient client = MinecraftClient.getInstance(); ClientWorld world = client.world; ClientPlayerEntity player = client.player; -- cgit From c8136497ef6198e6b8a426fc23ccadeefe27ebdb Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:39:45 +0800 Subject: Rename Dungeon Manager --- .../java/de/hysky/skyblocker/SkyblockerMod.java | 4 +- .../de/hysky/skyblocker/events/DungeonEvents.java | 1 + .../de/hysky/skyblocker/mixin/BatEntityMixin.java | 4 +- .../mixin/ClientPlayNetworkHandlerMixin.java | 4 +- .../skyblock/dungeon/secrets/DungeonManager.java | 681 +++++++++++++++++++++ .../skyblock/dungeon/secrets/DungeonMapUtils.java | 2 +- .../skyblock/dungeon/secrets/DungeonSecrets.java | 681 --------------------- .../skyblocker/skyblock/dungeon/secrets/Room.java | 38 +- .../skyblock/dungeon/secrets/DungeonRoomsDFU.java | 4 +- 9 files changed, 710 insertions(+), 709 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 5558217f..d1aa3153 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -6,7 +6,7 @@ import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.skyblock.*; import de.hysky.skyblocker.skyblock.dungeon.*; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.dungeon.secrets.SecretsTracker; import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud; import de.hysky.skyblocker.skyblock.item.*; @@ -96,7 +96,7 @@ public class SkyblockerMod implements ClientModInitializer { FishingHelper.init(); TabHud.init(); DungeonMap.init(); - DungeonSecrets.init(); + DungeonManager.init(); DungeonBlaze.init(); ChestValue.init(); FireFreezeStaffTimer.init(); diff --git a/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java index 9efa3607..bf7ba2b2 100644 --- a/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java +++ b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java @@ -7,6 +7,7 @@ import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; public class DungeonEvents { + // TODO Some rooms such as creeper beam and water board does not get matched public static final Event PUZZLE_MATCHED = EventFactory.createArrayBacked(RoomMatched.class, callbacks -> room -> { for (RoomMatched callback : callbacks) { callback.onRoomMatched(room); diff --git a/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java b/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java index dc2fa673..fa97e546 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/BatEntityMixin.java @@ -1,6 +1,6 @@ package de.hysky.skyblocker.mixin; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import net.minecraft.entity.EntityType; import net.minecraft.entity.mob.AmbientEntity; import net.minecraft.entity.passive.BatEntity; @@ -16,6 +16,6 @@ public abstract class BatEntityMixin extends AmbientEntity { @Override public void onRemoved() { super.onRemoved(); - DungeonSecrets.onBatRemoved(this); + DungeonManager.onBatRemoved(this); } } diff --git a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java index f177d2f8..7f1320c8 100644 --- a/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixin/ClientPlayNetworkHandlerMixin.java @@ -3,8 +3,8 @@ package de.hysky.skyblocker.mixin; import com.llamalad7.mixinextras.injector.WrapWithCondition; import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.skyblock.FishingHelper; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.waypoint.MythologicalRitual; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonSecrets; import de.hysky.skyblocker.utils.Utils; import dev.cbyrne.betterinject.annotations.Inject; import net.minecraft.client.MinecraftClient; @@ -28,7 +28,7 @@ public abstract class ClientPlayNetworkHandlerMixin { @ModifyVariable(method = "onItemPickupAnimation", at = @At(value = "STORE", ordinal = 0)) private ItemEntity skyblocker$onItemPickup(ItemEntity itemEntity, @Local LivingEntity collector) { - DungeonSecrets.onItemPickup(itemEntity, collector, collector == MinecraftClient.getInstance().player); + DungeonManager.onItemPickup(itemEntity, collector, collector == MinecraftClient.getInstance().player); return itemEntity; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java new file mode 100644 index 00000000..7705ca46 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -0,0 +1,681 @@ +package de.hysky.skyblocker.skyblock.dungeon.secrets; + +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.serialization.JsonOps; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import it.unimi.dsi.fastutil.objects.Object2ByteMap; +import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.command.argument.BlockPosArgumentType; +import net.minecraft.command.argument.PosArgument; +import net.minecraft.command.argument.TextArgumentType; +import net.minecraft.entity.Entity; +import net.minecraft.entity.ItemEntity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.mob.AmbientEntity; +import net.minecraft.entity.passive.BatEntity; +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.server.command.ServerCommandSource; +import net.minecraft.text.Text; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Identifier; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import net.minecraft.util.math.Vec3i; +import net.minecraft.world.World; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector2ic; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; +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 DungeonManager { + protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonManager.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. + * + * @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 de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. + */ + @SuppressWarnings("JavadocReference") + protected static final Object2ByteMap 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 de.hysky.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>> ROOMS_DATA = new HashMap<>(); + @NotNull + private static final Map rooms = new HashMap<>(); + private static final Map roomsJson = new HashMap<>(); + private static final Map waypointsJson = new HashMap<>(); + /** + * The map of dungeon room names to custom waypoints relative to the room. + */ + private static final Table customWaypoints = HashBasedTable.create(); + @Nullable + private static CompletableFuture 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(); + } + + public static Stream getRoomsStream() { + return rooms.values().stream(); + } + + @SuppressWarnings("unused") + public static JsonObject getRoomMetadata(String room) { + return roomsJson.get(room).getAsJsonObject(); + } + + public static JsonArray getRoomWaypoints(String room) { + return waypointsJson.get(room).getAsJsonArray(); + } + + /** + * @see #customWaypoints + */ + public static Map getCustomWaypoints(String room) { + return customWaypoints.row(room); + } + + /** + * @see #customWaypoints + */ + @SuppressWarnings("UnusedReturnValue") + public static SecretWaypoint addCustomWaypoint(String room, SecretWaypoint waypoint) { + return customWaypoints.put(room, waypoint.pos, waypoint); + } + + /** + * @see #customWaypoints + */ + public static void addCustomWaypoints(String room, Collection waypoints) { + for (SecretWaypoint waypoint : waypoints) { + addCustomWaypoint(room, waypoint); + } + } + + /** + * @see #customWaypoints + */ + @Nullable + public static SecretWaypoint removeCustomWaypoint(String room, BlockPos pos) { + return customWaypoints.remove(room, pos); + } + + /** + * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}. + * Use {@link #isRoomsLoaded()} to check for completion of loading. + */ + public static void init() { + if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching) { + return; + } + // Execute with MinecraftClient as executor since we need to wait for MinecraftClient#resourceManager to be set + CompletableFuture.runAsync(DungeonManager::load, MinecraftClient.getInstance()).exceptionally(e -> { + LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e); + return null; + }); + ClientLifecycleEvents.CLIENT_STOPPING.register(DungeonManager::saveCustomWaypoints); + Scheduler.INSTANCE.scheduleCyclic(DungeonManager::update, 10); + WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonManager::render); + ClientReceiveMessageEvents.GAME.register(DungeonManager::onChatMessage); + ClientReceiveMessageEvents.GAME_CANCELED.register(DungeonManager::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))) + .then(literal("getRelativePos").executes(DungeonManager::getRelativePos)) + .then(literal("getRelativeTargetPos").executes(DungeonManager::getRelativeTargetPos)) + .then(literal("addWaypoint").then(addCustomWaypointCommand(false))) + .then(literal("addWaypointRelatively").then(addCustomWaypointCommand(true))) + .then(literal("removeWaypoint").then(removeCustomWaypointCommand(false))) + .then(literal("removeWaypointRelatively").then(removeCustomWaypointCommand(true))) + )))); + ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); + } + + private static void load() { + long startTime = System.currentTimeMillis(); + List> dungeonFutures = new ArrayList<>(); + for (Map.Entry 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 Dungeon Secrets] 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 roomsMap = ROOMS_DATA.get(dungeon).get(roomShape); + synchronized (roomsMap) { + roomsMap.put(room, rooms); + } + LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room); + }).exceptionally(e -> { + LOGGER.error("[Skyblocker Dungeon Secrets] 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"))) { + loadJson(roomsReader, roomsJson); + loadJson(waypointsReader, waypointsJson); + LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secret waypoints json"); + } catch (Exception e) { + LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secret waypoints json", e); + } + })); + dungeonFutures.add(CompletableFuture.runAsync(() -> { + try (BufferedReader customWaypointsReader = Files.newBufferedReader(CUSTOM_WAYPOINTS_DIR)) { + SkyblockerMod.GSON.fromJson(customWaypointsReader, JsonObject.class).asMap().forEach((room, waypointsJson) -> + addCustomWaypoints(room, SecretWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, waypointsJson).resultOrPartial(LOGGER::error).orElseGet(ArrayList::new)) + ); + LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded custom dungeon secret waypoints"); + } catch (Exception e) { + LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load custom dungeon secret waypoints", e); + } + })); + roomsLoaded = CompletableFuture.allOf(dungeonFutures.toArray(CompletableFuture[]::new)).thenRun(() -> LOGGER.info("[Skyblocker Dungeon Secrets] Loaded dungeon secrets for {} dungeon(s), {} room shapes, {} rooms, and {} custom secret waypoints total in {} ms", ROOMS_DATA.size(), ROOMS_DATA.values().stream().mapToInt(Map::size).sum(), ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).mapToInt(Map::size).sum(), customWaypoints.size(), System.currentTimeMillis() - startTime)).exceptionally(e -> { + LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e); + return null; + }); + LOGGER.info("[Skyblocker Dungeon Secrets] Started loading dungeon secrets in (blocked main thread for) {} ms", System.currentTimeMillis() - startTime); + } + + private static void saveCustomWaypoints(MinecraftClient client) { + try (BufferedWriter writer = Files.newBufferedWriter(CUSTOM_WAYPOINTS_DIR)) { + JsonObject customWaypointsJson = new JsonObject(); + customWaypoints.rowMap().forEach((room, waypoints) -> + customWaypointsJson.add(room, SecretWaypoint.LIST_CODEC.encodeStart(JsonOps.INSTANCE, new ArrayList<>(waypoints.values())).resultOrPartial(LOGGER::error).orElseGet(JsonArray::new)) + ); + SkyblockerMod.GSON.toJson(customWaypointsJson, writer); + LOGGER.info("[Skyblocker Dungeon Secrets] Saved custom dungeon secret waypoints"); + } catch (Exception e) { + LOGGER.error("[Skyblocker Dungeon Secrets] Failed to save custom dungeon secret waypoints", e); + } + } + + private static int[] readRoom(Resource resource) throws RuntimeException { + try (ObjectInputStream in = new ObjectInputStream(new InflaterInputStream(resource.getInputStream()))) { + return (int[]) in.readObject(); + } catch (IOException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + /** + * Loads the json from the given {@link BufferedReader} into the given {@link Map}. + * + * @param reader the reader to read the json from + * @param map the map to load into + */ + private static void loadJson(BufferedReader reader, Map map) { + SkyblockerMod.GSON.fromJson(reader, JsonObject.class).asMap().forEach((room, jsonElement) -> map.put(room.toLowerCase().replaceAll(" ", "-"), jsonElement)); + } + + private static ArgumentBuilder> markSecretsCommand(boolean found) { + return argument("secretIndex", IntegerArgumentType.integer()).executes(context -> { + int secretIndex = IntegerArgumentType.getInteger(context, "secretIndex"); + if (markSecrets(secretIndex, found)) { + context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFound" : "skyblocker.dungeons.secrets.markSecretMissing", secretIndex))); + } else { + context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFoundUnable" : "skyblocker.dungeons.secrets.markSecretMissingUnable", secretIndex))); + } + return Command.SINGLE_SUCCESS; + }); + } + + private static int getRelativePos(CommandContext context) { + return getRelativePos(context.getSource(), context.getSource().getPlayer().getBlockPos()); + } + + private static int getRelativeTargetPos(CommandContext context) { + if (MinecraftClient.getInstance().crosshairTarget instanceof BlockHitResult blockHitResult && blockHitResult.getType() == HitResult.Type.BLOCK) { + return getRelativePos(context.getSource(), blockHitResult.getBlockPos()); + } else { + context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.noTarget"))); + } + return Command.SINGLE_SUCCESS; + } + + private static int getRelativePos(FabricClientCommandSource source, BlockPos pos) { + Room room = getRoomAtPhysical(pos); + if (isRoomMatched(room)) { + BlockPos relativePos = currentRoom.actualToRelative(pos); + source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), relativePos.getX(), relativePos.getY(), relativePos.getZ()))); + } else { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); + } + return Command.SINGLE_SUCCESS; + } + + private static ArgumentBuilder> addCustomWaypointCommand(boolean relative) { + return argument("pos", BlockPosArgumentType.blockPos()) + .then(argument("secretIndex", IntegerArgumentType.integer()) + .then(argument("category", SecretWaypoint.Category.CategoryArgumentType.category()) + .then(argument("name", TextArgumentType.text()).executes(context -> { + // TODO Less hacky way with custom ClientBlockPosArgumentType + BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null)); + return relative ? addCustomWaypointRelative(context, pos) : addCustomWaypoint(context, pos); + })) + ) + ); + } + + private static int addCustomWaypoint(CommandContext context, BlockPos pos) { + Room room = getRoomAtPhysical(pos); + if (isRoomMatched(room)) { + room.addCustomWaypoint(context, room.actualToRelative(pos)); + } else { + context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); + } + return Command.SINGLE_SUCCESS; + } + + private static int addCustomWaypointRelative(CommandContext context, BlockPos pos) { + if (isCurrentRoomMatched()) { + currentRoom.addCustomWaypoint(context, pos); + } else { + context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); + } + return Command.SINGLE_SUCCESS; + } + + private static ArgumentBuilder> removeCustomWaypointCommand(boolean relative) { + return argument("pos", BlockPosArgumentType.blockPos()) + .executes(context -> { + // TODO Less hacky way with custom ClientBlockPosArgumentType + BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null)); + return relative ? removeCustomWaypointRelative(context, pos) : removeCustomWaypoint(context, pos); + }); + } + + private static int removeCustomWaypoint(CommandContext context, BlockPos pos) { + Room room = getRoomAtPhysical(pos); + if (isRoomMatched(room)) { + room.removeCustomWaypoint(context, room.actualToRelative(pos)); + } else { + context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); + } + return Command.SINGLE_SUCCESS; + } + + private static int removeCustomWaypointRelative(CommandContext context, BlockPos pos) { + if (isCurrentRoomMatched()) { + currentRoom.removeCustomWaypoint(context, pos); + } else { + context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); + } + return Command.SINGLE_SUCCESS; + } + + /** + * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod. + *

+ * When entering a new dungeon, this method: + *
    + *
  • Gets the physical northwest corner position of the entrance room and saves it in {@link #physicalEntrancePos}.
  • + *
  • Do nothing until the dungeon map exists.
  • + *
  • Gets the upper left corner of entrance room on the map and saves it in {@link #mapEntrancePos}.
  • + *
  • Gets the size of a room on the map in pixels and saves it in {@link #mapRoomSize}.
  • + *
  • Creates a new {@link Room} with {@link Room.Type} {@link Room.Type.ENTRANCE ENTRANCE} and sets {@link #currentRoom}.
  • + *
+ * When processing an existing dungeon, this method: + *
    + *
  • Calculates the physical northwest corner and upper left corner on the map of the room the player is currently in.
  • + *
  • Gets the room type based on the map color.
  • + *
  • If the room has not been created (when the physical northwest corner is not in {@link #rooms}):
  • + *
      + *
    • If the room type is {@link Room.Type.ROOM}, gets the northwest corner of all connected room segments with {@link DungeonMapUtils#getRoomSegments(MapState, Vector2ic, int, byte)}. (For example, a 1x2 room has two room segments.)
    • + *
    • Create a new room.
    • + *
    + *
  • Sets {@link #currentRoom} to the current room, either created from the previous step or from {@link #rooms}.
  • + *
  • Calls {@link Room#update()} on {@link #currentRoom}.
  • + *
+ */ + @SuppressWarnings("JavadocReference") + private static void update() { + if (!Utils.isInDungeons()) { + if (mapEntrancePos != null) { + reset(); + } + return; + } + MinecraftClient client = MinecraftClient.getInstance(); + ClientPlayerEntity player = client.player; + if (player == null || client.world == null) { + return; + } + if (physicalEntrancePos == null) { + Vec3d playerPos = player.getPos(); + physicalEntrancePos = DungeonMapUtils.getPhysicalRoomPos(playerPos); + currentRoom = newRoom(Room.Type.ENTRANCE, physicalEntrancePos); + } + ItemStack stack = player.getInventory().main.get(8); + if (!stack.isOf(Items.FILLED_MAP)) { + return; + } + MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); + if (map == null) { + return; + } + if (mapEntrancePos == null || mapRoomSize == 0) { + ObjectIntPair mapEntrancePosAndSize = DungeonMapUtils.getMapEntrancePosAndRoomSize(map); + if (mapEntrancePosAndSize == null) { + return; + } + mapEntrancePos = mapEntrancePosAndSize.left(); + mapRoomSize = mapEntrancePosAndSize.rightInt(); + LOGGER.info("[Skyblocker Dungeon Secrets] Started dungeon with map room size {}, map entrance pos {}, player pos {}, and physical entrance pos {}", mapRoomSize, mapEntrancePos, client.player.getPos(), physicalEntrancePos); + } + + Vector2ic physicalPos = DungeonMapUtils.getPhysicalRoomPos(client.player.getPos()); + Vector2ic mapPos = DungeonMapUtils.getMapPosFromPhysical(physicalEntrancePos, mapEntrancePos, mapRoomSize, physicalPos); + Room room = rooms.get(physicalPos); + if (room == null) { + Room.Type type = DungeonMapUtils.getRoomType(map, mapPos); + if (type == null || type == Room.Type.UNKNOWN) { + return; + } + switch (type) { + case ENTRANCE, PUZZLE, TRAP, MINIBOSS, FAIRY, BLOOD -> room = newRoom(type, physicalPos); + case ROOM -> room = newRoom(type, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, mapPos, mapRoomSize, type.color))); + } + } + if (room != null && currentRoom != room) { + currentRoom = room; + } + currentRoom.update(); + } + + /** + * Creates a new room with the given type and physical positions, + * adds the room to {@link #rooms}, and sets {@link #currentRoom} to the new room. + * + * @param type the type of room to create + * @param physicalPositions the physical positions of the room + */ + @Nullable + private static Room newRoom(Room.Type type, Vector2ic... physicalPositions) { + try { + Room newRoom = new Room(type, physicalPositions); + for (Vector2ic physicalPos : physicalPositions) { + rooms.put(physicalPos, newRoom); + } + return newRoom; + } catch (IllegalArgumentException e) { + LOGGER.error("[Skyblocker Dungeon Secrets] Failed to create room", e); + } + return null; + } + + /** + * Renders the secret waypoints in {@link #currentRoom} if {@link #shouldProcess()} and {@link #currentRoom} is not null. + */ + private static void render(WorldRenderContext context) { + 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()} 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?!") + || message.equals("[BOSS] Maxor: WELL! WELL! WELL! LOOK WHO'S HERE!")) reset(); + } + + /** + * Calls {@link Room#onUseBlock(World, BlockHitResult)} on {@link #currentRoom} if {@link #isCurrentRoomMatched()}. + * Used to detect finding {@link SecretWaypoint.Category.CHEST} and {@link SecretWaypoint.Category.WITHER} secrets. + * + * @return {@link ActionResult#PASS} + */ + @SuppressWarnings("JavadocReference") + private static ActionResult onUseBlock(World world, BlockHitResult hitResult) { + if (isCurrentRoomMatched()) { + currentRoom.onUseBlock(world, hitResult); + } + return ActionResult.PASS; + } + + /** + * Calls {@link Room#onItemPickup(ItemEntity, LivingEntity)} on the room the {@code collector} is in if that room {@link #isRoomMatched(Room)}. + * Used to detect finding {@link SecretWaypoint.Category.ITEM} secrets. + * If the collector is the player, {@link #currentRoom} is used as an optimization. + */ + @SuppressWarnings("JavadocReference") + public static void onItemPickup(ItemEntity itemEntity, LivingEntity collector, boolean isPlayer) { + if (isPlayer) { + if (isCurrentRoomMatched()) { + currentRoom.onItemPickup(itemEntity, collector); + } + } else { + Room room = getRoomAtPhysical(collector.getPos()); + if (isRoomMatched(room)) { + room.onItemPickup(itemEntity, collector); + } + } + } + + /** + * Calls {@link Room#onBatRemoved(BatEntity)} on the room the {@code bat} is in if that room {@link #isRoomMatched(Room)}. + * Used to detect finding {@link SecretWaypoint.Category.BAT} secrets. + */ + @SuppressWarnings("JavadocReference") + public static void onBatRemoved(AmbientEntity bat) { + Room room = getRoomAtPhysical(bat.getPos()); + if (isRoomMatched(room)) { + room.onBatRemoved(bat); + } + } + + public static boolean markSecrets(int secretIndex, boolean found) { + if (isCurrentRoomMatched()) { + return currentRoom.markSecrets(secretIndex, found); + } + return false; + } + + /** + * Gets the room at the given physical position. + * + * @param pos the physical position + * @return the room at the given physical position, or null if there is no room at the given physical position + * @see #rooms + * @see DungeonMapUtils#getPhysicalRoomPos(Vec3d) + */ + @Nullable + private static Room getRoomAtPhysical(Vec3d pos) { + return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos)); + } + + /** + * Gets the room at the given physical position. + * + * @param pos the physical position + * @return the room at the given physical position, or null if there is no room at the given physical position + * @see #rooms + * @see DungeonMapUtils#getPhysicalRoomPos(Vec3i) + */ + @Nullable + private static Room getRoomAtPhysical(Vec3i pos) { + return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos)); + } + + /** + * Calls {@link #isRoomMatched(Room)} on {@link #currentRoom}. + * + * @return {@code true} if {@link #currentRoom} is not null and {@link #isRoomMatched(Room)} + */ + private static boolean isCurrentRoomMatched() { + return isRoomMatched(currentRoom); + } + + /** + * Calls {@link #shouldProcess()} and {@link Room#isMatched()} on the given room. + * + * @param room the room to check + * @return {@code true} if {@link #shouldProcess()}, the given room is not null, and {@link Room#isMatched()} on the given room + */ + @Contract("null -> false") + private static boolean isRoomMatched(@Nullable Room room) { + return shouldProcess() && room != null && room.isMatched(); + } + + /** + * Checks if {@link de.hysky.skyblocker.config.SkyblockerConfig.SecretWaypoints#enableRoomMatching room matching} is enabled and the player is in a dungeon. + * + * @return whether room matching and dungeon secrets should be processed + */ + private static boolean shouldProcess() { + return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching && Utils.isInDungeons(); + } + + /** + * Resets fields when leaving a dungeon or entering boss. + */ + private static void reset() { + mapEntrancePos = null; + mapRoomSize = 0; + physicalEntrancePos = null; + rooms.clear(); + currentRoom = null; + } +} 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 01f2c9fc..516c3bad 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 @@ -271,7 +271,7 @@ public class DungeonMapUtils { queue.add(newMapPos); } } - DungeonSecrets.LOGGER.debug("[Skyblocker] Found dungeon room segments: {}", Arrays.toString(segments.toArray())); + DungeonManager.LOGGER.debug("[Skyblocker] Found dungeon room segments: {}", Arrays.toString(segments.toArray())); return segments.toArray(Vector2ic[]::new); } 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 deleted file mode 100644 index b9e149c6..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java +++ /dev/null @@ -1,681 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon.secrets; - -import com.google.common.collect.HashBasedTable; -import com.google.common.collect.Table; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.mojang.brigadier.Command; -import com.mojang.brigadier.arguments.IntegerArgumentType; -import com.mojang.brigadier.builder.ArgumentBuilder; -import com.mojang.brigadier.builder.RequiredArgumentBuilder; -import com.mojang.brigadier.context.CommandContext; -import com.mojang.serialization.JsonOps; -import de.hysky.skyblocker.SkyblockerMod; -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.utils.Constants; -import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.objects.Object2ByteMap; -import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectIntPair; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; -import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; -import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; -import net.fabricmc.fabric.api.event.player.UseBlockCallback; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.command.argument.BlockPosArgumentType; -import net.minecraft.command.argument.PosArgument; -import net.minecraft.command.argument.TextArgumentType; -import net.minecraft.entity.Entity; -import net.minecraft.entity.ItemEntity; -import net.minecraft.entity.LivingEntity; -import net.minecraft.entity.mob.AmbientEntity; -import net.minecraft.entity.passive.BatEntity; -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.server.command.ServerCommandSource; -import net.minecraft.text.Text; -import net.minecraft.util.ActionResult; -import net.minecraft.util.Identifier; -import net.minecraft.util.hit.BlockHitResult; -import net.minecraft.util.hit.HitResult; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Vec3d; -import net.minecraft.util.math.Vec3i; -import net.minecraft.world.World; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.joml.Vector2ic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; -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"; - 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. - * - * @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 de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. - */ - @SuppressWarnings("JavadocReference") - protected static final Object2ByteMap 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 de.hysky.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>> ROOMS_DATA = new HashMap<>(); - @NotNull - private static final Map rooms = new HashMap<>(); - private static final Map roomsJson = new HashMap<>(); - private static final Map waypointsJson = new HashMap<>(); - /** - * The map of dungeon room names to custom waypoints relative to the room. - */ - private static final Table customWaypoints = HashBasedTable.create(); - @Nullable - private static CompletableFuture 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(); - } - - public static Stream getRoomsStream() { - return rooms.values().stream(); - } - - @SuppressWarnings("unused") - public static JsonObject getRoomMetadata(String room) { - return roomsJson.get(room).getAsJsonObject(); - } - - public static JsonArray getRoomWaypoints(String room) { - return waypointsJson.get(room).getAsJsonArray(); - } - - /** - * @see #customWaypoints - */ - public static Map getCustomWaypoints(String room) { - return customWaypoints.row(room); - } - - /** - * @see #customWaypoints - */ - @SuppressWarnings("UnusedReturnValue") - public static SecretWaypoint addCustomWaypoint(String room, SecretWaypoint waypoint) { - return customWaypoints.put(room, waypoint.pos, waypoint); - } - - /** - * @see #customWaypoints - */ - public static void addCustomWaypoints(String room, Collection waypoints) { - for (SecretWaypoint waypoint : waypoints) { - addCustomWaypoint(room, waypoint); - } - } - - /** - * @see #customWaypoints - */ - @Nullable - public static SecretWaypoint removeCustomWaypoint(String room, BlockPos pos) { - return customWaypoints.remove(room, pos); - } - - /** - * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}. - * Use {@link #isRoomsLoaded()} to check for completion of loading. - */ - public static void init() { - if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching) { - 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 Dungeon Secrets] Failed to load dungeon secrets", e); - return null; - }); - ClientLifecycleEvents.CLIENT_STOPPING.register(DungeonSecrets::saveCustomWaypoints); - Scheduler.INSTANCE.scheduleCyclic(DungeonSecrets::update, 10); - WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonSecrets::render); - ClientReceiveMessageEvents.GAME.register(DungeonSecrets::onChatMessage); - 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))) - .then(literal("getRelativePos").executes(DungeonSecrets::getRelativePos)) - .then(literal("getRelativeTargetPos").executes(DungeonSecrets::getRelativeTargetPos)) - .then(literal("addWaypoint").then(addCustomWaypointCommand(false))) - .then(literal("addWaypointRelatively").then(addCustomWaypointCommand(true))) - .then(literal("removeWaypoint").then(removeCustomWaypointCommand(false))) - .then(literal("removeWaypointRelatively").then(removeCustomWaypointCommand(true))) - )))); - ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); - } - - private static void load() { - long startTime = System.currentTimeMillis(); - List> dungeonFutures = new ArrayList<>(); - for (Map.Entry 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 Dungeon Secrets] 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 roomsMap = ROOMS_DATA.get(dungeon).get(roomShape); - synchronized (roomsMap) { - roomsMap.put(room, rooms); - } - LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room); - }).exceptionally(e -> { - LOGGER.error("[Skyblocker Dungeon Secrets] 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"))) { - loadJson(roomsReader, roomsJson); - loadJson(waypointsReader, waypointsJson); - LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secret waypoints json"); - } catch (Exception e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secret waypoints json", e); - } - })); - dungeonFutures.add(CompletableFuture.runAsync(() -> { - try (BufferedReader customWaypointsReader = Files.newBufferedReader(CUSTOM_WAYPOINTS_DIR)) { - SkyblockerMod.GSON.fromJson(customWaypointsReader, JsonObject.class).asMap().forEach((room, waypointsJson) -> - addCustomWaypoints(room, SecretWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, waypointsJson).resultOrPartial(LOGGER::error).orElseGet(ArrayList::new)) - ); - LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded custom dungeon secret waypoints"); - } catch (Exception e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load custom dungeon secret waypoints", e); - } - })); - roomsLoaded = CompletableFuture.allOf(dungeonFutures.toArray(CompletableFuture[]::new)).thenRun(() -> LOGGER.info("[Skyblocker Dungeon Secrets] Loaded dungeon secrets for {} dungeon(s), {} room shapes, {} rooms, and {} custom secret waypoints total in {} ms", ROOMS_DATA.size(), ROOMS_DATA.values().stream().mapToInt(Map::size).sum(), ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).mapToInt(Map::size).sum(), customWaypoints.size(), System.currentTimeMillis() - startTime)).exceptionally(e -> { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e); - return null; - }); - LOGGER.info("[Skyblocker Dungeon Secrets] Started loading dungeon secrets in (blocked main thread for) {} ms", System.currentTimeMillis() - startTime); - } - - private static void saveCustomWaypoints(MinecraftClient client) { - try (BufferedWriter writer = Files.newBufferedWriter(CUSTOM_WAYPOINTS_DIR)) { - JsonObject customWaypointsJson = new JsonObject(); - customWaypoints.rowMap().forEach((room, waypoints) -> - customWaypointsJson.add(room, SecretWaypoint.LIST_CODEC.encodeStart(JsonOps.INSTANCE, new ArrayList<>(waypoints.values())).resultOrPartial(LOGGER::error).orElseGet(JsonArray::new)) - ); - SkyblockerMod.GSON.toJson(customWaypointsJson, writer); - LOGGER.info("[Skyblocker Dungeon Secrets] Saved custom dungeon secret waypoints"); - } catch (Exception e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to save custom dungeon secret waypoints", e); - } - } - - private static int[] readRoom(Resource resource) throws RuntimeException { - try (ObjectInputStream in = new ObjectInputStream(new InflaterInputStream(resource.getInputStream()))) { - return (int[]) in.readObject(); - } catch (IOException | ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - /** - * Loads the json from the given {@link BufferedReader} into the given {@link Map}. - * - * @param reader the reader to read the json from - * @param map the map to load into - */ - private static void loadJson(BufferedReader reader, Map map) { - SkyblockerMod.GSON.fromJson(reader, JsonObject.class).asMap().forEach((room, jsonElement) -> map.put(room.toLowerCase().replaceAll(" ", "-"), jsonElement)); - } - - private static ArgumentBuilder> markSecretsCommand(boolean found) { - return argument("secretIndex", IntegerArgumentType.integer()).executes(context -> { - int secretIndex = IntegerArgumentType.getInteger(context, "secretIndex"); - if (markSecrets(secretIndex, found)) { - context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFound" : "skyblocker.dungeons.secrets.markSecretMissing", secretIndex))); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFoundUnable" : "skyblocker.dungeons.secrets.markSecretMissingUnable", secretIndex))); - } - return Command.SINGLE_SUCCESS; - }); - } - - private static int getRelativePos(CommandContext context) { - return getRelativePos(context.getSource(), context.getSource().getPlayer().getBlockPos()); - } - - private static int getRelativeTargetPos(CommandContext context) { - if (MinecraftClient.getInstance().crosshairTarget instanceof BlockHitResult blockHitResult && blockHitResult.getType() == HitResult.Type.BLOCK) { - return getRelativePos(context.getSource(), blockHitResult.getBlockPos()); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.noTarget"))); - } - return Command.SINGLE_SUCCESS; - } - - private static int getRelativePos(FabricClientCommandSource source, BlockPos pos) { - Room room = getRoomAtPhysical(pos); - if (isRoomMatched(room)) { - BlockPos relativePos = currentRoom.actualToRelative(pos); - source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), relativePos.getX(), relativePos.getY(), relativePos.getZ()))); - } else { - source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static ArgumentBuilder> addCustomWaypointCommand(boolean relative) { - return argument("pos", BlockPosArgumentType.blockPos()) - .then(argument("secretIndex", IntegerArgumentType.integer()) - .then(argument("category", SecretWaypoint.Category.CategoryArgumentType.category()) - .then(argument("name", TextArgumentType.text()).executes(context -> { - // TODO Less hacky way with custom ClientBlockPosArgumentType - BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null)); - return relative ? addCustomWaypointRelative(context, pos) : addCustomWaypoint(context, pos); - })) - ) - ); - } - - private static int addCustomWaypoint(CommandContext context, BlockPos pos) { - Room room = getRoomAtPhysical(pos); - if (isRoomMatched(room)) { - room.addCustomWaypoint(context, room.actualToRelative(pos)); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static int addCustomWaypointRelative(CommandContext context, BlockPos pos) { - if (isCurrentRoomMatched()) { - currentRoom.addCustomWaypoint(context, pos); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static ArgumentBuilder> removeCustomWaypointCommand(boolean relative) { - return argument("pos", BlockPosArgumentType.blockPos()) - .executes(context -> { - // TODO Less hacky way with custom ClientBlockPosArgumentType - BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null)); - return relative ? removeCustomWaypointRelative(context, pos) : removeCustomWaypoint(context, pos); - }); - } - - private static int removeCustomWaypoint(CommandContext context, BlockPos pos) { - Room room = getRoomAtPhysical(pos); - if (isRoomMatched(room)) { - room.removeCustomWaypoint(context, room.actualToRelative(pos)); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static int removeCustomWaypointRelative(CommandContext context, BlockPos pos) { - if (isCurrentRoomMatched()) { - currentRoom.removeCustomWaypoint(context, pos); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - /** - * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod. - *

- * When entering a new dungeon, this method: - *
    - *
  • Gets the physical northwest corner position of the entrance room and saves it in {@link #physicalEntrancePos}.
  • - *
  • Do nothing until the dungeon map exists.
  • - *
  • Gets the upper left corner of entrance room on the map and saves it in {@link #mapEntrancePos}.
  • - *
  • Gets the size of a room on the map in pixels and saves it in {@link #mapRoomSize}.
  • - *
  • Creates a new {@link Room} with {@link Room.Type} {@link Room.Type.ENTRANCE ENTRANCE} and sets {@link #currentRoom}.
  • - *
- * When processing an existing dungeon, this method: - *
    - *
  • Calculates the physical northwest corner and upper left corner on the map of the room the player is currently in.
  • - *
  • Gets the room type based on the map color.
  • - *
  • If the room has not been created (when the physical northwest corner is not in {@link #rooms}):
  • - *
      - *
    • If the room type is {@link Room.Type.ROOM}, gets the northwest corner of all connected room segments with {@link DungeonMapUtils#getRoomSegments(MapState, Vector2ic, int, byte)}. (For example, a 1x2 room has two room segments.)
    • - *
    • Create a new room.
    • - *
    - *
  • Sets {@link #currentRoom} to the current room, either created from the previous step or from {@link #rooms}.
  • - *
  • Calls {@link Room#update()} on {@link #currentRoom}.
  • - *
- */ - @SuppressWarnings("JavadocReference") - private static void update() { - if (!Utils.isInDungeons()) { - if (mapEntrancePos != null) { - reset(); - } - return; - } - MinecraftClient client = MinecraftClient.getInstance(); - ClientPlayerEntity player = client.player; - if (player == null || client.world == null) { - return; - } - if (physicalEntrancePos == null) { - Vec3d playerPos = player.getPos(); - physicalEntrancePos = DungeonMapUtils.getPhysicalRoomPos(playerPos); - currentRoom = newRoom(Room.Type.ENTRANCE, physicalEntrancePos); - } - ItemStack stack = player.getInventory().main.get(8); - if (!stack.isOf(Items.FILLED_MAP)) { - return; - } - MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); - if (map == null) { - return; - } - if (mapEntrancePos == null || mapRoomSize == 0) { - ObjectIntPair mapEntrancePosAndSize = DungeonMapUtils.getMapEntrancePosAndRoomSize(map); - if (mapEntrancePosAndSize == null) { - return; - } - mapEntrancePos = mapEntrancePosAndSize.left(); - mapRoomSize = mapEntrancePosAndSize.rightInt(); - LOGGER.info("[Skyblocker Dungeon Secrets] Started dungeon with map room size {}, map entrance pos {}, player pos {}, and physical entrance pos {}", mapRoomSize, mapEntrancePos, client.player.getPos(), physicalEntrancePos); - } - - Vector2ic physicalPos = DungeonMapUtils.getPhysicalRoomPos(client.player.getPos()); - Vector2ic mapPos = DungeonMapUtils.getMapPosFromPhysical(physicalEntrancePos, mapEntrancePos, mapRoomSize, physicalPos); - Room room = rooms.get(physicalPos); - if (room == null) { - Room.Type type = DungeonMapUtils.getRoomType(map, mapPos); - if (type == null || type == Room.Type.UNKNOWN) { - return; - } - switch (type) { - case ENTRANCE, PUZZLE, TRAP, MINIBOSS, FAIRY, BLOOD -> room = newRoom(type, physicalPos); - case ROOM -> room = newRoom(type, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, mapPos, mapRoomSize, type.color))); - } - } - if (room != null && currentRoom != room) { - currentRoom = room; - } - currentRoom.update(); - } - - /** - * Creates a new room with the given type and physical positions, - * adds the room to {@link #rooms}, and sets {@link #currentRoom} to the new room. - * - * @param type the type of room to create - * @param physicalPositions the physical positions of the room - */ - @Nullable - private static Room newRoom(Room.Type type, Vector2ic... physicalPositions) { - try { - Room newRoom = new Room(type, physicalPositions); - for (Vector2ic physicalPos : physicalPositions) { - rooms.put(physicalPos, newRoom); - } - return newRoom; - } catch (IllegalArgumentException e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to create room", e); - } - return null; - } - - /** - * Renders the secret waypoints in {@link #currentRoom} if {@link #shouldProcess()} and {@link #currentRoom} is not null. - */ - private static void render(WorldRenderContext context) { - 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()} 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?!") - || message.equals("[BOSS] Maxor: WELL! WELL! WELL! LOOK WHO'S HERE!")) reset(); - } - - /** - * Calls {@link Room#onUseBlock(World, BlockHitResult)} on {@link #currentRoom} if {@link #isCurrentRoomMatched()}. - * Used to detect finding {@link SecretWaypoint.Category.CHEST} and {@link SecretWaypoint.Category.WITHER} secrets. - * - * @return {@link ActionResult#PASS} - */ - @SuppressWarnings("JavadocReference") - private static ActionResult onUseBlock(World world, BlockHitResult hitResult) { - if (isCurrentRoomMatched()) { - currentRoom.onUseBlock(world, hitResult); - } - return ActionResult.PASS; - } - - /** - * Calls {@link Room#onItemPickup(ItemEntity, LivingEntity)} on the room the {@code collector} is in if that room {@link #isRoomMatched(Room)}. - * Used to detect finding {@link SecretWaypoint.Category.ITEM} secrets. - * If the collector is the player, {@link #currentRoom} is used as an optimization. - */ - @SuppressWarnings("JavadocReference") - public static void onItemPickup(ItemEntity itemEntity, LivingEntity collector, boolean isPlayer) { - if (isPlayer) { - if (isCurrentRoomMatched()) { - currentRoom.onItemPickup(itemEntity, collector); - } - } else { - Room room = getRoomAtPhysical(collector.getPos()); - if (isRoomMatched(room)) { - room.onItemPickup(itemEntity, collector); - } - } - } - - /** - * Calls {@link Room#onBatRemoved(BatEntity)} on the room the {@code bat} is in if that room {@link #isRoomMatched(Room)}. - * Used to detect finding {@link SecretWaypoint.Category.BAT} secrets. - */ - @SuppressWarnings("JavadocReference") - public static void onBatRemoved(AmbientEntity bat) { - Room room = getRoomAtPhysical(bat.getPos()); - if (isRoomMatched(room)) { - room.onBatRemoved(bat); - } - } - - public static boolean markSecrets(int secretIndex, boolean found) { - if (isCurrentRoomMatched()) { - return currentRoom.markSecrets(secretIndex, found); - } - return false; - } - - /** - * Gets the room at the given physical position. - * - * @param pos the physical position - * @return the room at the given physical position, or null if there is no room at the given physical position - * @see #rooms - * @see DungeonMapUtils#getPhysicalRoomPos(Vec3d) - */ - @Nullable - private static Room getRoomAtPhysical(Vec3d pos) { - return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos)); - } - - /** - * Gets the room at the given physical position. - * - * @param pos the physical position - * @return the room at the given physical position, or null if there is no room at the given physical position - * @see #rooms - * @see DungeonMapUtils#getPhysicalRoomPos(Vec3i) - */ - @Nullable - private static Room getRoomAtPhysical(Vec3i pos) { - return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos)); - } - - /** - * Calls {@link #isRoomMatched(Room)} on {@link #currentRoom}. - * - * @return {@code true} if {@link #currentRoom} is not null and {@link #isRoomMatched(Room)} - */ - private static boolean isCurrentRoomMatched() { - return isRoomMatched(currentRoom); - } - - /** - * Calls {@link #shouldProcess()} and {@link Room#isMatched()} on the given room. - * - * @param room the room to check - * @return {@code true} if {@link #shouldProcess()}, the given room is not null, and {@link Room#isMatched()} on the given room - */ - @Contract("null -> false") - private static boolean isRoomMatched(@Nullable Room room) { - return shouldProcess() && room != null && room.isMatched(); - } - - /** - * Checks if {@link de.hysky.skyblocker.config.SkyblockerConfig.SecretWaypoints#enableRoomMatching room matching} is enabled and the player is in a dungeon. - * - * @return whether room matching and dungeon secrets should be processed - */ - private static boolean shouldProcess() { - return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching && Utils.isInDungeons(); - } - - /** - * Resets fields when leaving a dungeon or entering boss. - */ - private static void reset() { - mapEntrancePos = null; - mapRoomSize = 0; - physicalEntrancePos = null; - rooms.clear(); - currentRoom = null; - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java index 0d3c6a87..fffe1bd2 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 @@ -103,7 +103,7 @@ public class Room { IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::x).toArray())); IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::y).toArray())); shape = getShape(segmentsX, segmentsY); - roomsData = DungeonSecrets.ROOMS_DATA.getOrDefault("catacombs", Collections.emptyMap()).getOrDefault(shape.shape.toLowerCase(), Collections.emptyMap()); + roomsData = DungeonManager.ROOMS_DATA.getOrDefault("catacombs", Collections.emptyMap()).getOrDefault(shape.shape.toLowerCase(), Collections.emptyMap()); possibleRooms = getPossibleRooms(segmentsX, segmentsY); } @@ -191,7 +191,7 @@ public class Room { } /** - * Adds a custom waypoint relative to this room to {@link DungeonSecrets#customWaypoints} and all existing instances of this room. + * Adds a custom waypoint relative to this room to {@link DungeonManager#customWaypoints} and all existing instances of this room. * * @param secretIndex the index of the secret waypoint * @param category the category of the secret waypoint @@ -201,8 +201,8 @@ public class Room { @SuppressWarnings("JavadocReference") private void addCustomWaypoint(int secretIndex, SecretWaypoint.Category category, Text waypointName, BlockPos pos) { SecretWaypoint waypoint = new SecretWaypoint(secretIndex, category, waypointName, pos); - DungeonSecrets.addCustomWaypoint(name, waypoint); - DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.addCustomWaypoint(waypoint)); + DungeonManager.addCustomWaypoint(name, waypoint); + DungeonManager.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.addCustomWaypoint(waypoint)); } /** @@ -228,7 +228,7 @@ public class Room { } /** - * Removes a custom waypoint relative to this room from {@link DungeonSecrets#customWaypoints} and all existing instances of this room. + * Removes a custom waypoint relative to this room from {@link DungeonManager#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 @@ -236,9 +236,9 @@ public class Room { @SuppressWarnings("JavadocReference") @Nullable private SecretWaypoint removeCustomWaypoint(BlockPos pos) { - SecretWaypoint waypoint = DungeonSecrets.removeCustomWaypoint(name, pos); + SecretWaypoint waypoint = DungeonManager.removeCustomWaypoint(name, pos); if (waypoint != null) { - DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.removeCustomWaypoint(waypoint.secretIndex, pos)); + DungeonManager.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.removeCustomWaypoint(waypoint.secretIndex, pos)); } return waypoint; } @@ -290,7 +290,7 @@ public class Room { // 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()) { + if (!type.needsScanning() || matchState != MatchState.MATCHING && matchState != MatchState.DOUBLE_CHECKING || !DungeonManager.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) { return; } ClientPlayerEntity player = client.player; @@ -304,7 +304,7 @@ public class Room { } } }).exceptionally(e -> { - DungeonSecrets.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown exception while matching room {}", this, e); + DungeonManager.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown exception while matching room {}", this, e); return null; }); } @@ -323,7 +323,7 @@ public class Room { *

* This method: *
    - *
  • Checks if the block type is included in the dungeon rooms data. See {@link DungeonSecrets#NUMERIC_ID}.
  • + *
  • Checks if the block type is included in the dungeon rooms data. See {@link DungeonManager#NUMERIC_ID}.
  • *
  • For each possible direction:
  • *
      *
    • Rotate and convert the position to a relative position. See {@link DungeonMapUtils#actualToRelative(Direction, Vector2ic, BlockPos)}.
    • @@ -364,7 +364,7 @@ public class Room { * @return whether room matching should end. Either a match is found or there are no valid rooms left */ private boolean checkBlock(ClientWorld world, BlockPos pos) { - byte id = DungeonSecrets.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); + byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); if (id == 0) { return false; } @@ -383,7 +383,7 @@ public class Room { if (matchingRoomsSize == 0) { // If no rooms match, reset the fields and scan again after 50 ticks. matchState = MatchState.FAILED; - DungeonSecrets.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks); + DungeonManager.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; @@ -394,20 +394,20 @@ public class Room { 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()); + DungeonManager.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) { // If double-checked, set state to matched and discard the no longer needed fields. matchState = MatchState.MATCHED; DungeonEvents.ROOM_MATCHED.invoker().onRoomMatched(this); - DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} confirmed after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks); + DungeonManager.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} confirmed after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks); discard(); return true; } return false; } else { - DungeonSecrets.LOGGER.debug("[Skyblocker Dungeon Secrets] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size()); + DungeonManager.LOGGER.debug("[Skyblocker Dungeon Secrets] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size()); return false; } } @@ -424,7 +424,7 @@ public class Room { } /** - * Loads the secret waypoints for the room from {@link DungeonSecrets#waypointsJson} once it has been matched + * Loads the secret waypoints for the room from {@link DungeonManager#waypointsJson} once it has been matched * and sets {@link #matchState} to {@link MatchState#DOUBLE_CHECKING}. * * @param directionRooms the direction, position, and name of the room @@ -432,7 +432,7 @@ public class Room { @SuppressWarnings("JavadocReference") private void roomMatched() { secretWaypoints = HashBasedTable.create(); - for (JsonElement waypointElement : DungeonSecrets.getRoomWaypoints(name)) { + for (JsonElement waypointElement : DungeonManager.getRoomWaypoints(name)) { JsonObject waypoint = waypointElement.getAsJsonObject(); String secretName = waypoint.get("secretName").getAsString(); Matcher secretIndexMatcher = SECRET_INDEX.matcher(secretName); @@ -440,7 +440,7 @@ public class Room { BlockPos pos = DungeonMapUtils.relativeToActual(direction, physicalCornerPos, waypoint); secretWaypoints.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos)); } - DungeonSecrets.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint); + DungeonManager.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint); matchState = MatchState.DOUBLE_CHECKING; } @@ -586,7 +586,7 @@ public class Room { */ private void onSecretFound(SecretWaypoint secretWaypoint, String msg, Object... args) { secretWaypoints.row(secretWaypoint.secretIndex).values().forEach(SecretWaypoint::setFound); - DungeonSecrets.LOGGER.info(msg, args); + DungeonManager.LOGGER.info(msg, args); } protected boolean markSecrets(int secretIndex, boolean found) { diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java index 3d2993cf..d45a2172 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java @@ -21,7 +21,7 @@ import java.util.zip.InflaterInputStream; * Utility class to convert the old dungeon rooms data from Dungeon Rooms Mod to a new format. * The new format is similar to DRM's format, but uses ints instead of longs and a custom numeric block id to store the block states. * The first byte is the x position, the second byte is the y position, the third byte is the z position, and the fourth byte is the custom numeric block id. - * Use {@link DungeonSecrets#NUMERIC_ID} to get the custom numeric block id of a block. + * Use {@link DungeonManager#NUMERIC_ID} to get the custom numeric block id of a block. * Run this manually when updating dungeon rooms data with DRM's data in {@code src/test/resources/assets/skyblocker/dungeons/dungeonrooms}. */ public class DungeonRoomsDFU { @@ -131,7 +131,7 @@ public class DungeonRoomsDFU { if (newId == null) { newId = ItemIdFix.fromId(oldId / 100); } - return x << 24 | y << 16 | z << 8 | DungeonSecrets.NUMERIC_ID.getByte(newId); + return x << 24 | y << 16 | z << 8 | DungeonManager.NUMERIC_ID.getByte(newId); } private static CompletableFuture save() { -- cgit From 5b1b97b1b6380fcd6a7670c01e633acdf41ae0dc Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Wed, 20 Dec 2023 16:39:17 +0800 Subject: Add match against command --- .../skyblock/dungeon/secrets/DungeonManager.java | 13 ++++++--- .../skyblocker/skyblock/dungeon/secrets/Room.java | 34 ++++++++++++++++++++-- .../skyblock/dungeon/secrets/SecretWaypoint.java | 6 ++-- 3 files changed, 43 insertions(+), 10 deletions(-) (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java index 7705ca46..6135e6b5 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -7,7 +7,7 @@ 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.arguments.StringArgumentType; import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.serialization.JsonOps; @@ -218,6 +218,7 @@ public class DungeonManager { .then(literal("addWaypointRelatively").then(addCustomWaypointCommand(true))) .then(literal("removeWaypoint").then(removeCustomWaypointCommand(false))) .then(literal("removeWaypointRelatively").then(removeCustomWaypointCommand(true))) + .then(literal("matchAgainst").then(matchAgainstCommand())) )))); ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); } @@ -304,7 +305,7 @@ public class DungeonManager { SkyblockerMod.GSON.fromJson(reader, JsonObject.class).asMap().forEach((room, jsonElement) -> map.put(room.toLowerCase().replaceAll(" ", "-"), jsonElement)); } - private static ArgumentBuilder> markSecretsCommand(boolean found) { + private static RequiredArgumentBuilder markSecretsCommand(boolean found) { return argument("secretIndex", IntegerArgumentType.integer()).executes(context -> { int secretIndex = IntegerArgumentType.getInteger(context, "secretIndex"); if (markSecrets(secretIndex, found)) { @@ -340,7 +341,7 @@ public class DungeonManager { return Command.SINGLE_SUCCESS; } - private static ArgumentBuilder> addCustomWaypointCommand(boolean relative) { + private static RequiredArgumentBuilder addCustomWaypointCommand(boolean relative) { return argument("pos", BlockPosArgumentType.blockPos()) .then(argument("secretIndex", IntegerArgumentType.integer()) .then(argument("category", SecretWaypoint.Category.CategoryArgumentType.category()) @@ -372,7 +373,7 @@ public class DungeonManager { return Command.SINGLE_SUCCESS; } - private static ArgumentBuilder> removeCustomWaypointCommand(boolean relative) { + private static RequiredArgumentBuilder removeCustomWaypointCommand(boolean relative) { return argument("pos", BlockPosArgumentType.blockPos()) .executes(context -> { // TODO Less hacky way with custom ClientBlockPosArgumentType @@ -400,6 +401,10 @@ public class DungeonManager { return Command.SINGLE_SUCCESS; } + private static RequiredArgumentBuilder matchAgainstCommand() { + return argument("room", StringArgumentType.string()).then(argument("direction", Room.Direction.DirectionArgumentType.direction())); + } + /** * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod. *

      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 fffe1bd2..96e3fadd 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,6 +6,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.context.CommandContext; +import com.mojang.serialization.Codec; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.events.DungeonEvents; import de.hysky.skyblocker.utils.Constants; @@ -23,11 +24,13 @@ import net.minecraft.block.MapColor; import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.world.ClientWorld; +import net.minecraft.command.argument.EnumArgumentType; import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.mob.AmbientEntity; import net.minecraft.registry.Registries; import net.minecraft.text.Text; +import net.minecraft.util.StringIdentifiable; import net.minecraft.util.hit.BlockHitResult; import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Box; @@ -650,11 +653,36 @@ public class Room { } } - public enum Direction { - NW, NE, SW, SE + enum Direction implements StringIdentifiable { + NW("northwest"), NE("northeast"), SW("southwest"), SE("southeast"); + private static final Codec CODEC = StringIdentifiable.createCodec(Direction::values); + private final String name; + + Direction(String name) { + this.name = name; + } + + @Override + public String asString() { + return name; + } + + static class DirectionArgumentType extends EnumArgumentType { + DirectionArgumentType() { + super(CODEC, Direction::values); + } + + static DirectionArgumentType direction() { + return new DirectionArgumentType(); + } + + static Direction getDirection(CommandContext context, String name) { + return context.getArgument(name, Direction.class); + } + } } - public enum MatchState { + private enum MatchState { MATCHING, DOUBLE_CHECKING, MATCHED, FAILED } } 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 75a0c20f..b7c19210 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 @@ -173,15 +173,15 @@ public class SecretWaypoint extends Waypoint { } static class CategoryArgumentType extends EnumArgumentType { - public CategoryArgumentType() { + CategoryArgumentType() { super(Category.CODEC, Category::values); } - public static CategoryArgumentType category() { + static CategoryArgumentType category() { return new CategoryArgumentType(); } - public static Category getCategory(CommandContext context, String name) { + static Category getCategory(CommandContext context, String name) { return context.getArgument(name, Category.class); } } -- cgit From 88eb4ce59bead62d86b8cece7a8be8e30a740e01 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:36:17 +0800 Subject: Implement match against command --- .../skyblock/dungeon/secrets/DebugRoom.java | 10 ++++ .../skyblock/dungeon/secrets/DungeonManager.java | 67 +++++++++++++++++++--- .../skyblocker/skyblock/dungeon/secrets/Room.java | 8 +-- 3 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java new file mode 100644 index 00000000..93e4be6e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java @@ -0,0 +1,10 @@ +package de.hysky.skyblocker.skyblock.dungeon.secrets; + +import org.jetbrains.annotations.NotNull; +import org.joml.Vector2ic; + +public class DebugRoom extends Room { + public DebugRoom(@NotNull Type type, @NotNull Vector2ic... physicalPositions) { + super(type, physicalPositions); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java index 6135e6b5..4364abff 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -12,10 +12,14 @@ import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.serialization.JsonOps; import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; +import it.unimi.dsi.fastutil.ints.IntRBTreeSet; +import it.unimi.dsi.fastutil.ints.IntSortedSet; +import it.unimi.dsi.fastutil.ints.IntSortedSets; import it.unimi.dsi.fastutil.objects.Object2ByteMap; import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIntPair; @@ -27,8 +31,9 @@ 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.block.Blocks; import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.command.CommandSource; import net.minecraft.command.argument.BlockPosArgumentType; import net.minecraft.command.argument.PosArgument; import net.minecraft.command.argument.TextArgumentType; @@ -41,6 +46,7 @@ import net.minecraft.item.FilledMapItem; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.item.map.MapState; +import net.minecraft.registry.Registry; import net.minecraft.resource.Resource; import net.minecraft.server.command.ServerCommandSource; import net.minecraft.text.Text; @@ -52,6 +58,7 @@ 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.apache.commons.lang3.tuple.MutableTriple; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -83,7 +90,7 @@ public class DungeonManager { /** * 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 de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. + * @implNote Not using {@link Registry#getId(Object) Registry#getId(Block)} and {@link Blocks Blocks} since this is also used by {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. */ @SuppressWarnings("JavadocReference") protected static final Object2ByteMap NUMERIC_ID = new Object2ByteOpenHashMap<>(Map.ofEntries( @@ -402,7 +409,52 @@ public class DungeonManager { } private static RequiredArgumentBuilder matchAgainstCommand() { - return argument("room", StringArgumentType.string()).then(argument("direction", Room.Direction.DirectionArgumentType.direction())); + return argument("room", StringArgumentType.string()).suggests((context, builder) -> CommandSource.suggestMatching(ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).map(Map::keySet).flatMap(Collection::stream), builder)).then(argument("direction", Room.Direction.DirectionArgumentType.direction()).executes(context -> { + if (physicalEntrancePos == null || mapEntrancePos == null || mapRoomSize == 0) { + context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cYou are not in a dungeon"))); + return Command.SINGLE_SUCCESS; + } + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) { + context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to get player or world"))); + return Command.SINGLE_SUCCESS; + } + ItemStack stack = client.player.getInventory().main.get(8); + if (!stack.isOf(Items.FILLED_MAP)) { + context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to get dungeon map"))); + return Command.SINGLE_SUCCESS; + } + MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); + if (map == null) { + context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to get dungeon map state"))); + return Command.SINGLE_SUCCESS; + } + + String roomName = StringArgumentType.getString(context, "room"); + Room.Direction direction = Room.Direction.DirectionArgumentType.getDirection(context, "direction"); + + Room room = null; + int[] roomData; + if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.PUZZLE.shape).get(roomName)) != null) { + room = new DebugRoom(Room.Type.PUZZLE, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())); + } else if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.TRAP.shape).get(roomName)) != null) { + room = new DebugRoom(Room.Type.TRAP, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())); + } else if ((roomData = ROOMS_DATA.get("catacombs").values().stream().map(Map::entrySet).flatMap(Collection::stream).filter(entry -> entry.getKey().equals(roomName)).findAny().map(Map.Entry::getValue).orElse(null)) != null) { + room = new DebugRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapPosFromPhysical(physicalEntrancePos, mapEntrancePos, mapRoomSize, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())), mapRoomSize, Room.Type.ROOM.color))); + } + + if (room == null) { + context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to find room with name " + roomName))); + return Command.SINGLE_SUCCESS; + } + IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::x).toArray())); + IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::y).toArray())); + room.roomsData = Map.of(roomName, roomData); + room.possibleRooms = List.of(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), List.of(roomName))); + Scheduler.INSTANCE.scheduleCyclic(room::update, 10); + + return Command.SINGLE_SUCCESS; + })); } /** @@ -438,16 +490,15 @@ public class DungeonManager { return; } MinecraftClient client = MinecraftClient.getInstance(); - ClientPlayerEntity player = client.player; - if (player == null || client.world == null) { + if (client.player == null || client.world == null) { return; } if (physicalEntrancePos == null) { - Vec3d playerPos = player.getPos(); + Vec3d playerPos = client.player.getPos(); physicalEntrancePos = DungeonMapUtils.getPhysicalRoomPos(playerPos); currentRoom = newRoom(Room.Type.ENTRANCE, physicalEntrancePos); } - ItemStack stack = player.getInventory().main.get(8); + ItemStack stack = client.player.getInventory().main.get(8); if (!stack.isOf(Items.FILLED_MAP)) { return; } @@ -665,7 +716,7 @@ public class DungeonManager { } /** - * Checks if {@link de.hysky.skyblocker.config.SkyblockerConfig.SecretWaypoints#enableRoomMatching room matching} is enabled and the player is in a dungeon. + * Checks if {@link SkyblockerConfig.SecretWaypoints#enableRoomMatching room matching} is enabled and the player is in a dungeon. * * @return whether room matching and dungeon secrets should be processed */ 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 96e3fadd..23302182 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 @@ -57,7 +57,7 @@ public class Room { @NotNull private final Type type; @NotNull - private final Set segments; + final Set segments; /** * The shape of the room. See {@link #getShape(IntSortedSet, IntSortedSet)}. @@ -67,11 +67,11 @@ public class Room { /** * The room data containing all rooms for a specific dungeon and {@link #shape}. */ - private Map roomsData; + Map roomsData; /** * Contains all possible dungeon rooms for this room. The list is gradually shrunk by checking blocks until only one room is left. */ - private List>> possibleRooms; + List>> possibleRooms; /** * Contains all blocks that have been checked to prevent checking the same block multiple times. */ @@ -632,7 +632,7 @@ public class Room { } } - private enum Shape { + enum Shape { ONE_BY_ONE("1x1"), ONE_BY_TWO("1x2"), ONE_BY_THREE("1x3"), -- cgit From 66b6be50ed9480d2d6e442c21ad16ed4bd48b2d6 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Wed, 20 Dec 2023 23:00:48 +0800 Subject: Render match against command --- .../skyblock/dungeon/secrets/DebugRoom.java | 38 +++++++++++++++ .../skyblock/dungeon/secrets/DungeonManager.java | 43 ++++++++++++----- .../skyblock/dungeon/secrets/DungeonMapUtils.java | 2 +- .../skyblocker/skyblock/dungeon/secrets/Room.java | 56 +++++++++++++++------- .../skyblock/dungeon/secrets/SecretWaypoint.java | 4 +- .../hysky/skyblocker/utils/waypoint/Waypoint.java | 10 ++-- .../resources/assets/skyblocker/lang/en_us.json | 2 +- 7 files changed, 117 insertions(+), 38 deletions(-) (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java index 93e4be6e..b686607b 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java @@ -1,10 +1,48 @@ package de.hysky.skyblocker.skyblock.dungeon.secrets; +import de.hysky.skyblocker.utils.waypoint.Waypoint; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.registry.Registries; +import net.minecraft.util.math.BlockPos; +import org.apache.commons.lang3.tuple.MutableTriple; import org.jetbrains.annotations.NotNull; import org.joml.Vector2ic; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + public class DebugRoom extends Room { + private final List checkedBlocks = Collections.synchronizedList(new ArrayList<>()); + public DebugRoom(@NotNull Type type, @NotNull Vector2ic... physicalPositions) { super(type, physicalPositions); } + + @Override + protected boolean checkBlock(ClientWorld world, BlockPos pos) { + byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); + if (id == 0) { + return false; + } + for (MutableTriple> directionRooms : possibleRooms) { + int block = posIdToInt(DungeonMapUtils.actualToRelative(directionRooms.getLeft(), directionRooms.getMiddle(), pos), id); + for (String room : directionRooms.getRight()) { + checkedBlocks.add(new Waypoint(pos, SecretWaypoint.TYPE_SUPPLIER, Arrays.binarySearch(roomsData.get(room), block) >= 0 ? Room.GREEN_COLOR_COMPONENTS : Room.RED_COLOR_COMPONENTS)); + } + } + return false; + } + + @Override + protected void render(WorldRenderContext context) { + super.render(context); + synchronized (checkedBlocks) { + for (Waypoint checkedBlock : checkedBlocks) { + checkedBlock.render(context); + } + } + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java index 4364abff..52915b98 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -14,6 +14,7 @@ import com.mojang.serialization.JsonOps; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; @@ -158,11 +159,13 @@ public class DungeonManager { @SuppressWarnings("unused") public static JsonObject getRoomMetadata(String room) { - return roomsJson.get(room).getAsJsonObject(); + JsonElement value = roomsJson.get(room); + return value != null ? value.getAsJsonObject() : null; } public static JsonArray getRoomWaypoints(String room) { - return waypointsJson.get(room).getAsJsonArray(); + JsonElement value = waypointsJson.get(room); + return value != null ? value.getAsJsonArray() : null; } /** @@ -225,8 +228,21 @@ public class DungeonManager { .then(literal("addWaypointRelatively").then(addCustomWaypointCommand(true))) .then(literal("removeWaypoint").then(removeCustomWaypointCommand(false))) .then(literal("removeWaypointRelatively").then(removeCustomWaypointCommand(true))) - .then(literal("matchAgainst").then(matchAgainstCommand())) )))); + if (Debug.debugEnabled()) { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets") + .then(literal("matchAgainst").then(matchAgainstCommand())) + .then(literal("clearSubRooms").executes(context -> { + if (currentRoom != null) { + currentRoom.subRooms.clear(); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rCleared sub rooms in the current room")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null")); + } + return Command.SINGLE_SUCCESS; + })) + )))); + } ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); } @@ -341,7 +357,7 @@ public class DungeonManager { Room room = getRoomAtPhysical(pos); if (isRoomMatched(room)) { BlockPos relativePos = currentRoom.actualToRelative(pos); - source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), relativePos.getX(), relativePos.getY(), relativePos.getZ()))); + source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), currentRoom.getDirection().asString(), relativePos.getX(), relativePos.getY(), relativePos.getZ()))); } else { source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); } @@ -411,22 +427,22 @@ public class DungeonManager { private static RequiredArgumentBuilder matchAgainstCommand() { return argument("room", StringArgumentType.string()).suggests((context, builder) -> CommandSource.suggestMatching(ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).map(Map::keySet).flatMap(Collection::stream), builder)).then(argument("direction", Room.Direction.DirectionArgumentType.direction()).executes(context -> { if (physicalEntrancePos == null || mapEntrancePos == null || mapRoomSize == 0) { - context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cYou are not in a dungeon"))); + context.getSource().sendError(Constants.PREFIX.get().append("§cYou are not in a dungeon")); return Command.SINGLE_SUCCESS; } MinecraftClient client = MinecraftClient.getInstance(); if (client.player == null || client.world == null) { - context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to get player or world"))); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get player or world")); return Command.SINGLE_SUCCESS; } ItemStack stack = client.player.getInventory().main.get(8); if (!stack.isOf(Items.FILLED_MAP)) { - context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to get dungeon map"))); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map")); return Command.SINGLE_SUCCESS; } MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); if (map == null) { - context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to get dungeon map state"))); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map state")); return Command.SINGLE_SUCCESS; } @@ -440,18 +456,23 @@ public class DungeonManager { } else if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.TRAP.shape).get(roomName)) != null) { room = new DebugRoom(Room.Type.TRAP, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())); } else if ((roomData = ROOMS_DATA.get("catacombs").values().stream().map(Map::entrySet).flatMap(Collection::stream).filter(entry -> entry.getKey().equals(roomName)).findAny().map(Map.Entry::getValue).orElse(null)) != null) { - room = new DebugRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapPosFromPhysical(physicalEntrancePos, mapEntrancePos, mapRoomSize, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())), mapRoomSize, Room.Type.ROOM.color))); + room = new DebugRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapRoomPos(map, mapEntrancePos, mapRoomSize), mapRoomSize, Room.Type.ROOM.color))); } if (room == null) { - context.getSource().sendError(Constants.PREFIX.get().append(Text.literal("§cFailed to find room with name " + roomName))); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to find room with name " + roomName)); return Command.SINGLE_SUCCESS; } IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::x).toArray())); IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::y).toArray())); room.roomsData = Map.of(roomName, roomData); room.possibleRooms = List.of(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), List.of(roomName))); - Scheduler.INSTANCE.scheduleCyclic(room::update, 10); + if (currentRoom != null) { + currentRoom.subRooms.add(room); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rMatching room " + roomName + " with direction " + direction + " against current room")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null")); + } return Command.SINGLE_SUCCESS; })); 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 516c3bad..b12bba62 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 @@ -130,7 +130,7 @@ public class DungeonMapUtils { 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); + return mapPos.add(2, 2).sub(offset).sub(Math.floorMod(mapPos.x(), mapRoomSizeWithGap), Math.floorMod(mapPos.y(), mapRoomSizeWithGap)).add(offset); } /** 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 23302182..40488717 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 @@ -2,6 +2,7 @@ package de.hysky.skyblocker.skyblock.dungeon.secrets; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.mojang.brigadier.arguments.IntegerArgumentType; @@ -52,8 +53,8 @@ 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}; + protected static final float[] RED_COLOR_COMPONENTS = {1, 0, 0}; + protected static final float[] GREEN_COLOR_COMPONENTS = {0, 1, 0}; @NotNull private final Type type; @NotNull @@ -67,11 +68,11 @@ public class Room { /** * The room data containing all rooms for a specific dungeon and {@link #shape}. */ - Map roomsData; + protected Map roomsData; /** * Contains all possible dungeon rooms for this room. The list is gradually shrunk by checking blocks until only one room is left. */ - List>> possibleRooms; + protected List>> possibleRooms; /** * Contains all blocks that have been checked to prevent checking the same block multiple times. */ @@ -88,12 +89,13 @@ public class Room { *
    • {@link MatchState#MATCHED} means that the room has a unique match ans has been double checked.
    • *
    • {@link MatchState#FAILED} means that the room has been checked and there is no match.
    • */ - private MatchState matchState = MatchState.MATCHING; + protected MatchState matchState = MatchState.MATCHING; private Table secretWaypoints; private String name; private Direction direction; private Vector2ic physicalCornerPos; + protected List subRooms = new ArrayList<>(); @Nullable private BlockPos doorPos; @Nullable @@ -126,6 +128,13 @@ public class Room { return name; } + /** + * Not null if {@link #isMatched()}. + */ + public Direction getDirection() { + return direction; + } + @Override public String toString() { 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); @@ -283,6 +292,10 @@ public class Room { return; } + for (Room subRoom : subRooms) { + subRoom.update(); + } + // Wither and blood door if (SkyblockerConfigManager.get().locations.dungeons.doorHighlight.enableDoorHighlight && doorPos == null) { doorPos = DungeonMapUtils.getWitherBloodDoorPos(world, segments); @@ -366,7 +379,7 @@ public class Room { * @param pos the position of the block to check * @return whether room matching should end. Either a match is found or there are no valid rooms left */ - private boolean checkBlock(ClientWorld world, BlockPos pos) { + protected boolean checkBlock(ClientWorld world, BlockPos pos) { byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); if (id == 0) { return false; @@ -422,7 +435,7 @@ public class Room { * @param id the custom numeric block id * @return the encoded integer */ - private int posIdToInt(BlockPos pos, byte id) { + protected int posIdToInt(BlockPos pos, byte id) { return pos.getX() << 24 | pos.getY() << 16 | pos.getZ() << 8 | id; } @@ -435,13 +448,16 @@ public class Room { @SuppressWarnings("JavadocReference") private void roomMatched() { secretWaypoints = HashBasedTable.create(); - for (JsonElement waypointElement : DungeonManager.getRoomWaypoints(name)) { - JsonObject waypoint = waypointElement.getAsJsonObject(); - String secretName = waypoint.get("secretName").getAsString(); - 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)); + JsonArray secretWaypointsJson = DungeonManager.getRoomWaypoints(name); + if (secretWaypointsJson != null) { + for (JsonElement waypointElement : secretWaypointsJson) { + JsonObject waypoint = waypointElement.getAsJsonObject(); + String secretName = waypoint.get("secretName").getAsString(); + 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)); + } } DungeonManager.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint); matchState = MatchState.DOUBLE_CHECKING; @@ -450,7 +466,7 @@ public class Room { /** * Resets fields for another round of matching after room matching fails. */ - private void reset() { + protected void reset() { 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); @@ -491,6 +507,10 @@ public class Room { * 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 (Room subRoom : subRooms) { + subRoom.render(context); + } + if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && isMatched()) { for (SecretWaypoint secretWaypoint : secretWaypoints.values()) { if (secretWaypoint.shouldRender()) { @@ -632,7 +652,7 @@ public class Room { } } - enum Shape { + protected enum Shape { ONE_BY_ONE("1x1"), ONE_BY_TWO("1x2"), ONE_BY_THREE("1x3"), @@ -653,7 +673,7 @@ public class Room { } } - enum Direction implements StringIdentifiable { + public enum Direction implements StringIdentifiable { NW("northwest"), NE("northeast"), SW("southwest"), SE("southeast"); private static final Codec CODEC = StringIdentifiable.createCodec(Direction::values); private final String name; @@ -682,7 +702,7 @@ public class Room { } } - private enum MatchState { + protected enum MatchState { MATCHING, DOUBLE_CHECKING, MATCHED, FAILED } } 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 b7c19210..98ffa157 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 @@ -29,7 +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); + private static final Logger LOGGER = LoggerFactory.getLogger(SecretWaypoint.class); public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( Codec.INT.fieldOf("secretIndex").forGetter(secretWaypoint -> secretWaypoint.secretIndex), Category.CODEC.fieldOf("category").forGetter(secretWaypoint -> secretWaypoint.category), @@ -39,7 +39,7 @@ public class SecretWaypoint extends Waypoint { public static final Codec> LIST_CODEC = CODEC.listOf(); static final List SECRET_ITEMS = List.of("Decoy", "Defuse Kit", "Dungeon Chest Key", "Healing VIII", "Inflatable Jerry", "Spirit Leap", "Training Weights", "Trap", "Treasure Talisman"); private static final Supplier CONFIG = () -> SkyblockerConfigManager.get().locations.dungeons.secretWaypoints; - private static final Supplier TYPE_SUPPLIER = () -> CONFIG.get().waypointType; + static final Supplier TYPE_SUPPLIER = () -> CONFIG.get().waypointType; final int secretIndex; final Category category; final Text name; diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java index eb30cf8d..3a1d364f 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java @@ -19,15 +19,15 @@ public class Waypoint { final boolean throughWalls; private boolean shouldRender; - protected Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents) { + public Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents) { this(pos, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, DEFAULT_LINE_WIDTH); } - protected Waypoint(BlockPos pos, Type type, float[] colorComponents, float alpha) { + public Waypoint(BlockPos pos, Type type, float[] colorComponents, float alpha) { this(pos, () -> type, colorComponents, alpha, DEFAULT_LINE_WIDTH); } - protected Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents, float alpha, float lineWidth) { + public Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents, float alpha, float lineWidth) { this(pos, typeSupplier, colorComponents, alpha, lineWidth, true); } @@ -35,11 +35,11 @@ public class Waypoint { this(pos, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, DEFAULT_LINE_WIDTH, throughWalls); } - protected Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls) { + public Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls) { this(pos, typeSupplier, colorComponents, alpha, lineWidth, throughWalls, true); } - protected Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls, boolean shouldRender) { + public Waypoint(BlockPos pos, Supplier typeSupplier, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls, boolean shouldRender) { this.pos = pos; this.box = new Box(pos); this.typeSupplier = typeSupplier; diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 8692c4f8..31f4f6ca 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -303,7 +303,7 @@ "skyblocker.dungeons.secrets.markSecretMissing": "§rMarked secret #%d as missing.", "skyblocker.dungeons.secrets.markSecretFoundUnable": "§cUnable to mark secret #%d as found.", "skyblocker.dungeons.secrets.markSecretMissingUnable": "§cUnable to mark secret #%d as missing.", - "skyblocker.dungeons.secrets.posMessage": "§rRoom: %s, X: %d, Y: %d, Z: %d", + "skyblocker.dungeons.secrets.posMessage": "§rRoom: %s, Direction: %s, X: %d, Y: %d, Z: %d", "skyblocker.dungeons.secrets.noTarget": "§cNo target block found! (Are you pointing at a block in range?)", "skyblocker.dungeons.secrets.notMatched": "§cThe current room is not matched! (Are you in a dungeon room?)", "skyblocker.dungeons.secrets.customWaypointAdded": "§rAdded a custom waypoint at X: %d, Y: %d, Z: %d for room %s secret #%d of category %s with name '%s'.", -- cgit From 003834e36b145791dd603858c924926be70e1281 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:53:52 +0800 Subject: Refactor puzzle solvers --- .../java/de/hysky/skyblocker/SkyblockerMod.java | 8 +- .../skyblocker/skyblock/dungeon/CreeperBeams.java | 250 --------------------- .../skyblocker/skyblock/dungeon/DungeonBlaze.java | 164 -------------- .../skyblocker/skyblock/dungeon/ThreeWeirdos.java | 39 ---- .../skyblocker/skyblock/dungeon/TicTacToe.java | 151 ------------- .../hysky/skyblocker/skyblock/dungeon/Trivia.java | 109 --------- .../skyblock/dungeon/puzzle/CreeperBeams.java | 250 +++++++++++++++++++++ .../skyblock/dungeon/puzzle/DungeonBlaze.java | 158 +++++++++++++ .../skyblock/dungeon/puzzle/DungeonPuzzle.java | 58 +++++ .../skyblock/dungeon/puzzle/ThreeWeirdos.java | 39 ++++ .../skyblock/dungeon/puzzle/TicTacToe.java | 145 ++++++++++++ .../skyblocker/skyblock/dungeon/puzzle/Trivia.java | 109 +++++++++ .../skyblock/dungeon/secrets/DebugRoom.java | 26 ++- .../skyblock/dungeon/secrets/DungeonManager.java | 67 +++--- .../skyblocker/skyblock/dungeon/secrets/Room.java | 28 ++- .../java/de/hysky/skyblocker/utils/Tickable.java | 5 + .../skyblocker/utils/chat/ChatMessageListener.java | 4 +- .../hysky/skyblocker/utils/render/Renderable.java | 7 + .../skyblock/dungeon/ThreeWeirdosTest.java | 19 -- .../skyblocker/skyblock/dungeon/TriviaTest.java | 33 --- .../skyblock/dungeon/puzzle/ThreeWeirdosTest.java | 19 ++ .../skyblock/dungeon/puzzle/TriviaTest.java | 33 +++ 22 files changed, 905 insertions(+), 816 deletions(-) delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java create mode 100644 src/main/java/de/hysky/skyblocker/utils/Tickable.java create mode 100644 src/main/java/de/hysky/skyblocker/utils/render/Renderable.java delete mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java delete mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java create mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java create mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index d1aa3153..9ce0df8d 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -5,7 +5,13 @@ 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.dungeon.*; +import de.hysky.skyblocker.skyblock.dungeon.DungeonMap; +import de.hysky.skyblocker.skyblock.dungeon.FireFreezeStaffTimer; +import de.hysky.skyblocker.skyblock.dungeon.GuardianHealth; +import de.hysky.skyblocker.skyblock.dungeon.LividColor; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.CreeperBeams; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonBlaze; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.TicTacToe; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.dungeon.secrets.SecretsTracker; import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java deleted file mode 100644 index 5c7a01f9..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java +++ /dev/null @@ -1,250 +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.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.objects.ObjectDoublePair; -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.minecraft.block.Block; -import net.minecraft.block.Blocks; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.entity.mob.CreeperEntity; -import net.minecraft.predicate.entity.EntityPredicates; -import net.minecraft.util.DyeColor; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Vec3d; -import org.joml.Intersectiond; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -public class CreeperBeams { - - private static final Logger LOGGER = LoggerFactory.getLogger(CreeperBeams.class.getName()); - - private static final float[][] COLORS = { - DyeColor.LIGHT_BLUE.getColorComponents(), - DyeColor.LIME.getColorComponents(), - DyeColor.YELLOW.getColorComponents(), - DyeColor.MAGENTA.getColorComponents(), - }; - private static final float[] GREEN_COLOR_COMPONENTS = DyeColor.GREEN.getColorComponents(); - - private static final int FLOOR_Y = 68; - private static final int BASE_Y = 74; - - private static ArrayList beams = new ArrayList<>(); - private static BlockPos base = null; - private static boolean solved = false; - - public static void init() { - Scheduler.INSTANCE.scheduleCyclic(CreeperBeams::update, 20); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(CreeperBeams::render); - ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); - } - - private static void reset() { - beams.clear(); - base = null; - solved = false; - } - - private static void update() { - - // don't do anything if the room is solved - if (solved) { - return; - } - - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - ClientPlayerEntity player = client.player; - - // clear state if not in dungeon - if (world == null || player == null || !Utils.isInDungeons()) { - return; - } - - // try to find base if not found and solve - if (base == null) { - base = findCreeperBase(player, world); - if (base == null) { - return; - } - Vec3d creeperPos = new Vec3d(base.getX() + 0.5, BASE_Y + 1.75, base.getZ() + 0.5); - ArrayList targets = findTargets(world, base); - beams = findLines(creeperPos, targets); - } - - // update the beam states - beams.forEach(b -> b.updateState(world)); - - // check if the room is solved - if (!isTarget(world, base)) { - solved = true; - } - } - - // find the sea lantern block beneath the creeper - private static BlockPos findCreeperBase(ClientPlayerEntity player, ClientWorld world) { - - // find all creepers - List creepers = world.getEntitiesByClass( - CreeperEntity.class, - player.getBoundingBox().expand(50D), - EntityPredicates.VALID_ENTITY); - - if (creepers.isEmpty()) { - return null; - } - - // (sanity) check: - // if the creeper isn't above a sea lantern, it's not the target. - for (CreeperEntity ce : creepers) { - Vec3d creeperPos = ce.getPos(); - BlockPos potentialBase = BlockPos.ofFloored(creeperPos.x, BASE_Y, creeperPos.z); - if (isTarget(world, potentialBase)) { - return potentialBase; - } - } - - return null; - - } - - // find the sea lanterns (and the ONE prismarine ty hypixel) in the room - private static ArrayList findTargets(ClientWorld world, BlockPos basePos) { - ArrayList targets = new ArrayList<>(); - - BlockPos start = new BlockPos(basePos.getX() - 15, BASE_Y + 12, basePos.getZ() - 15); - BlockPos end = new BlockPos(basePos.getX() + 16, FLOOR_Y, basePos.getZ() + 16); - - for (BlockPos pos : BlockPos.iterate(start, end)) { - if (isTarget(world, pos)) { - targets.add(new BlockPos(pos)); - } - } - return targets; - } - - // generate lines between targets and finally find the solution - private static ArrayList findLines(Vec3d creeperPos, ArrayList targets) { - - ArrayList> allLines = new ArrayList<>(); - - // optimize this a little bit by - // only generating lines "one way", i.e. 1 -> 2 but not 2 -> 1 - for (int i = 0; i < targets.size(); i++) { - for (int j = i + 1; j < targets.size(); j++) { - Beam beam = new Beam(targets.get(i), targets.get(j)); - double dist = Intersectiond.distancePointLine( - creeperPos.x, creeperPos.y, creeperPos.z, - beam.line[0].x, beam.line[0].y, beam.line[0].z, - beam.line[1].x, beam.line[1].y, beam.line[1].z); - allLines.add(ObjectDoublePair.of(beam, dist)); - } - } - - // this feels a bit heavy-handed, but it works for now. - - ArrayList result = new ArrayList<>(); - allLines.sort(Comparator.comparingDouble(ObjectDoublePair::rightDouble)); - - while (result.size() < 4 && !allLines.isEmpty()) { - Beam solution = allLines.get(0).left(); - result.add(solution); - - // remove the line we just added and other lines that use blocks we're using for - // that line - allLines.remove(0); - allLines.removeIf(beam -> solution.containsComponentOf(beam.left())); - } - - if (result.size() != 4) { - LOGGER.error("Not enough solutions found. This is bad..."); - } - - return result; - } - - private static void render(WorldRenderContext wrc) { - - // don't render if solved or disabled - if (solved || !SkyblockerConfigManager.get().locations.dungeons.creeperSolver) { - return; - } - - // lines.size() is always <= 4 so no issues OOB issues with the colors here. - for (int i = 0; i < beams.size(); i++) { - beams.get(i).render(wrc, COLORS[i]); - } - } - - private static boolean isTarget(ClientWorld world, BlockPos pos) { - Block block = world.getBlockState(pos).getBlock(); - return block == Blocks.SEA_LANTERN || block == Blocks.PRISMARINE; - } - - // helper class to hold all the things needed to render a beam - private static class Beam { - - // raw block pos of target - public BlockPos blockOne; - public BlockPos blockTwo; - - // middle of targets used for rendering the line - public Vec3d[] line = new Vec3d[2]; - - // boxes used for rendering the block outline - public Box outlineOne; - public Box outlineTwo; - - // state: is this beam created/inputted or not? - private boolean toDo = true; - - public Beam(BlockPos a, BlockPos b) { - blockOne = a; - blockTwo = b; - line[0] = new Vec3d(a.getX() + 0.5, a.getY() + 0.5, a.getZ() + 0.5); - line[1] = new Vec3d(b.getX() + 0.5, b.getY() + 0.5, b.getZ() + 0.5); - outlineOne = new Box(a); - outlineTwo = new Box(b); - } - - // used to filter the list of all beams so that no two beams share a target - public boolean containsComponentOf(Beam other) { - return this.blockOne.equals(other.blockOne) - || this.blockOne.equals(other.blockTwo) - || this.blockTwo.equals(other.blockOne) - || this.blockTwo.equals(other.blockTwo); - } - - // update the state: is the beam created or not? - public void updateState(ClientWorld world) { - toDo = !(world.getBlockState(blockOne).getBlock() == Blocks.PRISMARINE - && world.getBlockState(blockTwo).getBlock() == Blocks.PRISMARINE); - } - - // render either in a color if not created or faintly green if created - public void render(WorldRenderContext wrc, float[] color) { - if (toDo) { - RenderHelper.renderOutline(wrc, outlineOne, color, 3, false); - RenderHelper.renderOutline(wrc, outlineTwo, color, 3, false); - RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2); - } else { - RenderHelper.renderOutline(wrc, outlineOne, GREEN_COLOR_COMPONENTS, 1, false); - RenderHelper.renderOutline(wrc, outlineTwo, GREEN_COLOR_COMPONENTS, 1, false); - RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1); - } - } - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java deleted file mode 100644 index aabef183..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java +++ /dev/null @@ -1,164 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.events.DungeonEvents; -import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.objects.ObjectIntPair; -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.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.entity.decoration.ArmorStandEntity; -import net.minecraft.predicate.entity.EntityPredicates; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Vec3d; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -/** - * This class provides functionality to render outlines around Blaze entities - */ -public class DungeonBlaze { - private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName()); - private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F}; - private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f}; - - private static boolean inBlaze; - private static ArmorStandEntity highestBlaze = null; - private static ArmorStandEntity lowestBlaze = null; - private static ArmorStandEntity nextHighestBlaze = null; - private static ArmorStandEntity nextLowestBlaze = null; - - public static void init() { - DungeonEvents.PUZZLE_MATCHED.register(room -> { - if (room.getName().startsWith("blaze-room")) { - inBlaze = true; - } - }); - Scheduler.INSTANCE.scheduleCyclic(DungeonBlaze::update, 4); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(DungeonBlaze::blazeRenderer); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> inBlaze = false); - } - - /** - * Updates the state of Blaze entities and triggers the rendering process if necessary. - */ - public static void update() { - if (!inBlaze) { - return; - } - ClientWorld world = MinecraftClient.getInstance().world; - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (world == null || player == null || !Utils.isInDungeons()) return; - List> blazes = getBlazesInWorld(world, player); - sortBlazes(blazes); - updateBlazeEntities(blazes); - } - - /** - * Retrieves Blaze entities in the world and parses their health information. - * - * @param world The client world to search for Blaze entities. - * @return A list of Blaze entities and their associated health. - */ - private static List> getBlazesInWorld(ClientWorld world, ClientPlayerEntity player) { - List> blazes = new ArrayList<>(); - for (ArmorStandEntity blaze : world.getEntitiesByClass(ArmorStandEntity.class, player.getBoundingBox().expand(500D), EntityPredicates.NOT_MOUNTED)) { - String blazeName = blaze.getName().getString(); - if (blazeName.contains("Blaze") && blazeName.contains("/")) { - try { - int health = Integer.parseInt((blazeName.substring(blazeName.indexOf("/") + 1, blazeName.length() - 1)).replaceAll(",", "")); - blazes.add(ObjectIntPair.of(blaze, health)); - } catch (NumberFormatException e) { - handleException(e); - } - } - } - return blazes; - } - - /** - * Sorts the Blaze entities based on their health values. - * - * @param blazes The list of Blaze entities to be sorted. - */ - private static void sortBlazes(List> blazes) { - blazes.sort(Comparator.comparingInt(ObjectIntPair::rightInt)); - } - - /** - * Updates information about Blaze entities based on sorted list. - * - * @param blazes The sorted list of Blaze entities with associated health values. - */ - private static void updateBlazeEntities(List> blazes) { - if (!blazes.isEmpty()) { - lowestBlaze = blazes.get(0).left(); - int highestIndex = blazes.size() - 1; - highestBlaze = blazes.get(highestIndex).left(); - if (blazes.size() > 1) { - nextLowestBlaze = blazes.get(1).left(); - nextHighestBlaze = blazes.get(highestIndex - 1).left(); - } - } - } - - /** - * Renders outlines for Blaze entities based on health and position. - * - * @param wrc The WorldRenderContext used for rendering. - */ - public static void blazeRenderer(WorldRenderContext wrc) { - try { - if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfigManager.get().locations.dungeons.blazeSolver) { - if (highestBlaze.getY() < 69) { - renderBlazeOutline(highestBlaze, nextHighestBlaze, wrc); - } - if (lowestBlaze.getY() > 69) { - renderBlazeOutline(lowestBlaze, nextLowestBlaze, wrc); - } - } - } catch (Exception e) { - handleException(e); - } - } - - /** - * Renders outlines for Blaze entities and connections between them. - * - * @param blaze The Blaze entity for which to render an outline. - * @param nextBlaze The next Blaze entity for connection rendering. - * @param wrc The WorldRenderContext used for rendering. - */ - private static void renderBlazeOutline(ArmorStandEntity blaze, ArmorStandEntity nextBlaze, WorldRenderContext wrc) { - Box blazeBox = blaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); - RenderHelper.renderOutline(wrc, blazeBox, GREEN_COLOR_COMPONENTS, 5f, false); - - if (nextBlaze != null && nextBlaze.isAlive() && nextBlaze != blaze) { - Box nextBlazeBox = nextBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); - RenderHelper.renderOutline(wrc, nextBlazeBox, WHITE_COLOR_COMPONENTS, 5f, false); - - Vec3d blazeCenter = blazeBox.getCenter(); - Vec3d nextBlazeCenter = nextBlazeBox.getCenter(); - - RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f); - } - } - - /** - * Handles exceptions by logging and printing stack traces. - * - * @param e The exception to handle. - */ - private static void handleException(Exception e) { - LOGGER.error("[Skyblocker BlazeRenderer] Encountered an unknown exception", e); - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java deleted file mode 100644 index e1ab2fa8..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.utils.chat.ChatFilterResult; -import de.hysky.skyblocker.utils.chat.ChatPatternListener; -import net.minecraft.client.MinecraftClient; -import net.minecraft.entity.decoration.ArmorStandEntity; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import java.util.regex.Matcher; - -public class ThreeWeirdos extends ChatPatternListener { - public ThreeWeirdos() { - super("^§e\\[NPC] §c([A-Z][a-z]+)§f: (?:The reward is(?: not in my chest!|n't in any of our chests\\.)|My chest (?:doesn't have the reward\\. We are all telling the truth\\.|has the reward and I'm telling the truth!)|At least one of them is lying, and the reward is not in §c§c[A-Z][a-z]+'s §rchest\\!|Both of them are telling the truth\\. Also, §c§c[A-Z][a-z]+ §rhas the reward in their chest\\!)$"); - } - - @Override - public ChatFilterResult state() { - return SkyblockerConfigManager.get().locations.dungeons.solveThreeWeirdos ? null : ChatFilterResult.PASS; - } - - @Override - public boolean onMatch(Text message, Matcher matcher) { - MinecraftClient client = MinecraftClient.getInstance(); - if (client.player == null || client.world == null) return false; - client.world.getEntitiesByClass( - ArmorStandEntity.class, - client.player.getBoundingBox().expand(3), - entity -> { - Text customName = entity.getCustomName(); - return customName != null && customName.getString().equals(matcher.group(1)); - } - ).forEach( - entity -> entity.setCustomName(Text.of(Formatting.GREEN + matcher.group(1))) - ); - return false; - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java deleted file mode 100644 index 2bb3e4e0..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java +++ /dev/null @@ -1,151 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.events.DungeonEvents; -import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils; -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.minecraft.block.Block; -import net.minecraft.block.Blocks; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.entity.decoration.ItemFrameEntity; -import net.minecraft.item.FilledMapItem; -import net.minecraft.item.map.MapState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Direction; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -/** - * Thanks to Danker for a reference implementation! - */ -public class TicTacToe { - private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class); - private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F}; - private static boolean inTicTacToe; - private static Box nextBestMoveToMake = null; - - public static void init() { - DungeonEvents.PUZZLE_MATCHED.register(room -> { - if (room.getName().startsWith("tic-tac-toe")) { - inTicTacToe = true; - } - }); - Scheduler.INSTANCE.scheduleCyclic(TicTacToe::tick, 4); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(TicTacToe::solutionRenderer); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> inTicTacToe = false); - } - - public static void tick() { - if (!inTicTacToe) { - return; - } - - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - ClientPlayerEntity player = client.player; - - nextBestMoveToMake = null; - - if (world == null || player == null || !Utils.isInDungeons()) return; - - //Search within 21 blocks for item frames that contain maps - Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21); - List itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); - - try { - //Only attempt to solve if its the player's turn - if (itemFramesThatHoldMaps.size() != 9 && itemFramesThatHoldMaps.size() % 2 == 1) { - char[][] board = new char[3][3]; - BlockPos leftmostRow = null; - int sign = 1; - char facing = 'X'; - - for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) { - MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); - - if (mapState == null) continue; - - int column = 0, row; - sign = 1; - - //Find position of the item frame relative to where it is on the tic tac toe board - if (itemFrame.getHorizontalFacing() == Direction.SOUTH || itemFrame.getHorizontalFacing() == Direction.WEST) sign = -1; - BlockPos itemFramePos = BlockPos.ofFloored(itemFrame.getX(), itemFrame.getY(), itemFrame.getZ()); - - for (int i = 2; i >= 0; i--) { - int realI = i * sign; - BlockPos blockPos = itemFramePos; - - if (itemFrame.getX() % 0.5 == 0) { - blockPos = itemFramePos.add(realI, 0, 0); - } else if (itemFrame.getZ() % 0.5 == 0) { - blockPos = itemFramePos.add(0, 0, realI); - facing = 'Z'; - } - - Block block = world.getBlockState(blockPos).getBlock(); - if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) { - leftmostRow = blockPos; - column = i; - - break; - } - } - - //Determine the row of the item frame - if (itemFrame.getY() == 72.5) { - row = 0; - } else if (itemFrame.getY() == 71.5) { - row = 1; - } else if (itemFrame.getY() == 70.5) { - row = 2; - } else { - continue; - } - - - //Get the color of the middle pixel of the map which determines whether its X or O - int middleColor = mapState.colors[8256] & 255; - - if (middleColor == 114) { - board[row][column] = 'X'; - } else if (middleColor == 33) { - board[row][column] = 'O'; - } - - int bestMove = TicTacToeUtils.getBestMove(board) - 1; - - if (leftmostRow != null) { - double drawX = facing == 'X' ? leftmostRow.getX() - sign * (bestMove % 3) : leftmostRow.getX(); - double drawY = 72 - (double) (bestMove / 3); - double drawZ = facing == 'Z' ? leftmostRow.getZ() - sign * (bestMove % 3) : leftmostRow.getZ(); - - nextBestMoveToMake = new Box(drawX, drawY, drawZ, drawX + 1, drawY + 1, drawZ + 1); - } - } - } - } catch (Exception e) { - LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while determining a tic tac toe solution!", e); - } - } - - private static void solutionRenderer(WorldRenderContext context) { - try { - if (SkyblockerConfigManager.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) { - RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5, false); - } - } catch (Exception e) { - LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while rendering the tic tac toe solution!", e); - } - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java deleted file mode 100644 index 21bbdce0..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java +++ /dev/null @@ -1,109 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.waypoint.FairySouls; -import de.hysky.skyblocker.utils.chat.ChatFilterResult; -import de.hysky.skyblocker.utils.chat.ChatPatternListener; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - -import com.mojang.logging.LogUtils; - -import java.util.*; -import java.util.regex.Matcher; - -public class Trivia extends ChatPatternListener { - private static final Logger LOGGER = LogUtils.getLogger(); - private static final Map answers; - private List solutions = Collections.emptyList(); - - public Trivia() { - super("^ +(?:([A-Za-z,' ]*\\?)|§6 ([ⓐⓑⓒ]) §a([a-zA-Z0-9 ]+))$"); - } - - @Override - public ChatFilterResult state() { - return SkyblockerConfigManager.get().locations.dungeons.solveTrivia ? ChatFilterResult.FILTER : ChatFilterResult.PASS; - } - - @Override - public boolean onMatch(Text message, Matcher matcher) { - String riddle = matcher.group(3); - if (riddle != null) { - if (!solutions.contains(riddle)) { - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (player != null) - MinecraftClient.getInstance().player.sendMessage(Text.of(" " + Formatting.GOLD + matcher.group(2) + Formatting.RED + " " + riddle), false); - return player != null; - } - } else updateSolutions(matcher.group(0)); - return false; - } - - private void updateSolutions(String question) { - try { - String trimmedQuestion = question.trim(); - if (trimmedQuestion.equals("What SkyBlock year is it?")) { - long currentTime = System.currentTimeMillis() / 1000L; - long diff = currentTime - 1560276000; - int year = (int) (diff / 446400 + 1); - solutions = Collections.singletonList("Year " + year); - } else { - String[] questionAnswers = answers.get(trimmedQuestion); - if (questionAnswers != null) solutions = Arrays.asList(questionAnswers); - } - } catch (Exception e) { //Hopefully the solver doesn't go south - LOGGER.error("[Skyblocker] Failed to update the Trivia puzzle answers!", e); - } - } - - static { - answers = Collections.synchronizedMap(new HashMap<>()); - answers.put("What is the status of The Watcher?", new String[]{"Stalker"}); - answers.put("What is the status of Bonzo?", new String[]{"New Necromancer"}); - answers.put("What is the status of Scarf?", new String[]{"Apprentice Necromancer"}); - answers.put("What is the status of The Professor?", new String[]{"Professor"}); - answers.put("What is the status of Thorn?", new String[]{"Shaman Necromancer"}); - answers.put("What is the status of Livid?", new String[]{"Master Necromancer"}); - answers.put("What is the status of Sadan?", new String[]{"Necromancer Lord"}); - answers.put("What is the status of Maxor?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Goldor?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Storm?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Necron?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Maxor, Storm, Goldor and Necron?", new String[]{"The Wither Lords"}); - answers.put("Which brother is on the Spider's Den?", new String[]{"Rick"}); - answers.put("What is the name of Rick's brother?", new String[]{"Pat"}); - answers.put("What is the name of the Painter in the Hub?", new String[]{"Marco"}); - answers.put("What is the name of the person that upgrades pets?", new String[]{"Kat"}); - answers.put("What is the name of the lady of the Nether?", new String[]{"Elle"}); - answers.put("Which villager in the Village gives you a Rogue Sword?", new String[]{"Jamie"}); - answers.put("How many unique minions are there?", new String[]{"59 Minions"}); - answers.put("Which of these enemies does not spawn in the Spider's Den?", new String[]{"Zombie Spider", "Cave Spider", "Wither Skeleton", "Dashing Spooder", "Broodfather", "Night Spider"}); - answers.put("Which of these monsters only spawns at night?", new String[]{"Zombie Villager", "Ghast"}); - answers.put("Which of these is not a dragon in The End?", new String[]{"Zoomer Dragon", "Weak Dragon", "Stonk Dragon", "Holy Dragon", "Boomer Dragon", "Booger Dragon", "Older Dragon", "Elder Dragon", "Stable Dragon", "Professor Dragon"}); - FairySouls.runAsyncAfterFairySoulsLoad(() -> { - answers.put("How many total Fairy Souls are there?", getFairySoulsSizeString(null)); - answers.put("How many Fairy Souls are there in Spider's Den?", getFairySoulsSizeString("combat_1")); - answers.put("How many Fairy Souls are there in The End?", getFairySoulsSizeString("combat_3")); - answers.put("How many Fairy Souls are there in The Farming Islands?", getFairySoulsSizeString("farming_1")); - answers.put("How many Fairy Souls are there in Crimson Isle?", getFairySoulsSizeString("crimson_isle")); - answers.put("How many Fairy Souls are there in The Park?", getFairySoulsSizeString("foraging_1")); - answers.put("How many Fairy Souls are there in Jerry's Workshop?", getFairySoulsSizeString("winter")); - answers.put("How many Fairy Souls are there in Hub?", getFairySoulsSizeString("hub")); - answers.put("How many Fairy Souls are there in The Hub?", getFairySoulsSizeString("hub")); - answers.put("How many Fairy Souls are there in Deep Caverns?", getFairySoulsSizeString("mining_2")); - answers.put("How many Fairy Souls are there in Gold Mine?", getFairySoulsSizeString("mining_1")); - answers.put("How many Fairy Souls are there in Dungeon Hub?", getFairySoulsSizeString("dungeon_hub")); - }); - } - - @NotNull - private static String[] getFairySoulsSizeString(@Nullable String location) { - return new String[]{"%d Fairy Souls".formatted(FairySouls.getFairySoulsSize(location))}; - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java new file mode 100644 index 00000000..8de1e3fe --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java @@ -0,0 +1,250 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.ObjectDoublePair; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.joml.Intersectiond; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class CreeperBeams extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(CreeperBeams.class.getName()); + + private static final float[][] COLORS = { + DyeColor.LIGHT_BLUE.getColorComponents(), + DyeColor.LIME.getColorComponents(), + DyeColor.YELLOW.getColorComponents(), + DyeColor.MAGENTA.getColorComponents(), + }; + private static final float[] GREEN_COLOR_COMPONENTS = DyeColor.GREEN.getColorComponents(); + + private static final int FLOOR_Y = 68; + private static final int BASE_Y = 74; + private static final CreeperBeams INSTANCE = new CreeperBeams("creeper", "creeper-room"); + + private static ArrayList beams = new ArrayList<>(); + private static BlockPos base = null; + + private CreeperBeams(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + @Override + public void reset() { + super.reset(); + beams.clear(); + base = null; + } + + @Override + public void tick() { + + // don't do anything if the room is solved + if (!shouldSolve()) { + return; + } + + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + ClientPlayerEntity player = client.player; + + // clear state if not in dungeon + if (world == null || player == null || !Utils.isInDungeons()) { + return; + } + + // try to find base if not found and solve + if (base == null) { + base = findCreeperBase(player, world); + if (base == null) { + return; + } + Vec3d creeperPos = new Vec3d(base.getX() + 0.5, BASE_Y + 1.75, base.getZ() + 0.5); + ArrayList targets = findTargets(world, base); + beams = findLines(creeperPos, targets); + } + + // update the beam states + beams.forEach(b -> b.updateState(world)); + + // check if the room is solved + if (!isTarget(world, base)) { + reset(); + } + } + + // find the sea lantern block beneath the creeper + private static BlockPos findCreeperBase(ClientPlayerEntity player, ClientWorld world) { + + // find all creepers + List creepers = world.getEntitiesByClass( + CreeperEntity.class, + player.getBoundingBox().expand(50D), + EntityPredicates.VALID_ENTITY); + + if (creepers.isEmpty()) { + return null; + } + + // (sanity) check: + // if the creeper isn't above a sea lantern, it's not the target. + for (CreeperEntity ce : creepers) { + Vec3d creeperPos = ce.getPos(); + BlockPos potentialBase = BlockPos.ofFloored(creeperPos.x, BASE_Y, creeperPos.z); + if (isTarget(world, potentialBase)) { + return potentialBase; + } + } + + return null; + + } + + // find the sea lanterns (and the ONE prismarine ty hypixel) in the room + private static ArrayList findTargets(ClientWorld world, BlockPos basePos) { + ArrayList targets = new ArrayList<>(); + + BlockPos start = new BlockPos(basePos.getX() - 15, BASE_Y + 12, basePos.getZ() - 15); + BlockPos end = new BlockPos(basePos.getX() + 16, FLOOR_Y, basePos.getZ() + 16); + + for (BlockPos pos : BlockPos.iterate(start, end)) { + if (isTarget(world, pos)) { + targets.add(new BlockPos(pos)); + } + } + return targets; + } + + // generate lines between targets and finally find the solution + private static ArrayList findLines(Vec3d creeperPos, ArrayList targets) { + + ArrayList> allLines = new ArrayList<>(); + + // optimize this a little bit by + // only generating lines "one way", i.e. 1 -> 2 but not 2 -> 1 + for (int i = 0; i < targets.size(); i++) { + for (int j = i + 1; j < targets.size(); j++) { + Beam beam = new Beam(targets.get(i), targets.get(j)); + double dist = Intersectiond.distancePointLine( + creeperPos.x, creeperPos.y, creeperPos.z, + beam.line[0].x, beam.line[0].y, beam.line[0].z, + beam.line[1].x, beam.line[1].y, beam.line[1].z); + allLines.add(ObjectDoublePair.of(beam, dist)); + } + } + + // this feels a bit heavy-handed, but it works for now. + + ArrayList result = new ArrayList<>(); + allLines.sort(Comparator.comparingDouble(ObjectDoublePair::rightDouble)); + + while (result.size() < 4 && !allLines.isEmpty()) { + Beam solution = allLines.get(0).left(); + result.add(solution); + + // remove the line we just added and other lines that use blocks we're using for + // that line + allLines.remove(0); + allLines.removeIf(beam -> solution.containsComponentOf(beam.left())); + } + + if (result.size() != 4) { + LOGGER.error("Not enough solutions found. This is bad..."); + } + + return result; + } + + @Override + public void render(WorldRenderContext wrc) { + + // don't render if solved or disabled + if (!shouldSolve() || !SkyblockerConfigManager.get().locations.dungeons.creeperSolver) { + return; + } + + // lines.size() is always <= 4 so no issues OOB issues with the colors here. + for (int i = 0; i < beams.size(); i++) { + beams.get(i).render(wrc, COLORS[i]); + } + } + + private static boolean isTarget(ClientWorld world, BlockPos pos) { + Block block = world.getBlockState(pos).getBlock(); + return block == Blocks.SEA_LANTERN || block == Blocks.PRISMARINE; + } + + // helper class to hold all the things needed to render a beam + private static class Beam { + + // raw block pos of target + public BlockPos blockOne; + public BlockPos blockTwo; + + // middle of targets used for rendering the line + public Vec3d[] line = new Vec3d[2]; + + // boxes used for rendering the block outline + public Box outlineOne; + public Box outlineTwo; + + // state: is this beam created/inputted or not? + private boolean toDo = true; + + public Beam(BlockPos a, BlockPos b) { + blockOne = a; + blockTwo = b; + line[0] = new Vec3d(a.getX() + 0.5, a.getY() + 0.5, a.getZ() + 0.5); + line[1] = new Vec3d(b.getX() + 0.5, b.getY() + 0.5, b.getZ() + 0.5); + outlineOne = new Box(a); + outlineTwo = new Box(b); + } + + // used to filter the list of all beams so that no two beams share a target + public boolean containsComponentOf(Beam other) { + return this.blockOne.equals(other.blockOne) + || this.blockOne.equals(other.blockTwo) + || this.blockTwo.equals(other.blockOne) + || this.blockTwo.equals(other.blockTwo); + } + + // update the state: is the beam created or not? + public void updateState(ClientWorld world) { + toDo = !(world.getBlockState(blockOne).getBlock() == Blocks.PRISMARINE + && world.getBlockState(blockTwo).getBlock() == Blocks.PRISMARINE); + } + + // render either in a color if not created or faintly green if created + public void render(WorldRenderContext wrc, float[] color) { + if (toDo) { + RenderHelper.renderOutline(wrc, outlineOne, color, 3, false); + RenderHelper.renderOutline(wrc, outlineTwo, color, 3, false); + RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2); + } else { + RenderHelper.renderOutline(wrc, outlineOne, GREEN_COLOR_COMPONENTS, 1, false); + RenderHelper.renderOutline(wrc, outlineTwo, GREEN_COLOR_COMPONENTS, 1, false); + RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java new file mode 100644 index 00000000..5774eaef --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java @@ -0,0 +1,158 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * This class provides functionality to render outlines around Blaze entities + */ +public class DungeonBlaze extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName()); + private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F}; + private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f}; + private static final DungeonBlaze INSTANCE = new DungeonBlaze("blaze", "blaze-room-1-high", "blaze-room-1-low"); + + private static ArmorStandEntity highestBlaze = null; + private static ArmorStandEntity lowestBlaze = null; + private static ArmorStandEntity nextHighestBlaze = null; + private static ArmorStandEntity nextLowestBlaze = null; + + private DungeonBlaze(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + /** + * Updates the state of Blaze entities and triggers the rendering process if necessary. + */ + @Override + public void tick() { + if (!shouldSolve()) { + return; + } + ClientWorld world = MinecraftClient.getInstance().world; + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (world == null || player == null || !Utils.isInDungeons()) return; + List> blazes = getBlazesInWorld(world, player); + sortBlazes(blazes); + updateBlazeEntities(blazes); + } + + /** + * Retrieves Blaze entities in the world and parses their health information. + * + * @param world The client world to search for Blaze entities. + * @return A list of Blaze entities and their associated health. + */ + private static List> getBlazesInWorld(ClientWorld world, ClientPlayerEntity player) { + List> blazes = new ArrayList<>(); + for (ArmorStandEntity blaze : world.getEntitiesByClass(ArmorStandEntity.class, player.getBoundingBox().expand(500D), EntityPredicates.NOT_MOUNTED)) { + String blazeName = blaze.getName().getString(); + if (blazeName.contains("Blaze") && blazeName.contains("/")) { + try { + int health = Integer.parseInt((blazeName.substring(blazeName.indexOf("/") + 1, blazeName.length() - 1)).replaceAll(",", "")); + blazes.add(ObjectIntPair.of(blaze, health)); + } catch (NumberFormatException e) { + handleException(e); + } + } + } + return blazes; + } + + /** + * Sorts the Blaze entities based on their health values. + * + * @param blazes The list of Blaze entities to be sorted. + */ + private static void sortBlazes(List> blazes) { + blazes.sort(Comparator.comparingInt(ObjectIntPair::rightInt)); + } + + /** + * Updates information about Blaze entities based on sorted list. + * + * @param blazes The sorted list of Blaze entities with associated health values. + */ + private static void updateBlazeEntities(List> blazes) { + if (!blazes.isEmpty()) { + lowestBlaze = blazes.get(0).left(); + int highestIndex = blazes.size() - 1; + highestBlaze = blazes.get(highestIndex).left(); + if (blazes.size() > 1) { + nextLowestBlaze = blazes.get(1).left(); + nextHighestBlaze = blazes.get(highestIndex - 1).left(); + } + } + } + + /** + * Renders outlines for Blaze entities based on health and position. + * + * @param wrc The WorldRenderContext used for rendering. + */ + @Override + public void render(WorldRenderContext wrc) { + try { + if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfigManager.get().locations.dungeons.blazeSolver) { + if (highestBlaze.getY() < 69) { + renderBlazeOutline(highestBlaze, nextHighestBlaze, wrc); + } + if (lowestBlaze.getY() > 69) { + renderBlazeOutline(lowestBlaze, nextLowestBlaze, wrc); + } + } + } catch (Exception e) { + handleException(e); + } + } + + /** + * Renders outlines for Blaze entities and connections between them. + * + * @param blaze The Blaze entity for which to render an outline. + * @param nextBlaze The next Blaze entity for connection rendering. + * @param wrc The WorldRenderContext used for rendering. + */ + private static void renderBlazeOutline(ArmorStandEntity blaze, ArmorStandEntity nextBlaze, WorldRenderContext wrc) { + Box blazeBox = blaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); + RenderHelper.renderOutline(wrc, blazeBox, GREEN_COLOR_COMPONENTS, 5f, false); + + if (nextBlaze != null && nextBlaze.isAlive() && nextBlaze != blaze) { + Box nextBlazeBox = nextBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); + RenderHelper.renderOutline(wrc, nextBlazeBox, WHITE_COLOR_COMPONENTS, 5f, false); + + Vec3d blazeCenter = blazeBox.getCenter(); + Vec3d nextBlazeCenter = nextBlazeBox.getCenter(); + + RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f); + } + } + + /** + * Handles exceptions by logging and printing stack traces. + * + * @param e The exception to handle. + */ + private static void handleException(Exception e) { + LOGGER.error("[Skyblocker BlazeRenderer] Encountered an unknown exception", e); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java new file mode 100644 index 00000000..04446e60 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java @@ -0,0 +1,58 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import com.mojang.brigadier.Command; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.events.DungeonEvents; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; +import de.hysky.skyblocker.utils.render.Renderable; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public abstract class DungeonPuzzle implements Tickable, Renderable { + private final String puzzleName; + @NotNull + private final Set roomNames; + private boolean shouldSolve; + + public DungeonPuzzle(String puzzleName, String... roomName) { + this(puzzleName, Set.of(roomName)); + } + + public DungeonPuzzle(String puzzleName, @NotNull Set roomNames) { + this.puzzleName = puzzleName; + this.roomNames = roomNames; + DungeonEvents.PUZZLE_MATCHED.register(room -> { + if (roomNames.contains(room.getName())) { + room.addSubProcess(this); + shouldSolve = true; + } + }); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("solvePuzzle").then(literal(puzzleName).executes(context -> { + Room currentRoom = DungeonManager.getCurrentRoom(); + if (currentRoom != null) { + currentRoom.addSubProcess(this); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§aSolving " + puzzleName + " puzzle in the current room.")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); + } + return Command.SINGLE_SUCCESS; + })))))); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + } + + public boolean shouldSolve() { + return shouldSolve; + } + + public void reset() { + shouldSolve = false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java new file mode 100644 index 00000000..c5e55f93 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; +import de.hysky.skyblocker.utils.chat.ChatPatternListener; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.regex.Matcher; + +public class ThreeWeirdos extends ChatPatternListener { + public ThreeWeirdos() { + super("^§e\\[NPC] §c([A-Z][a-z]+)§f: (?:The reward is(?: not in my chest!|n't in any of our chests\\.)|My chest (?:doesn't have the reward\\. We are all telling the truth\\.|has the reward and I'm telling the truth!)|At least one of them is lying, and the reward is not in §c§c[A-Z][a-z]+'s §rchest\\!|Both of them are telling the truth\\. Also, §c§c[A-Z][a-z]+ §rhas the reward in their chest\\!)$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().locations.dungeons.solveThreeWeirdos ? null : ChatFilterResult.PASS; + } + + @Override + public boolean onMatch(Text message, Matcher matcher) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) return false; + client.world.getEntitiesByClass( + ArmorStandEntity.class, + client.player.getBoundingBox().expand(3), + entity -> { + Text customName = entity.getCustomName(); + return customName != null && customName.getString().equals(matcher.group(1)); + } + ).forEach( + entity -> entity.setCustomName(Text.of(Formatting.GREEN + matcher.group(1))) + ); + return false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java new file mode 100644 index 00000000..90028a4f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java @@ -0,0 +1,145 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.decoration.ItemFrameEntity; +import net.minecraft.item.FilledMapItem; +import net.minecraft.item.map.MapState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Thanks to Danker for a reference implementation! + */ +public class TicTacToe extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class); + private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F}; + private static final TicTacToe INSTANCE = new TicTacToe("tic-tac-toe", "tic-tac-toe-1"); + private static Box nextBestMoveToMake = null; + + private TicTacToe(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + @Override + public void tick() { + if (!shouldSolve()) { + return; + } + + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + ClientPlayerEntity player = client.player; + + nextBestMoveToMake = null; + + if (world == null || player == null || !Utils.isInDungeons()) return; + + //Search within 21 blocks for item frames that contain maps + Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21); + List itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); + + try { + //Only attempt to solve if its the player's turn + if (itemFramesThatHoldMaps.size() != 9 && itemFramesThatHoldMaps.size() % 2 == 1) { + char[][] board = new char[3][3]; + BlockPos leftmostRow = null; + int sign = 1; + char facing = 'X'; + + for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) { + MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); + + if (mapState == null) continue; + + int column = 0, row; + sign = 1; + + //Find position of the item frame relative to where it is on the tic tac toe board + if (itemFrame.getHorizontalFacing() == Direction.SOUTH || itemFrame.getHorizontalFacing() == Direction.WEST) sign = -1; + BlockPos itemFramePos = BlockPos.ofFloored(itemFrame.getX(), itemFrame.getY(), itemFrame.getZ()); + + for (int i = 2; i >= 0; i--) { + int realI = i * sign; + BlockPos blockPos = itemFramePos; + + if (itemFrame.getX() % 0.5 == 0) { + blockPos = itemFramePos.add(realI, 0, 0); + } else if (itemFrame.getZ() % 0.5 == 0) { + blockPos = itemFramePos.add(0, 0, realI); + facing = 'Z'; + } + + Block block = world.getBlockState(blockPos).getBlock(); + if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) { + leftmostRow = blockPos; + column = i; + + break; + } + } + + //Determine the row of the item frame + if (itemFrame.getY() == 72.5) { + row = 0; + } else if (itemFrame.getY() == 71.5) { + row = 1; + } else if (itemFrame.getY() == 70.5) { + row = 2; + } else { + continue; + } + + + //Get the color of the middle pixel of the map which determines whether its X or O + int middleColor = mapState.colors[8256] & 255; + + if (middleColor == 114) { + board[row][column] = 'X'; + } else if (middleColor == 33) { + board[row][column] = 'O'; + } + + int bestMove = TicTacToeUtils.getBestMove(board) - 1; + + if (leftmostRow != null) { + double drawX = facing == 'X' ? leftmostRow.getX() - sign * (bestMove % 3) : leftmostRow.getX(); + double drawY = 72 - (double) (bestMove / 3); + double drawZ = facing == 'Z' ? leftmostRow.getZ() - sign * (bestMove % 3) : leftmostRow.getZ(); + + nextBestMoveToMake = new Box(drawX, drawY, drawZ, drawX + 1, drawY + 1, drawZ + 1); + } + } + } + } catch (Exception e) { + LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while determining a tic tac toe solution!", e); + } + } + + @Override + public void render(WorldRenderContext context) { + try { + if (SkyblockerConfigManager.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) { + RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5, false); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while rendering the tic tac toe solution!", e); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java new file mode 100644 index 00000000..0f73457c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java @@ -0,0 +1,109 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.waypoint.FairySouls; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; +import de.hysky.skyblocker.utils.chat.ChatPatternListener; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import java.util.*; +import java.util.regex.Matcher; + +public class Trivia extends ChatPatternListener { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Map answers; + private List solutions = Collections.emptyList(); + + public Trivia() { + super("^ +(?:([A-Za-z,' ]*\\?)|§6 ([ⓐⓑⓒ]) §a([a-zA-Z0-9 ]+))$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().locations.dungeons.solveTrivia ? ChatFilterResult.FILTER : ChatFilterResult.PASS; + } + + @Override + public boolean onMatch(Text message, Matcher matcher) { + String riddle = matcher.group(3); + if (riddle != null) { + if (!solutions.contains(riddle)) { + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player != null) + MinecraftClient.getInstance().player.sendMessage(Text.of(" " + Formatting.GOLD + matcher.group(2) + Formatting.RED + " " + riddle), false); + return player != null; + } + } else updateSolutions(matcher.group(0)); + return false; + } + + private void updateSolutions(String question) { + try { + String trimmedQuestion = question.trim(); + if (trimmedQuestion.equals("What SkyBlock year is it?")) { + long currentTime = System.currentTimeMillis() / 1000L; + long diff = currentTime - 1560276000; + int year = (int) (diff / 446400 + 1); + solutions = Collections.singletonList("Year " + year); + } else { + String[] questionAnswers = answers.get(trimmedQuestion); + if (questionAnswers != null) solutions = Arrays.asList(questionAnswers); + } + } catch (Exception e) { //Hopefully the solver doesn't go south + LOGGER.error("[Skyblocker] Failed to update the Trivia puzzle answers!", e); + } + } + + static { + answers = Collections.synchronizedMap(new HashMap<>()); + answers.put("What is the status of The Watcher?", new String[]{"Stalker"}); + answers.put("What is the status of Bonzo?", new String[]{"New Necromancer"}); + answers.put("What is the status of Scarf?", new String[]{"Apprentice Necromancer"}); + answers.put("What is the status of The Professor?", new String[]{"Professor"}); + answers.put("What is the status of Thorn?", new String[]{"Shaman Necromancer"}); + answers.put("What is the status of Livid?", new String[]{"Master Necromancer"}); + answers.put("What is the status of Sadan?", new String[]{"Necromancer Lord"}); + answers.put("What is the status of Maxor?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Goldor?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Storm?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Necron?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Maxor, Storm, Goldor and Necron?", new String[]{"The Wither Lords"}); + answers.put("Which brother is on the Spider's Den?", new String[]{"Rick"}); + answers.put("What is the name of Rick's brother?", new String[]{"Pat"}); + answers.put("What is the name of the Painter in the Hub?", new String[]{"Marco"}); + answers.put("What is the name of the person that upgrades pets?", new String[]{"Kat"}); + answers.put("What is the name of the lady of the Nether?", new String[]{"Elle"}); + answers.put("Which villager in the Village gives you a Rogue Sword?", new String[]{"Jamie"}); + answers.put("How many unique minions are there?", new String[]{"59 Minions"}); + answers.put("Which of these enemies does not spawn in the Spider's Den?", new String[]{"Zombie Spider", "Cave Spider", "Wither Skeleton", "Dashing Spooder", "Broodfather", "Night Spider"}); + answers.put("Which of these monsters only spawns at night?", new String[]{"Zombie Villager", "Ghast"}); + answers.put("Which of these is not a dragon in The End?", new String[]{"Zoomer Dragon", "Weak Dragon", "Stonk Dragon", "Holy Dragon", "Boomer Dragon", "Booger Dragon", "Older Dragon", "Elder Dragon", "Stable Dragon", "Professor Dragon"}); + FairySouls.runAsyncAfterFairySoulsLoad(() -> { + answers.put("How many total Fairy Souls are there?", getFairySoulsSizeString(null)); + answers.put("How many Fairy Souls are there in Spider's Den?", getFairySoulsSizeString("combat_1")); + answers.put("How many Fairy Souls are there in The End?", getFairySoulsSizeString("combat_3")); + answers.put("How many Fairy Souls are there in The Farming Islands?", getFairySoulsSizeString("farming_1")); + answers.put("How many Fairy Souls are there in Crimson Isle?", getFairySoulsSizeString("crimson_isle")); + answers.put("How many Fairy Souls are there in The Park?", getFairySoulsSizeString("foraging_1")); + answers.put("How many Fairy Souls are there in Jerry's Workshop?", getFairySoulsSizeString("winter")); + answers.put("How many Fairy Souls are there in Hub?", getFairySoulsSizeString("hub")); + answers.put("How many Fairy Souls are there in The Hub?", getFairySoulsSizeString("hub")); + answers.put("How many Fairy Souls are there in Deep Caverns?", getFairySoulsSizeString("mining_2")); + answers.put("How many Fairy Souls are there in Gold Mine?", getFairySoulsSizeString("mining_1")); + answers.put("How many Fairy Souls are there in Dungeon Hub?", getFairySoulsSizeString("dungeon_hub")); + }); + } + + @NotNull + private static String[] getFairySoulsSizeString(@Nullable String location) { + return new String[]{"%d Fairy Souls".formatted(FairySouls.getFairySoulsSize(location))}; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java index b686607b..931d1d69 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java @@ -1,26 +1,38 @@ package de.hysky.skyblocker.skyblock.dungeon.secrets; import de.hysky.skyblocker.utils.waypoint.Waypoint; +import it.unimi.dsi.fastutil.ints.IntRBTreeSet; +import it.unimi.dsi.fastutil.ints.IntSortedSet; +import it.unimi.dsi.fastutil.ints.IntSortedSets; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.minecraft.client.world.ClientWorld; import net.minecraft.registry.Registries; import net.minecraft.util.math.BlockPos; import org.apache.commons.lang3.tuple.MutableTriple; -import org.jetbrains.annotations.NotNull; import org.joml.Vector2ic; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; public class DebugRoom extends Room { private final List checkedBlocks = Collections.synchronizedList(new ArrayList<>()); - public DebugRoom(@NotNull Type type, @NotNull Vector2ic... physicalPositions) { + public DebugRoom(Type type, Vector2ic... physicalPositions) { super(type, physicalPositions); } + public static DebugRoom ofSinglePossibleRoom(Type type, Vector2ic physicalPositions, String roomName, int[] roomData, Direction direction) { + return ofSinglePossibleRoom(type, new Vector2ic[]{physicalPositions}, roomName, roomData, direction); + } + + public static DebugRoom ofSinglePossibleRoom(Type type, Vector2ic[] physicalPositions, String roomName, int[] roomData, Direction direction) { + DebugRoom room = new DebugRoom(type, physicalPositions); + IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::x).toArray())); + IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::y).toArray())); + room.roomsData = Map.of(roomName, roomData); + room.possibleRooms = List.of(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), List.of(roomName))); + return room; + } + @Override protected boolean checkBlock(ClientWorld world, BlockPos pos) { byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); @@ -37,7 +49,7 @@ public class DebugRoom extends Room { } @Override - protected void render(WorldRenderContext context) { + public void render(WorldRenderContext context) { super.render(context); synchronized (checkedBlocks) { for (Waypoint checkedBlock : checkedBlocks) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java index 52915b98..722ecd85 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -18,9 +18,6 @@ import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.ints.IntRBTreeSet; -import it.unimi.dsi.fastutil.ints.IntSortedSet; -import it.unimi.dsi.fastutil.ints.IntSortedSets; import it.unimi.dsi.fastutil.objects.Object2ByteMap; import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIntPair; @@ -43,6 +40,7 @@ import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.mob.AmbientEntity; import net.minecraft.entity.passive.BatEntity; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.FilledMapItem; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; @@ -59,7 +57,6 @@ 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.apache.commons.lang3.tuple.MutableTriple; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -200,6 +197,10 @@ public class DungeonManager { return customWaypoints.remove(room, pos); } + public static Room getCurrentRoom() { + return currentRoom; + } + /** * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}. * Use {@link #isRoomsLoaded()} to check for completion of loading. @@ -232,12 +233,13 @@ public class DungeonManager { if (Debug.debugEnabled()) { ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets") .then(literal("matchAgainst").then(matchAgainstCommand())) - .then(literal("clearSubRooms").executes(context -> { + .then(literal("clearSubProcesses").executes(context -> { if (currentRoom != null) { - currentRoom.subRooms.clear(); - context.getSource().sendFeedback(Constants.PREFIX.get().append("§rCleared sub rooms in the current room")); + currentRoom.tickables.clear(); + currentRoom.renderables.clear(); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rCleared sub processes in the current room.")); } else { - context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null")); + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); } return Command.SINGLE_SUCCESS; })) @@ -427,57 +429,58 @@ public class DungeonManager { private static RequiredArgumentBuilder matchAgainstCommand() { return argument("room", StringArgumentType.string()).suggests((context, builder) -> CommandSource.suggestMatching(ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).map(Map::keySet).flatMap(Collection::stream), builder)).then(argument("direction", Room.Direction.DirectionArgumentType.direction()).executes(context -> { if (physicalEntrancePos == null || mapEntrancePos == null || mapRoomSize == 0) { - context.getSource().sendError(Constants.PREFIX.get().append("§cYou are not in a dungeon")); + context.getSource().sendError(Constants.PREFIX.get().append("§cYou are not in a dungeon.")); return Command.SINGLE_SUCCESS; } MinecraftClient client = MinecraftClient.getInstance(); if (client.player == null || client.world == null) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get player or world")); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get player or world.")); return Command.SINGLE_SUCCESS; } ItemStack stack = client.player.getInventory().main.get(8); if (!stack.isOf(Items.FILLED_MAP)) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map")); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map.")); return Command.SINGLE_SUCCESS; } MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); if (map == null) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map state")); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map state.")); return Command.SINGLE_SUCCESS; } String roomName = StringArgumentType.getString(context, "room"); Room.Direction direction = Room.Direction.DirectionArgumentType.getDirection(context, "direction"); - Room room = null; - int[] roomData; - if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.PUZZLE.shape).get(roomName)) != null) { - room = new DebugRoom(Room.Type.PUZZLE, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())); - } else if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.TRAP.shape).get(roomName)) != null) { - room = new DebugRoom(Room.Type.TRAP, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())); - } else if ((roomData = ROOMS_DATA.get("catacombs").values().stream().map(Map::entrySet).flatMap(Collection::stream).filter(entry -> entry.getKey().equals(roomName)).findAny().map(Map.Entry::getValue).orElse(null)) != null) { - room = new DebugRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapRoomPos(map, mapEntrancePos, mapRoomSize), mapRoomSize, Room.Type.ROOM.color))); - } - + Room room = newDebugRoom(roomName, direction, client.player, map); if (room == null) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to find room with name " + roomName)); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to find room with name " + roomName + ".")); return Command.SINGLE_SUCCESS; } - IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::x).toArray())); - IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::y).toArray())); - room.roomsData = Map.of(roomName, roomData); - room.possibleRooms = List.of(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), List.of(roomName))); if (currentRoom != null) { - currentRoom.subRooms.add(room); - context.getSource().sendFeedback(Constants.PREFIX.get().append("§rMatching room " + roomName + " with direction " + direction + " against current room")); + currentRoom.addSubProcess(room); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rMatching room " + roomName + " with direction " + direction + " against current room.")); } else { - context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null")); + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); } return Command.SINGLE_SUCCESS; })); } + @Nullable + private static Room newDebugRoom(String roomName, Room.Direction direction, PlayerEntity player, MapState map) { + Room room = null; + int[] roomData; + if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.PUZZLE.shape).get(roomName)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.PUZZLE, DungeonMapUtils.getPhysicalRoomPos(player.getPos()), roomName, roomData, direction); + } else if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.TRAP.shape).get(roomName)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.TRAP, DungeonMapUtils.getPhysicalRoomPos(player.getPos()), roomName, roomData, direction); + } else if ((roomData = ROOMS_DATA.get("catacombs").values().stream().map(Map::entrySet).flatMap(Collection::stream).filter(entry -> entry.getKey().equals(roomName)).findAny().map(Map.Entry::getValue).orElse(null)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapRoomPos(map, mapEntrancePos, mapRoomSize), mapRoomSize, Room.Type.ROOM.color)), roomName, roomData, direction); + } + return room; + } + /** * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod. *

      @@ -499,7 +502,7 @@ public class DungeonManager { *
    • Create a new room.
    • *
    *
  • Sets {@link #currentRoom} to the current room, either created from the previous step or from {@link #rooms}.
  • - *
  • Calls {@link Room#update()} on {@link #currentRoom}.
  • + *
  • Calls {@link Room#tick()} on {@link #currentRoom}.
  • *
*/ @SuppressWarnings("JavadocReference") @@ -553,7 +556,7 @@ public class DungeonManager { if (room != null && currentRoom != room) { currentRoom = room; } - currentRoom.update(); + currentRoom.tick(); } /** 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 40488717..d59bf7bf 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 @@ -11,7 +11,9 @@ import com.mojang.serialization.Codec; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.events.DungeonEvents; import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.render.Renderable; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.ints.IntRBTreeSet; import it.unimi.dsi.fastutil.ints.IntSortedSet; @@ -49,7 +51,7 @@ import java.util.concurrent.CompletableFuture; import java.util.regex.Matcher; import java.util.regex.Pattern; -public class Room { +public class Room implements Tickable, Renderable { 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); @@ -80,7 +82,7 @@ public class Room { /** * The task that is used to check blocks. This is used to ensure only one such task can run at a time. */ - private CompletableFuture findRoom; + protected CompletableFuture findRoom; private int doubleCheckBlocks; /** * Represents the matching state of the room with the following possible values: @@ -95,7 +97,8 @@ public class Room { private Direction direction; private Vector2ic physicalCornerPos; - protected List subRooms = new ArrayList<>(); + protected List tickables = new ArrayList<>(); + protected List renderables = new ArrayList<>(); @Nullable private BlockPos doorPos; @Nullable @@ -266,6 +269,11 @@ public class Room { secretWaypoints.remove(secretIndex, actualPos); } + public void addSubProcess(T process) { + tickables.add(process); + renderables.add(process); + } + /** * Updates the room. *

@@ -285,15 +293,16 @@ public class Room { * */ @SuppressWarnings("JavadocReference") - protected void update() { + @Override + public void tick() { MinecraftClient client = MinecraftClient.getInstance(); ClientWorld world = client.world; if (world == null) { return; } - for (Room subRoom : subRooms) { - subRoom.update(); + for (Tickable tickable : tickables) { + tickable.tick(); } // Wither and blood door @@ -506,9 +515,10 @@ public class Room { /** * 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 (Room subRoom : subRooms) { - subRoom.render(context); + @Override + public void render(WorldRenderContext context) { + for (Renderable renderable : renderables) { + renderable.render(context); } if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && isMatched()) { diff --git a/src/main/java/de/hysky/skyblocker/utils/Tickable.java b/src/main/java/de/hysky/skyblocker/utils/Tickable.java new file mode 100644 index 00000000..9b7b2e3f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Tickable.java @@ -0,0 +1,5 @@ +package de.hysky.skyblocker.utils; + +public interface Tickable { + void tick(); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java index 2c75ef0a..42f890b7 100644 --- a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java @@ -5,8 +5,8 @@ import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.skyblock.barn.HungryHiker; import de.hysky.skyblocker.skyblock.barn.TreasureHunter; import de.hysky.skyblocker.skyblock.dungeon.Reparty; -import de.hysky.skyblocker.skyblock.dungeon.ThreeWeirdos; -import de.hysky.skyblocker.skyblock.dungeon.Trivia; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.ThreeWeirdos; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.Trivia; import de.hysky.skyblocker.skyblock.dwarven.Fetchur; import de.hysky.skyblocker.skyblock.dwarven.Puzzler; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; diff --git a/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java b/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java new file mode 100644 index 00000000..b7743153 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java @@ -0,0 +1,7 @@ +package de.hysky.skyblocker.utils.render; + +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; + +public interface Renderable { + void render(WorldRenderContext context); +} diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java deleted file mode 100644 index 3772fd75..00000000 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; -import org.junit.jupiter.api.Test; - -class ThreeWeirdosTest extends ChatPatternListenerTest { - public ThreeWeirdosTest() { - super(new ThreeWeirdos()); - } - - @Test - void test1() { - assertGroup("§e[NPC] §cBaxter§f: My chest doesn't have the reward. We are all telling the truth.", 1, "Baxter"); - } - @Test - void test2() { - assertGroup("§e[NPC] §cHope§f: The reward isn't in any of our chests.", 1, "Hope"); - } -} \ No newline at end of file diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java deleted file mode 100644 index 1df5a8e1..00000000 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; -import org.junit.jupiter.api.Test; - -class TriviaTest extends ChatPatternListenerTest { - public TriviaTest() { - super(new Trivia()); - } - - @Test - void anyQuestion1() { - assertGroup(" What is the first question?", 1, "What is the first question?"); - } - - @Test - void anyQestion2() { - assertGroup(" How many questions are there?", 1, "How many questions are there?"); - } - - @Test - void answer1() { - assertGroup(" §6 ⓐ §aAnswer 1", 3, "Answer 1"); - } - @Test - void answer2() { - assertGroup(" §6 ⓑ §aAnswer 2", 3, "Answer 2"); - } - @Test - void answer3() { - assertGroup(" §6 ⓒ §aAnswer 3", 3, "Answer 3"); - } -} \ No newline at end of file diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java new file mode 100644 index 00000000..22683698 --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; +import org.junit.jupiter.api.Test; + +class ThreeWeirdosTest extends ChatPatternListenerTest { + public ThreeWeirdosTest() { + super(new ThreeWeirdos()); + } + + @Test + void test1() { + assertGroup("§e[NPC] §cBaxter§f: My chest doesn't have the reward. We are all telling the truth.", 1, "Baxter"); + } + @Test + void test2() { + assertGroup("§e[NPC] §cHope§f: The reward isn't in any of our chests.", 1, "Hope"); + } +} \ No newline at end of file diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java new file mode 100644 index 00000000..55a59a68 --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java @@ -0,0 +1,33 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; +import org.junit.jupiter.api.Test; + +class TriviaTest extends ChatPatternListenerTest { + public TriviaTest() { + super(new Trivia()); + } + + @Test + void anyQuestion1() { + assertGroup(" What is the first question?", 1, "What is the first question?"); + } + + @Test + void anyQestion2() { + assertGroup(" How many questions are there?", 1, "How many questions are there?"); + } + + @Test + void answer1() { + assertGroup(" §6 ⓐ §aAnswer 1", 3, "Answer 1"); + } + @Test + void answer2() { + assertGroup(" §6 ⓑ §aAnswer 2", 3, "Answer 2"); + } + @Test + void answer3() { + assertGroup(" §6 ⓒ §aAnswer 3", 3, "Answer 3"); + } +} \ No newline at end of file -- cgit From 229a3c50e8febdd0d2994ab9d462604f9f523056 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:34:45 +0800 Subject: Fix fairy room door highlight --- .../skyblocker/skyblock/dungeon/secrets/DungeonManager.java | 6 +++++- .../de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java | 10 +++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) (limited to 'src/main/java/de/hysky/skyblocker') diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java index 722ecd85..70a0fd8c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -215,7 +215,7 @@ public class DungeonManager { return null; }); ClientLifecycleEvents.CLIENT_STOPPING.register(DungeonManager::saveCustomWaypoints); - Scheduler.INSTANCE.scheduleCyclic(DungeonManager::update, 10); + Scheduler.INSTANCE.scheduleCyclic(DungeonManager::update, 5); WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonManager::render); ClientReceiveMessageEvents.GAME.register(DungeonManager::onChatMessage); ClientReceiveMessageEvents.GAME_CANCELED.register(DungeonManager::onChatMessage); @@ -554,6 +554,10 @@ public class DungeonManager { } } if (room != null && currentRoom != room) { + if (currentRoom != null && room.getType() == Room.Type.FAIRY) { + currentRoom.nextRoom = room; + room.keyFound = currentRoom.keyFound; + } currentRoom = room; } currentRoom.tick(); 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 d59bf7bf..4857e8fe 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 @@ -99,11 +99,16 @@ public class Room implements Tickable, Renderable { protected List tickables = new ArrayList<>(); protected List renderables = new ArrayList<>(); + /** + * Stores the next room in the dungeon. Currently only used if the next room is the fairy room. + */ + @Nullable + protected Room nextRoom; @Nullable private BlockPos doorPos; @Nullable private Box doorBox; - private boolean keyFound; + protected boolean keyFound; public Room(@NotNull Type type, @NotNull Vector2ic... physicalPositions) { this.type = type; @@ -633,6 +638,9 @@ public class Room implements Tickable, Renderable { } protected void keyFound() { + if (nextRoom != null && nextRoom.type == Type.FAIRY) { + nextRoom.keyFound = true; + } keyFound = true; } -- cgit