diff options
author | Kevin <92656833+kevinthegreat1@users.noreply.github.com> | 2024-01-01 14:50:01 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-01-01 01:50:01 -0500 |
commit | 37eb5bfad25b1e0c3326ed27744c38f81513b5e4 (patch) | |
tree | f218b51ff9fe54597f7126115547fb15f12bbb4e | |
parent | 8005dd9afe963a461619ee3da603d8202292840b (diff) | |
download | Skyblocker-37eb5bfad25b1e0c3326ed27744c38f81513b5e4.tar.gz Skyblocker-37eb5bfad25b1e0c3326ed27744c38f81513b5e4.tar.bz2 Skyblocker-37eb5bfad25b1e0c3326ed27744c38f81513b5e4.zip |
Waterboard Solver (#467)
* Add waterboard detection and parsing
* Solve waterboard
* Render lines through walls
* Fix lines
* Add lever highlights
14 files changed, 604 insertions, 60 deletions
diff --git a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java index 9ce0df8d..190bda4f 100644 --- a/src/main/java/de/hysky/skyblocker/SkyblockerMod.java +++ b/src/main/java/de/hysky/skyblocker/SkyblockerMod.java @@ -12,6 +12,7 @@ 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.puzzle.waterboard.Waterboard; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.dungeon.secrets.SecretsTracker; import de.hysky.skyblocker.skyblock.dwarven.DwarvenHud; @@ -104,6 +105,7 @@ public class SkyblockerMod implements ClientModInitializer { DungeonMap.init(); DungeonManager.init(); DungeonBlaze.init(); + Waterboard.init(); ChestValue.init(); FireFreezeStaffTimer.init(); GuardianHealth.init(); 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 index 8de1e3fe..db195003 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/CreeperBeams.java @@ -37,13 +37,13 @@ public class CreeperBeams extends DungeonPuzzle { 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 final CreeperBeams INSTANCE = new CreeperBeams(); private static ArrayList<Beam> beams = new ArrayList<>(); private static BlockPos base = null; - private CreeperBeams(String puzzleName, String... roomName) { - super(puzzleName, roomName); + private CreeperBeams() { + super("creeper", "creeper-room"); } public static void init() { @@ -57,38 +57,34 @@ public class CreeperBeams extends DungeonPuzzle { } @Override - public void tick() { + public void tick(MinecraftClient client) { // 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()) { + if (client.world == null || client.player == null || !Utils.isInDungeons()) { return; } // try to find base if not found and solve if (base == null) { - base = findCreeperBase(player, world); + base = findCreeperBase(client.player, client.world); if (base == null) { return; } Vec3d creeperPos = new Vec3d(base.getX() + 0.5, BASE_Y + 1.75, base.getZ() + 0.5); - ArrayList<BlockPos> targets = findTargets(world, base); + ArrayList<BlockPos> targets = findTargets(client.world, base); beams = findLines(creeperPos, targets); } // update the beam states - beams.forEach(b -> b.updateState(world)); + beams.forEach(b -> b.updateState(client.world)); // check if the room is solved - if (!isTarget(world, base)) { + if (!isTarget(client.world, base)) { reset(); } } @@ -239,11 +235,11 @@ public class CreeperBeams extends DungeonPuzzle { if (toDo) { RenderHelper.renderOutline(wrc, outlineOne, color, 3, false); RenderHelper.renderOutline(wrc, outlineTwo, color, 3, false); - RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2); + RenderHelper.renderLinesFromPoints(wrc, line, color, 1, 2, false); } 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); + RenderHelper.renderLinesFromPoints(wrc, line, GREEN_COLOR_COMPONENTS, 0.75f, 1, false); } } } 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 index 5774eaef..6b435d3c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/DungeonBlaze.java @@ -26,15 +26,15 @@ 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 final DungeonBlaze INSTANCE = new DungeonBlaze(); 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); + private DungeonBlaze() { + super("blaze", "blaze-room-1-high", "blaze-room-1-low"); } public static void init() { @@ -44,14 +44,12 @@ public class DungeonBlaze extends DungeonPuzzle { * Updates the state of Blaze entities and triggers the rendering process if necessary. */ @Override - public void tick() { + public void tick(MinecraftClient client) { if (!shouldSolve()) { return; } - ClientWorld world = MinecraftClient.getInstance().world; - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (world == null || player == null || !Utils.isInDungeons()) return; - List<ObjectIntPair<ArmorStandEntity>> blazes = getBlazesInWorld(world, player); + if (client.world == null || client.player == null || !Utils.isInDungeons()) return; + List<ObjectIntPair<ArmorStandEntity>> blazes = getBlazesInWorld(client.world, client.player); sortBlazes(blazes); updateBlazeEntities(blazes); } @@ -143,7 +141,7 @@ public class DungeonBlaze extends DungeonPuzzle { Vec3d blazeCenter = blazeBox.getCenter(); Vec3d nextBlazeCenter = nextBlazeBox.getCenter(); - RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f); + RenderHelper.renderLinesFromPoints(wrc, new Vec3d[]{blazeCenter, nextBlazeCenter}, WHITE_COLOR_COMPONENTS, 1f, 5f, false); } } 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 04446e60..f5e0461d 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 @@ -17,7 +17,7 @@ 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; + protected final String puzzleName; @NotNull private final Set<String> roomNames; private boolean shouldSolve; @@ -35,16 +35,17 @@ public abstract class DungeonPuzzle implements Tickable, Renderable { shouldSolve = true; } }); - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("solvePuzzle").then(literal(puzzleName).executes(context -> { + 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((handler, sender, client) -> reset()); } 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 index 90028a4f..c8043288 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/TicTacToe.java @@ -8,8 +8,6 @@ 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; @@ -27,33 +25,29 @@ import java.util.List; 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 final TicTacToe INSTANCE = new TicTacToe(); private static Box nextBestMoveToMake = null; - private TicTacToe(String puzzleName, String... roomName) { - super(puzzleName, roomName); + private TicTacToe() { + super("tic-tac-toe", "tic-tac-toe-1"); } public static void init() { } @Override - public void tick() { + public void tick(MinecraftClient client) { 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; + if (client.world == null || client.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<ItemFrameEntity> itemFramesThatHoldMaps = world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); + Box searchBox = new Box(client.player.getX() - 21, client.player.getY() - 21, client.player.getZ() - 21, client.player.getX() + 21, client.player.getY() + 21, client.player.getZ() + 21); + List<ItemFrameEntity> itemFramesThatHoldMaps = client.world.getEntitiesByClass(ItemFrameEntity.class, searchBox, ItemFrameEntity::containsMap); try { //Only attempt to solve if its the player's turn @@ -64,7 +58,7 @@ public class TicTacToe extends DungeonPuzzle { char facing = 'X'; for (ItemFrameEntity itemFrame : itemFramesThatHoldMaps) { - MapState mapState = world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); + MapState mapState = client.world.getMapState(FilledMapItem.getMapName(itemFrame.getMapId().getAsInt())); if (mapState == null) continue; @@ -86,7 +80,7 @@ public class TicTacToe extends DungeonPuzzle { facing = 'Z'; } - Block block = world.getBlockState(blockPos).getBlock(); + Block block = client.world.getBlockState(blockPos).getBlock(); if (block == Blocks.AIR || block == Blocks.STONE_BUTTON) { leftmostRow = blockPos; column = i; 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 new file mode 100644 index 00000000..0279fed8 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Cell.java @@ -0,0 +1,51 @@ +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 new file mode 100644 index 00000000..bb8da61d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Switch.java @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000..0006e6e2 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/waterboard/Waterboard.java @@ -0,0 +1,450 @@ +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.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.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 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.util.DyeColor; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +import net.minecraft.util.hit.HitResult; +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 = DyeColor.LIME.getColorComponents(); + + 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"); + UseBlockCallback.EVENT.register(this::onUseBlock); + if (Debug.debugEnabled()) { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE).then(literal("dungeons").then(literal("puzzle").then(literal(puzzleName) + .then(literal("printBoard").executes(context -> { + context.getSource().sendFeedback(Constants.PREFIX.get().append(boardToString(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(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; + })) + ))))); + } + } + + public static void init() { + } + + private static String boardToString(Cell[][] cells) { + StringBuilder sb = new StringBuilder(); + for (Cell[] row : cells) { + sb.append("\n"); + for (Cell cell : row) { + if (cell == null) { + sb.append('?'); + } else if (cell instanceof SwitchCell switchCell) { + sb.append(switchCell.id); + } else switch (cell.type) { + case BLOCK -> sb.append('#'); + case EMPTY -> sb.append('.'); + } + } + } + return sb.toString(); + } + + @Override + public void tick(MinecraftClient client) { + if (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).getFlowSpeed(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).getFlowSpeed(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 (!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 (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 + ']'; + } + } +} 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 70a0fd8c..bd10767f 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 @@ -16,9 +16,11 @@ import de.hysky.skyblocker.config.SkyblockerConfig; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.debug.Debug; import de.hysky.skyblocker.utils.Constants; +import de.hysky.skyblocker.utils.Tickable; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.Object2ByteMap; +import it.unimi.dsi.fastutil.objects.Object2ByteMaps; import it.unimi.dsi.fastutil.objects.Object2ByteOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectIntPair; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; @@ -91,7 +93,7 @@ public class DungeonManager { * @implNote Not using {@link Registry#getId(Object) Registry#getId(Block)} and {@link Blocks Blocks} since this is also used by {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU}, which runs outside of Minecraft. */ @SuppressWarnings("JavadocReference") - protected static final Object2ByteMap<String> NUMERIC_ID = new Object2ByteOpenHashMap<>(Map.ofEntries( + protected static final Object2ByteMap<String> NUMERIC_ID = Object2ByteMaps.unmodifiable(new Object2ByteOpenHashMap<>(Map.ofEntries( Map.entry("minecraft:stone", (byte) 1), Map.entry("minecraft:diorite", (byte) 2), Map.entry("minecraft:polished_diorite", (byte) 3), @@ -113,7 +115,7 @@ public class DungeonManager { Map.entry("minecraft:gray_terracotta", (byte) 19), Map.entry("minecraft:cyan_terracotta", (byte) 20), Map.entry("minecraft:black_terracotta", (byte) 21) - )); + ))); /** * Block data for dungeon rooms. See {@link de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonRoomsDFU DungeonRoomsDFU} for format details and how it's generated. * All access to this map must check {@link #isRoomsLoaded()} to prevent concurrent modification. @@ -502,7 +504,7 @@ public class DungeonManager { * <li> Create a new room. </li> * </ul> * <li> Sets {@link #currentRoom} to the current room, either created from the previous step or from {@link #rooms}. </li> - * <li> Calls {@link Room#tick()} on {@link #currentRoom}. </li> + * <li> Calls {@link Tickable#tick(MinecraftClient)} on {@link #currentRoom}. </li> * </ul> */ @SuppressWarnings("JavadocReference") @@ -560,7 +562,7 @@ public class DungeonManager { } currentRoom = room; } - currentRoom.tick(); + currentRoom.tick(client); } /** @@ -728,7 +730,7 @@ public class DungeonManager { * * @return {@code true} if {@link #currentRoom} is not null and {@link #isRoomMatched(Room)} */ - private static boolean isCurrentRoomMatched() { + public static boolean isCurrentRoomMatched() { return isRoomMatched(currentRoom); } 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 4857e8fe..3e25a8f1 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 @@ -299,20 +299,18 @@ public class Room implements Tickable, Renderable { */ @SuppressWarnings("JavadocReference") @Override - public void tick() { - MinecraftClient client = MinecraftClient.getInstance(); - ClientWorld world = client.world; - if (world == null) { + public void tick(MinecraftClient client) { + if (client.world == null) { return; } for (Tickable tickable : tickables) { - tickable.tick(); + tickable.tick(client); } // Wither and blood door if (SkyblockerConfigManager.get().locations.dungeons.doorHighlight.enableDoorHighlight && doorPos == null) { - doorPos = DungeonMapUtils.getWitherBloodDoorPos(world, segments); + doorPos = DungeonMapUtils.getWitherBloodDoorPos(client.world, segments); if (doorPos != null) { doorBox = new Box(doorPos.getX(), doorPos.getY(), doorPos.getZ(), doorPos.getX() + DOOR_SIZE.getX(), doorPos.getY() + DOOR_SIZE.getY(), doorPos.getZ() + DOOR_SIZE.getZ()); } @@ -329,7 +327,7 @@ public class Room implements Tickable, Renderable { } findRoom = CompletableFuture.runAsync(() -> { for (BlockPos pos : BlockPos.iterate(player.getBlockPos().add(-5, -5, -5), player.getBlockPos().add(5, 5, 5))) { - if (segments.contains(DungeonMapUtils.getPhysicalRoomPos(pos)) && notInDoorway(pos) && checkedBlocks.add(pos) && checkBlock(world, pos)) { + if (segments.contains(DungeonMapUtils.getPhysicalRoomPos(pos)) && notInDoorway(pos) && checkedBlocks.add(pos) && checkBlock(client.world, pos)) { break; } } @@ -506,14 +504,14 @@ public class Room implements Tickable, Renderable { /** * Fails if !{@link #isMatched()} */ - protected BlockPos actualToRelative(BlockPos pos) { + public BlockPos actualToRelative(BlockPos pos) { return DungeonMapUtils.actualToRelative(direction, physicalCornerPos, pos); } /** * Fails if !{@link #isMatched()} */ - protected BlockPos relativeToActual(BlockPos pos) { + public BlockPos relativeToActual(BlockPos pos) { return DungeonMapUtils.relativeToActual(direction, physicalCornerPos, pos); } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java index 7849aff7..6629c377 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/waypoint/MythologicalRitual.java @@ -146,10 +146,10 @@ public class MythologicalRitual { } if (burrow.confirmed != TriState.FALSE) { if (burrow.nextBurrowLine != null) { - RenderHelper.renderLinesFromPoints(context, burrow.nextBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F); + RenderHelper.renderLinesFromPoints(context, burrow.nextBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F, false); } if (burrow.echoBurrowLine != null) { - RenderHelper.renderLinesFromPoints(context, burrow.echoBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F); + RenderHelper.renderLinesFromPoints(context, burrow.echoBurrowLine, ORANGE_COLOR_COMPONENTS, 0.5F, 5F, false); } } } diff --git a/src/main/java/de/hysky/skyblocker/utils/Tickable.java b/src/main/java/de/hysky/skyblocker/utils/Tickable.java index 9b7b2e3f..dff34e19 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Tickable.java +++ b/src/main/java/de/hysky/skyblocker/utils/Tickable.java @@ -1,5 +1,7 @@ package de.hysky.skyblocker.utils; +import net.minecraft.client.MinecraftClient; + public interface Tickable { - void tick(); + void tick(MinecraftClient client); } diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java index 7526c0a8..05514d02 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java @@ -140,8 +140,9 @@ public class RenderHelper { * @param colorComponents An array of R, G and B color components * @param alpha The alpha of the lines * @param lineWidth The width of the lines + * @param throughWalls Whether to render through walls or not */ - public static void renderLinesFromPoints(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, float lineWidth) { + public static void renderLinesFromPoints(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, float lineWidth, boolean throughWalls) { Vec3d camera = context.camera().getPos(); MatrixStack matrices = context.matrixStack(); @@ -163,6 +164,7 @@ public class RenderHelper { RenderSystem.defaultBlendFunc(); RenderSystem.disableCull(); RenderSystem.enableDepthTest(); + RenderSystem.depthFunc(throughWalls ? GL11.GL_ALWAYS : GL11.GL_LEQUAL); buffer.begin(DrawMode.LINE_STRIP, VertexFormats.LINES); @@ -182,6 +184,7 @@ public class RenderHelper { GL11.glDisable(GL11.GL_LINE_SMOOTH); RenderSystem.lineWidth(1f); RenderSystem.enableCull(); + RenderSystem.depthFunc(GL11.GL_LEQUAL); } public static void renderQuad(WorldRenderContext context, Vec3d[] points, float[] colorComponents, float alpha, boolean throughWalls) { diff --git a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java index 3a1d364f..2f9c9f63 100644 --- a/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java +++ b/src/main/java/de/hysky/skyblocker/utils/waypoint/Waypoint.java @@ -19,6 +19,10 @@ public class Waypoint { final boolean throughWalls; private boolean shouldRender; + public Waypoint(BlockPos pos, Type type, float[] colorComponents) { + this(pos, type, colorComponents, DEFAULT_HIGHLIGHT_ALPHA); + } + public Waypoint(BlockPos pos, Supplier<Type> typeSupplier, float[] colorComponents) { this(pos, typeSupplier, colorComponents, DEFAULT_HIGHLIGHT_ALPHA, DEFAULT_LINE_WIDTH); } @@ -62,6 +66,10 @@ public class Waypoint { this.shouldRender = true; } + public void toggle() { + this.shouldRender = !this.shouldRender; + } + protected float[] getColorComponents() { return colorComponents; } |