From 75706bd04c8cb1f1cc6126f4348351d9fe34bfc2 Mon Sep 17 00:00:00 2001 From: Kevin <92656833+kevinthegreat1@users.noreply.github.com> Date: Sun, 27 Jul 2025 03:04:45 +0800 Subject: Fancy Dungeon Map (#1239) * Add spirit leap map * Add fancy dungeon map * Fetch profiles on dungeon load and add dungeon loaded event * Add player arrow * Add player marker tracking * Add hover and click to teleport * Apply suggestions * Add show self head option * Only show map in leap overlay while clearing * Add inventory key to close leap overlay * Add leap overlay scaling and migrate config * Add layout widget * Add cache invalidation * keep only the past 100 player matches (5 seconds) * Add player head render helper and clean up RenderHelper * Rename config field * Remove draw head by name * Prevent default decorations * Fix merge conflicts * Delete old FishingHelper.java * Use player list for player detection * Use map decoration list for rendering and account for special player list order * Fix show self head * Improve dungeon player matching error handling * HudHelper not helping --- .../config/categories/DungeonsCategory.java | 50 ++- .../skyblocker/config/configs/DungeonsConfig.java | 16 +- .../de/hysky/skyblocker/events/DungeonEvents.java | 44 +- .../mixins/HandledScreenProviderMixin.java | 2 +- .../hysky/skyblocker/mixins/MapRendererMixin.java | 22 + .../mixins/accessors/MapStateAccessor.java | 14 + .../crimson/kuudra/ArrowPoisonWarning.java | 6 +- .../skyblock/crimson/kuudra/DangerWarning.java | 3 +- .../skyblocker/skyblock/dungeon/DungeonClass.java | 3 +- .../skyblocker/skyblock/dungeon/DungeonMap.java | 220 ++++++--- .../skyblocker/skyblock/dungeon/LeapOverlay.java | 174 +++++--- .../skyblock/dungeon/puzzle/boulder/Boulder.java | 3 +- .../skyblock/dungeon/secrets/DungeonManager.java | 23 +- .../skyblock/dungeon/secrets/DungeonMapUtils.java | 493 +++++++++++---------- .../dungeon/secrets/DungeonPlayerManager.java | 177 ++++++-- .../skyblock/dungeon/secrets/SecretsTracker.java | 4 +- .../skyblocker/skyblock/fishing/FishingHelper.java | 4 +- .../skyblock/rift/HealingMelonIndicator.java | 3 +- .../boss/demonlord/FirePillarAnnouncer.java | 3 +- .../slayers/boss/vampire/ManiaIndicator.java | 3 +- .../slayers/boss/vampire/StakeIndicator.java | 3 +- .../slayers/boss/vampire/TwinClawsIndicator.java | 3 +- .../tabhud/widget/DungeonPlayerWidget.java | 18 +- .../skyblocker/utils/render/RenderHelper.java | 65 +-- .../utils/render/title/TitleContainer.java | 45 +- 25 files changed, 885 insertions(+), 516 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/mixins/MapRendererMixin.java create mode 100644 src/main/java/de/hysky/skyblocker/mixins/accessors/MapStateAccessor.java (limited to 'src/main/java') 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 357b31f8..fa67eef8 100644 --- a/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java +++ b/src/main/java/de/hysky/skyblocker/config/categories/DungeonsCategory.java @@ -67,14 +67,6 @@ public class DungeonsCategory { newValue -> config.dungeons.classBasedPlayerGlow = newValue) .controller(ConfigUtils.createBooleanController()) .build()) - .option(Option.createBuilder() - .name(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay")) - .description(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay.@Tooltip")) - .binding(defaults.dungeons.spiritLeapOverlay, - () -> config.dungeons.spiritLeapOverlay, - newValue -> config.dungeons.spiritLeapOverlay = newValue) - .controller(ConfigUtils.createBooleanController()) - .build()) .option(Option.createBuilder() .name(Text.translatable("skyblocker.config.dungeons.starredMobGlow")) .description(Text.translatable("skyblocker.config.dungeons.starredMobGlow.@Tooltip")) @@ -143,6 +135,20 @@ public class DungeonsCategory { newValue -> config.dungeons.dungeonMap.enableMap = newValue) .controller(ConfigUtils.createBooleanController()) .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.map.fancyMap")) + .binding(defaults.dungeons.dungeonMap.fancyMap, + () -> config.dungeons.dungeonMap.fancyMap, + newValue -> config.dungeons.dungeonMap.fancyMap = newValue) + .controller(ConfigUtils.createBooleanController()) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.map.showSelfHead")) + .binding(defaults.dungeons.dungeonMap.showSelfHead, + () -> config.dungeons.dungeonMap.showSelfHead, + newValue -> config.dungeons.dungeonMap.showSelfHead = newValue) + .controller(ConfigUtils.createBooleanController()) + .build()) .option(Option.createBuilder() .name(Text.translatable("skyblocker.config.dungeons.map.mapScaling")) .binding(defaults.dungeons.dungeonMap.mapScaling, @@ -157,6 +163,34 @@ public class DungeonsCategory { .build()) .build()) + // Spirit Leap Overlay + .group(OptionGroup.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay")) + .collapsed(true) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay.enableLeapOverlay")) + .description(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay.enableLeapOverlay.@Tooltip")) + .binding(defaults.dungeons.leapOverlay.enableLeapOverlay, + () -> config.dungeons.leapOverlay.enableLeapOverlay, + newValue -> config.dungeons.leapOverlay.enableLeapOverlay = newValue) + .controller(ConfigUtils.createBooleanController()) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay.showMap")) + .binding(defaults.dungeons.leapOverlay.showMap, + () -> config.dungeons.leapOverlay.showMap, + newValue -> config.dungeons.leapOverlay.showMap = newValue) + .controller(ConfigUtils.createBooleanController()) + .build()) + .option(Option.createBuilder() + .name(Text.translatable("skyblocker.config.dungeons.spiritLeapOverlay.scale")) + .binding(defaults.dungeons.leapOverlay.scale, + () -> config.dungeons.leapOverlay.scale, + newValue -> config.dungeons.leapOverlay.scale = newValue) + .controller(FloatController.createBuilder().range(1f, 2f).slider(0.05f).build()) + .build()) + .build()) + // Puzzle Solver .group(OptionGroup.createBuilder() .name(Text.translatable("skyblocker.config.dungeons.puzzle")) 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 81c1cd9b..4fd9ade9 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/DungeonsConfig.java @@ -19,8 +19,6 @@ public class DungeonsConfig { public boolean classBasedPlayerGlow = true; - public boolean spiritLeapOverlay = true; - public boolean starredMobGlow = false; public boolean starredMobBoundingBoxes = true; @@ -35,6 +33,8 @@ public class DungeonsConfig { public DungeonMap dungeonMap = new DungeonMap(); + public SpiritLeapOverlay leapOverlay = new SpiritLeapOverlay(); + public PuzzleSolvers puzzleSolvers = new PuzzleSolvers(); public TheProfessor theProfessor = new TheProfessor(); @@ -60,6 +60,10 @@ public class DungeonsConfig { public static class DungeonMap { public boolean enableMap = true; + public boolean fancyMap = true; + + public boolean showSelfHead = true; + public float mapScaling = 1f; public int mapX = 2; @@ -67,6 +71,14 @@ public class DungeonsConfig { public int mapY = 2; } + public static class SpiritLeapOverlay { + public boolean enableLeapOverlay = true; + + public boolean showMap = true; + + public float scale = 1.2f; + } + public static class PuzzleSolvers { public boolean solveTicTacToe = true; diff --git a/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java index 27f9624c..b8448ac3 100644 --- a/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java +++ b/src/main/java/de/hysky/skyblocker/events/DungeonEvents.java @@ -7,6 +7,24 @@ import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; public class DungeonEvents { + /** + * Called when the player loads into a dungeon after the location is sent to the scoreboard. + */ + public static final Event DUNGEON_LOADED = EventFactory.createArrayBacked(DungeonLoaded.class, callbacks -> () -> { + for (DungeonLoaded callback : callbacks) { + callback.onDungeonLoaded(); + } + }); + + /** + * Called after the dungeons starts and after the tab has changed to include additional information about the run such as each player's class. + */ + public static final Event DUNGEON_STARTED = EventFactory.createArrayBacked(DungeonStarted.class, callbacks -> () -> { + for (DungeonStarted callback : callbacks) { + callback.onDungeonStarted(); + } + }); + public static final Event PUZZLE_MATCHED = EventFactory.createArrayBacked(RoomMatched.class, callbacks -> room -> { for (RoomMatched callback : callbacks) { callback.onRoomMatched(room); @@ -22,28 +40,10 @@ public class DungeonEvents { } }); - /** - * Note: This event fires after the tab has changed to include additional information about the run such as each player's class. - */ - public static final Event DUNGEON_STARTED = EventFactory.createArrayBacked(DungeonStarted.class, callbacks -> () -> { - for (DungeonStarted callback : callbacks) { - callback.onDungeonStarted(); - } - }); - - /** - * Called when the player loads into a dungeon once Mort has been located. - */ - public static final Event DUNGEON_LOADED = EventFactory.createArrayBacked(DungeonLoaded.class, callbacks -> () -> { - for (DungeonLoaded callback : callbacks) { - callback.onDungeonLoaded(); - } - }); - @Environment(EnvType.CLIENT) @FunctionalInterface - public interface RoomMatched { - void onRoomMatched(Room room); + public interface DungeonLoaded { + void onDungeonLoaded(); } @Environment(EnvType.CLIENT) @@ -54,7 +54,7 @@ public class DungeonEvents { @Environment(EnvType.CLIENT) @FunctionalInterface - public interface DungeonLoaded { - void onDungeonLoaded(); + public interface RoomMatched { + void onRoomMatched(Room room); } } diff --git a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java index d1edb51c..29bc6ee9 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/HandledScreenProviderMixin.java @@ -107,7 +107,7 @@ public interface HandledScreenProviderMixin { } // Leap Overlay - case GenericContainerScreenHandler containerScreenHandler when Utils.isInDungeons() && SkyblockerConfigManager.get().dungeons.spiritLeapOverlay && nameLowercase.contains(LeapOverlay.TITLE.toLowerCase()) -> { + case GenericContainerScreenHandler containerScreenHandler when Utils.isInDungeons() && SkyblockerConfigManager.get().dungeons.leapOverlay.enableLeapOverlay && nameLowercase.contains(LeapOverlay.TITLE.toLowerCase()) -> { client.player.currentScreenHandler = containerScreenHandler; client.setScreen(new LeapOverlay(containerScreenHandler)); diff --git a/src/main/java/de/hysky/skyblocker/mixins/MapRendererMixin.java b/src/main/java/de/hysky/skyblocker/mixins/MapRendererMixin.java new file mode 100644 index 00000000..b0af491a --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/MapRendererMixin.java @@ -0,0 +1,22 @@ +package de.hysky.skyblocker.mixins; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.render.MapRenderer; +import net.minecraft.item.map.MapDecoration; +import net.minecraft.item.map.MapDecorationTypes; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(MapRenderer.class) +public class MapRendererMixin { + @ModifyExpressionValue(method = "createDecoration", at = @At(value = "INVOKE", target = "Lnet/minecraft/item/map/MapDecoration;isAlwaysRendered()Z")) + private boolean preventDecorationInDungeons(boolean alwaysRendered, @Local(argsOnly = true) MapDecoration decoration) { + // Allow alwaysRendered if + // 1. not in dungeons OR + // 2. the decoration type is frame (self player) and don't show self head + return (!Utils.isInDungeons() || decoration.type().value().equals(MapDecorationTypes.FRAME.value()) && !SkyblockerConfigManager.get().dungeons.dungeonMap.showSelfHead) && alwaysRendered; + } +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/MapStateAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/MapStateAccessor.java new file mode 100644 index 00000000..cbf7764b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/MapStateAccessor.java @@ -0,0 +1,14 @@ +package de.hysky.skyblocker.mixins.accessors; + +import net.minecraft.item.map.MapDecoration; +import net.minecraft.item.map.MapState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(MapState.class) +public interface MapStateAccessor { + @Accessor + Map getDecorations(); +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/ArrowPoisonWarning.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/ArrowPoisonWarning.java index c0d371de..17f6416c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/ArrowPoisonWarning.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/ArrowPoisonWarning.java @@ -7,8 +7,8 @@ import de.hysky.skyblocker.config.configs.CrimsonIsleConfig; import de.hysky.skyblocker.skyblock.crimson.kuudra.Kuudra.KuudraPhase; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.render.title.Title; +import de.hysky.skyblocker.utils.render.title.TitleContainer; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.BowItem; @@ -44,9 +44,9 @@ public class ArrowPoisonWarning { } if (!hasToxicArrowPoison) { - RenderHelper.displayInTitleContainerAndPlaySound(NONE_TITLE, THREE_SECONDS); + TitleContainer.addTitleAndPlaySound(NONE_TITLE, THREE_SECONDS); } else if (arrowPoisonAmount < CONFIG.get().arrowPoisonThreshold) { - RenderHelper.displayInTitleContainerAndPlaySound(LOW_TITLE, THREE_SECONDS); + TitleContainer.addTitleAndPlaySound(LOW_TITLE, THREE_SECONDS); } } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/DangerWarning.java b/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/DangerWarning.java index 80028405..db9e1e96 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/DangerWarning.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/crimson/kuudra/DangerWarning.java @@ -5,7 +5,6 @@ import java.util.function.Supplier; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; import de.hysky.skyblocker.utils.Utils; -import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.render.title.Title; import de.hysky.skyblocker.utils.render.title.TitleContainer; import de.hysky.skyblocker.utils.scheduler.Scheduler; @@ -37,7 +36,7 @@ public class DangerWarning { Title title = getDangerTitle(under); if (title != null) { - RenderHelper.displayInTitleContainerAndPlaySound(title); + TitleContainer.addTitleAndPlaySound(title); return; } else if (i == 5) { //Prevent removing the title prematurely diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonClass.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonClass.java index 66f03dbe..aeb0d171 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonClass.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonClass.java @@ -3,6 +3,7 @@ package de.hysky.skyblocker.skyblock.dungeon; import de.hysky.skyblocker.skyblock.entity.MobGlow; import de.hysky.skyblocker.skyblock.tabhud.util.Ico; import net.minecraft.item.ItemStack; +import net.minecraft.util.math.ColorHelper; import java.util.Arrays; import java.util.Map; @@ -26,7 +27,7 @@ public enum DungeonClass { DungeonClass(String name, int color, ItemStack icon) { this.name = name; - this.color = color; + this.color = ColorHelper.fullAlpha(color); this.icon = icon; } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java index 548ba51b..2966b08c 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/DungeonMap.java @@ -3,8 +3,13 @@ package de.hysky.skyblocker.skyblock.dungeon; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.DungeonsConfig; +import de.hysky.skyblocker.mixins.accessors.MapStateAccessor; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonMapUtils; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonPlayerManager; import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.scheduler.Scheduler; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; @@ -17,73 +22,174 @@ import net.minecraft.client.render.LightmapTextureManager; import net.minecraft.client.render.MapRenderState; import net.minecraft.client.render.MapRenderer; import net.minecraft.client.render.VertexConsumerProvider; -import net.minecraft.client.util.math.MatrixStack; import net.minecraft.component.DataComponentTypes; import net.minecraft.component.type.MapIdComponent; +import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.FilledMapItem; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; +import net.minecraft.item.map.MapDecoration; +import net.minecraft.item.map.MapDecorationTypes; import net.minecraft.item.map.MapState; import net.minecraft.util.Identifier; +import net.minecraft.util.math.RotationAxis; +import net.minecraft.world.World; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector2d; +import org.joml.Vector2dc; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; public class DungeonMap { + private static final Logger LOGGER = LoggerFactory.getLogger(DungeonMap.class); private static final Identifier DUNGEON_MAP = Identifier.of(SkyblockerMod.NAMESPACE, "dungeon_map"); - private static final MapIdComponent DEFAULT_MAP_ID_COMPONENT = new MapIdComponent(1024); - private static final MapRenderState MAP_RENDER_STATE = new MapRenderState(); - private static MapIdComponent cachedMapIdComponent = null; + private static final MapIdComponent DEFAULT_MAP_ID_COMPONENT = new MapIdComponent(1024); + private static final MapRenderState MAP_RENDER_STATE = new MapRenderState(); + private static MapIdComponent cachedMapIdComponent = null; - @Init - public static void init() { + @Init + public static void init() { HudLayerRegistrationCallback.EVENT.register(d -> d.attachLayerAfter(IdentifiedLayer.STATUS_EFFECTS, DUNGEON_MAP, (context, tickCounter) -> render(context))); - ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker") - .then(ClientCommandManager.literal("hud") - .then(ClientCommandManager.literal("dungeon") - .executes(Scheduler.queueOpenScreenCommand(DungeonMapConfigScreen::new)) - ) - ) - )); - ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); - } - - public static void render(MatrixStack matrices) { - MinecraftClient client = MinecraftClient.getInstance(); - if (client.player == null || client.world == null) return; - - MapIdComponent mapId = getMapIdComponent(client.player.getInventory().getMainStacks().get(8)); - - MapState state = FilledMapItem.getMapState(mapId, client.world); - if (state == null) return; - - int x = SkyblockerConfigManager.get().dungeons.dungeonMap.mapX; - int y = SkyblockerConfigManager.get().dungeons.dungeonMap.mapY; - float scaling = SkyblockerConfigManager.get().dungeons.dungeonMap.mapScaling; - VertexConsumerProvider.Immediate vertices = client.getBufferBuilders().getEffectVertexConsumers(); - MapRenderer mapRenderer = client.getMapRenderer(); - - matrices.push(); - matrices.translate(x, y, 0); - matrices.scale(scaling, scaling, 0f); - mapRenderer.update(mapId, state, MAP_RENDER_STATE); - mapRenderer.draw(MAP_RENDER_STATE, matrices, vertices, false, LightmapTextureManager.MAX_LIGHT_COORDINATE); - vertices.draw(); - matrices.pop(); - } - - public static MapIdComponent getMapIdComponent(ItemStack stack) { - if (stack.isOf(Items.FILLED_MAP) && stack.contains(DataComponentTypes.MAP_ID)) { - MapIdComponent mapIdComponent = stack.get(DataComponentTypes.MAP_ID); - cachedMapIdComponent = mapIdComponent; - return mapIdComponent; - } else return cachedMapIdComponent != null ? cachedMapIdComponent : DEFAULT_MAP_ID_COMPONENT; - } - - private static void render(DrawContext context) { - if (Utils.isInDungeons() && DungeonScore.isDungeonStarted() && !DungeonManager.isInBoss() && SkyblockerConfigManager.get().dungeons.dungeonMap.enableMap) { - render(context.getMatrices()); - } - } - - private static void reset() { - cachedMapIdComponent = null; - } + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal("skyblocker") + .then(ClientCommandManager.literal("hud") + .then(ClientCommandManager.literal("dungeon") + .executes(Scheduler.queueOpenScreenCommand(DungeonMapConfigScreen::new)) + ) + ) + )); + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> reset()); + } + + private static boolean shouldProcess() { + return Utils.isInDungeons() && DungeonScore.isDungeonStarted() && !DungeonManager.isInBoss(); + } + + private static void render(DrawContext context) { + DungeonsConfig.DungeonMap dungeonMap = SkyblockerConfigManager.get().dungeons.dungeonMap; + if (shouldProcess() && dungeonMap.enableMap) { + render(context, dungeonMap.mapX, dungeonMap.mapY, dungeonMap.mapScaling, dungeonMap.fancyMap); + } + } + + public static void render(DrawContext context, int x, int y, float scale, boolean fancy) { + render(context, x, y, scale, fancy, Integer.MIN_VALUE, Integer.MIN_VALUE, null); + } + + /** + * @return the {@link UUID} of the hovered player head, or null if no player head is hovered. + */ + @Nullable + public static UUID render(DrawContext context, int x, int y, float scale, boolean fancy, int mouseX, int mouseY, @Nullable UUID enlarge) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null || client.world == null) return null; + + MapIdComponent mapId = getMapIdComponent(client.player.getInventory().getMainStacks().get(8)); + MapState state = FilledMapItem.getMapState(mapId, client.world); + if (state == null) return null; + + VertexConsumerProvider.Immediate vertices = client.getBufferBuilders().getEffectVertexConsumers(); + MapRenderer mapRenderer = client.getMapRenderer(); + + context.getMatrices().push(); + context.getMatrices().translate(x, y, 0); + context.getMatrices().scale(scale, scale, 0f); + mapRenderer.update(mapId, state, MAP_RENDER_STATE); + mapRenderer.draw(MAP_RENDER_STATE, context.getMatrices(), vertices, fancy, LightmapTextureManager.MAX_LIGHT_COORDINATE); + vertices.draw(); + + UUID hoveredHead = null; + if (fancy) hoveredHead = renderPlayerHeads(context, client.world, state, mouseX / scale, mouseY / scale, enlarge); + context.getMatrices().pop(); + return hoveredHead; + } + + public static MapIdComponent getMapIdComponent(ItemStack stack) { + if (stack.isOf(Items.FILLED_MAP) && stack.contains(DataComponentTypes.MAP_ID)) { + MapIdComponent mapIdComponent = stack.get(DataComponentTypes.MAP_ID); + cachedMapIdComponent = mapIdComponent; + return mapIdComponent; + } else return cachedMapIdComponent != null ? cachedMapIdComponent : DEFAULT_MAP_ID_COMPONENT; + } + + @Nullable + private static UUID renderPlayerHeads(DrawContext context, World world, MapState state, double mouseX, double mouseY, @Nullable UUID enlarge) { + if (!DungeonManager.isClearingDungeon()) return null; + + // Used to index through the player list to find which dungeon player corresponds to which map decoration. + // Start at 1 because the first entry in the player list is the self player. + int i = 1; + UUID hovered = null; + for (Map.Entry mapDecoration : ((MapStateAccessor) state).getDecorations().entrySet()) { + // Get the corresponding dungeon player for the map decoration. + DungeonPlayerManager.DungeonPlayer dungeonPlayer = null; + // If the map decoration is the self player, use the first player in this list. The self player is always the first player in the list. + if (mapDecoration.getValue().type().value().equals(MapDecorationTypes.FRAME.value())) { + if (!SkyblockerConfigManager.get().dungeons.dungeonMap.showSelfHead) continue; + dungeonPlayer = DungeonPlayerManager.getPlayers()[0]; + } else while (i < DungeonPlayerManager.getPlayers().length && (dungeonPlayer == null || !dungeonPlayer.alive())) { // Find the next alive player in the player list. + dungeonPlayer = DungeonPlayerManager.getPlayers()[i]; + i++; + } + + // If we still didn't find a valid dungeon player after searching though the entire player list, something is wrong. + if (dungeonPlayer == null) { + dungeonPlayerError(mapDecoration.getKey(), "not found", i - 1, DungeonPlayerManager.getPlayers(), ((MapStateAccessor) state).getDecorations()); + continue; + } else if (!dungeonPlayer.alive()) { + dungeonPlayerError(mapDecoration.getKey(), "not alive", i - 1, DungeonPlayerManager.getPlayers(), ((MapStateAccessor) state).getDecorations()); + continue; + } else if (dungeonPlayer.uuid() == null) { + dungeonPlayerError(mapDecoration.getKey(), "has null uuid", i - 1, DungeonPlayerManager.getPlayers(), ((MapStateAccessor) state).getDecorations()); + continue; + } + PlayerRenderState player = PlayerRenderState.of(world, dungeonPlayer, mapDecoration.getValue()); + + // Actually render the player head + context.getMatrices().push(); + context.getMatrices().translate(player.mapPos().x(), player.mapPos().y(), 0); + context.getMatrices().multiply(RotationAxis.POSITIVE_Z.rotationDegrees(player.deg() + 180)); + + if (player.uuid().equals(enlarge)) { + // Enlarge the player head when the corresponding button is hovered + context.getMatrices().scale(2, 2, 1); + } else if (hovered == null && isPlayerHovered(player, mouseX, mouseY)) { + // Enlarge the player head when hovered + context.getMatrices().scale(2, 2, 1); + hovered = player.uuid(); + } + RenderHelper.drawPlayerHead(context, -4, -4, 8, player.uuid()); + context.drawBorder(-5, -5, 10, 10, dungeonPlayer.dungeonClass().color()); + context.fill(-1, -7, 1, -5, dungeonPlayer.dungeonClass().color()); + context.getMatrices().pop(); + } + return hovered; + } + + private static void dungeonPlayerError(String decorationId, String reason, int i, DungeonPlayerManager.DungeonPlayer[] dungeonPlayers, Map mapDecorations) { + LOGGER.error("[Skyblocker Dungeon Map] Dungeon player for map decoration '{}' {}. Player list index (zero-indexed): {}. Player list: {}. Map decorations: {}", decorationId, reason, i, Arrays.toString(dungeonPlayers), mapDecorations); + } + + public static boolean isPlayerHovered(PlayerRenderState player, double mouseX, double mouseY) { + return player.mapPos().distanceSquared(mouseX, mouseY) < 16; + } + + private static void reset() { + cachedMapIdComponent = null; + } + + public record PlayerRenderState(UUID uuid, String name, Vector2dc mapPos, float deg) { + public static PlayerRenderState of(@NotNull World world, @NotNull DungeonPlayerManager.DungeonPlayer dungeonPlayer, @NotNull MapDecoration mapDecoration) { + // Use the player entity if it exists, since it gives the most accurate position and rotation + PlayerEntity playerEntity = world.getPlayerByUuid(dungeonPlayer.uuid()); + Vector2dc mapPos = playerEntity != null ? DungeonMapUtils.getMapPosFromPhysical(DungeonManager.getPhysicalEntrancePos(), DungeonManager.getMapEntrancePos(), DungeonManager.getMapRoomSize(), playerEntity.getPos()) : new Vector2d(mapDecoration.x() / 2d + 64, mapDecoration.z() / 2d + 64); + float deg = playerEntity != null ? playerEntity.getYaw() : mapDecoration.rotation() * 360 / 16.0F; + + return new PlayerRenderState(dungeonPlayer.uuid(), dungeonPlayer.name(), mapPos, deg); + } + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java index 25e7e16d..c7915890 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/LeapOverlay.java @@ -1,26 +1,17 @@ package de.hysky.skyblocker.skyblock.dungeon; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.UUID; -import java.util.function.Supplier; - -import org.jetbrains.annotations.Nullable; -import org.lwjgl.glfw.GLFW; - import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.config.configs.DungeonsConfig; +import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonManager; import de.hysky.skyblocker.skyblock.dungeon.secrets.DungeonPlayerManager; import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.render.RenderHelper; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; -import net.minecraft.client.gui.widget.ButtonWidget; -import net.minecraft.client.gui.widget.GridWidget; -import net.minecraft.client.gui.widget.SimplePositioningWidget; -import net.minecraft.client.realms.util.RealmsUtil; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.*; import net.minecraft.client.gui.widget.GridWidget.Adder; import net.minecraft.client.render.RenderLayer; import net.minecraft.client.util.math.MatrixStack; @@ -36,23 +27,26 @@ import net.minecraft.text.Text; import net.minecraft.util.Colors; import net.minecraft.util.Identifier; import net.minecraft.util.math.ColorHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.*; +import java.util.function.Supplier; public class LeapOverlay extends Screen implements ScreenHandlerListener { public static final String TITLE = "Spirit Leap"; private static final MinecraftClient CLIENT = MinecraftClient.getInstance(); private static final Identifier BUTTON = Identifier.of(SkyblockerMod.NAMESPACE, "button/button"); private static final Identifier BUTTON_HIGHLIGHTED = Identifier.of(SkyblockerMod.NAMESPACE, "button/button_highlighted"); + private static final DungeonsConfig.SpiritLeapOverlay CONFIG = SkyblockerConfigManager.get().dungeons.leapOverlay; private static final int BUTTON_SPACING = 8; - private static final float SCALE = 1.5f; - private static final int BUTTON_WIDTH = (int) (130f * SCALE); - private static final int BUTTON_HEIGHT = (int) (50f * SCALE); - /** - * Compares first by class name then by player name. - */ - private static final Comparator COMPARATOR = Comparator.comparing(ref -> ref.dungeonClass().displayName()) - .thenComparing(PlayerReference::name); + private static final int BUTTON_WIDTH = 130; + private static final int BUTTON_HEIGHT = 50; private final GenericContainerScreenHandler handler; - private final List references = new ArrayList<>(); + private final SortedSet references = new TreeSet<>(); + @Nullable + private UUID hovered; public LeapOverlay(GenericContainerScreenHandler handler) { super(Text.literal("Skyblocker Leap Overlay")); @@ -63,20 +57,27 @@ public class LeapOverlay extends Screen implements ScreenHandlerListener { handler.addListener(this); } + public static boolean shouldShowMap() { + return DungeonManager.isClearingDungeon() && CONFIG.showMap; + } + @Override protected void init() { - GridWidget gridWidget = new GridWidget(); - gridWidget.setSpacing(BUTTON_SPACING); + DirectionalLayoutWidget layout = DirectionalLayoutWidget.vertical(); + layout.spacing(32).getMainPositioner().alignHorizontalCenter(); - Adder adder = gridWidget.createAdder(2); + if (shouldShowMap()) layout.add(new MapWidget(0, 0)); + GridWidget gridWidget = new GridWidget().setSpacing(BUTTON_SPACING); + Adder adder = gridWidget.createAdder(2); for (PlayerReference reference : references) { - adder.add(new PlayerButton(0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, reference)); + adder.add(new PlayerButton(0, 0, (int) (BUTTON_WIDTH * CONFIG.scale), (int) (BUTTON_HEIGHT * CONFIG.scale), reference)); } + layout.add(gridWidget); - gridWidget.refreshPositions(); - SimplePositioningWidget.setPos(gridWidget, 0, 0, this.width, this.height, 0.5f, 0.5f); - gridWidget.forEachChild(this::addDrawableChild); + layout.refreshPositions(); + SimplePositioningWidget.setPos(layout, 0, 0, this.width, this.height); + layout.forEachChild(this::addDrawableChild); } @Override @@ -99,34 +100,28 @@ public class LeapOverlay extends Screen implements ScreenHandlerListener { default -> null; }; - PlayerReference reference = new PlayerReference(uuid, name, dungeonClass, status, handler.syncId, slotId); - tryInsertReference(reference); + updateReference(new PlayerReference(uuid, name, dungeonClass, status, handler.syncId, slotId)); } } @Override public void onPropertyUpdate(ScreenHandler handler, int property, int value) {} - /** - * Inserts the {@code reference} into the list if it doesn't exist or updates current value then updates the screen. - */ - private void tryInsertReference(PlayerReference reference) { - Optional existing = references.stream() - .filter(ref -> ref.uuid().equals(reference.uuid())) - .findAny(); - - if (existing.isEmpty()) { - references.add(reference); - references.sort(COMPARATOR); - - this.clearAndInit(); - } else if (!existing.get().equals(reference)) { - references.remove(existing.get()); - references.add(reference); - references.sort(COMPARATOR); - - this.clearAndInit(); + private void updateReference(PlayerReference reference) { + references.remove(reference); + references.add(reference); + clearAndInit(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (super.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } else if (this.client.options.inventoryKey.matchesKey(keyCode, scanCode)) { + this.close(); + return true; } + return false; } @Override @@ -152,9 +147,35 @@ public class LeapOverlay extends Screen implements ScreenHandlerListener { } } - private static class PlayerButton extends ButtonWidget { + public class MapWidget extends ClickableWidget { + public MapWidget(int x, int y) { + super(x, y, (int) (128 * CONFIG.scale), (int) (128 * CONFIG.scale), Text.translatable("skyblocker.config.dungeons.map.fancyMap")); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + LeapOverlay.this.hovered = DungeonMap.render(context, getX(), getY(), CONFIG.scale, true, mouseX - getX(), mouseY - getY(), hoveredElement(mouseX, mouseY).filter(PlayerButton.class::isInstance).map(PlayerButton.class::cast).map(p -> p.reference.uuid()).orElse(null)); + context.drawBorder(getX(), getY(), (int) (128 * CONFIG.scale), (int) (128 * CONFIG.scale), -1); + } + + @Override + public void onClick(double mouseX, double mouseY) { + if (LeapOverlay.this.hovered == null) return; + + assert client != null && client.player != null && client.interactionManager != null; + references.stream() + .filter(ref -> ref.uuid().equals(LeapOverlay.this.hovered)) + .findAny() + .ifPresent(ref -> client.interactionManager.clickSlot(ref.syncId(), ref.slotId(), GLFW.GLFW_MOUSE_BUTTON_LEFT, SlotActionType.PICKUP, client.player)); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + } + + private class PlayerButton extends ButtonWidget { private static final int BORDER_THICKNESS = 2; - private static final int HEAD_SIZE = 32; + private static final int HEAD_SIZE = 24; private final PlayerReference reference; private PlayerButton(int x, int y, int width, int height, PlayerReference reference) { @@ -164,30 +185,30 @@ public class LeapOverlay extends Screen implements ScreenHandlerListener { @Override protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { - Identifier texture = this.isSelected() ? BUTTON_HIGHLIGHTED : BUTTON; + Identifier texture = this.isSelected() || reference.uuid().equals(LeapOverlay.this.hovered) ? BUTTON_HIGHLIGHTED : BUTTON; context.drawGuiTexture(RenderLayer::getGuiTextured, texture, this.getX(), this.getY(), this.getWidth(), this.getHeight()); + MatrixStack matrices = context.getMatrices(); + float scale = CONFIG.scale; int baseX = this.getX() + BORDER_THICKNESS; int centreX = this.getX() + (this.getWidth() >> 1); int centreY = this.getY() + (this.getHeight() >> 1); + int halfFontHeight = (int) (CLIENT.textRenderer.fontHeight * scale) >> 1; //Draw Player Head - RealmsUtil.drawPlayerHead(context, baseX + 4, centreY - (HEAD_SIZE >> 1), HEAD_SIZE, reference.uuid()); - - MatrixStack matrices = context.getMatrices(); - int halfFontHeight = (int) (CLIENT.textRenderer.fontHeight * SCALE) >> 1; + RenderHelper.drawPlayerHead(context, baseX + 4, centreY - ((int) (HEAD_SIZE * scale) >> 1), (int) (HEAD_SIZE * scale), reference.uuid()); //Draw class as heading matrices.push(); matrices.translate(centreX, this.getY() + halfFontHeight, 0f); - matrices.scale(SCALE, SCALE, 1f); - context.drawCenteredTextWithShadow(CLIENT.textRenderer, reference.dungeonClass().displayName(), 0, 0, ColorHelper.fullAlpha(reference.dungeonClass().color())); + matrices.scale(scale, scale, 1f); + context.drawCenteredTextWithShadow(CLIENT.textRenderer, reference.dungeonClass().displayName(), 0, 0, reference.dungeonClass().color()); matrices.pop(); //Draw name next to head matrices.push(); - matrices.translate(baseX + HEAD_SIZE + 8, centreY - halfFontHeight, 0f); - matrices.scale(SCALE, SCALE, 1f); + matrices.translate(baseX + HEAD_SIZE * scale + 8, centreY - halfFontHeight, 0f); + matrices.scale(scale, scale, 1f); context.drawTextWithShadow(CLIENT.textRenderer, Text.literal(reference.name()), 0, 0, Colors.WHITE); matrices.pop(); @@ -195,15 +216,12 @@ public class LeapOverlay extends Screen implements ScreenHandlerListener { //Text matrices.push(); matrices.translate(centreX, this.getY() + this.getHeight() - (halfFontHeight * 3), 0f); - matrices.scale(SCALE, SCALE, 1f); + matrices.scale(scale, scale, 1f); context.drawCenteredTextWithShadow(CLIENT.textRenderer, reference.status().text.get(), 0, 0, Colors.WHITE); matrices.pop(); //Overlay - matrices.push(); - matrices.scale(1f, 1f, 1f); context.fill(this.getX(), this.getY(), this.getX() + this.getWidth(), this.getY() + this.getHeight(), reference.status().overlayColor); - matrices.pop(); } } @@ -213,7 +231,27 @@ public class LeapOverlay extends Screen implements ScreenHandlerListener { } } - private record PlayerReference(UUID uuid, String name, DungeonClass dungeonClass, @Nullable PlayerStatus status, int syncId, int slotId) {} + private record PlayerReference(UUID uuid, String name, DungeonClass dungeonClass, @Nullable PlayerStatus status, int syncId, int slotId) implements Comparable { + /** + * Compares first by class name then by player name. + */ + private static final Comparator COMPARATOR = Comparator.comparing(ref -> ref.dungeonClass().displayName()).thenComparing(PlayerReference::name); + + @Override + public boolean equals(Object obj) { + return obj instanceof PlayerReference playerRef && uuid.equals(playerRef.uuid); + } + + @Override + public int hashCode() { + return uuid.hashCode(); + } + + @Override + public int compareTo(@NotNull LeapOverlay.PlayerReference o) { + return COMPARATOR.compare(this, o); + } + } private enum PlayerStatus { DEAD(() -> Text.translatable("text.skyblocker.dead").withColor(Colors.RED), ColorHelper.withAlpha(64, Colors.LIGHT_RED)), diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java index 1250b763..af321b62 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/puzzle/boulder/Boulder.java @@ -8,6 +8,7 @@ import de.hysky.skyblocker.skyblock.dungeon.secrets.Room; import de.hysky.skyblocker.utils.ColorUtils; import de.hysky.skyblocker.utils.render.RenderHelper; import de.hysky.skyblocker.utils.render.title.Title; +import de.hysky.skyblocker.utils.render.title.TitleContainer; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext; import net.minecraft.block.Block; import net.minecraft.block.Blocks; @@ -119,7 +120,7 @@ public class Boulder extends DungeonPuzzle { } else { // If no solution is found, display a title message and reset the puzzle Title title = new Title("skyblocker.dungeons.puzzle.boulder.noSolution", Formatting.GREEN); - RenderHelper.displayInTitleContainerAndPlaySound(title, 15); + TitleContainer.addTitleAndPlaySound(title, 15); reset(); } } 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 3ade007c..6cafb5ca 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 @@ -218,6 +218,20 @@ public class DungeonManager { return customWaypoints.remove(room, pos); } + @Nullable + public static Vector2ic getMapEntrancePos() { + return mapEntrancePos; + } + + public static int getMapRoomSize() { + return mapRoomSize; + } + + @Nullable + public static Vector2ic getPhysicalEntrancePos() { + return physicalEntrancePos; + } + /** * not null if {@link #isCurrentRoomMatched()} */ @@ -476,7 +490,7 @@ 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) { + if (!isClearingDungeon()) { context.getSource().sendError(Constants.PREFIX.get().append("§cYou are not in a dungeon.")); return Command.SINGLE_SUCCESS; } @@ -788,6 +802,13 @@ public class DungeonManager { return rooms.get(DungeonMapUtils.getPhysicalRoomPos(pos)); } + /** + * @return {@code true} if the player is in the main clearing phase of a dungeon. + */ + public static boolean isClearingDungeon() { + return physicalEntrancePos != null && mapEntrancePos != null && mapRoomSize != 0; + } + /** * Calls {@link #isRoomMatched(Room)} on {@link #currentRoom}. * diff --git a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java index 7defb751..819d2826 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/dungeon/secrets/DungeonMapUtils.java @@ -8,223 +8,230 @@ import net.minecraft.item.map.MapDecoration; import net.minecraft.item.map.MapDecorationTypes; import net.minecraft.item.map.MapState; import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Position; import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3i; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.joml.RoundingMode; +import org.joml.Vector2d; +import org.joml.Vector2dc; import org.joml.Vector2i; import org.joml.Vector2ic; import java.util.*; public class DungeonMapUtils { - public static final byte BLACK_COLOR = MapColor.BLACK.getRenderColorByte(MapColor.Brightness.LOWEST); - public static final byte WHITE_COLOR = MapColor.WHITE.getRenderColorByte(MapColor.Brightness.HIGH); + public static final byte BLACK_COLOR = MapColor.BLACK.getRenderColorByte(MapColor.Brightness.LOWEST); + public static final byte WHITE_COLOR = MapColor.WHITE.getRenderColorByte(MapColor.Brightness.HIGH); - public static byte getColor(MapState map, @Nullable Vector2ic pos) { - return pos == null ? -1 : getColor(map, pos.x(), pos.y()); - } + public static byte getColor(MapState map, @Nullable Vector2ic pos) { + return pos == null ? -1 : getColor(map, pos.x(), pos.y()); + } - public static byte getColor(MapState map, int x, int z) { - if (x < 0 || z < 0 || x >= 128 || z >= 128) { - return -1; - } - return map.colors[x + (z << 7)]; - } + public static byte getColor(MapState map, int x, int z) { + if (x < 0 || z < 0 || x >= 128 || z >= 128) { + return -1; + } + return map.colors[x + (z << 7)]; + } - public static boolean isEntranceColor(MapState map, int x, int z) { - return getColor(map, x, z) == Room.Type.ENTRANCE.color; - } + public static boolean isEntranceColor(MapState map, int x, int z) { + return getColor(map, x, z) == Room.Type.ENTRANCE.color; + } - public static boolean isEntranceColor(MapState map, @Nullable Vector2ic pos) { - return getColor(map, pos) == Room.Type.ENTRANCE.color; - } + public static boolean isEntranceColor(MapState map, @Nullable Vector2ic pos) { + return getColor(map, pos) == Room.Type.ENTRANCE.color; + } - @Nullable - private static Vector2i getMapPlayerPos(MapState map) { - for (MapDecoration decoration : map.getDecorations()) { - if (decoration.type().value().equals(MapDecorationTypes.FRAME.value())) { - return new Vector2i((decoration.x() >> 1) + 64, (decoration.z() >> 1) + 64); - } - } - return null; - } + @Nullable + private static Vector2i getMapPlayerPos(MapState map) { + for (MapDecoration decoration : map.getDecorations()) { + if (decoration.type().value().equals(MapDecorationTypes.FRAME.value())) { + return new Vector2i((decoration.x() >> 1) + 64, (decoration.z() >> 1) + 64); + } + } + return null; + } - @Nullable - public static ObjectIntPair getMapEntrancePosAndRoomSize(@NotNull MapState map) { - Vector2ic mapPos = getMapPlayerPos(map); - if (mapPos == null) { - return null; - } - Queue posToCheck = new ArrayDeque<>(); - Set checked = new HashSet<>(); - posToCheck.add(mapPos); - checked.add(mapPos); - while ((mapPos = posToCheck.poll()) != null) { - if (isEntranceColor(map, mapPos)) { - ObjectIntPair mapEntranceAndRoomSizePos = getMapEntrancePosAndRoomSizeAt(map, mapPos); - if (mapEntranceAndRoomSizePos.rightInt() > 0) { - return mapEntranceAndRoomSizePos; - } - } - Vector2ic pos = new Vector2i(mapPos).sub(10, 0); - if (checked.add(pos)) { - posToCheck.add(pos); - } - pos = new Vector2i(mapPos).sub(0, 10); - if (checked.add(pos)) { - posToCheck.add(pos); - } - pos = new Vector2i(mapPos).add(10, 0); - if (checked.add(pos)) { - posToCheck.add(pos); - } - pos = new Vector2i(mapPos).add(0, 10); - if (checked.add(pos)) { - posToCheck.add(pos); - } - } - return null; - } + @Nullable + public static ObjectIntPair getMapEntrancePosAndRoomSize(@NotNull MapState map) { + Vector2ic mapPos = getMapPlayerPos(map); + if (mapPos == null) { + return null; + } + Queue posToCheck = new ArrayDeque<>(); + Set checked = new HashSet<>(); + posToCheck.add(mapPos); + checked.add(mapPos); + while ((mapPos = posToCheck.poll()) != null) { + if (isEntranceColor(map, mapPos)) { + ObjectIntPair mapEntranceAndRoomSizePos = getMapEntrancePosAndRoomSizeAt(map, mapPos); + if (mapEntranceAndRoomSizePos.rightInt() > 0) { + return mapEntranceAndRoomSizePos; + } + } + Vector2ic pos = new Vector2i(mapPos).sub(10, 0); + if (checked.add(pos)) { + posToCheck.add(pos); + } + pos = new Vector2i(mapPos).sub(0, 10); + if (checked.add(pos)) { + posToCheck.add(pos); + } + pos = new Vector2i(mapPos).add(10, 0); + if (checked.add(pos)) { + posToCheck.add(pos); + } + pos = new Vector2i(mapPos).add(0, 10); + if (checked.add(pos)) { + posToCheck.add(pos); + } + } + return null; + } - private static ObjectIntPair getMapEntrancePosAndRoomSizeAt(MapState map, Vector2ic mapPosImmutable) { - Vector2i mapPos = new Vector2i(mapPosImmutable); - // noinspection StatementWithEmptyBody - while (isEntranceColor(map, mapPos.sub(1, 0))) { - } - mapPos.add(1, 0); - //noinspection StatementWithEmptyBody - while (isEntranceColor(map, mapPos.sub(0, 1))) { - } - return ObjectIntPair.of(mapPos.add(0, 1), getMapRoomSize(map, mapPos)); - } + private static ObjectIntPair getMapEntrancePosAndRoomSizeAt(MapState map, Vector2ic mapPosImmutable) { + Vector2i mapPos = new Vector2i(mapPosImmutable); + // noinspection StatementWithEmptyBody + while (isEntranceColor(map, mapPos.sub(1, 0))) { + } + mapPos.add(1, 0); + //noinspection StatementWithEmptyBody + while (isEntranceColor(map, mapPos.sub(0, 1))) { + } + return ObjectIntPair.of(mapPos.add(0, 1), getMapRoomSize(map, mapPos)); + } - public static int getMapRoomSize(MapState map, Vector2ic mapEntrancePos) { - int i = -1; - //noinspection StatementWithEmptyBody - while (isEntranceColor(map, mapEntrancePos.x() + ++i, mapEntrancePos.y())) { - } - return i > 5 ? i : 0; - } + public static int getMapRoomSize(MapState map, Vector2ic mapEntrancePos) { + int i = -1; + //noinspection StatementWithEmptyBody + while (isEntranceColor(map, mapEntrancePos.x() + ++i, mapEntrancePos.y())) { + } + return i > 5 ? i : 0; + } - /** - * Gets the map position of the top left corner of the room the player is in. - * - * @param map the map - * @param mapEntrancePos the map position of the top left corner of the entrance - * @param mapRoomSize the size of a room on the map - * @return the map position of the top left corner of the room the player is in - * @implNote {@code mapPos} is shifted by 2 so room borders are evenly split. - * {@code mapPos} is then shifted by {@code offset} to align the top left most room at (0, 0) - * so subtracting the modulo will give the top left corner of the room shifted by {@code offset}. - * Finally, {@code mapPos} is shifted back by {@code offset} to its intended position. - */ - @Nullable - public static Vector2ic getMapRoomPos(MapState map, Vector2ic mapEntrancePos, int mapRoomSize) { - int mapRoomSizeWithGap = mapRoomSize + 4; - Vector2i mapPos = getMapPlayerPos(map); - if (mapPos == null) { - return null; - } - Vector2ic offset = new Vector2i(mapEntrancePos.x() % mapRoomSizeWithGap, mapEntrancePos.y() % mapRoomSizeWithGap); - return mapPos.add(2, 2).sub(offset).sub(Math.floorMod(mapPos.x(), mapRoomSizeWithGap), Math.floorMod(mapPos.y(), mapRoomSizeWithGap)).add(offset); - } + /** + * Gets the map position of the top left corner of the room the player is in. + * + * @param map the map + * @param mapEntrancePos the map position of the top left corner of the entrance + * @param mapRoomSize the size of a room on the map + * @return the map position of the top left corner of the room the player is in + * @implNote {@code mapPos} is shifted by 2 so room borders are evenly split. + * {@code mapPos} is then shifted by {@code offset} to align the top left most room at (0, 0) + * so subtracting the modulo will give the top left corner of the room shifted by {@code offset}. + * Finally, {@code mapPos} is shifted back by {@code offset} to its intended position. + */ + @Nullable + public static Vector2ic getMapRoomPos(MapState map, Vector2ic mapEntrancePos, int mapRoomSize) { + int mapRoomSizeWithGap = mapRoomSize + 4; + Vector2i mapPos = getMapPlayerPos(map); + if (mapPos == null) { + return null; + } + Vector2ic offset = new Vector2i(mapEntrancePos.x() % mapRoomSizeWithGap, mapEntrancePos.y() % mapRoomSizeWithGap); + return mapPos.add(2, 2).sub(offset).sub(Math.floorMod(mapPos.x(), mapRoomSizeWithGap), Math.floorMod(mapPos.y(), mapRoomSizeWithGap)).add(offset); + } - /** - * Gets the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room. - * - * @param physicalEntrancePos the physical position of the northwest corner of the entrance room - * @param mapEntrancePos the map position of the top left corner of the entrance room - * @param mapRoomSize the size of a room on the map - * @param physicalPos the physical position of the northwest corner of the room - * @return the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room - */ - public static Vector2ic getMapPosFromPhysical(Vector2ic physicalEntrancePos, Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalPos) { - return new Vector2i(physicalPos).sub(physicalEntrancePos).div(32).mul(mapRoomSize + 4).add(mapEntrancePos); - } + /** + * Gets the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room. + * + * @param physicalEntrancePos the physical position of the northwest corner of the entrance room + * @param mapEntrancePos the map position of the top left corner of the entrance room + * @param mapRoomSize the size of a room on the map + * @param physicalPos the physical position of the northwest corner of the room + * @return the map position of the top left corner of the room corresponding to the physical position of the northwest corner of a room + */ + public static Vector2ic getMapPosFromPhysical(Vector2ic physicalEntrancePos, Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalPos) { + return new Vector2i(physicalPos).sub(physicalEntrancePos).div(32).mul(mapRoomSize + 4).add(mapEntrancePos); + } - /** - * Gets the map pos for the room that could be the furthest north-west on the map - * (doesn't mean the room has to exist, it's just the furthest possible room) - * - * @param mapEntrancePos The map pos of the entrance room - * @param mapRoomSize The size of a room on the map - * @return The map pos for the room that could be the furthest north-east on the map - */ - public static Vector2i getMapPosForNWMostRoom(Vector2ic mapEntrancePos, int mapRoomSize) { - return new Vector2i(Math.floorMod(mapEntrancePos.x(), (mapRoomSize + 4)), Math.floorMod(mapEntrancePos.y(), (mapRoomSize + 4))); - } + public static Vector2dc getMapPosFromPhysical(Vector2ic physicalEntrancePos, Vector2ic mapEntrancePos, int mapRoomSize, Position physicalPos) { + return new Vector2d(physicalPos.getX(), physicalPos.getZ()).sub(physicalEntrancePos.x(), physicalEntrancePos.y()).div(32).mul(mapRoomSize + 4).add(mapEntrancePos.x(), mapEntrancePos.y()); + } - /** - * @see #getPhysicalRoomPos(double, double) - */ - @NotNull - public static Vector2ic getPhysicalRoomPos(@NotNull Vec3d pos) { - return getPhysicalRoomPos(pos.getX(), pos.getZ()); - } + /** + * Gets the map pos for the room that could be the furthest north-west on the map + * (doesn't mean the room has to exist, it's just the furthest possible room) + * + * @param mapEntrancePos The map pos of the entrance room + * @param mapRoomSize The size of a room on the map + * @return The map pos for the room that could be the furthest north-east on the map + */ + public static Vector2i getMapPosForNWMostRoom(Vector2ic mapEntrancePos, int mapRoomSize) { + return new Vector2i(Math.floorMod(mapEntrancePos.x(), (mapRoomSize + 4)), Math.floorMod(mapEntrancePos.y(), (mapRoomSize + 4))); + } - /** - * @see #getPhysicalRoomPos(double, double) - */ - @NotNull - public static Vector2ic getPhysicalRoomPos(@NotNull Vec3i pos) { - return getPhysicalRoomPos(pos.getX(), pos.getZ()); - } + /** + * @see #getPhysicalRoomPos(double, double) + */ + @NotNull + public static Vector2ic getPhysicalRoomPos(@NotNull Vec3d pos) { + return getPhysicalRoomPos(pos.getX(), pos.getZ()); + } - /** - * Gets the physical position of the northwest corner of the room the given coordinate is in. Hypixel Skyblock Dungeons are aligned to a 32 by 32 blocks grid, allowing corners to be calculated through math. - * - * @param x the x position of the coordinate to calculate - * @param z the z position of the coordinate to calculate - * @return the physical position of the northwest corner of the room the player is in - * @implNote {@code physicalPos} is shifted by 0.5 so room borders are evenly split. - * {@code physicalPos} is further shifted by 8 because Hypixel offset dungeons by 8 blocks in Skyblock 0.12.3. - * Subtracting the modulo gives the northwest corner of the room shifted by 8. Finally, {@code physicalPos} is shifted back by 8 to its intended position. - */ - @NotNull - public static Vector2ic getPhysicalRoomPos(double x, double z) { - Vector2i physicalPos = new Vector2i(x + 8.5, z + 8.5, RoundingMode.TRUNCATE); - return physicalPos.sub(Math.floorMod(physicalPos.x(), 32), Math.floorMod(physicalPos.y(), 32)).sub(8, 8); - } + /** + * @see #getPhysicalRoomPos(double, double) + */ + @NotNull + public static Vector2ic getPhysicalRoomPos(@NotNull Vec3i pos) { + return getPhysicalRoomPos(pos.getX(), pos.getZ()); + } - public static Vector2ic[] getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic... mapPositions) { - for (int i = 0; i < mapPositions.length; i++) { - mapPositions[i] = getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, mapPositions[i]); - } - return mapPositions; - } + /** + * Gets the physical position of the northwest corner of the room the given coordinate is in. Hypixel Skyblock Dungeons are aligned to a 32 by 32 blocks grid, allowing corners to be calculated through math. + * + * @param x the x position of the coordinate to calculate + * @param z the z position of the coordinate to calculate + * @return the physical position of the northwest corner of the room the player is in + * @implNote {@code physicalPos} is shifted by 0.5 so room borders are evenly split. + * {@code physicalPos} is further shifted by 8 because Hypixel offset dungeons by 8 blocks in Skyblock 0.12.3. + * Subtracting the modulo gives the northwest corner of the room shifted by 8. Finally, {@code physicalPos} is shifted back by 8 to its intended position. + */ + @NotNull + public static Vector2ic getPhysicalRoomPos(double x, double z) { + Vector2i physicalPos = new Vector2i(x + 8.5, z + 8.5, RoundingMode.TRUNCATE); + return physicalPos.sub(Math.floorMod(physicalPos.x(), 32), Math.floorMod(physicalPos.y(), 32)).sub(8, 8); + } - /** - * Gets the physical position of the northwest corner of the room corresponding to the map position of the top left corner of a room. - * - * @param mapEntrancePos the map position of the top left corner of the entrance room - * @param mapRoomSize the size of a room on the map - * @param physicalEntrancePos the physical position of the northwest corner of the entrance room - * @param mapPos the map position of the top left corner of the room - * @return the physical position of the northwest corner of the room corresponding to the map position of the top left corner of a room - */ - public static Vector2ic getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic mapPos) { - return new Vector2i(mapPos).sub(mapEntrancePos).div(mapRoomSize + 4).mul(32).add(physicalEntrancePos); - } + public static Vector2ic[] getPhysicalPosFromMap(Vector2ic mapEntrancePos, int mapRoomSize, Vector2ic physicalEntrancePos, Vector2ic... mapPositions) { + for (int i = 0; i < mapPositions.length; i++) { + mapPositions[i] = getPhysicalPosFromMap(mapEntrancePos, mapRoomSize, physicalEntrancePos, mapPositions[i]); + } + return mapPositions; + } - public static Vector2ic getPhysicalCornerPos(Room.Direction direction, IntSortedSet segmentsX, IntSortedSet segmentsY) { - return switch (direction) { - case NW -> new Vector2i(segmentsX.firstInt(), segmentsY.firstInt()); - case NE -> new Vector2i(segmentsX.lastInt() + 30, segmentsY.firstInt()); - case SW -> new Vector2i(segmentsX.firstInt(), segmentsY.lastInt() + 30); - case SE -> new Vector2i(segmentsX.lastInt() + 30, segmentsY.lastInt() + 30); - }; - } + /** + * Gets the physical position of the northwest corner of the room corresponding to the map position of the top left corner of a room. + * + * @param mapEntrancePos the map position of the top left corner of the entrance room + * @param ma