diff options
| author | SpaceMonkeyy86 <jackreynolds9128@gmail.com> | 2025-06-14 14:17:57 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-14 22:17:57 +0800 |
| commit | 2d63e6088aa588556b588070e826ce6c0ec1492d (patch) | |
| tree | 633424000f41029eae67981d4ff607c870deff66 /src/main/java | |
| parent | cd29a1475253ff3ca8098765875a45fdbefd75e7 (diff) | |
| download | Skyblocker-2d63e6088aa588556b588070e826ce6c0ec1492d.tar.gz Skyblocker-2d63e6088aa588556b588070e826ce6c0ec1492d.tar.bz2 Skyblocker-2d63e6088aa588556b588070e826ce6c0ec1492d.zip | |
One flow waterboard solver (#1283)
* Port Desco19's waterboard solver to Skyblocker
* Many waterboard tweaks
* Update watertimes.json to newer version and fix leftover water check
* Visualize water paths on board
* General improvements
* Preview lever effects
* Fix preview hitboxes being too small in rooms with an entrance from below
* Refactor into multiple files and add config options
* Benchmark all solutions and improve two of them
* Show indicator line from next lever to the lever after that
* Optimize many of the slower solutions
* Add marks support for easier solution iteration
* Tweak comments
* Clean up one flow waterboard solver
* Add suggested comments
* Add lever type argument type and debug command
* Verify json file
* Make commands debug only and make feedback translatable
* Update VerifyJsonTest
* Make color codes less scuffed
---------
Co-authored-by: Kevinthegreat <92656833+kevinthegreat1@users.noreply.github.com>
Diffstat (limited to 'src/main/java')
10 files changed, 953 insertions, 550 deletions
diff --git a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java index 200745bc..9be3165a 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -155,11 +155,25 @@ public class DungeonsCategory { .option(Option.<Boolean>createBuilder() .name(Text.translatable("skyblocker.config.dungeons.puzzle.solveWaterboard")) .description(OptionDescription.of(Text.translatable("skyblocker.config.dungeons.puzzle.solveWaterboard.@Tooltip"))) - .binding(defaults.dungeons.puzzleSolvers.solveWaterboard, - () -> config.dungeons.puzzleSolvers.solveWaterboard, - newValue -> config.dungeons.puzzleSolvers.solveWaterboard = newValue) + .binding(defaults.dungeons.puzzleSolvers.waterboardOneFlow, + () -> config.dungeons.puzzleSolvers.waterboardOneFlow, + newValue -> config.dungeons.puzzleSolvers.waterboardOneFlow = newValue) .controller(ConfigUtils::createBooleanController) .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.puzzle.previewWaterPath")) + .binding(defaults.dungeons.puzzleSolvers.previewWaterPath, + () -> config.dungeons.puzzleSolvers.previewWaterPath, + newValue -> config.dungeons.puzzleSolvers.previewWaterPath = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.<Boolean>createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.puzzle.previewLeverEffects")) + .binding(defaults.dungeons.puzzleSolvers.previewLeverEffects, + () -> config.dungeons.puzzleSolvers.previewLeverEffects, + newValue -> config.dungeons.puzzleSolvers.previewLeverEffects = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) .option(Option.<Boolean>createBuilder() .name(Text.translatable("skyblocker.config.dungeons.puzzle.blazeSolver")) .description(OptionDescription.of(Text.translatable("skyblocker.config.dungeons.puzzle.blazeSolver.@Tooltip"))) diff --git a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java index 8b80f5ac..2b605bcc 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java @@ -96,8 +96,17 @@ public class DungeonsConfig { @SerialEntry public boolean creeperSolver = true; + @Deprecated + public transient boolean solveWaterboard = true; + @SerialEntry - public boolean solveWaterboard = true; + public boolean waterboardOneFlow = true; + + @SerialEntry + public boolean previewWaterPath = true; + + @SerialEntry + public boolean previewLeverEffects = true; @SerialEntry public boolean blazeSolver = true; 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 index 42034d9f..c0dddb88 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonPuzzle.java @@ -36,17 +36,17 @@ public abstract class DungeonPuzzle implements Tickable, Renderable, Resettable shouldSolve = true; } }); - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("puzzle").then(literal(puzzleName).then(literal("solve").executes(context -> { - Room currentRoom = DungeonManager.getCurrentRoom(); - if (currentRoom != null) { - reset(); - 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; - }))))))); + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("puzzle").then(literal(puzzleName).then(literal("solve").executes(context -> { + Room currentRoom = DungeonManager.getCurrentRoom(); + if (currentRoom != null) { + reset(); + 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(this); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Cell.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Cell.java deleted file mode 100644 index 0279fed8..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Cell.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard; - -public class Cell { - public static final Cell BLOCK = new Cell(Type.BLOCK); - public static final Cell EMPTY = new Cell(Type.EMPTY); - public final Type type; - - private Cell(Type type) { - this.type = type; - } - - public boolean isOpen() { - return type == Type.EMPTY; - } - - public static class SwitchCell extends Cell { - public final int id; - private boolean open; - - public SwitchCell(int id) { - super(Type.SWITCH); - this.id = id; - } - - public static SwitchCell ofOpened(int id) { - SwitchCell switchCell = new SwitchCell(id); - switchCell.open = true; - return switchCell; - } - - @Override - public boolean equals(Object obj) { - return super.equals(obj) || obj instanceof SwitchCell switchCell && id == switchCell.id && open == switchCell.open; - } - - @Override - public boolean isOpen() { - return open; - } - - public void toggle() { - open = !open; - } - } - - public enum Type { - BLOCK, - EMPTY, - SWITCH - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Switch.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Switch.java deleted file mode 100644 index bb8da61d..00000000 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Switch.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard; - -import org.jetbrains.annotations.NotNull; - -import java.util.AbstractCollection; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -public class Switch extends AbstractCollection<Cell.SwitchCell> { - public final int id; - public final List<Cell.SwitchCell> cells = new ArrayList<>(); - - public Switch(int id) { - this.id = id; - } - - @Override - @NotNull - public Iterator<Cell.SwitchCell> iterator() { - return cells.iterator(); - } - - @Override - public int size() { - return cells.size(); - } - - @Override - public boolean add(Cell.SwitchCell cell) { - return cells.add(cell); - } - - public void toggle() { - for (Cell.SwitchCell cell : cells) { - cell.toggle(); - } - } -} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java index 708a86ee..a89918f4 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java @@ -1,453 +1,110 @@ package de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard; -import com.google.common.collect.Multimap; -import com.google.common.collect.MultimapBuilder; -import com.mojang.brigadier.Command; -import com.mojang.brigadier.arguments.IntegerArgumentType; -import de.hysky.skyblocker.SkyblockerMod; -import de.hysky.skyblocker.annotations.Init; -import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.debug.Debug; -import de.hysky.skyblocker.skyblock.dungeon.puzzle.DungeonPuzzle; -import de.hysky.skyblocker.skyblock.dungeon.puzzle.waterboard.Cell.SwitchCell; -import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; -import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; -import de.hysky.skyblocker.utils.ColorUtils; -import de.hysky.skyblocker.utils.Constants; -import de.hysky.skyblocker.utils.render.RenderHelper; -import de.hysky.skyblocker.utils.scheduler.Scheduler; -import de.hysky.skyblocker.utils.waypoint.Waypoint; -import it.unimi.dsi.fastutil.ints.IntArrayList; -import it.unimi.dsi.fastutil.ints.IntList; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.Object2IntMaps; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; -import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; -import net.fabricmc.fabric.api.event.player.UseBlockCallback; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.serialization.Codec; import net.minecraft.block.Block; -import net.minecraft.block.BlockState; import net.minecraft.block.Blocks; -import net.minecraft.block.LeverBlock; -import net.minecraft.client.MinecraftClient; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.fluid.Fluids; -import net.minecraft.fluid.WaterFluid; -import net.minecraft.util.ActionResult; +import net.minecraft.command.argument.EnumArgumentType; import net.minecraft.util.DyeColor; -import net.minecraft.util.Hand; -import net.minecraft.util.hit.BlockHitResult; -import net.minecraft.util.hit.HitResult; +import net.minecraft.util.StringIdentifiable; import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.Direction; -import net.minecraft.util.math.Vec3d; -import net.minecraft.world.World; -import org.joml.Vector2i; -import org.joml.Vector2ic; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.*; -import java.util.concurrent.CompletableFuture; - -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; -import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; - -public class Waterboard extends DungeonPuzzle { - private static final Logger LOGGER = LoggerFactory.getLogger(Waterboard.class); - public static final Waterboard INSTANCE = new Waterboard(); - private static final Object2IntMap<Block> SWITCH_BLOCKS = Object2IntMaps.unmodifiable(new Object2IntOpenHashMap<>(Map.of( - Blocks.COAL_BLOCK, 1, - Blocks.GOLD_BLOCK, 2, - Blocks.QUARTZ_BLOCK, 3, - Blocks.DIAMOND_BLOCK, 4, - Blocks.EMERALD_BLOCK, 5, - Blocks.TERRACOTTA, 6 - ))); - private static final BlockPos[] SWITCH_POSITIONS = new BlockPos[]{ - new BlockPos(20, 61, 10), - new BlockPos(20, 61, 15), - new BlockPos(20, 61, 20), - new BlockPos(10, 61, 20), - new BlockPos(10, 61, 15), - new BlockPos(10, 61, 10) - }; - public static final BlockPos WATER_LEVER = new BlockPos(15, 60, 5); - private static final float[] LIME_COLOR_COMPONENTS = ColorUtils.getFloatComponents(DyeColor.LIME); - - private CompletableFuture<Void> solve; - private final Cell[][] cells = new Cell[19][19]; - private final Switch[] switches = new Switch[]{new Switch(0), new Switch(1), new Switch(2), new Switch(3), new Switch(4), new Switch(5)}; - private int doors = 0; - private final Result[] results = new Result[64]; - private int currentCombination; - private final IntList bestCombinations = new IntArrayList(); - private final Waypoint[] waypoints = new Waypoint[7]; - /** - * Used to check the water lever state since the block state does not update immediately after the lever is toggled. - */ - private boolean bestCombinationsUpdated; - - private Waterboard() { - super("waterboard", "water-puzzle"); - } - - @Init - public static void init() { - UseBlockCallback.EVENT.register(INSTANCE::onUseBlock); - if (Debug.debugEnabled()) { - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("puzzle").then(literal(INSTANCE.puzzleName) - .then(literal("printBoard").executes(context -> { - context.getSource().sendFeedback(Constants.PREFIX.get().append(boardToString(INSTANCE.cells))); - return Command.SINGLE_SUCCESS; - })).then(literal("printDoors").executes(context -> { - context.getSource().sendFeedback(Constants.PREFIX.get().append(Integer.toBinaryString(INSTANCE.doors))); - return Command.SINGLE_SUCCESS; - })).then(literal("printSimulationResults").then(argument("combination", IntegerArgumentType.integer(0, 63)).executes(context -> { - context.getSource().sendFeedback(Constants.PREFIX.get().append(INSTANCE.results[IntegerArgumentType.getInteger(context, "combination")].toString())); - return Command.SINGLE_SUCCESS; - }))).then(literal("printCurrentCombination").executes(context -> { - context.getSource().sendFeedback(Constants.PREFIX.get().append(Integer.toBinaryString(INSTANCE.currentCombination))); - return Command.SINGLE_SUCCESS; - })).then(literal("printBestCombination").executes(context -> { - context.getSource().sendFeedback(Constants.PREFIX.get().append(INSTANCE.bestCombinations.toString())); - return Command.SINGLE_SUCCESS; - })) - ))))); - } - } - - private static String boardToString(Cell[][] cells) { - StringBuilder sb = new StringBuilder(); - for (Cell[] row : cells) { - sb.append("\n"); - for (Cell cell : row) { - switch (cell) { - case SwitchCell switchCell -> sb.append(switchCell.id); - case Cell c when c.type == Cell.Type.BLOCK -> sb.append('#'); - case Cell c when c.type == Cell.Type.EMPTY -> sb.append('.'); - - case null, default -> sb.append('?'); - } - } - } - return sb.toString(); - } - - @Override - public void tick(MinecraftClient client) { - if (!SkyblockerConfigManager.get().dungeons.puzzleSolvers.solveWaterboard || client.world == null || !DungeonManager.isCurrentRoomMatched() || solve != null && !solve.isDone()) { - return; - } - Room room = DungeonManager.getCurrentRoom(); - solve = CompletableFuture.runAsync(() -> { - Changed changed = updateBoard(client.world, room); - if (changed == Changed.NONE) { - return; - } - if (results[0] == null) { - updateSwitches(); - simulateCombinations(); - clearSwitches(); - } - if (bestCombinations.isEmpty() || changed.doorChanged()) { - findBestCombinations(); - bestCombinationsUpdated = true; - } - }).exceptionally(e -> { - LOGGER.error("[Skyblocker Waterboard] Encountered an unknown exception while solving waterboard.", e); - return null; - }); - if (waypoints[0] == null) { - for (int i = 0; i < 6; i++) { - waypoints[i] = new Waypoint(room.relativeToActual(SWITCH_POSITIONS[i]), Waypoint.Type.HIGHLIGHT, LIME_COLOR_COMPONENTS); - } - waypoints[6] = new Waypoint(room.relativeToActual(WATER_LEVER), Waypoint.Type.HIGHLIGHT, LIME_COLOR_COMPONENTS); - waypoints[6].setFound(); - } - } - - private Changed updateBoard(World world, Room room) { - // Parse the waterboard. - BlockPos.Mutable pos = new BlockPos.Mutable(24, 78, 26); - Changed changed = Changed.NONE; - for (int row = 0; row < cells.length; pos.move(cells[row].length, -1, 0), row++) { - for (int col = 0; col < cells[row].length; pos.move(Direction.WEST), col++) { - Cell cell = parseBlock(world, room, pos); - if (!cell.equals(cells[row][col])) { - cells[row][col] = cell; - changed = changed.onCellChanged(); - } - } - } - - // Parse door states. - pos.set(15, 57, 15); - int prevDoors = doors; - doors = 0; - for (int i = 0; i < 5; pos.move(Direction.SOUTH), i++) { - doors |= world.getBlockState(room.relativeToActual(pos)).isAir() ? 1 << i : 0; - } - if (doors != prevDoors) { - changed = changed.onDoorChanged(); - } - - // Parse current combination of switches based on the levers. - currentCombination = 0; - for (int i = 0; i < 6; i++) { - currentCombination |= getSwitchState(world, room, i); - } - - return changed; - } - - private Cell parseBlock(World world, Room room, BlockPos.Mutable pos) { - // Check if the block is a switch. - BlockState state = world.getBlockState(room.relativeToActual(pos)); - int switch_ = SWITCH_BLOCKS.getInt(state.getBlock()); - if (switch_-- > 0) { - return new SwitchCell(switch_); - } - // Check if the block is an opened switch by checking the block behind it. - int switchBehind = SWITCH_BLOCKS.getInt(world.getBlockState(room.relativeToActual(pos.move(Direction.SOUTH))).getBlock()); - pos.move(Direction.NORTH); - if (switchBehind-- > 0) { - return SwitchCell.ofOpened(switchBehind); - } - - // Check if the block is empty otherwise the block is a wall. - return state.isAir() || state.isOf(Blocks.WATER) ? Cell.EMPTY : Cell.BLOCK; - } - - private static int getSwitchState(World world, Room room, int i) { - BlockState state = world.getBlockState(room.relativeToActual(SWITCH_POSITIONS[i])); - return state.contains(LeverBlock.POWERED) && state.get(LeverBlock.POWERED) ? 1 << i : 0; - } - - private void updateSwitches() { - clearSwitches(); - for (Cell[] row : cells) { - for (Cell cell : row) { - if (cell instanceof SwitchCell switchCell) { - switches[switchCell.id].add(switchCell); - } - } - } - } - - private void simulateCombinations() { - for (int combination = 0; combination < (1 << 6); combination++) { - for (int switchIndex = 0; switchIndex < 6; switchIndex++) { - if ((combination & (1 << switchIndex)) != 0) { - switches[switchIndex].toggle(); - } - } - results[combination] = simulateCombination(); - for (int switchIndex = 0; switchIndex < 6; switchIndex++) { - if ((combination & (1 << switchIndex)) != 0) { - switches[switchIndex].toggle(); - } - } - } - } - - private Result simulateCombination() { - List<Vector2i> waters = new ArrayList<>(); - waters.add(new Vector2i(9, 0)); - Result result = new Result(); - while (!waters.isEmpty()) { - List<Vector2i> newWaters = new ArrayList<>(); - for (Iterator<Vector2i> watersIt = waters.iterator(); watersIt.hasNext(); ) { - Vector2i water = watersIt.next(); - // Check if the water has reached a door. - if (water.y == 18) { - switch (water.x) { - case 0 -> result.reachedDoors |= 1 << 4; - case 4 -> result.reachedDoors |= 1 << 3; - case 9 -> result.reachedDoors |= 1 << 2; - case 14 -> result.reachedDoors |= 1 << 1; - case 18 -> result.reachedDoors |= 1; - } - watersIt.remove(); - continue; - } - // Check if the water can flow down. - if (water.y < 18 && cells[water.y + 1][water.x].isOpen()) { - result.putPath(water, 0); - water.add(0, 1); - continue; - } - - // Get the offset to the first block on the left and the right that can flow down. - int leftFlowDownOffset = findFlowDown(water, false); - int rightFlowDownOffset = findFlowDown(water, true); - // Check if left down is in range and is closer than right down. - // Note 1: The yarn name "getFlowSpeed" is incorrect as it actually returns the maximum distance that water will check for a hole to flow towards. - // Note 2: Skyblock's maximum offset is 5 instead of 4 for some reason. - if (-leftFlowDownOffset <= ((WaterFluid) Fluids.WATER).getMaxFlowDistance(null) + 1 && -leftFlowDownOffset < rightFlowDownOffset) { - result.putPath(water, leftFlowDownOffset); - water.add(leftFlowDownOffset, 1); - continue; - } - // Check if right down is in range and closer than left down. - if (rightFlowDownOffset <= ((WaterFluid) Fluids.WATER).getMaxFlowDistance(null) + 1 && rightFlowDownOffset < -leftFlowDownOffset) { - result.putPath(water, rightFlowDownOffset); - water.add(rightFlowDownOffset, 1); - continue; - } - - // Else flow to both sides if in range. - if (leftFlowDownOffset > Integer.MIN_VALUE + 1) { - result.putPath(water, leftFlowDownOffset); - newWaters.add(new Vector2i(water).add(leftFlowDownOffset, 1)); - } - if (rightFlowDownOffset < Integer.MAX_VALUE) { - result.putPath(water, rightFlowDownOffset); - newWaters.add(new Vector2i(water).add(rightFlowDownOffset, 1)); - } - watersIt.remove(); - } - waters.addAll(newWaters); - } - return result; - } - - /** - * Finds the first block on the left that can flow down. - */ - private int findFlowDown(Vector2i water, boolean direction) { - for (int i = 0; water.x + i >= 0 && water.x + i < 19 && i > -8 && i < 8 && cells[water.y][water.x + i].isOpen(); i += direction ? 1 : -1) { - if (cells[water.y + 1][water.x + i].isOpen()) { - return i; - } - } - return direction ? Integer.MAX_VALUE : Integer.MIN_VALUE + 1; - } - - private void findBestCombinations() { - bestCombinations.clear(); - for (int combination = 0, bestScore = 0; combination < (1 << 6); combination++) { - int newScore = Integer.bitCount(results[combination].reachedDoors ^ doors); - if (newScore >= bestScore) { - if (newScore > bestScore) { - bestCombinations.clear(); - bestScore = newScore; - } - bestCombinations.add(combination); - } - } - } - - @Override - public void render(WorldRenderContext context) { - if (!SkyblockerConfigManager.get().dungeons.puzzleSolvers.solveWaterboard || !DungeonManager.isCurrentRoomMatched()) return; - Room room = DungeonManager.getCurrentRoom(); - - // Render the best combination. - @SuppressWarnings("resource") - BlockState state = context.world().getBlockState(room.relativeToActual(WATER_LEVER)); - // bestCombinationsUpdated is needed because bestCombinations does not update immediately after the lever is turned off. - if (waypoints[0] != null && bestCombinationsUpdated && state.contains(LeverBlock.POWERED) && !state.get(LeverBlock.POWERED)) { - bestCombinations.intStream().mapToObj(bestCombination -> currentCombination ^ bestCombination).min(Comparator.comparingInt(Integer::bitCount)).ifPresent(bestDifference -> { - for (int i = 0; i < 6; i++) { - if ((bestDifference & 1 << i) != 0) { - waypoints[i].render(context); - } - } - if (bestDifference == 0 && !waypoints[6].shouldRender()) { - waypoints[6].setMissing(); - } - }); - } - if (waypoints[6] != null && waypoints[6].shouldRender()) { - waypoints[6].render(context); - } - - // Render the current path of the water. - BlockPos.Mutable pos = new BlockPos.Mutable(15, 79, 26); - RenderHelper.renderLinesFromPoints(context, new Vec3d[]{Vec3d.ofCenter(room.relativeToActual(pos)), Vec3d.ofCenter(room.relativeToActual(pos.move(Direction.DOWN)))}, LIME_COLOR_COMPONENTS, 1f, 5f, true); - Result currentResult = results[currentCombination]; - if (currentResult != null) { - for (Map.Entry<Vector2ic, Integer> entry : currentResult.path.entries()) { - Vec3d start = Vec3d.ofCenter(room.relativeToActual(pos.set(24 - entry.getKey().x(), 78 - entry.getKey().y(), 26))); - Vec3d middle = Vec3d.ofCenter(room.relativeToActual(pos.move(Direction.WEST, entry.getValue()))); - Vec3d end = Vec3d.ofCenter(room.relativeToActual(pos.move(Direction.DOWN))); - RenderHelper.renderLinesFromPoints(context, new Vec3d[]{start, middle}, LIME_COLOR_COMPONENTS, 1f, 5f, true); - RenderHelper.renderLinesFromPoints(context, new Vec3d[]{middle, end}, LIME_COLOR_COMPONENTS, 1f, 5f, true); - } - } - } - - private ActionResult onUseBlock(PlayerEntity player, World world, Hand hand, BlockHitResult blockHitResult) { - BlockState state = world.getBlockState(blockHitResult.getBlockPos()); - if (SkyblockerConfigManager.get().dungeons.puzzleSolvers.solveWaterboard && blockHitResult.getType() == HitResult.Type.BLOCK && waypoints[6] != null && DungeonManager.isCurrentRoomMatched() && blockHitResult.getBlockPos().equals(DungeonManager.getCurrentRoom().relativeToActual(WATER_LEVER)) && state.contains(LeverBlock.POWERED)) { - if (!state.get(LeverBlock.POWERED)) { - bestCombinationsUpdated = false; - Scheduler.INSTANCE.schedule(() -> waypoints[6].setMissing(), 50); - } - waypoints[6].setFound(); - } - return ActionResult.PASS; - } - - @Override - public void reset() { - super.reset(); - solve = null; - for (Cell[] row : cells) { - Arrays.fill(row, null); - } - clearSwitches(); - doors = 0; - Arrays.fill(results, null); - currentCombination = 0; - bestCombinations.clear(); - Arrays.fill(waypoints, null); - } - - public void clearSwitches() { - for (Switch switch_ : switches) { - switch_.clear(); - } - } - - private enum Changed { - NONE, CELL, DOOR, BOTH; - - private boolean cellChanged() { - return this == CELL || this == BOTH; - } - - private boolean doorChanged() { - return this == DOOR || this == BOTH; - } - - private Changed onCellChanged() { - return switch (this) { - case NONE, CELL -> Changed.CELL; - case DOOR, BOTH -> Changed.BOTH; - }; - } - - private Changed onDoorChanged() { - return switch (this) { - case NONE, DOOR -> Changed.DOOR; - case CELL, BOTH -> Changed.BOTH; - }; - } - } - - public static class Result { - private int reachedDoors; - private final Multimap<Vector2ic, Integer> path = MultimapBuilder.hashKeys().arrayListValues().build(); - - public boolean putPath(Vector2i water, int offset) { - return path.put(new Vector2i(water), offset); - } - - @Override - public String toString() { - return "Result[reachedDoors=" + Integer.toBinaryString(reachedDoors) + ", path=" + path + ']'; - } - } +public class Waterboard { + public static final int BOARD_MIN_X = 6; + public static final int BOARD_MAX_X = 24; + public static final int BOARD_MIN_Y = 58; + public static final int BOARD_MAX_Y = 81; + public static final int BOARD_Z = 26; + // The top center of the grid, between the first two toggleable blocks + public static final BlockPos WATER_ENTRANCE_POSITION = new BlockPos(15, 78, 26); + + public enum LeverType implements StringIdentifiable { + COAL(Blocks.COAL_BLOCK, new BlockPos(20, 61, 10), DyeColor.RED, new BlockPos[]{ + new BlockPos(0, -2, 0), new BlockPos(2, -1, 1), + null, new BlockPos(5, -1, 0) + }), + GOLD(Blocks.GOLD_BLOCK, new BlockPos(20, 61, 15), DyeColor.YELLOW, new BlockPos[]{ + new BlockPos(1, -1, 0), new BlockPos(3, -2, 0), + new BlockPos(-4, -1, 1), new BlockPos(1, 0, 0) + }), + QUARTZ(Blocks.QUARTZ_BLOCK, new BlockPos(20, 61, 20), DyeColor.LIGHT_GRAY, new BlockPos[]{ + new BlockPos(1, -4, 1), new BlockPos(-1, 0, 0), + new BlockPos(1, 0, 0), new BlockPos(-1, 0, 1) + }), + DIAMOND(Blocks.DIAMOND_BLOCK, new BlockPos(10, 61, 20), DyeColor.CYAN, new BlockPos[]{ + new BlockPos(0, -5, 1), new BlockPos(-2, -1, 0), + new BlockPos(-1, 0, 1), new BlockPos(-3, -4, 1) + }), + EMERALD(Blocks.EMERALD_BLOCK, new BlockPos(10, 61, 15), DyeColor.LIME, new BlockPos[]{ + new BlockPos(-1, -10, 1), new BlockPos(1, 0, 1), + new BlockPos(-6, 0, 0), new BlockPos(1, -4, 0) + }), + TERRACOTTA(Blocks.TERRACOTTA, new BlockPos(10, 61, 10), DyeColor.ORANGE, new BlockPos[]{ + new BlockPos(-1, -1, 1), new BlockPos(0, -3, 1), + null, new BlockPos(-4, -5, 1) + }), + WATER(Blocks.LAVA, new BlockPos(15, 60, 5), DyeColor.LIGHT_BLUE, null); + + private static final Codec<LeverType> CODEC = StringIdentifiable.createCodec(LeverType::values); + + public final Block block; + public final BlockPos leverPos; + public final DyeColor color; + // Holds positions where the corresponding block is present in the initial state of each variant, offset from the water entrance position + // This is more reliable at detecting if the lever is active than looking at the lever's block state + public final BlockPos[] initialPositions; + + LeverType(Block block, BlockPos leverPos, DyeColor color, BlockPos[] initialPositions) { + this.block = block; + this.leverPos = leverPos; + this.color = color; + this.initialPositions = initialPositions; + } + + public static LeverType fromName(String name) { + for (LeverType leverType : LeverType.values()) { + if (leverType.name().equalsIgnoreCase(name)) { + return leverType; + } + } + return null; + } + + public static LeverType fromBlock(Block block) { + for (LeverType leverType : LeverType.values()) { + if (leverType.block == block) { + return leverType; + } + } + return null; + } + + public static LeverType fromPos(BlockPos leverPos) { + for (LeverType leverType : LeverType.values()) { + if (leverPos.equals(leverType.leverPos)) { + return leverType; + } + } + return null; + } + + @Override + public String asString() { + return name().toLowerCase(); + } + + public static class LeverTypeArgumentType extends EnumArgumentType<LeverType> { + private LeverTypeArgumentType() { + super(CODEC, LeverType::values); + } + + public static LeverTypeArgumentType leverType() { + return new LeverTypeArgumentType(); + } + + publ |
