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') 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') 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') 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') 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.getPhysicalRoomP