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') 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 +++++--- 5 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/events/DungeonEvents.java (limited to 'src/main/java') 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); -- 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') 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 +- 8 files changed, 708 insertions(+), 707 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') 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.m