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 - 1 file changed, 1 deletion(-) (limited to 'src/main/java/de/hysky/skyblocker/SkyblockerMod.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); -- cgit From c8136497ef6198e6b8a426fc23ccadeefe27ebdb Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Tue, 19 Dec 2023 16:39:45 +0800 Subject: Rename Dungeon Manager --- .../java/de/hysky/skyblocker/SkyblockerMod.java | 4 +- .../de/hysky/skyblocker/events/DungeonEvents.java | 1 + .../de/hysky/skyblocker/mixin/BatEntityMixin.java | 4 +- .../mixin/ClientPlayNetworkHandlerMixin.java | 4 +- .../skyblock/dungeon/secrets/DungeonManager.java | 681 +++++++++++++++++++++ .../skyblock/dungeon/secrets/DungeonMapUtils.java | 2 +- .../skyblock/dungeon/secrets/DungeonSecrets.java | 681 --------------------- .../skyblocker/skyblock/dungeon/secrets/Room.java | 38 +- .../skyblock/dungeon/secrets/DungeonRoomsDFU.java | 4 +- 9 files changed, 710 insertions(+), 709 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonSecrets.java (limited to 'src/main/java/de/hysky/skyblocker/SkyblockerMod.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.minecraft.util.hit.HitResult; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Vec3d; -import net.minecraft.util.math.Vec3i; -import net.minecraft.world.World; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.joml.Vector2ic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.stream.Stream; -import java.util.zip.InflaterInputStream; - -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; - -public class DungeonSecrets { - protected static final Logger LOGGER = LoggerFactory.getLogger(DungeonSecrets.class); - private static final String DUNGEONS_PATH = "dungeons"; - private static final Path CUSTOM_WAYPOINTS_DIR = SkyblockerMod.CONFIG_DIR.resolve("custom_secret_waypoints.json"); - private static final Pattern KEY_FOUND = Pattern.compile("^(?:\\[.+] )?(?\\w+) has obtained (?Wither|Blood) Key!$"); - /** - * Maps the block identifier string to a custom numeric block id used in dungeon rooms data. - * - * @implNote Not using {@link net.minecraft.registry.Registry#getId(Object) Registry#getId(Block)} and {@link net.minecraft.block.Blocks Blocks} since this is also used by {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. - */ - @SuppressWarnings("JavadocReference") - protected static final Object2ByteMap NUMERIC_ID = new Object2ByteOpenHashMap<>(Map.ofEntries( - Map.entry("minecraft:stone", (byte) 1), - Map.entry("minecraft:diorite", (byte) 2), - Map.entry("minecraft:polished_diorite", (byte) 3), - Map.entry("minecraft:andesite", (byte) 4), - Map.entry("minecraft:polished_andesite", (byte) 5), - Map.entry("minecraft:grass_block", (byte) 6), - Map.entry("minecraft:dirt", (byte) 7), - Map.entry("minecraft:coarse_dirt", (byte) 8), - Map.entry("minecraft:cobblestone", (byte) 9), - Map.entry("minecraft:bedrock", (byte) 10), - Map.entry("minecraft:oak_leaves", (byte) 11), - Map.entry("minecraft:gray_wool", (byte) 12), - Map.entry("minecraft:double_stone_slab", (byte) 13), - Map.entry("minecraft:mossy_cobblestone", (byte) 14), - Map.entry("minecraft:clay", (byte) 15), - Map.entry("minecraft:stone_bricks", (byte) 16), - Map.entry("minecraft:mossy_stone_bricks", (byte) 17), - Map.entry("minecraft:chiseled_stone_bricks", (byte) 18), - Map.entry("minecraft:gray_terracotta", (byte) 19), - Map.entry("minecraft:cyan_terracotta", (byte) 20), - Map.entry("minecraft:black_terracotta", (byte) 21) - )); - /** - * Block data for dungeon rooms. See {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU} for format details and how it's generated. - * All access to this map must check {@link #isRoomsLoaded()} to prevent concurrent modification. - */ - @SuppressWarnings("JavadocReference") - protected static final HashMap>> ROOMS_DATA = new HashMap<>(); - @NotNull - private static final Map rooms = new HashMap<>(); - private static final Map roomsJson = new HashMap<>(); - private static final Map waypointsJson = new HashMap<>(); - /** - * The map of dungeon room names to custom waypoints relative to the room. - */ - private static final Table customWaypoints = HashBasedTable.create(); - @Nullable - private static CompletableFuture roomsLoaded; - /** - * The map position of the top left corner of the entrance room. - */ - @Nullable - private static Vector2ic mapEntrancePos; - /** - * The size of a room on the map. - */ - private static int mapRoomSize; - /** - * The physical position of the northwest corner of the entrance room. - */ - @Nullable - private static Vector2ic physicalEntrancePos; - private static Room currentRoom; - - public static boolean isRoomsLoaded() { - return roomsLoaded != null && roomsLoaded.isDone(); - } - - public static Stream getRoomsStream() { - return rooms.values().stream(); - } - - @SuppressWarnings("unused") - public static JsonObject getRoomMetadata(String room) { - return roomsJson.get(room).getAsJsonObject(); - } - - public static JsonArray getRoomWaypoints(String room) { - return waypointsJson.get(room).getAsJsonArray(); - } - - /** - * @see #customWaypoints - */ - public static Map getCustomWaypoints(String room) { - return customWaypoints.row(room); - } - - /** - * @see #customWaypoints - */ - @SuppressWarnings("UnusedReturnValue") - public static SecretWaypoint addCustomWaypoint(String room, SecretWaypoint waypoint) { - return customWaypoints.put(room, waypoint.pos, waypoint); - } - - /** - * @see #customWaypoints - */ - public static void addCustomWaypoints(String room, Collection waypoints) { - for (SecretWaypoint waypoint : waypoints) { - addCustomWaypoint(room, waypoint); - } - } - - /** - * @see #customWaypoints - */ - @Nullable - public static SecretWaypoint removeCustomWaypoint(String room, BlockPos pos) { - return customWaypoints.remove(room, pos); - } - - /** - * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}. - * Use {@link #isRoomsLoaded()} to check for completion of loading. - */ - public static void init() { - if (!SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching) { - return; - } - // Execute with MinecraftClient as executor since we need to wait for MinecraftClient#resourceManager to be set - CompletableFuture.runAsync(DungeonSecrets::load, MinecraftClient.getInstance()).exceptionally(e -> { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e); - return null; - }); - ClientLifecycleEvents.CLIENT_STOPPING.register(DungeonSecrets::saveCustomWaypoints); - Scheduler.INSTANCE.scheduleCyclic(DungeonSecrets::update, 10); - WorldRenderEvents.AFTER_TRANSLUCENT.register(DungeonSecrets::render); - ClientReceiveMessageEvents.GAME.register(DungeonSecrets::onChatMessage); - ClientReceiveMessageEvents.GAME_CANCELED.register(DungeonSecrets::onChatMessage); - UseBlockCallback.EVENT.register((player, world, hand, hitResult) -> onUseBlock(world, hitResult)); - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets") - .then(literal("markAsFound").then(markSecretsCommand(true))) - .then(literal("markAsMissing").then(markSecretsCommand(false))) - .then(literal("getRelativePos").executes(DungeonSecrets::getRelativePos)) - .then(literal("getRelativeTargetPos").executes(DungeonSecrets::getRelativeTargetPos)) - .then(literal("addWaypoint").then(addCustomWaypointCommand(false))) - .then(literal("addWaypointRelatively").then(addCustomWaypointCommand(true))) - .then(literal("removeWaypoint").then(removeCustomWaypointCommand(false))) - .then(literal("removeWaypointRelatively").then(removeCustomWaypointCommand(true))) - )))); - ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); - } - - private static void load() { - long startTime = System.currentTimeMillis(); - List> dungeonFutures = new ArrayList<>(); - for (Map.Entry resourceEntry : MinecraftClient.getInstance().getResourceManager().findResources(DUNGEONS_PATH, id -> id.getPath().endsWith(".skeleton")).entrySet()) { - String[] path = resourceEntry.getKey().getPath().split("/"); - if (path.length != 4) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets, invalid resource identifier {}", resourceEntry.getKey()); - break; - } - String dungeon = path[1]; - String roomShape = path[2]; - String room = path[3].substring(0, path[3].length() - ".skeleton".length()); - ROOMS_DATA.computeIfAbsent(dungeon, dungeonKey -> new HashMap<>()); - ROOMS_DATA.get(dungeon).computeIfAbsent(roomShape, roomShapeKey -> new HashMap<>()); - dungeonFutures.add(CompletableFuture.supplyAsync(() -> readRoom(resourceEntry.getValue())).thenAcceptAsync(rooms -> { - Map roomsMap = ROOMS_DATA.get(dungeon).get(roomShape); - synchronized (roomsMap) { - roomsMap.put(room, rooms); - } - LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room); - }).exceptionally(e -> { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets dungeon {} room shape {} room {}", dungeon, roomShape, room, e); - return null; - })); - } - dungeonFutures.add(CompletableFuture.runAsync(() -> { - try (BufferedReader roomsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/dungeonrooms.json")); BufferedReader waypointsReader = MinecraftClient.getInstance().getResourceManager().openAsReader(new Identifier(SkyblockerMod.NAMESPACE, "dungeons/secretlocations.json"))) { - loadJson(roomsReader, roomsJson); - loadJson(waypointsReader, waypointsJson); - LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded dungeon secret waypoints json"); - } catch (Exception e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secret waypoints json", e); - } - })); - dungeonFutures.add(CompletableFuture.runAsync(() -> { - try (BufferedReader customWaypointsReader = Files.newBufferedReader(CUSTOM_WAYPOINTS_DIR)) { - SkyblockerMod.GSON.fromJson(customWaypointsReader, JsonObject.class).asMap().forEach((room, waypointsJson) -> - addCustomWaypoints(room, SecretWaypoint.LIST_CODEC.parse(JsonOps.INSTANCE, waypointsJson).resultOrPartial(LOGGER::error).orElseGet(ArrayList::new)) - ); - LOGGER.debug("[Skyblocker Dungeon Secrets] Loaded custom dungeon secret waypoints"); - } catch (Exception e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load custom dungeon secret waypoints", e); - } - })); - roomsLoaded = CompletableFuture.allOf(dungeonFutures.toArray(CompletableFuture[]::new)).thenRun(() -> LOGGER.info("[Skyblocker Dungeon Secrets] Loaded dungeon secrets for {} dungeon(s), {} room shapes, {} rooms, and {} custom secret waypoints total in {} ms", ROOMS_DATA.size(), ROOMS_DATA.values().stream().mapToInt(Map::size).sum(), ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).mapToInt(Map::size).sum(), customWaypoints.size(), System.currentTimeMillis() - startTime)).exceptionally(e -> { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to load dungeon secrets", e); - return null; - }); - LOGGER.info("[Skyblocker Dungeon Secrets] Started loading dungeon secrets in (blocked main thread for) {} ms", System.currentTimeMillis() - startTime); - } - - private static void saveCustomWaypoints(MinecraftClient client) { - try (BufferedWriter writer = Files.newBufferedWriter(CUSTOM_WAYPOINTS_DIR)) { - JsonObject customWaypointsJson = new JsonObject(); - customWaypoints.rowMap().forEach((room, waypoints) -> - customWaypointsJson.add(room, SecretWaypoint.LIST_CODEC.encodeStart(JsonOps.INSTANCE, new ArrayList<>(waypoints.values())).resultOrPartial(LOGGER::error).orElseGet(JsonArray::new)) - ); - SkyblockerMod.GSON.toJson(customWaypointsJson, writer); - LOGGER.info("[Skyblocker Dungeon Secrets] Saved custom dungeon secret waypoints"); - } catch (Exception e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to save custom dungeon secret waypoints", e); - } - } - - private static int[] readRoom(Resource resource) throws RuntimeException { - try (ObjectInputStream in = new ObjectInputStream(new InflaterInputStream(resource.getInputStream()))) { - return (int[]) in.readObject(); - } catch (IOException | ClassNotFoundException e) { - throw new RuntimeException(e); - } - } - - /** - * Loads the json from the given {@link BufferedReader} into the given {@link Map}. - * - * @param reader the reader to read the json from - * @param map the map to load into - */ - private static void loadJson(BufferedReader reader, Map map) { - SkyblockerMod.GSON.fromJson(reader, JsonObject.class).asMap().forEach((room, jsonElement) -> map.put(room.toLowerCase().replaceAll(" ", "-"), jsonElement)); - } - - private static ArgumentBuilder> markSecretsCommand(boolean found) { - return argument("secretIndex", IntegerArgumentType.integer()).executes(context -> { - int secretIndex = IntegerArgumentType.getInteger(context, "secretIndex"); - if (markSecrets(secretIndex, found)) { - context.getSource().sendFeedback(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFound" : "skyblocker.dungeons.secrets.markSecretMissing", secretIndex))); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable(found ? "skyblocker.dungeons.secrets.markSecretFoundUnable" : "skyblocker.dungeons.secrets.markSecretMissingUnable", secretIndex))); - } - return Command.SINGLE_SUCCESS; - }); - } - - private static int getRelativePos(CommandContext context) { - return getRelativePos(context.getSource(), context.getSource().getPlayer().getBlockPos()); - } - - private static int getRelativeTargetPos(CommandContext context) { - if (MinecraftClient.getInstance().crosshairTarget instanceof BlockHitResult blockHitResult && blockHitResult.getType() == HitResult.Type.BLOCK) { - return getRelativePos(context.getSource(), blockHitResult.getBlockPos()); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.noTarget"))); - } - return Command.SINGLE_SUCCESS; - } - - private static int getRelativePos(FabricClientCommandSource source, BlockPos pos) { - Room room = getRoomAtPhysical(pos); - if (isRoomMatched(room)) { - BlockPos relativePos = currentRoom.actualToRelative(pos); - source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.posMessage", currentRoom.getName(), relativePos.getX(), relativePos.getY(), relativePos.getZ()))); - } else { - source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static ArgumentBuilder> addCustomWaypointCommand(boolean relative) { - return argument("pos", BlockPosArgumentType.blockPos()) - .then(argument("secretIndex", IntegerArgumentType.integer()) - .then(argument("category", SecretWaypoint.Category.CategoryArgumentType.category()) - .then(argument("name", TextArgumentType.text()).executes(context -> { - // TODO Less hacky way with custom ClientBlockPosArgumentType - BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null)); - return relative ? addCustomWaypointRelative(context, pos) : addCustomWaypoint(context, pos); - })) - ) - ); - } - - private static int addCustomWaypoint(CommandContext context, BlockPos pos) { - Room room = getRoomAtPhysical(pos); - if (isRoomMatched(room)) { - room.addCustomWaypoint(context, room.actualToRelative(pos)); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static int addCustomWaypointRelative(CommandContext context, BlockPos pos) { - if (isCurrentRoomMatched()) { - currentRoom.addCustomWaypoint(context, pos); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static ArgumentBuilder> removeCustomWaypointCommand(boolean relative) { - return argument("pos", BlockPosArgumentType.blockPos()) - .executes(context -> { - // TODO Less hacky way with custom ClientBlockPosArgumentType - BlockPos pos = context.getArgument("pos", PosArgument.class).toAbsoluteBlockPos(new ServerCommandSource(null, context.getSource().getPosition(), context.getSource().getRotation(), null, 0, null, null, null, null)); - return relative ? removeCustomWaypointRelative(context, pos) : removeCustomWaypoint(context, pos); - }); - } - - private static int removeCustomWaypoint(CommandContext context, BlockPos pos) { - Room room = getRoomAtPhysical(pos); - if (isRoomMatched(room)) { - room.removeCustomWaypoint(context, room.actualToRelative(pos)); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - private static int removeCustomWaypointRelative(CommandContext context, BlockPos pos) { - if (isCurrentRoomMatched()) { - currentRoom.removeCustomWaypoint(context, pos); - } else { - context.getSource().sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.dungeons.secrets.notMatched"))); - } - return Command.SINGLE_SUCCESS; - } - - /** - * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod. - *

- * When entering a new dungeon, this method: - *
    - *
  • Gets the physical northwest corner position of the entrance room and saves it in {@link #physicalEntrancePos}.
  • - *
  • Do nothing until the dungeon map exists.
  • - *
  • Gets the upper left corner of entrance room on the map and saves it in {@link #mapEntrancePos}.
  • - *
  • Gets the size of a room on the map in pixels and saves it in {@link #mapRoomSize}.
  • - *
  • Creates a new {@link Room} with {@link Room.Type} {@link Room.Type.ENTRANCE ENTRANCE} and sets {@link #currentRoom}.
  • - *
- * When processing an existing dungeon, this method: - *
    - *
  • Calculates the physical northwest corner and upper left corner on the map of the room the player is currently in.
  • - *
  • Gets the room type based on the map color.
  • - *
  • If the room has not been created (when the physical northwest corner is not in {@link #rooms}):
  • - *
      - *
    • If the room type is {@link Room.Type.ROOM}, gets the northwest corner of all connected room segments with {@link DungeonMapUtils#getRoomSegments(MapState, Vector2ic, int, byte)}. (For example, a 1x2 room has two room segments.)
    • - *
    • Create a new room.
    • - *
    - *
  • Sets {@link #currentRoom} to the current room, either created from the previous step or from {@link #rooms}.
  • - *
  • Calls {@link Room#update()} on {@link #currentRoom}.
  • - *
- */ - @SuppressWarnings("JavadocReference") - private static void update() { - if (!Utils.isInDungeons()) { - if (mapEntrancePos != null) { - reset(); - } - return; - } - MinecraftClient client = MinecraftClient.getInstance(); - ClientPlayerEntity player = client.player; - if (player == null || client.world == null) { - return; - } - if (physicalEntrancePos == null) { - Vec3d playerPos = player.getPos(); - physicalEntrancePos = DungeonMapUtils.getPhysicalRoomPos(playerPos); - currentRoom = newRoom(Room.Type.ENTRANCE, physicalEntrancePos); - } - ItemStack stack = player.getInventory().main.get(8); - if (!stack.isOf(Items.FILLED_MAP)) { - return; - } - MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); - if (map == null) { - return; - } - if (mapEntrancePos == null || mapRoomSize == 0) { - ObjectIntPair mapEntrancePosAndSize = DungeonMapUtils.getMapEntrancePosAndRoomSize(map); - if (mapEntrancePosAndSize == null) { - return; - } - mapEntrancePos = mapEntrancePosAndSize.left(); - mapRoomSize = mapEntrancePosAndSize.rightInt(); - LOGGER.info("[Skyblocker Dungeon Secrets] Started dungeon with map room size {}, map entrance pos {}, player pos {}, and physical entrance pos {}", mapRoomSize, mapEntrancePos, client.player.getPos(), physicalEntrancePos); - } - - Vector2ic physicalPos = DungeonMapUtils.getPhysicalRoomPos(client.player.getPos()); - Vector2ic mapPos = DungeonMapUtils.getMapPosFromPhysical(physicalEntrancePos, mapEntrancePos, mapRoomSize, physicalPos); - Room room = rooms.get(physicalPos); - if (room == null) { - Room.Type type = DungeonMapUtils.getRoomType(map, mapPos); - if (type == null || type == Room.Type.UNKNOWN) { - return; - } - switch (type) { - case ENTRANCE, PUZZLE, TRAP, MINIBOSS, FAIRY, BLOOD -> room = newRoom(type, physicalPos); - case ROOM -> room = newRoom(type, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, mapPos, mapRoomSize, type.color))); - } - } - if (room != null && currentRoom != room) { - currentRoom = room; - } - currentRoom.update(); - } - - /** - * Creates a new room with the given type and physical positions, - * adds the room to {@link #rooms}, and sets {@link #currentRoom} to the new room. - * - * @param type the type of room to create - * @param physicalPositions the physical positions of the room - */ - @Nullable - private static Room newRoom(Room.Type type, Vector2ic... physicalPositions) { - try { - Room newRoom = new Room(type, physicalPositions); - for (Vector2ic physicalPos : physicalPositions) { - rooms.put(physicalPos, newRoom); - } - return newRoom; - } catch (IllegalArgumentException e) { - LOGGER.error("[Skyblocker Dungeon Secrets] Failed to create room", e); - } - return null; - } - - /** - * Renders the secret waypoints in {@link #currentRoom} if {@link #shouldProcess()} and {@link #currentRoom} is not null. - */ - private static void render(WorldRenderContext context) { - if (shouldProcess() && currentRoom != null) { - currentRoom.render(context); - } - } - - /** - * Calls {@link Room#onChatMessage(String)} on {@link #currentRoom} if the message is an overlay message and {@link #isCurrentRoomMatched()} and processes key obtained messages. - *

Used to detect when all secrets in a room are found and detect when a wither or blood door is unlocked. - * To process key obtained messages, this method checks if door highlight is enabled and if the message matches a key obtained message. - * Then, it calls {@link Room#keyFound()} on {@link #currentRoom} if the client's player is the one who obtained the key. - * Otherwise, it calls {@link Room#keyFound()} on the room the player who obtained the key is in. - */ - private static void onChatMessage(Text text, boolean overlay) { - if (!shouldProcess()) { - return; - } - - String message = text.getString(); - - if (overlay && isCurrentRoomMatched()) { - currentRoom.onChatMessage(message); - } - - // Process key found messages for door highlight - if (SkyblockerConfigManager.get().locations.dungeons.doorHighlight.enableDoorHighlight) { - Matcher matcher = KEY_FOUND.matcher(message); - if (matcher.matches()) { - String name = matcher.group("name"); - MinecraftClient client = MinecraftClient.getInstance(); - if (client.player != null && client.player.getGameProfile().getName().equals(name)) { - if (currentRoom != null) { - currentRoom.keyFound(); - } else { - LOGGER.warn("[Skyblocker Dungeon Door] The current room at the current player {} does not exist", name); - } - } else if (client.world != null) { - Optional posOptional = client.world.getPlayers().stream().filter(player -> player.getGameProfile().getName().equals(name)).findAny().map(Entity::getPos); - if (posOptional.isPresent()) { - Room room = getRoomAtPhysical(posOptional.get()); - if (room != null) { - room.keyFound(); - } else { - LOGGER.warn("[Skyblocker Dungeon Door] Failed to find room at player {} with position {}", name, posOptional.get()); - } - } else { - LOGGER.warn("[Skyblocker Dungeon Door] Failed to find player {}", name); - } - } - } - } - - if (message.equals("[BOSS] Bonzo: Gratz for making it this far, but I'm basically unbeatable.") || message.equals("[BOSS] Scarf: This is where the journey ends for you, Adventurers.") - || message.equals("[BOSS] The Professor: I was burdened with terrible news recently...") || message.equals("[BOSS] Thorn: Welcome Adventurers! I am Thorn, the Spirit! And host of the Vegan Trials!") - || message.equals("[BOSS] Livid: Welcome, you've arrived right on time. I am Livid, the Master of Shadows.") || message.equals("[BOSS] Sadan: So you made it all the way here... Now you wish to defy me? Sadan?!") - || message.equals("[BOSS] Maxor: WELL! WELL! WELL! LOOK WHO'S HERE!")) reset(); - } - - /** - * Calls {@link Room#onUseBlock(World, BlockHitResult)} on {@link #currentRoom} if {@link #isCurrentRoomMatched()}. - * Used to detect finding {@link SecretWaypoint.Category.CHEST} and {@link SecretWaypoint.Category.WITHER} secrets. - * - * @return {@link ActionResult#PASS} - */ - @SuppressWarnings("JavadocReference") - private static ActionResult onUseBlock(World world, BlockHitResult hitResult) { - if (isCurrentRoomMatched()) { - currentRoom.onUseBlock(world, hitResult); - } - return ActionResult.PASS; - } - - /** - * Calls {@link Room#onItemPickup(ItemEntity, LivingEntity)} on the room the {@code collector} is in if that room {@link #isRoomMatched(Room)}. - * Used to detect finding {@link SecretWaypoint.Category.ITEM} secrets. - * If the collector is the player, {@link #currentRoom} is used as an optimization. - */ - @SuppressWarnings("JavadocReference") - public static void onItemPickup(ItemEntity itemEntity, LivingEntity collector, boolean isPlayer) { - if (isPlayer) { - if (isCurrentRoomMatched()) { - currentRoom.onItemPickup(itemEntity, collector); - } - } else { - Room room = getRoomAtPhysical(collector.getPos()); - if (isRoomMatched(room)) { - room.onItemPickup(itemEntity, collector); - } - } - } - - /** - * Calls {@link Room#onBatRemoved(BatEntity)} on the room the {@code bat} is in if that room {@link #isRoomMatched(Room)}. - * Used to detect finding {@link SecretWaypoint.Category.BAT} secrets. - */ - @SuppressWarnings("JavadocReference") - public static void onBatRemoved(AmbientEntity bat) { - Room room = getRoomAtPhysical(bat.getPos()); - if (isRoomMatched(room)) { - room.onBatRemoved(bat); - } - } - - public static boolean markSecrets(int secretIndex, boolean found) { - if (isCurrentRoomMatched()) { - return currentRoom.markSecrets(secretIndex, found); - } - return false; - } - - /** - * Gets the room at the given physical position. - * - * @param pos the physical position - * @return the room at the given physical position, or null if there is no room at the given physical position - * @see #rooms - * @see DungeonMapUtils#getPhysicalRoomPos(Vec3d) - */ - @Nullable - private static Room getRoomAtPhysical(Vec3d pos) { - return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos)); - } - - /** - * Gets the room at the given physical position. - * - * @param pos the physical position - * @return the room at the given physical position, or null if there is no room at the given physical position - * @see #rooms - * @see DungeonMapUtils#getPhysicalRoomPos(Vec3i) - */ - @Nullable - private static Room getRoomAtPhysical(Vec3i pos) { - return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos)); - } - - /** - * Calls {@link #isRoomMatched(Room)} on {@link #currentRoom}. - * - * @return {@code true} if {@link #currentRoom} is not null and {@link #isRoomMatched(Room)} - */ - private static boolean isCurrentRoomMatched() { - return isRoomMatched(currentRoom); - } - - /** - * Calls {@link #shouldProcess()} and {@link Room#isMatched()} on the given room. - * - * @param room the room to check - * @return {@code true} if {@link #shouldProcess()}, the given room is not null, and {@link Room#isMatched()} on the given room - */ - @Contract("null -> false") - private static boolean isRoomMatched(@Nullable Room room) { - return shouldProcess() && room != null && room.isMatched(); - } - - /** - * Checks if {@link de.hysky.skyblocker.config.SkyblockerConfig.SecretWaypoints#enableRoomMatching room matching} is enabled and the player is in a dungeon. - * - * @return whether room matching and dungeon secrets should be processed - */ - private static boolean shouldProcess() { - return SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableRoomMatching && Utils.isInDungeons(); - } - - /** - * Resets fields when leaving a dungeon or entering boss. - */ - private static void reset() { - mapEntrancePos = null; - mapRoomSize = 0; - physicalEntrancePos = null; - rooms.clear(); - currentRoom = null; - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java index 0d3c6a87..fffe1bd2 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/Room.java @@ -103,7 +103,7 @@ public class Room { IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::x).toArray())); IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(segments.stream().mapToInt(Vector2ic::y).toArray())); shape = getShape(segmentsX, segmentsY); - roomsData = DungeonSecrets.ROOMS_DATA.getOrDefault("catacombs", Collections.emptyMap()).getOrDefault(shape.shape.toLowerCase(), Collections.emptyMap()); + roomsData = DungeonManager.ROOMS_DATA.getOrDefault("catacombs", Collections.emptyMap()).getOrDefault(shape.shape.toLowerCase(), Collections.emptyMap()); possibleRooms = getPossibleRooms(segmentsX, segmentsY); } @@ -191,7 +191,7 @@ public class Room { } /** - * Adds a custom waypoint relative to this room to {@link DungeonSecrets#customWaypoints} and all existing instances of this room. + * Adds a custom waypoint relative to this room to {@link DungeonManager#customWaypoints} and all existing instances of this room. * * @param secretIndex the index of the secret waypoint * @param category the category of the secret waypoint @@ -201,8 +201,8 @@ public class Room { @SuppressWarnings("JavadocReference") private void addCustomWaypoint(int secretIndex, SecretWaypoint.Category category, Text waypointName, BlockPos pos) { SecretWaypoint waypoint = new SecretWaypoint(secretIndex, category, waypointName, pos); - DungeonSecrets.addCustomWaypoint(name, waypoint); - DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.addCustomWaypoint(waypoint)); + DungeonManager.addCustomWaypoint(name, waypoint); + DungeonManager.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.addCustomWaypoint(waypoint)); } /** @@ -228,7 +228,7 @@ public class Room { } /** - * Removes a custom waypoint relative to this room from {@link DungeonSecrets#customWaypoints} and all existing instances of this room. + * Removes a custom waypoint relative to this room from {@link DungeonManager#customWaypoints} and all existing instances of this room. * * @param pos the position of the secret waypoint relative to this room * @return the removed secret waypoint or {@code null} if there was no secret waypoint at the given position @@ -236,9 +236,9 @@ public class Room { @SuppressWarnings("JavadocReference") @Nullable private SecretWaypoint removeCustomWaypoint(BlockPos pos) { - SecretWaypoint waypoint = DungeonSecrets.removeCustomWaypoint(name, pos); + SecretWaypoint waypoint = DungeonManager.removeCustomWaypoint(name, pos); if (waypoint != null) { - DungeonSecrets.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.removeCustomWaypoint(waypoint.secretIndex, pos)); + DungeonManager.getRoomsStream().filter(r -> name.equals(r.getName())).forEach(r -> r.removeCustomWaypoint(waypoint.secretIndex, pos)); } return waypoint; } @@ -290,7 +290,7 @@ public class Room { // Room scanning and matching // Logical AND has higher precedence than logical OR - if (!type.needsScanning() || matchState != MatchState.MATCHING && matchState != MatchState.DOUBLE_CHECKING || !DungeonSecrets.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) { + if (!type.needsScanning() || matchState != MatchState.MATCHING && matchState != MatchState.DOUBLE_CHECKING || !DungeonManager.isRoomsLoaded() || findRoom != null && !findRoom.isDone()) { return; } ClientPlayerEntity player = client.player; @@ -304,7 +304,7 @@ public class Room { } } }).exceptionally(e -> { - DungeonSecrets.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown exception while matching room {}", this, e); + DungeonManager.LOGGER.error("[Skyblocker Dungeon Secrets] Encountered an unknown exception while matching room {}", this, e); return null; }); } @@ -323,7 +323,7 @@ public class Room { *

* This method: *
    - *
  • Checks if the block type is included in the dungeon rooms data. See {@link DungeonSecrets#NUMERIC_ID}.
  • + *
  • Checks if the block type is included in the dungeon rooms data. See {@link DungeonManager#NUMERIC_ID}.
  • *
  • For each possible direction:
  • *
      *
    • Rotate and convert the position to a relative position. See {@link DungeonMapUtils#actualToRelative(Direction, Vector2ic, BlockPos)}.
    • @@ -364,7 +364,7 @@ public class Room { * @return whether room matching should end. Either a match is found or there are no valid rooms left */ private boolean checkBlock(ClientWorld world, BlockPos pos) { - byte id = DungeonSecrets.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); + byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); if (id == 0) { return false; } @@ -383,7 +383,7 @@ public class Room { if (matchingRoomsSize == 0) { // If no rooms match, reset the fields and scan again after 50 ticks. matchState = MatchState.FAILED; - DungeonSecrets.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks); + DungeonManager.LOGGER.warn("[Skyblocker Dungeon Secrets] No dungeon room matched after checking {} block(s) including double checking {} block(s)", checkedBlocks.size(), doubleCheckBlocks); Scheduler.INSTANCE.schedule(() -> matchState = MatchState.MATCHING, 50); reset(); return true; @@ -394,20 +394,20 @@ public class Room { name = directionRoom.getRight().get(0); direction = directionRoom.getLeft(); physicalCornerPos = directionRoom.getMiddle(); - DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s), starting double checking", name, checkedBlocks.size()); + DungeonManager.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} matched after checking {} block(s), starting double checking", name, checkedBlocks.size()); roomMatched(); return false; } else if (matchState == MatchState.DOUBLE_CHECKING && ++doubleCheckBlocks >= 10) { // If double-checked, set state to matched and discard the no longer needed fields. matchState = MatchState.MATCHED; DungeonEvents.ROOM_MATCHED.invoker().onRoomMatched(this); - DungeonSecrets.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} confirmed after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks); + DungeonManager.LOGGER.info("[Skyblocker Dungeon Secrets] Room {} confirmed after checking {} block(s) including double checking {} block(s)", name, checkedBlocks.size(), doubleCheckBlocks); discard(); return true; } return false; } else { - DungeonSecrets.LOGGER.debug("[Skyblocker Dungeon Secrets] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size()); + DungeonManager.LOGGER.debug("[Skyblocker Dungeon Secrets] {} room(s) remaining after checking {} block(s)", matchingRoomsSize, checkedBlocks.size()); return false; } } @@ -424,7 +424,7 @@ public class Room { } /** - * Loads the secret waypoints for the room from {@link DungeonSecrets#waypointsJson} once it has been matched + * Loads the secret waypoints for the room from {@link DungeonManager#waypointsJson} once it has been matched * and sets {@link #matchState} to {@link MatchState#DOUBLE_CHECKING}. * * @param directionRooms the direction, position, and name of the room @@ -432,7 +432,7 @@ public class Room { @SuppressWarnings("JavadocReference") private void roomMatched() { secretWaypoints = HashBasedTable.create(); - for (JsonElement waypointElement : DungeonSecrets.getRoomWaypoints(name)) { + for (JsonElement waypointElement : DungeonManager.getRoomWaypoints(name)) { JsonObject waypoint = waypointElement.getAsJsonObject(); String secretName = waypoint.get("secretName").getAsString(); Matcher secretIndexMatcher = SECRET_INDEX.matcher(secretName); @@ -440,7 +440,7 @@ public class Room { BlockPos pos = DungeonMapUtils.relativeToActual(direction, physicalCornerPos, waypoint); secretWaypoints.put(secretIndex, pos, new SecretWaypoint(secretIndex, waypoint, secretName, pos)); } - DungeonSecrets.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint); + DungeonManager.getCustomWaypoints(name).values().forEach(this::addCustomWaypoint); matchState = MatchState.DOUBLE_CHECKING; } @@ -586,7 +586,7 @@ public class Room { */ private void onSecretFound(SecretWaypoint secretWaypoint, String msg, Object... args) { secretWaypoints.row(secretWaypoint.secretIndex).values().forEach(SecretWaypoint::setFound); - DungeonSecrets.LOGGER.info(msg, args); + DungeonManager.LOGGER.info(msg, args); } protected boolean markSecrets(int secretIndex, boolean found) { diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java index 3d2993cf..d45a2172 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonRoomsDFU.java @@ -21,7 +21,7 @@ import java.util.zip.InflaterInputStream; * Utility class to convert the old dungeon rooms data from Dungeon Rooms Mod to a new format. * The new format is similar to DRM's format, but uses ints instead of longs and a custom numeric block id to store the block states. * The first byte is the x position, the second byte is the y position, the third byte is the z position, and the fourth byte is the custom numeric block id. - * Use {@link DungeonSecrets#NUMERIC_ID} to get the custom numeric block id of a block. + * Use {@link DungeonManager#NUMERIC_ID} to get the custom numeric block id of a block. * Run this manually when updating dungeon rooms data with DRM's data in {@code src/test/resources/assets/skyblocker/dungeons/dungeonrooms}. */ public class DungeonRoomsDFU { @@ -131,7 +131,7 @@ public class DungeonRoomsDFU { if (newId == null) { newId = ItemIdFix.fromId(oldId / 100); } - return x << 24 | y << 16 | z << 8 | DungeonSecrets.NUMERIC_ID.getByte(newId); + return x << 24 | y << 16 | z << 8 | DungeonManager.NUMERIC_ID.getByte(newId); } private static CompletableFuture save() { -- cgit From 003834e36b145791dd603858c924926be70e1281 Mon Sep 17 00:00:00 2001 From: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:53:52 +0800 Subject: Refactor puzzle solvers --- .../java/de/hysky/skyblocker/SkyblockerMod.java | 8 +- .../skyblocker/skyblock/dungeon/CreeperBeams.java | 250 --------------------- .../skyblocker/skyblock/dungeon/DungeonBlaze.java | 164 -------------- .../skyblocker/skyblock/dungeon/ThreeWeirdos.java | 39 ---- .../skyblocker/skyblock/dungeon/TicTacToe.java | 151 ------------- .../hysky/skyblocker/skyblock/dungeon/Trivia.java | 109 --------- .../skyblock/dungeon/puzzle/CreeperBeams.java | 250 +++++++++++++++++++++ .../skyblock/dungeon/puzzle/DungeonBlaze.java | 158 +++++++++++++ .../skyblock/dungeon/puzzle/DungeonPuzzle.java | 58 +++++ .../skyblock/dungeon/puzzle/ThreeWeirdos.java | 39 ++++ .../skyblock/dungeon/puzzle/TicTacToe.java | 145 ++++++++++++ .../skyblocker/skyblock/dungeon/puzzle/Trivia.java | 109 +++++++++ .../skyblock/dungeon/secrets/DebugRoom.java | 26 ++- .../skyblock/dungeon/secrets/DungeonManager.java | 67 +++--- .../skyblocker/skyblock/dungeon/secrets/Room.java | 28 ++- .../java/de/hysky/skyblocker/utils/Tickable.java | 5 + .../skyblocker/utils/chat/ChatMessageListener.java | 4 +- .../hysky/skyblocker/utils/render/Renderable.java | 7 + .../skyblock/dungeon/ThreeWeirdosTest.java | 19 -- .../skyblocker/skyblock/dungeon/TriviaTest.java | 33 --- .../skyblock/dungeon/puzzle/ThreeWeirdosTest.java | 19 ++ .../skyblock/dungeon/puzzle/TriviaTest.java | 33 +++ 22 files changed, 905 insertions(+), 816 deletions(-) delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java delete mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java create mode 100644 src/main/java/de/hysky/skyblocker/utils/Tickable.java create mode 100644 src/main/java/de/hysky/skyblocker/utils/render/Renderable.java delete mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java delete mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java create mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java create mode 100644 src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java (limited to 'src/main/java/de/hysky/skyblocker/SkyblockerMod.java') diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index d1aa3153..9ce0df8d 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -5,7 +5,13 @@ import com.google.gson.GsonBuilder; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.skyblock.*; -import de.hysky.skyblocker.skyblock.dungeon.*; +import de.hysky.skyblocker.skyblock.dungeon.DungeonMap; +import de.hysky.skyblocker.skyblock.dungeon.FireFreezeStaffTimer; +import de.hysky.skyblocker.skyblock.dungeon.GuardianHealth; +import de.hysky.skyblocker.skyblock.dungeon.LividColor; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.CreeperBeams; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonBlaze; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.TicTacToe; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.dungeon.secrets.SecretsTracker; import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java deleted file mode 100644 index 5c7a01f9..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/CreeperBeams.java +++ /dev/null @@ -1,250 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.objects.ObjectDoublePair; -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; -import net.minecraft.block.Block; -import net.minecraft.block.Blocks; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.entity.mob.CreeperEntity; -import net.minecraft.predicate.entity.EntityPredicates; -import net.minecraft.util.DyeColor; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Vec3d; -import org.joml.Intersectiond; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -public class CreeperBeams { - - private static final Logger LOGGER = LoggerFactory.getLogger(CreeperBeams.class.getName()); - - private static final float[][] COLORS = { - DyeColor.LIGHT_BLUE.getColorComponents(), - DyeColor.LIME.getColorComponents(), - DyeColor.YELLOW.getColorComponents(), - DyeColor.MAGENTA.getColorComponents(), - }; - private static final float[] GREEN_COLOR_COMPONENTS = DyeColor.GREEN.getColorComponents(); - - private static final int FLOOR_Y = 68; - private static final int BASE_Y = 74; - - private static ArrayList beams = new ArrayList<>(); - private static BlockPos base = null; - private static boolean solved = false; - - public static void init() { - Scheduler.INSTANCE.scheduleCyclic(CreeperBeams::update, 20); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(CreeperBeams::render); - ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); - } - - private static void reset() { - beams.clear(); - base = null; - solved = false; - } - - private static void update() { - - // don't do anything if the room is solved - if (solved) { - return; - } - - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - ClientPlayerEntity player = client.player; - - // clear state if not in dungeon - if (world == null || player == null || !Utils.isInDungeons()) { - return; - } - - // try to find base if not found and solve - if (base == null) { - base = findCreeperBase(player, world); - if (base == null) { - return; - } - Vec3d creeperPos = new Vec3d(base.getX() + 0.5, BASE_Y + 1.75, base.getZ() + 0.5); - ArrayList targets = findTargets(world, base); - beams = findLines(creeperPos, targets); - } - - // update the beam states - beams.forEach(b -> b.updateState(world)); - - // check if the room is solved - if (!isTarget(world, base)) { - solved = true; - } - } - - // find the sea lantern block beneath the creeper - private static BlockPos findCreeperBase(ClientPlayerEntity player, ClientWorld world) { - - // find all creepers - List creepers = world.getEntitiesByClass( - CreeperEntity.class, - player.getBoundingBox().expand(50D), - EntityPredicates.VALID_ENTITY); - - if (creepers.isEmpty()) { - return null; - } - - // (sanity) check: - // if the creeper isn't above a sea lantern, it's not the target. - for (CreeperEntity ce : creepers) { - Vec3d creeperPos = ce.getPos(); - BlockPos potentialBase = BlockPos.ofFloored(creeperPos.x, BASE_Y, creeperPos.z); - if (isTarget(world, potentialBase)) { - return potentialBase; - } - } - - return null; - - } - - // find the sea lanterns (and the ONE prismarine ty hypixel) in the room - private static ArrayList findTargets(ClientWorld world, BlockPos basePos) { - ArrayList targets = new ArrayList<>(); - - BlockPos start = new BlockPos(basePos.getX() - 15, BASE_Y + 12, basePos.getZ() - 15); - BlockPos end = new BlockPos(basePos.getX() + 16, FLOOR_Y, basePos.getZ() + 16); - - for (BlockPos pos : BlockPos.iterate(start, end)) { - if (isTarget(world, pos)) { - targets.add(new BlockPos(pos)); - } - } - return targets; - } - - // generate lines between targets and finally find the solution - private static ArrayList findLines(Vec3d creeperPos, ArrayList targets) { - - ArrayList> allLines = new ArrayList<>(); - - // optimize this a little bit by - // only generating lines "one way", i.e. 1 -> 2 but not 2 -> 1 - for (int i = 0; i < targets.size(); i++) { - for (int j = i + 1; j < targets.size(); j++) { - Beam beam = new Beam(targets.get(i), targets.get(j)); - double dist = Intersectiond.distancePointLine( - creeperPos.x, creeperPos.y, creeperPos.z, - beam.line[0].x, beam.line[0].y, beam.line[0].z, - beam.line[1].x, beam.line[1].y, beam.line[1].z); - allLines.add(ObjectDoublePair.of(beam, dist)); - } - } - - // this feels a bit heavy-handed, but it works for now. - - ArrayList result = new ArrayList<>(); - allLines.sort(Comparator.comparingDouble(ObjectDoublePair::rightDouble)); - - while (result.size() < 4 && !allLines.isEmpty()) { - Beam solution = allLines.get(0).left(); - result.add(solution); - - // remove the line we just added and other lines that use blocks we're using for - // that line - allLines.remove(0); - allLines.removeIf(beam -> solution.containsComponentOf(beam.left())); - } - - if (result.size() != 4) { - LOGGER.error("Not enough solutions found. This is bad..."); - } - - return result; - } - - private static void render(WorldRenderContext wrc) { - - // don't render if solved or disabled - if (solved || !SkyblockerConfigManager.get().locations.dungeons.creeperSolver) { - return; - } - - // lines.size() is always <= 4 so no issues OOB issues with the colors here. - for (int i = 0; i < beams.size(); i++) { - beams.get(i).render(wrc, COLORS[i]); - } - } - - private static boolean isTarget(ClientWorld world, BlockPos pos) { - Block block = world.getBlockState(pos).getBlock(); - return block == Blocks.SEA_LANTERN || block == Blocks.PRISMARINE; - } - - // helper class to hold all the things needed to render a beam - private static class Beam { - - // raw block pos of target - public BlockPos blockOne; - public BlockPos blockTwo; - - // middle of targets used for rendering the line - public Vec3d[] line = new Vec3d[2]; - - // boxes used for rendering the block outline - public Box outlineOne; - public Box outlineTwo; - - // state: is this beam created/inputted or not? - private boolean toDo = true; - - public Beam(BlockPos a, BlockPos b) { - blockOne = a; - blockTwo = b; - line[0] = new Vec3d(a.getX() + 0.5, a.getY() + 0.5, a.getZ() + 0.5); - line[1] = new Vec3d(b.getX() + 0.5, b.getY() + 0.5, b.getZ() + 0.5); - outlineOne = new Box(a); - outlineTwo = new Box(b); - } - - // used to filter the list of all beams so that no two beams share a target - public boolean containsComponentOf(Beam other) { - return this.blockOne.equals(other.blockOne) - || this.blockOne.equals(other.blockTwo) - || this.blockTwo.equals(other.blockOne) - || this.blockTwo.equals(other.blockTwo); - } - - // update the state: is the beam created or not? - public void updateState(ClientWorld world) { - toDo = !(world.getBlockState(blockOne).getBlock() == Blocks.PRISMARINE - && world.getBlockState(blockTwo).getBlock() == Blocks.PRISMARINE); - } - - // render either in a color if not created or faintly green if created - public void render(WorldRenderContext wrc, float[] color) { - if (toDo) { - RenderHelper.renderOutline(wrc, outlineOne, color, 3, false); - RenderHelper.renderOutline(wrc, outlineTwo, color, 3, false); - RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2); - } else { - RenderHelper.renderOutline(wrc, outlineOne, GREEN_COLOR_COMPONENTS, 1, false); - RenderHelper.renderOutline(wrc, outlineTwo, GREEN_COLOR_COMPONENTS, 1, false); - RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1); - } - } - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java deleted file mode 100644 index aabef183..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonBlaze.java +++ /dev/null @@ -1,164 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.events.DungeonEvents; -import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.objects.ObjectIntPair; -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.entity.decoration.ArmorStandEntity; -import net.minecraft.predicate.entity.EntityPredicates; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Vec3d; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; - -/** - * This class provides functionality to render outlines around Blaze entities - */ -public class DungeonBlaze { - private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName()); - private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F}; - private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f}; - - private static boolean inBlaze; - private static ArmorStandEntity highestBlaze = null; - private static ArmorStandEntity lowestBlaze = null; - private static ArmorStandEntity nextHighestBlaze = null; - private static ArmorStandEntity nextLowestBlaze = null; - - public static void init() { - DungeonEvents.PUZZLE_MATCHED.register(room -> { - if (room.getName().startsWith("blaze-room")) { - inBlaze = true; - } - }); - Scheduler.INSTANCE.scheduleCyclic(DungeonBlaze::update, 4); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(DungeonBlaze::blazeRenderer); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> inBlaze = false); - } - - /** - * Updates the state of Blaze entities and triggers the rendering process if necessary. - */ - public static void update() { - if (!inBlaze) { - return; - } - ClientWorld world = MinecraftClient.getInstance().world; - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (world == null || player == null || !Utils.isInDungeons()) return; - List> blazes = getBlazesInWorld(world, player); - sortBlazes(blazes); - updateBlazeEntities(blazes); - } - - /** - * Retrieves Blaze entities in the world and parses their health information. - * - * @param world The client world to search for Blaze entities. - * @return A list of Blaze entities and their associated health. - */ - private static List> getBlazesInWorld(ClientWorld world, ClientPlayerEntity player) { - List> blazes = new ArrayList<>(); - for (ArmorStandEntity blaze : world.getEntitiesByClass(ArmorStandEntity.class, player.getBoundingBox().expand(500D), EntityPredicates.NOT_MOUNTED)) { - String blazeName = blaze.getName().getString(); - if (blazeName.contains("Blaze") && blazeName.contains("/")) { - try { - int health = Integer.parseInt((blazeName.substring(blazeName.indexOf("/") + 1, blazeName.length() - 1)).replaceAll(",", "")); - blazes.add(ObjectIntPair.of(blaze, health)); - } catch (NumberFormatException e) { - handleException(e); - } - } - } - return blazes; - } - - /** - * Sorts the Blaze entities based on their health values. - * - * @param blazes The list of Blaze entities to be sorted. - */ - private static void sortBlazes(List> blazes) { - blazes.sort(Comparator.comparingInt(ObjectIntPair::rightInt)); - } - - /** - * Updates information about Blaze entities based on sorted list. - * - * @param blazes The sorted list of Blaze entities with associated health values. - */ - private static void updateBlazeEntities(List> blazes) { - if (!blazes.isEmpty()) { - lowestBlaze = blazes.get(0).left(); - int highestIndex = blazes.size() - 1; - highestBlaze = blazes.get(highestIndex).left(); - if (blazes.size() > 1) { - nextLowestBlaze = blazes.get(1).left(); - nextHighestBlaze = blazes.get(highestIndex - 1).left(); - } - } - } - - /** - * Renders outlines for Blaze entities based on health and position. - * - * @param wrc The WorldRenderContext used for rendering. - */ - public static void blazeRenderer(WorldRenderContext wrc) { - try { - if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfigManager.get().locations.dungeons.blazeSolver) { - if (highestBlaze.getY() < 69) { - renderBlazeOutline(highestBlaze, nextHighestBlaze, wrc); - } - if (lowestBlaze.getY() > 69) { - renderBlazeOutline(lowestBlaze, nextLowestBlaze, wrc); - } - } - } catch (Exception e) { - handleException(e); - } - } - - /** - * Renders outlines for Blaze entities and connections between them. - * - * @param blaze The Blaze entity for which to render an outline. - * @param nextBlaze The next Blaze entity for connection rendering. - * @param wrc The WorldRenderContext used for rendering. - */ - private static void renderBlazeOutline(ArmorStandEntity blaze, ArmorStandEntity nextBlaze, WorldRenderContext wrc) { - Box blazeBox = blaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); - RenderHelper.renderOutline(wrc, blazeBox, GREEN_COLOR_COMPONENTS, 5f, false); - - if (nextBlaze != null && nextBlaze.isAlive() && nextBlaze != blaze) { - Box nextBlazeBox = nextBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); - RenderHelper.renderOutline(wrc, nextBlazeBox, WHITE_COLOR_COMPONENTS, 5f, false); - - Vec3d blazeCenter = blazeBox.getCenter(); - Vec3d nextBlazeCenter = nextBlazeBox.getCenter(); - - RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f); - } - } - - /** - * Handles exceptions by logging and printing stack traces. - * - * @param e The exception to handle. - */ - private static void handleException(Exception e) { - LOGGER.error("[Skyblocker BlazeRenderer] Encountered an unknown exception", e); - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java deleted file mode 100644 index e1ab2fa8..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdos.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.utils.chat.ChatFilterResult; -import de.hysky.skyblocker.utils.chat.ChatPatternListener; -import net.minecraft.client.MinecraftClient; -import net.minecraft.entity.decoration.ArmorStandEntity; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; - -import java.util.regex.Matcher; - -public class ThreeWeirdos extends ChatPatternListener { - public ThreeWeirdos() { - super("^§e\\[NPC] §c([A-Z][a-z]+)§f: (?:The reward is(?: not in my chest!|n't in any of our chests\\.)|My chest (?:doesn't have the reward\\. We are all telling the truth\\.|has the reward and I'm telling the truth!)|At least one of them is lying, and the reward is not in §c§c[A-Z][a-z]+'s §rchest\\!|Both of them are telling the truth\\. Also, §c§c[A-Z][a-z]+ §rhas the reward in their chest\\!)$"); - } - - @Override - public ChatFilterResult state() { - return SkyblockerConfigManager.get().locations.dungeons.solveThreeWeirdos ? null : ChatFilterResult.PASS; - } - - @Override - public boolean onMatch(Text message, Matcher matcher) { - MinecraftClient client = MinecraftClient.getInstance(); - if (client.player == null || client.world == null) return false; - client.world.getEntitiesByClass( - ArmorStandEntity.class, - client.player.getBoundingBox().expand(3), - entity -> { - Text customName = entity.getCustomName(); - return customName != null && customName.getString().equals(matcher.group(1)); - } - ).forEach( - entity -> entity.setCustomName(Text.of(Formatting.GREEN + matcher.group(1))) - ); - return false; - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java deleted file mode 100644 index 2bb3e4e0..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/TicTacToe.java +++ /dev/null @@ -1,151 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.events.DungeonEvents; -import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils; -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; -import net.minecraft.block.Block; -import net.minecraft.block.Blocks; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.client.world.ClientWorld; -import net.minecraft.entity.decoration.ItemFrameEntity; -import net.minecraft.item.FilledMapItem; -import net.minecraft.item.map.MapState; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Direction; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -/** - * Thanks to Danker for a reference implementation! - */ -public class TicTacToe { - private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class); - private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F}; - private static boolean inTicTacToe; - private static Box nextBestMoveToMake = null; - - public static void init() { - DungeonEvents.PUZZLE_MATCHED.register(room -> { - if (room.getName().startsWith("tic-tac-toe")) { - inTicTacToe = true; - } - }); - Scheduler.INSTANCE.scheduleCyclic(TicTacToe::tick, 4); - WorldRenderEvents.BEFORE_DEBUG_RENDER.register(TicTacToe::solutionRenderer); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> inTicTacToe = false); - } - - public static void tick() { - if (!inTicTacToe) { - return; - } - - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - ClientPlayerEntity player = client.player; - - nextBestMoveToMake = null; - - if (world == null || player == null || !Utils.isInDungeons()) return; - - //Search within 21 blocks for item frames that contain maps - Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21); - List itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); - - try { - //Only attempt to solve if its the player's turn - if (itemFramesThatHoldMaps.size() != 9 && itemFramesThatHoldMaps.size() % 2 == 1) { - char[][] board = new char[3][3]; - BlockPos leftmostRow = null; - int sign = 1; - char facing = 'X'; - - for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) { - MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); - - if (mapState == null) continue; - - int column = 0, row; - sign = 1; - - //Find position of the item frame relative to where it is on the tic tac toe board - if (itemFrame.getHorizontalFacing() == Direction.SOUTH || itemFrame.getHorizontalFacing() == Direction.WEST) sign = -1; - BlockPos itemFramePos = BlockPos.ofFloored(itemFrame.getX(), itemFrame.getY(), itemFrame.getZ()); - - for (int i = 2; i >= 0; i--) { - int realI = i * sign; - BlockPos blockPos = itemFramePos; - - if (itemFrame.getX() % 0.5 == 0) { - blockPos = itemFramePos.add(realI, 0, 0); - } else if (itemFrame.getZ() % 0.5 == 0) { - blockPos = itemFramePos.add(0, 0, realI); - facing = 'Z'; - } - - Block block = world.getBlockState(blockPos).getBlock(); - if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) { - leftmostRow = blockPos; - column = i; - - break; - } - } - - //Determine the row of the item frame - if (itemFrame.getY() == 72.5) { - row = 0; - } else if (itemFrame.getY() == 71.5) { - row = 1; - } else if (itemFrame.getY() == 70.5) { - row = 2; - } else { - continue; - } - - - //Get the color of the middle pixel of the map which determines whether its X or O - int middleColor = mapState.colors[8256] & 255; - - if (middleColor == 114) { - board[row][column] = 'X'; - } else if (middleColor == 33) { - board[row][column] = 'O'; - } - - int bestMove = TicTacToeUtils.getBestMove(board) - 1; - - if (leftmostRow != null) { - double drawX = facing == 'X' ? leftmostRow.getX() - sign * (bestMove % 3) : leftmostRow.getX(); - double drawY = 72 - (double) (bestMove / 3); - double drawZ = facing == 'Z' ? leftmostRow.getZ() - sign * (bestMove % 3) : leftmostRow.getZ(); - - nextBestMoveToMake = new Box(drawX, drawY, drawZ, drawX + 1, drawY + 1, drawZ + 1); - } - } - } - } catch (Exception e) { - LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while determining a tic tac toe solution!", e); - } - } - - private static void solutionRenderer(WorldRenderContext context) { - try { - if (SkyblockerConfigManager.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) { - RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5, false); - } - } catch (Exception e) { - LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while rendering the tic tac toe solution!", e); - } - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java deleted file mode 100644 index 21bbdce0..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/Trivia.java +++ /dev/null @@ -1,109 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.waypoint.FairySouls; -import de.hysky.skyblocker.utils.chat.ChatFilterResult; -import de.hysky.skyblocker.utils.chat.ChatPatternListener; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.text.Text; -import net.minecraft.util.Formatting; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - -import com.mojang.logging.LogUtils; - -import java.util.*; -import java.util.regex.Matcher; - -public class Trivia extends ChatPatternListener { - private static final Logger LOGGER = LogUtils.getLogger(); - private static final Map answers; - private List solutions = Collections.emptyList(); - - public Trivia() { - super("^ +(?:([A-Za-z,' ]*\\?)|§6 ([ⓐⓑⓒ]) §a([a-zA-Z0-9 ]+))$"); - } - - @Override - public ChatFilterResult state() { - return SkyblockerConfigManager.get().locations.dungeons.solveTrivia ? ChatFilterResult.FILTER : ChatFilterResult.PASS; - } - - @Override - public boolean onMatch(Text message, Matcher matcher) { - String riddle = matcher.group(3); - if (riddle != null) { - if (!solutions.contains(riddle)) { - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (player != null) - MinecraftClient.getInstance().player.sendMessage(Text.of(" " + Formatting.GOLD + matcher.group(2) + Formatting.RED + " " + riddle), false); - return player != null; - } - } else updateSolutions(matcher.group(0)); - return false; - } - - private void updateSolutions(String question) { - try { - String trimmedQuestion = question.trim(); - if (trimmedQuestion.equals("What SkyBlock year is it?")) { - long currentTime = System.currentTimeMillis() / 1000L; - long diff = currentTime - 1560276000; - int year = (int) (diff / 446400 + 1); - solutions = Collections.singletonList("Year " + year); - } else { - String[] questionAnswers = answers.get(trimmedQuestion); - if (questionAnswers != null) solutions = Arrays.asList(questionAnswers); - } - } catch (Exception e) { //Hopefully the solver doesn't go south - LOGGER.error("[Skyblocker] Failed to update the Trivia puzzle answers!", e); - } - } - - static { - answers = Collections.synchronizedMap(new HashMap<>()); - answers.put("What is the status of The Watcher?", new String[]{"Stalker"}); - answers.put("What is the status of Bonzo?", new String[]{"New Necromancer"}); - answers.put("What is the status of Scarf?", new String[]{"Apprentice Necromancer"}); - answers.put("What is the status of The Professor?", new String[]{"Professor"}); - answers.put("What is the status of Thorn?", new String[]{"Shaman Necromancer"}); - answers.put("What is the status of Livid?", new String[]{"Master Necromancer"}); - answers.put("What is the status of Sadan?", new String[]{"Necromancer Lord"}); - answers.put("What is the status of Maxor?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Goldor?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Storm?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Necron?", new String[]{"The Wither Lords"}); - answers.put("What is the status of Maxor, Storm, Goldor and Necron?", new String[]{"The Wither Lords"}); - answers.put("Which brother is on the Spider's Den?", new String[]{"Rick"}); - answers.put("What is the name of Rick's brother?", new String[]{"Pat"}); - answers.put("What is the name of the Painter in the Hub?", new String[]{"Marco"}); - answers.put("What is the name of the person that upgrades pets?", new String[]{"Kat"}); - answers.put("What is the name of the lady of the Nether?", new String[]{"Elle"}); - answers.put("Which villager in the Village gives you a Rogue Sword?", new String[]{"Jamie"}); - answers.put("How many unique minions are there?", new String[]{"59 Minions"}); - answers.put("Which of these enemies does not spawn in the Spider's Den?", new String[]{"Zombie Spider", "Cave Spider", "Wither Skeleton", "Dashing Spooder", "Broodfather", "Night Spider"}); - answers.put("Which of these monsters only spawns at night?", new String[]{"Zombie Villager", "Ghast"}); - answers.put("Which of these is not a dragon in The End?", new String[]{"Zoomer Dragon", "Weak Dragon", "Stonk Dragon", "Holy Dragon", "Boomer Dragon", "Booger Dragon", "Older Dragon", "Elder Dragon", "Stable Dragon", "Professor Dragon"}); - FairySouls.runAsyncAfterFairySoulsLoad(() -> { - answers.put("How many total Fairy Souls are there?", getFairySoulsSizeString(null)); - answers.put("How many Fairy Souls are there in Spider's Den?", getFairySoulsSizeString("combat_1")); - answers.put("How many Fairy Souls are there in The End?", getFairySoulsSizeString("combat_3")); - answers.put("How many Fairy Souls are there in The Farming Islands?", getFairySoulsSizeString("farming_1")); - answers.put("How many Fairy Souls are there in Crimson Isle?", getFairySoulsSizeString("crimson_isle")); - answers.put("How many Fairy Souls are there in The Park?", getFairySoulsSizeString("foraging_1")); - answers.put("How many Fairy Souls are there in Jerry's Workshop?", getFairySoulsSizeString("winter")); - answers.put("How many Fairy Souls are there in Hub?", getFairySoulsSizeString("hub")); - answers.put("How many Fairy Souls are there in The Hub?", getFairySoulsSizeString("hub")); - answers.put("How many Fairy Souls are there in Deep Caverns?", getFairySoulsSizeString("mining_2")); - answers.put("How many Fairy Souls are there in Gold Mine?", getFairySoulsSizeString("mining_1")); - answers.put("How many Fairy Souls are there in Dungeon Hub?", getFairySoulsSizeString("dungeon_hub")); - }); - } - - @NotNull - private static String[] getFairySoulsSizeString(@Nullable String location) { - return new String[]{"%d Fairy Souls".formatted(FairySouls.getFairySoulsSize(location))}; - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java new file mode 100644 index 00000000..8de1e3fe --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java @@ -0,0 +1,250 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.ObjectDoublePair; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.mob.CreeperEntity; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.DyeColor; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.joml.Intersectiond; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class CreeperBeams extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(CreeperBeams.class.getName()); + + private static final float[][] COLORS = { + DyeColor.LIGHT_BLUE.getColorComponents(), + DyeColor.LIME.getColorComponents(), + DyeColor.YELLOW.getColorComponents(), + DyeColor.MAGENTA.getColorComponents(), + }; + private static final float[] GREEN_COLOR_COMPONENTS = DyeColor.GREEN.getColorComponents(); + + private static final int FLOOR_Y = 68; + private static final int BASE_Y = 74; + private static final CreeperBeams INSTANCE = new CreeperBeams("creeper", "creeper-room"); + + private static ArrayList beams = new ArrayList<>(); + private static BlockPos base = null; + + private CreeperBeams(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + @Override + public void reset() { + super.reset(); + beams.clear(); + base = null; + } + + @Override + public void tick() { + + // don't do anything if the room is solved + if (!shouldSolve()) { + return; + } + + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + ClientPlayerEntity player = client.player; + + // clear state if not in dungeon + if (world == null || player == null || !Utils.isInDungeons()) { + return; + } + + // try to find base if not found and solve + if (base == null) { + base = findCreeperBase(player, world); + if (base == null) { + return; + } + Vec3d creeperPos = new Vec3d(base.getX() + 0.5, BASE_Y + 1.75, base.getZ() + 0.5); + ArrayList targets = findTargets(world, base); + beams = findLines(creeperPos, targets); + } + + // update the beam states + beams.forEach(b -> b.updateState(world)); + + // check if the room is solved + if (!isTarget(world, base)) { + reset(); + } + } + + // find the sea lantern block beneath the creeper + private static BlockPos findCreeperBase(ClientPlayerEntity player, ClientWorld world) { + + // find all creepers + List creepers = world.getEntitiesByClass( + CreeperEntity.class, + player.getBoundingBox().expand(50D), + EntityPredicates.VALID_ENTITY); + + if (creepers.isEmpty()) { + return null; + } + + // (sanity) check: + // if the creeper isn't above a sea lantern, it's not the target. + for (CreeperEntity ce : creepers) { + Vec3d creeperPos = ce.getPos(); + BlockPos potentialBase = BlockPos.ofFloored(creeperPos.x, BASE_Y, creeperPos.z); + if (isTarget(world, potentialBase)) { + return potentialBase; + } + } + + return null; + + } + + // find the sea lanterns (and the ONE prismarine ty hypixel) in the room + private static ArrayList findTargets(ClientWorld world, BlockPos basePos) { + ArrayList targets = new ArrayList<>(); + + BlockPos start = new BlockPos(basePos.getX() - 15, BASE_Y + 12, basePos.getZ() - 15); + BlockPos end = new BlockPos(basePos.getX() + 16, FLOOR_Y, basePos.getZ() + 16); + + for (BlockPos pos : BlockPos.iterate(start, end)) { + if (isTarget(world, pos)) { + targets.add(new BlockPos(pos)); + } + } + return targets; + } + + // generate lines between targets and finally find the solution + private static ArrayList findLines(Vec3d creeperPos, ArrayList targets) { + + ArrayList> allLines = new ArrayList<>(); + + // optimize this a little bit by + // only generating lines "one way", i.e. 1 -> 2 but not 2 -> 1 + for (int i = 0; i < targets.size(); i++) { + for (int j = i + 1; j < targets.size(); j++) { + Beam beam = new Beam(targets.get(i), targets.get(j)); + double dist = Intersectiond.distancePointLine( + creeperPos.x, creeperPos.y, creeperPos.z, + beam.line[0].x, beam.line[0].y, beam.line[0].z, + beam.line[1].x, beam.line[1].y, beam.line[1].z); + allLines.add(ObjectDoublePair.of(beam, dist)); + } + } + + // this feels a bit heavy-handed, but it works for now. + + ArrayList result = new ArrayList<>(); + allLines.sort(Comparator.comparingDouble(ObjectDoublePair::rightDouble)); + + while (result.size() < 4 && !allLines.isEmpty()) { + Beam solution = allLines.get(0).left(); + result.add(solution); + + // remove the line we just added and other lines that use blocks we're using for + // that line + allLines.remove(0); + allLines.removeIf(beam -> solution.containsComponentOf(beam.left())); + } + + if (result.size() != 4) { + LOGGER.error("Not enough solutions found. This is bad..."); + } + + return result; + } + + @Override + public void render(WorldRenderContext wrc) { + + // don't render if solved or disabled + if (!shouldSolve() || !SkyblockerConfigManager.get().locations.dungeons.creeperSolver) { + return; + } + + // lines.size() is always <= 4 so no issues OOB issues with the colors here. + for (int i = 0; i < beams.size(); i++) { + beams.get(i).render(wrc, COLORS[i]); + } + } + + private static boolean isTarget(ClientWorld world, BlockPos pos) { + Block block = world.getBlockState(pos).getBlock(); + return block == Blocks.SEA_LANTERN || block == Blocks.PRISMARINE; + } + + // helper class to hold all the things needed to render a beam + private static class Beam { + + // raw block pos of target + public BlockPos blockOne; + public BlockPos blockTwo; + + // middle of targets used for rendering the line + public Vec3d[] line = new Vec3d[2]; + + // boxes used for rendering the block outline + public Box outlineOne; + public Box outlineTwo; + + // state: is this beam created/inputted or not? + private boolean toDo = true; + + public Beam(BlockPos a, BlockPos b) { + blockOne = a; + blockTwo = b; + line[0] = new Vec3d(a.getX() + 0.5, a.getY() + 0.5, a.getZ() + 0.5); + line[1] = new Vec3d(b.getX() + 0.5, b.getY() + 0.5, b.getZ() + 0.5); + outlineOne = new Box(a); + outlineTwo = new Box(b); + } + + // used to filter the list of all beams so that no two beams share a target + public boolean containsComponentOf(Beam other) { + return this.blockOne.equals(other.blockOne) + || this.blockOne.equals(other.blockTwo) + || this.blockTwo.equals(other.blockOne) + || this.blockTwo.equals(other.blockTwo); + } + + // update the state: is the beam created or not? + public void updateState(ClientWorld world) { + toDo = !(world.getBlockState(blockOne).getBlock() == Blocks.PRISMARINE + && world.getBlockState(blockTwo).getBlock() == Blocks.PRISMARINE); + } + + // render either in a color if not created or faintly green if created + public void render(WorldRenderContext wrc, float[] color) { + if (toDo) { + RenderHelper.renderOutline(wrc, outlineOne, color, 3, false); + RenderHelper.renderOutline(wrc, outlineTwo, color, 3, false); + RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2); + } else { + RenderHelper.renderOutline(wrc, outlineOne, GREEN_COLOR_COMPONENTS, 1, false); + RenderHelper.renderOutline(wrc, outlineTwo, GREEN_COLOR_COMPONENTS, 1, false); + RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java new file mode 100644 index 00000000..5774eaef --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java @@ -0,0 +1,158 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import it.unimi.dsi.fastutil.objects.ObjectIntPair; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.predicate.entity.EntityPredicates; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * This class provides functionality to render outlines around Blaze entities + */ +public class DungeonBlaze extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(DungeonBlaze.class.getName()); + private static final float[] GREEN_COLOR_COMPONENTS = {0.0F, 1.0F, 0.0F}; + private static final float[] WHITE_COLOR_COMPONENTS = {1.0f, 1.0f, 1.0f}; + private static final DungeonBlaze INSTANCE = new DungeonBlaze("blaze", "blaze-room-1-high", "blaze-room-1-low"); + + private static ArmorStandEntity highestBlaze = null; + private static ArmorStandEntity lowestBlaze = null; + private static ArmorStandEntity nextHighestBlaze = null; + private static ArmorStandEntity nextLowestBlaze = null; + + private DungeonBlaze(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + /** + * Updates the state of Blaze entities and triggers the rendering process if necessary. + */ + @Override + public void tick() { + if (!shouldSolve()) { + return; + } + ClientWorld world = MinecraftClient.getInstance().world; + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (world == null || player == null || !Utils.isInDungeons()) return; + List> blazes = getBlazesInWorld(world, player); + sortBlazes(blazes); + updateBlazeEntities(blazes); + } + + /** + * Retrieves Blaze entities in the world and parses their health information. + * + * @param world The client world to search for Blaze entities. + * @return A list of Blaze entities and their associated health. + */ + private static List> getBlazesInWorld(ClientWorld world, ClientPlayerEntity player) { + List> blazes = new ArrayList<>(); + for (ArmorStandEntity blaze : world.getEntitiesByClass(ArmorStandEntity.class, player.getBoundingBox().expand(500D), EntityPredicates.NOT_MOUNTED)) { + String blazeName = blaze.getName().getString(); + if (blazeName.contains("Blaze") && blazeName.contains("/")) { + try { + int health = Integer.parseInt((blazeName.substring(blazeName.indexOf("/") + 1, blazeName.length() - 1)).replaceAll(",", "")); + blazes.add(ObjectIntPair.of(blaze, health)); + } catch (NumberFormatException e) { + handleException(e); + } + } + } + return blazes; + } + + /** + * Sorts the Blaze entities based on their health values. + * + * @param blazes The list of Blaze entities to be sorted. + */ + private static void sortBlazes(List> blazes) { + blazes.sort(Comparator.comparingInt(ObjectIntPair::rightInt)); + } + + /** + * Updates information about Blaze entities based on sorted list. + * + * @param blazes The sorted list of Blaze entities with associated health values. + */ + private static void updateBlazeEntities(List> blazes) { + if (!blazes.isEmpty()) { + lowestBlaze = blazes.get(0).left(); + int highestIndex = blazes.size() - 1; + highestBlaze = blazes.get(highestIndex).left(); + if (blazes.size() > 1) { + nextLowestBlaze = blazes.get(1).left(); + nextHighestBlaze = blazes.get(highestIndex - 1).left(); + } + } + } + + /** + * Renders outlines for Blaze entities based on health and position. + * + * @param wrc The WorldRenderContext used for rendering. + */ + @Override + public void render(WorldRenderContext wrc) { + try { + if (highestBlaze != null && lowestBlaze != null && highestBlaze.isAlive() && lowestBlaze.isAlive() && SkyblockerConfigManager.get().locations.dungeons.blazeSolver) { + if (highestBlaze.getY() < 69) { + renderBlazeOutline(highestBlaze, nextHighestBlaze, wrc); + } + if (lowestBlaze.getY() > 69) { + renderBlazeOutline(lowestBlaze, nextLowestBlaze, wrc); + } + } + } catch (Exception e) { + handleException(e); + } + } + + /** + * Renders outlines for Blaze entities and connections between them. + * + * @param blaze The Blaze entity for which to render an outline. + * @param nextBlaze The next Blaze entity for connection rendering. + * @param wrc The WorldRenderContext used for rendering. + */ + private static void renderBlazeOutline(ArmorStandEntity blaze, ArmorStandEntity nextBlaze, WorldRenderContext wrc) { + Box blazeBox = blaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); + RenderHelper.renderOutline(wrc, blazeBox, GREEN_COLOR_COMPONENTS, 5f, false); + + if (nextBlaze != null && nextBlaze.isAlive() && nextBlaze != blaze) { + Box nextBlazeBox = nextBlaze.getBoundingBox().expand(0.3, 0.9, 0.3).offset(0, -1.1, 0); + RenderHelper.renderOutline(wrc, nextBlazeBox, WHITE_COLOR_COMPONENTS, 5f, false); + + Vec3d blazeCenter = blazeBox.getCenter(); + Vec3d nextBlazeCenter = nextBlazeBox.getCenter(); + + RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f); + } + } + + /** + * Handles exceptions by logging and printing stack traces. + * + * @param e The exception to handle. + */ + private static void handleException(Exception e) { + LOGGER.error("[Skyblocker BlazeRenderer] Encountered an unknown exception", e); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java new file mode 100644 index 00000000..04446e60 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java @@ -0,0 +1,58 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import com.mojang.brigadier.Command; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.events.DungeonEvents; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; +import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; +import de.hysky.skyblocker.utils.render.Renderable; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; + +public abstract class DungeonPuzzle implements Tickable, Renderable { + private final String puzzleName; + @NotNull + private final Set roomNames; + private boolean shouldSolve; + + public DungeonPuzzle(String puzzleName, String... roomName) { + this(puzzleName, Set.of(roomName)); + } + + public DungeonPuzzle(String puzzleName, @NotNull Set roomNames) { + this.puzzleName = puzzleName; + this.roomNames = roomNames; + DungeonEvents.PUZZLE_MATCHED.register(room -> { + if (roomNames.contains(room.getName())) { + room.addSubProcess(this); + shouldSolve = true; + } + }); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("solvePuzzle").then(literal(puzzleName).executes(context -> { + Room currentRoom = DungeonManager.getCurrentRoom(); + if (currentRoom != null) { + currentRoom.addSubProcess(this); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§aSolving " + puzzleName + " puzzle in the current room.")); + } else { + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); + } + return Command.SINGLE_SUCCESS; + })))))); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + } + + public boolean shouldSolve() { + return shouldSolve; + } + + public void reset() { + shouldSolve = false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java new file mode 100644 index 00000000..c5e55f93 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdos.java @@ -0,0 +1,39 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; +import de.hysky.skyblocker.utils.chat.ChatPatternListener; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.decoration.ArmorStandEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; + +import java.util.regex.Matcher; + +public class ThreeWeirdos extends ChatPatternListener { + public ThreeWeirdos() { + super("^§e\\[NPC] §c([A-Z][a-z]+)§f: (?:The reward is(?: not in my chest!|n't in any of our chests\\.)|My chest (?:doesn't have the reward\\. We are all telling the truth\\.|has the reward and I'm telling the truth!)|At least one of them is lying, and the reward is not in §c§c[A-Z][a-z]+'s §rchest\\!|Both of them are telling the truth\\. Also, §c§c[A-Z][a-z]+ §rhas the reward in their chest\\!)$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().locations.dungeons.solveThreeWeirdos ? null : ChatFilterResult.PASS; + } + + @Override + public boolean onMatch(Text message, Matcher matcher) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) return false; + client.world.getEntitiesByClass( + ArmorStandEntity.class, + client.player.getBoundingBox().expand(3), + entity -> { + Text customName = entity.getCustomName(); + return customName != null && customName.getString().equals(matcher.group(1)); + } + ).forEach( + entity -> entity.setCustomName(Text.of(Formatting.GREEN + matcher.group(1))) + ); + return false; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java new file mode 100644 index 00000000..90028a4f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java @@ -0,0 +1,145 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; +import de.hysky.skyblocker.utils.tictactoe.TicTacToeUtils; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.minecraft.block.Block; +import net.minecraft.block.Blocks; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.world.ClientWorld; +import net.minecraft.entity.decoration.ItemFrameEntity; +import net.minecraft.item.FilledMapItem; +import net.minecraft.item.map.MapState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Direction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Thanks to Danker for a reference implementation! + */ +public class TicTacToe extends DungeonPuzzle { + private static final Logger LOGGER = LoggerFactory.getLogger(TicTacToe.class); + private static final float[] RED_COLOR_COMPONENTS = {1.0F, 0.0F, 0.0F}; + private static final TicTacToe INSTANCE = new TicTacToe("tic-tac-toe", "tic-tac-toe-1"); + private static Box nextBestMoveToMake = null; + + private TicTacToe(String puzzleName, String... roomName) { + super(puzzleName, roomName); + } + + public static void init() { + } + + @Override + public void tick() { + if (!shouldSolve()) { + return; + } + + MinecraftClient client = MinecraftClient.getInstance(); + ClientWorld world = client.world; + ClientPlayerEntity player = client.player; + + nextBestMoveToMake = null; + + if (world == null || player == null || !Utils.isInDungeons()) return; + + //Search within 21 blocks for item frames that contain maps + Box searchBox = new Box(player.getX() - 21, player.getY() - 21, player.getZ() - 21, player.getX() + 21, player.getY() + 21, player.getZ() + 21); + List itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); + + try { + //Only attempt to solve if its the player's turn + if (itemFramesThatHoldMaps.size() != 9 && itemFramesThatHoldMaps.size() % 2 == 1) { + char[][] board = new char[3][3]; + BlockPos leftmostRow = null; + int sign = 1; + char facing = 'X'; + + for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) { + MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); + + if (mapState == null) continue; + + int column = 0, row; + sign = 1; + + //Find position of the item frame relative to where it is on the tic tac toe board + if (itemFrame.getHorizontalFacing() == Direction.SOUTH || itemFrame.getHorizontalFacing() == Direction.WEST) sign = -1; + BlockPos itemFramePos = BlockPos.ofFloored(itemFrame.getX(), itemFrame.getY(), itemFrame.getZ()); + + for (int i = 2; i >= 0; i--) { + int realI = i * sign; + BlockPos blockPos = itemFramePos; + + if (itemFrame.getX() % 0.5 == 0) { + blockPos = itemFramePos.add(realI, 0, 0); + } else if (itemFrame.getZ() % 0.5 == 0) { + blockPos = itemFramePos.add(0, 0, realI); + facing = 'Z'; + } + + Block block = world.getBlockState(blockPos).getBlock(); + if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) { + leftmostRow = blockPos; + column = i; + + break; + } + } + + //Determine the row of the item frame + if (itemFrame.getY() == 72.5) { + row = 0; + } else if (itemFrame.getY() == 71.5) { + row = 1; + } else if (itemFrame.getY() == 70.5) { + row = 2; + } else { + continue; + } + + + //Get the color of the middle pixel of the map which determines whether its X or O + int middleColor = mapState.colors[8256] & 255; + + if (middleColor == 114) { + board[row][column] = 'X'; + } else if (middleColor == 33) { + board[row][column] = 'O'; + } + + int bestMove = TicTacToeUtils.getBestMove(board) - 1; + + if (leftmostRow != null) { + double drawX = facing == 'X' ? leftmostRow.getX() - sign * (bestMove % 3) : leftmostRow.getX(); + double drawY = 72 - (double) (bestMove / 3); + double drawZ = facing == 'Z' ? leftmostRow.getZ() - sign * (bestMove % 3) : leftmostRow.getZ(); + + nextBestMoveToMake = new Box(drawX, drawY, drawZ, drawX + 1, drawY + 1, drawZ + 1); + } + } + } + } catch (Exception e) { + LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while determining a tic tac toe solution!", e); + } + } + + @Override + public void render(WorldRenderContext context) { + try { + if (SkyblockerConfigManager.get().locations.dungeons.solveTicTacToe && nextBestMoveToMake != null) { + RenderHelper.renderOutline(context, nextBestMoveToMake, RED_COLOR_COMPONENTS, 5, false); + } + } catch (Exception e) { + LOGGER.error("[Skyblocker Tic Tac Toe] Encountered an exception while rendering the tic tac toe solution!", e); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java new file mode 100644 index 00000000..0f73457c --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/Trivia.java @@ -0,0 +1,109 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.waypoint.FairySouls; +import de.hysky.skyblocker.utils.chat.ChatFilterResult; +import de.hysky.skyblocker.utils.chat.ChatPatternListener; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import com.mojang.logging.LogUtils; + +import java.util.*; +import java.util.regex.Matcher; + +public class Trivia extends ChatPatternListener { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Map answers; + private List solutions = Collections.emptyList(); + + public Trivia() { + super("^ +(?:([A-Za-z,' ]*\\?)|§6 ([ⓐⓑⓒ]) §a([a-zA-Z0-9 ]+))$"); + } + + @Override + public ChatFilterResult state() { + return SkyblockerConfigManager.get().locations.dungeons.solveTrivia ? ChatFilterResult.FILTER : ChatFilterResult.PASS; + } + + @Override + public boolean onMatch(Text message, Matcher matcher) { + String riddle = matcher.group(3); + if (riddle != null) { + if (!solutions.contains(riddle)) { + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player != null) + MinecraftClient.getInstance().player.sendMessage(Text.of(" " + Formatting.GOLD + matcher.group(2) + Formatting.RED + " " + riddle), false); + return player != null; + } + } else updateSolutions(matcher.group(0)); + return false; + } + + private void updateSolutions(String question) { + try { + String trimmedQuestion = question.trim(); + if (trimmedQuestion.equals("What SkyBlock year is it?")) { + long currentTime = System.currentTimeMillis() / 1000L; + long diff = currentTime - 1560276000; + int year = (int) (diff / 446400 + 1); + solutions = Collections.singletonList("Year " + year); + } else { + String[] questionAnswers = answers.get(trimmedQuestion); + if (questionAnswers != null) solutions = Arrays.asList(questionAnswers); + } + } catch (Exception e) { //Hopefully the solver doesn't go south + LOGGER.error("[Skyblocker] Failed to update the Trivia puzzle answers!", e); + } + } + + static { + answers = Collections.synchronizedMap(new HashMap<>()); + answers.put("What is the status of The Watcher?", new String[]{"Stalker"}); + answers.put("What is the status of Bonzo?", new String[]{"New Necromancer"}); + answers.put("What is the status of Scarf?", new String[]{"Apprentice Necromancer"}); + answers.put("What is the status of The Professor?", new String[]{"Professor"}); + answers.put("What is the status of Thorn?", new String[]{"Shaman Necromancer"}); + answers.put("What is the status of Livid?", new String[]{"Master Necromancer"}); + answers.put("What is the status of Sadan?", new String[]{"Necromancer Lord"}); + answers.put("What is the status of Maxor?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Goldor?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Storm?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Necron?", new String[]{"The Wither Lords"}); + answers.put("What is the status of Maxor, Storm, Goldor and Necron?", new String[]{"The Wither Lords"}); + answers.put("Which brother is on the Spider's Den?", new String[]{"Rick"}); + answers.put("What is the name of Rick's brother?", new String[]{"Pat"}); + answers.put("What is the name of the Painter in the Hub?", new String[]{"Marco"}); + answers.put("What is the name of the person that upgrades pets?", new String[]{"Kat"}); + answers.put("What is the name of the lady of the Nether?", new String[]{"Elle"}); + answers.put("Which villager in the Village gives you a Rogue Sword?", new String[]{"Jamie"}); + answers.put("How many unique minions are there?", new String[]{"59 Minions"}); + answers.put("Which of these enemies does not spawn in the Spider's Den?", new String[]{"Zombie Spider", "Cave Spider", "Wither Skeleton", "Dashing Spooder", "Broodfather", "Night Spider"}); + answers.put("Which of these monsters only spawns at night?", new String[]{"Zombie Villager", "Ghast"}); + answers.put("Which of these is not a dragon in The End?", new String[]{"Zoomer Dragon", "Weak Dragon", "Stonk Dragon", "Holy Dragon", "Boomer Dragon", "Booger Dragon", "Older Dragon", "Elder Dragon", "Stable Dragon", "Professor Dragon"}); + FairySouls.runAsyncAfterFairySoulsLoad(() -> { + answers.put("How many total Fairy Souls are there?", getFairySoulsSizeString(null)); + answers.put("How many Fairy Souls are there in Spider's Den?", getFairySoulsSizeString("combat_1")); + answers.put("How many Fairy Souls are there in The End?", getFairySoulsSizeString("combat_3")); + answers.put("How many Fairy Souls are there in The Farming Islands?", getFairySoulsSizeString("farming_1")); + answers.put("How many Fairy Souls are there in Crimson Isle?", getFairySoulsSizeString("crimson_isle")); + answers.put("How many Fairy Souls are there in The Park?", getFairySoulsSizeString("foraging_1")); + answers.put("How many Fairy Souls are there in Jerry's Workshop?", getFairySoulsSizeString("winter")); + answers.put("How many Fairy Souls are there in Hub?", getFairySoulsSizeString("hub")); + answers.put("How many Fairy Souls are there in The Hub?", getFairySoulsSizeString("hub")); + answers.put("How many Fairy Souls are there in Deep Caverns?", getFairySoulsSizeString("mining_2")); + answers.put("How many Fairy Souls are there in Gold Mine?", getFairySoulsSizeString("mining_1")); + answers.put("How many Fairy Souls are there in Dungeon Hub?", getFairySoulsSizeString("dungeon_hub")); + }); + } + + @NotNull + private static String[] getFairySoulsSizeString(@Nullable String location) { + return new String[]{"%d Fairy Souls".formatted(FairySouls.getFairySoulsSize(location))}; + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java index b686607b..931d1d69 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DebugRoom.java @@ -1,26 +1,38 @@ package de.hysky.skyblocker.skyblock.dungeon.secrets; import de.hysky.skyblocker.utils.waypoint.Waypoint; +import it.unimi.dsi.fastutil.ints.IntRBTreeSet; +import it.unimi.dsi.fastutil.ints.IntSortedSet; +import it.unimi.dsi.fastutil.ints.IntSortedSets; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.minecraft.client.world.ClientWorld; import net.minecraft.registry.Registries; import net.minecraft.util.math.BlockPos; import org.apache.commons.lang3.tuple.MutableTriple; -import org.jetbrains.annotations.NotNull; import org.joml.Vector2ic; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; +import java.util.*; public class DebugRoom extends Room { private final List checkedBlocks = Collections.synchronizedList(new ArrayList<>()); - public DebugRoom(@NotNull Type type, @NotNull Vector2ic... physicalPositions) { + public DebugRoom(Type type, Vector2ic... physicalPositions) { super(type, physicalPositions); } + public static DebugRoom ofSinglePossibleRoom(Type type, Vector2ic physicalPositions, String roomName, int[] roomData, Direction direction) { + return ofSinglePossibleRoom(type, new Vector2ic[]{physicalPositions}, roomName, roomData, direction); + } + + public static DebugRoom ofSinglePossibleRoom(Type type, Vector2ic[] physicalPositions, String roomName, int[] roomData, Direction direction) { + DebugRoom room = new DebugRoom(type, physicalPositions); + IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::x).toArray())); + IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::y).toArray())); + room.roomsData = Map.of(roomName, roomData); + room.possibleRooms = List.of(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), List.of(roomName))); + return room; + } + @Override protected boolean checkBlock(ClientWorld world, BlockPos pos) { byte id = DungeonManager.NUMERIC_ID.getByte(Registries.BLOCK.getId(world.getBlockState(pos).getBlock()).toString()); @@ -37,7 +49,7 @@ public class DebugRoom extends Room { } @Override - protected void render(WorldRenderContext context) { + public void render(WorldRenderContext context) { super.render(context); synchronized (checkedBlocks) { for (Waypoint checkedBlock : checkedBlocks) { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java index 52915b98..722ecd85 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonManager.java @@ -18,9 +18,6 @@ import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; -import it.unimi.dsi.fastutil.ints.IntRBTreeSet; -import it.unimi.dsi.fastutil.ints.IntSortedSet; -import it.unimi.dsi.fastutil.ints.IntSortedSets; import it.unimi.dsi.fastutil.objects.Object2ByteMap; import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIntPair; @@ -43,6 +40,7 @@ import net.minecraft.entity.ItemEntity; import net.minecraft.entity.LivingEntity; import net.minecraft.entity.mob.AmbientEntity; import net.minecraft.entity.passive.BatEntity; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.FilledMapItem; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; @@ -59,7 +57,6 @@ import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3i; import net.minecraft.world.World; -import org.apache.commons.lang3.tuple.MutableTriple; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -200,6 +197,10 @@ public class DungeonManager { return customWaypoints.remove(room, pos); } + public static Room getCurrentRoom() { + return currentRoom; + } + /** * Loads the dungeon secrets asynchronously from {@code /assets/skyblocker/dungeons}. * Use {@link #isRoomsLoaded()} to check for completion of loading. @@ -232,12 +233,13 @@ public class DungeonManager { if (Debug.debugEnabled()) { ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("secrets") .then(literal("matchAgainst").then(matchAgainstCommand())) - .then(literal("clearSubRooms").executes(context -> { + .then(literal("clearSubProcesses").executes(context -> { if (currentRoom != null) { - currentRoom.subRooms.clear(); - context.getSource().sendFeedback(Constants.PREFIX.get().append("§rCleared sub rooms in the current room")); + currentRoom.tickables.clear(); + currentRoom.renderables.clear(); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rCleared sub processes in the current room.")); } else { - context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null")); + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); } return Command.SINGLE_SUCCESS; })) @@ -427,57 +429,58 @@ public class DungeonManager { private static RequiredArgumentBuilder matchAgainstCommand() { return argument("room", StringArgumentType.string()).suggests((context, builder) -> CommandSource.suggestMatching(ROOMS_DATA.values().stream().map(Map::values).flatMap(Collection::stream).map(Map::keySet).flatMap(Collection::stream), builder)).then(argument("direction", Room.Direction.DirectionArgumentType.direction()).executes(context -> { if (physicalEntrancePos == null || mapEntrancePos == null || mapRoomSize == 0) { - context.getSource().sendError(Constants.PREFIX.get().append("§cYou are not in a dungeon")); + context.getSource().sendError(Constants.PREFIX.get().append("§cYou are not in a dungeon.")); return Command.SINGLE_SUCCESS; } MinecraftClient client = MinecraftClient.getInstance(); if (client.player == null || client.world == null) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get player or world")); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get player or world.")); return Command.SINGLE_SUCCESS; } ItemStack stack = client.player.getInventory().main.get(8); if (!stack.isOf(Items.FILLED_MAP)) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map")); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map.")); return Command.SINGLE_SUCCESS; } MapState map = FilledMapItem.getMapState(FilledMapItem.getMapId(stack), client.world); if (map == null) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map state")); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to get dungeon map state.")); return Command.SINGLE_SUCCESS; } String roomName = StringArgumentType.getString(context, "room"); Room.Direction direction = Room.Direction.DirectionArgumentType.getDirection(context, "direction"); - Room room = null; - int[] roomData; - if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.PUZZLE.shape).get(roomName)) != null) { - room = new DebugRoom(Room.Type.PUZZLE, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())); - } else if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.TRAP.shape).get(roomName)) != null) { - room = new DebugRoom(Room.Type.TRAP, DungeonMapUtils.getPhysicalRoomPos(client.player.getPos())); - } else if ((roomData = ROOMS_DATA.get("catacombs").values().stream().map(Map::entrySet).flatMap(Collection::stream).filter(entry -> entry.getKey().equals(roomName)).findAny().map(Map.Entry::getValue).orElse(null)) != null) { - room = new DebugRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapRoomPos(map, mapEntrancePos, mapRoomSize), mapRoomSize, Room.Type.ROOM.color))); - } - + Room room = newDebugRoom(roomName, direction, client.player, map); if (room == null) { - context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to find room with name " + roomName)); + context.getSource().sendError(Constants.PREFIX.get().append("§cFailed to find room with name " + roomName + ".")); return Command.SINGLE_SUCCESS; } - IntSortedSet segmentsX = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::x).toArray())); - IntSortedSet segmentsY = IntSortedSets.unmodifiable(new IntRBTreeSet(room.segments.stream().mapToInt(Vector2ic::y).toArray())); - room.roomsData = Map.of(roomName, roomData); - room.possibleRooms = List.of(MutableTriple.of(direction, DungeonMapUtils.getPhysicalCornerPos(direction, segmentsX, segmentsY), List.of(roomName))); if (currentRoom != null) { - currentRoom.subRooms.add(room); - context.getSource().sendFeedback(Constants.PREFIX.get().append("§rMatching room " + roomName + " with direction " + direction + " against current room")); + currentRoom.addSubProcess(room); + context.getSource().sendFeedback(Constants.PREFIX.get().append("§rMatching room " + roomName + " with direction " + direction + " against current room.")); } else { - context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null")); + context.getSource().sendError(Constants.PREFIX.get().append("§cCurrent room is null.")); } return Command.SINGLE_SUCCESS; })); } + @Nullable + private static Room newDebugRoom(String roomName, Room.Direction direction, PlayerEntity player, MapState map) { + Room room = null; + int[] roomData; + if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.PUZZLE.shape).get(roomName)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.PUZZLE, DungeonMapUtils.getPhysicalRoomPos(player.getPos()), roomName, roomData, direction); + } else if ((roomData = ROOMS_DATA.get("catacombs").get(Room.Shape.TRAP.shape).get(roomName)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.TRAP, DungeonMapUtils.getPhysicalRoomPos(player.getPos()), roomName, roomData, direction); + } else if ((roomData = ROOMS_DATA.get("catacombs").values().stream().map(Map::entrySet).flatMap(Collection::stream).filter(entry -> entry.getKey().equals(roomName)).findAny().map(Map.Entry::getValue).orElse(null)) != null) { + room = DebugRoom.ofSinglePossibleRoom(Room.Type.ROOM, DungeonMapUtils.getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, DungeonMapUtils.getRoomSegments(map, DungeonMapUtils.getMapRoomPos(map, mapEntrancePos, mapRoomSize), mapRoomSize, Room.Type.ROOM.color)), roomName, roomData, direction); + } + return room; + } + /** * Updates the dungeon. The general idea is similar to the Dungeon Rooms Mod. *

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

@@ -285,15 +293,16 @@ public class Room { * */ @SuppressWarnings("JavadocReference") - protected void update() { + @Override + public void tick() { MinecraftClient client = MinecraftClient.getInstance(); ClientWorld world = client.world; if (world == null) { return; } - for (Room subRoom : subRooms) { - subRoom.update(); + for (Tickable tickable : tickables) { + tickable.tick(); } // Wither and blood door @@ -506,9 +515,10 @@ public class Room { /** * Calls {@link SecretWaypoint#render(WorldRenderContext)} on {@link #secretWaypoints all secret waypoints} and renders a highlight around the wither or blood door, if it exists. */ - protected void render(WorldRenderContext context) { - for (Room subRoom : subRooms) { - subRoom.render(context); + @Override + public void render(WorldRenderContext context) { + for (Renderable renderable : renderables) { + renderable.render(context); } if (SkyblockerConfigManager.get().locations.dungeons.secretWaypoints.enableSecretWaypoints && isMatched()) { diff --git a/src/main/java/de/hysky/skyblocker/utils/Tickable.java b/src/main/java/de/hysky/skyblocker/utils/Tickable.java new file mode 100644 index 00000000..9b7b2e3f --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/Tickable.java @@ -0,0 +1,5 @@ +package de.hysky.skyblocker.utils; + +public interface Tickable { + void tick(); +} diff --git a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java index 2c75ef0a..42f890b7 100644 --- a/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java +++ b/src/main/java/de/hysky/skyblocker/utils/chat/ChatMessageListener.java @@ -5,8 +5,8 @@ import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.skyblock.barn.HungryHiker; import de.hysky.skyblocker.skyblock.barn.TreasureHunter; import de.hysky.skyblocker.skyblock.dungeon.Reparty; -import de.hysky.skyblocker.skyblock.dungeon.ThreeWeirdos; -import de.hysky.skyblocker.skyblock.dungeon.Trivia; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.ThreeWeirdos; +import de.hysky.skyblocker.skyblock.dungeon.puzzle.Trivia; import de.hysky.skyblocker.skyblock.dwarven.Fetchur; import de.hysky.skyblocker.skyblock.dwarven.Puzzler; import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; diff --git a/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java b/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java new file mode 100644 index 00000000..b7743153 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/Renderable.java @@ -0,0 +1,7 @@ +package de.hysky.skyblocker.utils.render; + +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; + +public interface Renderable { + void render(WorldRenderContext context); +} diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java deleted file mode 100644 index 3772fd75..00000000 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/ThreeWeirdosTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; -import org.junit.jupiter.api.Test; - -class ThreeWeirdosTest extends ChatPatternListenerTest { - public ThreeWeirdosTest() { - super(new ThreeWeirdos()); - } - - @Test - void test1() { - assertGroup("§e[NPC] §cBaxter§f: My chest doesn't have the reward. We are all telling the truth.", 1, "Baxter"); - } - @Test - void test2() { - assertGroup("§e[NPC] §cHope§f: The reward isn't in any of our chests.", 1, "Hope"); - } -} \ No newline at end of file diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java deleted file mode 100644 index 1df5a8e1..00000000 --- a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/TriviaTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon; - -import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; -import org.junit.jupiter.api.Test; - -class TriviaTest extends ChatPatternListenerTest { - public TriviaTest() { - super(new Trivia()); - } - - @Test - void anyQuestion1() { - assertGroup(" What is the first question?", 1, "What is the first question?"); - } - - @Test - void anyQestion2() { - assertGroup(" How many questions are there?", 1, "How many questions are there?"); - } - - @Test - void answer1() { - assertGroup(" §6 ⓐ §aAnswer 1", 3, "Answer 1"); - } - @Test - void answer2() { - assertGroup(" §6 ⓑ §aAnswer 2", 3, "Answer 2"); - } - @Test - void answer3() { - assertGroup(" §6 ⓒ §aAnswer 3", 3, "Answer 3"); - } -} \ No newline at end of file diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java new file mode 100644 index 00000000..22683698 --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/ThreeWeirdosTest.java @@ -0,0 +1,19 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; +import org.junit.jupiter.api.Test; + +class ThreeWeirdosTest extends ChatPatternListenerTest { + public ThreeWeirdosTest() { + super(new ThreeWeirdos()); + } + + @Test + void test1() { + assertGroup("§e[NPC] §cBaxter§f: My chest doesn't have the reward. We are all telling the truth.", 1, "Baxter"); + } + @Test + void test2() { + assertGroup("§e[NPC] §cHope§f: The reward isn't in any of our chests.", 1, "Hope"); + } +} \ No newline at end of file diff --git a/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java new file mode 100644 index 00000000..55a59a68 --- /dev/null +++ b/src/test/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TriviaTest.java @@ -0,0 +1,33 @@ +package de.hysky.skyblocker.skyblock.dungeon.puzzle; + +import de.hysky.skyblocker.utils.chat.ChatPatternListenerTest; +import org.junit.jupiter.api.Test; + +class TriviaTest extends ChatPatternListenerTest { + public TriviaTest() { + super(new Trivia()); + } + + @Test + void anyQuestion1() { + assertGroup(" What is the first question?", 1, "What is the first question?"); + } + + @Test + void anyQestion2() { + assertGroup(" How many questions are there?", 1, "How many questions are there?"); + } + + @Test + void answer1() { + assertGroup(" §6 ⓐ §aAnswer 1", 3, "Answer 1"); + } + @Test + void answer2() { + assertGroup(" §6 ⓑ §aAnswer 2", 3, "Answer 2"); + } + @Test + void answer3() { + assertGroup(" §6 ⓒ §aAnswer 3", 3, "Answer 3"); + } +} \ No newline at end of file -- cgit