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 1597cffc5d..10673e48fa 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -315,6 +315,26 @@ public static ConfigCategory create(SkyblockerConfig defaults, SkyblockerConfig .build()) .build()) + // Waypoints for goldor phase in f7/m7 + .group(OptionGroup.createBuilder().name(Text.translatable("skyblocker.config.dungeons.goldorWaypoints")) + .collapsed(true) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.goldorWaypoints.enableGoldorWaypoints")) + .binding(defaults.dungeons.goldor.enableGoldorWaypoints, + () -> config.dungeons.goldor.enableGoldorWaypoints, + newValue -> config.dungeons.goldor.enableGoldorWaypoints = newValue) + .controller(ConfigUtils::createBooleanController) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.goldorWaypoints.waypointType")) + .description(OptionDescription.of(Text.translatable("skyblocker.config.uiAndVisuals.waypoints.waypointType.@Tooltip"))) + .binding(defaults.dungeons.goldor.waypointType, + () -> config.dungeons.goldor.waypointType, + newValue -> config.dungeons.goldor.waypointType = newValue) + .controller(ConfigUtils::createEnumCyclingListController) + .build()) + .build()) + // Dungeon Secret Waypoints .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.dungeons.secretWaypoints")) 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 43fe866db1..d0e9229343 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java @@ -48,6 +48,9 @@ public class DungeonsConfig { @SerialEntry public Devices devices = new Devices(); + @SerialEntry + public Goldor goldor = new Goldor(); + @SerialEntry public SecretWaypoints secretWaypoints = new SecretWaypoints(); @@ -156,6 +159,14 @@ public static class Devices { public boolean solveLightsOn = true; } + public static class Goldor { + @SerialEntry + public boolean enableGoldorWaypoints = true; + + @SerialEntry + public Waypoint.Type waypointType = Waypoint.Type.WAYPOINT; + } + public static class SecretWaypoints { @SerialEntry public boolean enableRoomMatching = true; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/GoldorWaypointsManager.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/GoldorWaypointsManager.java new file mode 100644 index 0000000000..6d995ce883 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/GoldorWaypointsManager.java @@ -0,0 +1,246 @@ +package de.hysky.skyblocker.skyblock.dungeon; + +import com.google.gson.JsonArray; +import com.google.gson.JsonParser; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.annotations.Init; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.waypoint.NamedWaypoint; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.entity.Entity; +import net.minecraft.text.Text; +import net.minecraft.text.TextCodecs; +import net.minecraft.util.Identifier; +import net.minecraft.util.StringIdentifiable; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Vec3d; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GoldorWaypointsManager { + private static final Logger LOGGER = LoggerFactory.getLogger(GoldorWaypointsManager.class); + + private static final ObjectArrayList TERMINALS = new ObjectArrayList<>(); + private static final ObjectArrayList DEVICES = new ObjectArrayList<>(); + private static final ObjectArrayList LEVERS = new ObjectArrayList<>(); + + private static final String TERMINALS_START = "[BOSS] Storm: I should have known that I stood no chance."; + private static final Pattern TERMINAL_ACTIVATED = Pattern.compile("^(?\\w+) activated a terminal! \\(\\d/\\d\\)$"); + private static final Pattern DEVICE_ACTIVATED = Pattern.compile("^(?\\w+) completed a device! \\(\\d/\\d\\)$"); + private static final Pattern LEVER_ACTIVATED = Pattern.compile("^(?\\w+) activated a lever! \\(\\d/\\d\\)$"); + private static final Pattern PHASE_COMPLETE = Pattern.compile("^(?\\w+) (?:activated a (?:terminal|lever)|completed a device)! (?:\\(7/7\\)|\\(8/8\\))$"); + private static final String CORE_ENTRANCE = "The Core entrance is opening!"; + private static final Codec> CODEC = GoldorWaypoint.CODEC.listOf(); + + // If the waypoints are loaded + private static boolean loaded = false; + // If this should be processed + private static boolean active = false; + // The current set of terminals, each phase is delimited by a gate + private static short currentPhase = 0; + + @Init + public static void init() { + WorldRenderEvents.AFTER_TRANSLUCENT.register(GoldorWaypointsManager::render); + ClientLifecycleEvents.CLIENT_STARTED.register(GoldorWaypointsManager::load); + ClientReceiveMessageEvents.GAME.register(GoldorWaypointsManager::onChatMessage); + ClientReceiveMessageEvents.GAME_CANCELED.register(GoldorWaypointsManager::onChatMessage); + ClientPlayConnectionEvents.JOIN.register(((handler, sender, client) -> reset())); + } + + private static void load(MinecraftClient client) { + CompletableFuture terminals = loadWaypoints(client, Identifier.of(SkyblockerMod.NAMESPACE, "dungeons/goldorwaypoints.json")); + + terminals.whenComplete((_result, _throwable) -> loaded = true); + } + + private static CompletableFuture loadWaypoints(MinecraftClient client, Identifier file) { + return CompletableFuture.supplyAsync(() -> { + try (BufferedReader reader = client.getResourceManager().openAsReader(file)) { + JsonArray arr = JsonParser.parseReader(reader).getAsJsonArray(); + + return CODEC.parse(JsonOps.INSTANCE, arr).getOrThrow(); + } catch (Exception e) { + LOGGER.error("[Skyblocker Goldor Waypoints] Failed to load waypoints from: {}", file, e); + + return List.of(); + } + }).thenAccept(list -> list.forEach(waypoint -> { + switch (waypoint.kind) { + case TERMINAL -> TERMINALS.add(waypoint); + case DEVICE -> DEVICES.add(waypoint); + case LEVER -> LEVERS.add(waypoint); + } + })); + } + + /** + * Checks if we should process messages + * + * @return true if we should process messages + */ + private static boolean shouldProcessMsgs() { + return (loaded && SkyblockerConfigManager.get().dungeons.goldor.enableGoldorWaypoints && Utils.isInDungeons() && DungeonManager.isInBoss() && DungeonManager.getBoss().isFloor(7)); + } + + /** + * Given a list of waypoints to operate on and a player name, hides the visible waypoint that is closest to the player + * + * @param waypoints The list of waypoints to operate on + * @param playerName The name of the player to check against + */ + private static void removeNearestWaypoint(ObjectArrayList waypoints, String playerName) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.world == null) return; + + Optional posOptional = client.world.getPlayers().stream().filter(player -> player.getGameProfile().getName().equals(playerName)).findAny().map(Entity::getPos); + + if (posOptional.isPresent()) { + Vec3d pos = posOptional.get(); + + waypoints.stream().filter(GoldorWaypoint::shouldRender).min(Comparator.comparingDouble(waypoint -> waypoint.centerPos.squaredDistanceTo(pos))).map(waypoint -> { + waypoint.setShouldRender(false); + return null; + }); + } + } + + /** + * Resets state, disabling waypoint rendering (but setting all waypoints to be ready for the next run) + */ + private static void reset() { + active = false; + currentPhase = 0; + enableAll(TERMINALS); + enableAll(DEVICES); + enableAll(LEVERS); + } + + /** + * Enables rendering for all waypoints in a set + * + * @param waypoints The set of waypoints to enable rendering for + */ + private static void enableAll(ObjectArrayList waypoints) { + waypoints.forEach(waypoint -> waypoint.setShouldRender(true)); + } + + /** + * Convenience method to extract the player name from a message + * + * @param matcher The matcher to extract the name from + * @return The player name, or null if the matcher didn't match + */ + @Nullable + private static String getPlayerName(Matcher matcher) { + return matcher.matches() ? matcher.group("name") : null; + } + + private static void onChatMessage(Text text, boolean overlay) { + if (overlay || !shouldProcessMsgs()) return; + String message = text.getString(); + + if (active) { + if (PHASE_COMPLETE.matcher(message).matches()) { + currentPhase++; + } else { + String playerName; + + if ((playerName = getPlayerName(TERMINAL_ACTIVATED.matcher(message))) != null) { + removeNearestWaypoint(TERMINALS, playerName); + } else if ((playerName = getPlayerName(DEVICE_ACTIVATED.matcher(message))) != null) { + removeNearestWaypoint(DEVICES, playerName); + } else if ((playerName = getPlayerName(LEVER_ACTIVATED.matcher(message))) != null) { + removeNearestWaypoint(LEVERS, playerName); + } else if (message.equals(CORE_ENTRANCE)) { + active = false; + } + } + } else { + if (message.equals(TERMINALS_START)) { + enableAll(TERMINALS); + enableAll(DEVICES); + enableAll(LEVERS); + active = true; + } + } + } + + private static void renderWaypoints(WorldRenderContext context, ObjectArrayList waypoints) { + for (GoldorWaypoint waypoint : waypoints) { + if (waypoint.phase == currentPhase && waypoint.shouldRender()) { + waypoint.render(context); + } + } + } + + private static void render(WorldRenderContext context) { + if (active) { + renderWaypoints(context, TERMINALS); + renderWaypoints(context, DEVICES); + renderWaypoints(context, LEVERS); + } + } + + private static class GoldorWaypoint extends NamedWaypoint { + public static final Codec CODEC = RecordCodecBuilder.create(i -> i.group( + WaypointTargetKind.CODEC.fieldOf("kind").forGetter(w -> w.kind), + Codec.INT.fieldOf("phase").forGetter(customWaypoint -> customWaypoint.phase), + TextCodecs.CODEC.fieldOf("name").forGetter(NamedWaypoint::getName), + BlockPos.CODEC.fieldOf("pos").forGetter(customWaypoint -> customWaypoint.pos) + ).apply(i, GoldorWaypoint::new)); + + private static final Supplier TYPE_SUPPLIER = () -> SkyblockerConfigManager.get().dungeons.goldor.waypointType; + + final WaypointTargetKind kind; + final int phase; + + GoldorWaypoint(WaypointTargetKind kind, int phase, Text name, BlockPos pos) { + super(pos, name, TYPE_SUPPLIER, kind.colorComponents, 0.25F, true); + this.kind = kind; + this.phase = phase; + } + + /** + * The different classes of waypoints + */ + enum WaypointTargetKind implements StringIdentifiable { + TERMINAL(0, 255, 0), + DEVICE(0, 0, 255), + LEVER(255, 255, 0); + + private static final Codec CODEC = StringIdentifiable.createBasicCodec(WaypointTargetKind::values); + private final float[] colorComponents; + + WaypointTargetKind(int r, int g, int b) { + this.colorComponents = new float[]{r / 255F, g / 255F, b / 255F}; + } + + @Override + public String asString() { + return name().toLowerCase(); + } + } + } +} diff --git a/src/main/resources/assets/skyblocker/dungeons/goldorwaypoints.json b/src/main/resources/assets/skyblocker/dungeons/goldorwaypoints.json new file mode 100644 index 0000000000..f1a633bc0b --- /dev/null +++ b/src/main/resources/assets/skyblocker/dungeons/goldorwaypoints.json @@ -0,0 +1,292 @@ +[ + { + "kind": "device", + "phase": 0, + "name": "Device", + "pos": [ + 110, + 121, + 91 + ] + }, + { + "kind": "terminal", + "phase": 0, + "name": "Terminal #1", + "pos": [ + 111, + 113, + 73 + ] + }, + { + "kind": "terminal", + "phase": 0, + "name": "Terminal #2", + "pos": [ + 111, + 119, + 79 + ] + }, + { + "kind": "terminal", + "phase": 0, + "name": "Terminal #3", + "pos": [ + 89, + 112, + 92 + ] + }, + { + "kind": "terminal", + "phase": 0, + "name": "Terminal #4", + "pos": [ + 89, + 122, + 101 + ] + }, + { + "kind": "lever", + "phase": 0, + "name": "Lever", + "pos": [ + 106, + 124, + 113 + ] + }, + { + "kind": "lever", + "phase": 0, + "name": "Lever", + "pos": [ + 94, + 124, + 113 + ] + }, + { + "kind": "device", + "phase": 1, + "name": "Device", + "pos": [ + 60, + 132, + 143 + ] + }, + { + "kind": "terminal", + "phase": 1, + "name": "Terminal #1", + "pos": [ + 68, + 109, + 121 + ] + }, + { + "kind": "terminal", + "phase": 1, + "name": "Terminal #2", + "pos": [ + 59, + 120, + 122 + ] + }, + { + "kind": "terminal", + "phase": 1, + "name": "Terminal #3", + "pos": [ + 47, + 109, + 121 + ] + }, + { + "kind": "terminal", + "phase": 1, + "name": "Terminal #4", + "pos": [ + 40, + 124, + 122 + ] + }, + { + "kind": "terminal", + "phase": 1, + "name": "Terminal #5", + "pos": [ + 39, + 108, + 143 + ] + }, + { + "kind": "lever", + "phase": 1, + "name": "Lever", + "pos": [ + 27, + 124, + 127 + ] + }, + { + "kind": "lever", + "phase": 1, + "name": "Lever", + "pos": [ + 23, + 132, + 138 + ] + }, + { + "kind": "device", + "phase": 2, + "name": "Device", + "pos": [ + 0, + 120, + 77 + ] + }, + { + "kind": "terminal", + "phase": 2, + "name": "Terminal #1", + "pos": [ + -3, + 109, + 112 + ] + }, + { + "kind": "terminal", + "phase": 2, + "name": "Terminal #2", + "pos": [ + -3, + 119, + 93 + ] + }, + { + "kind": "terminal", + "phase": 2, + "name": "Terminal #3", + "pos": [ + 19, + 123, + 93 + ] + }, + { + "kind": "terminal", + "phase": 2, + "name": "Terminal #4", + "pos": [ + -3, + 109, + 77 + ] + }, + { + "kind": "lever", + "phase": 2, + "name": "Lever", + "pos": [ + 14, + 122, + 55 + ] + }, + { + "kind": "lever", + "phase": 2, + "name": "Lever", + "pos": [ + 2, + 122, + 55 + ] + }, + { + "kind": "device", + "phase": 3, + "name": "Device", + "pos": [ + 63, + 127, + 35 + ] + }, + { + "kind": "terminal", + "phase": 3, + "name": "Terminal #1", + "pos": [ + 41, + 109, + 29 + ] + }, + { + "kind": "terminal", + "phase": 3, + "name": "Terminal #2", + "pos": [ + 44, + 121, + 29 + ] + }, + { + "kind": "terminal", + "phase": 3, + "name": "Terminal #3", + "pos": [ + 67, + 109, + 29 + ] + }, + { + "kind": "terminal", + "phase": 3, + "name": "Terminal #4", + "pos": [ + 72, + 115, + 48 + ] + }, + { + "kind": "lever", + "phase": 3, + "name": "Lever", + "pos": [ + 86, + 128, + 46 + ] + }, + { + "kind": "lever", + "phase": 3, + "name": "Lever", + "pos": [ + 84, + 121, + 34 + ] + } +] \ No newline at end of file diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index c590d37605..ce3ede2684 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -77,6 +77,10 @@ "skyblocker.config.dungeons.croesusHelper": "Croesus Helper", "skyblocker.config.dungeons.croesusHelper.@Tooltip": "Gray out chests that have already been opened.", + "skyblocker.config.dungeons.goldorWaypoints": "Goldor Tasks Waypoints (F7/M7)", + "skyblocker.config.dungeons.goldorWaypoints.enableGoldorWaypoints": "Enable waypoints for terminals/devices/levers during Goldor", + "skyblocker.config.dungeons.goldorWaypoints.waypointType": "Task waypoint type", + "skyblocker.config.dungeons.devices": "Device Solvers (F7/M7)", "skyblocker.config.dungeons.devices.solveLightsOn": "Solve Lights On", "skyblocker.config.dungeons.devices.solveLightsOn.@Tooltip": "Highlights the correct levers to click in red",