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