nageyqma·git
HTTPS https://git.nageyqma.icu/nageyqma/odin_next.git SSH git@git.nageyqma.icu:nageyqma/odin_next.git

HTTPS 链接可直接 git clone;推送代码请使用 SSH。

仅展示默认分支 next 最近 100 条提交。

fix readme syntaxnageyqma · 2026-09-28 · 6f04056
查看变更
README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index ae087d7..9a1cd6b 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-#odin-next
+# odin-next
 ---
 
 This is a branch to add support for latest minecraft version.
 
-# OdinFabric
\ No newline at end of file
+# OdinFabric
26.3nageyqma · 2026-09-28 · f0b3d72
查看变更
README.md                                                                   |  5 +++++
 gradle.properties                                                           | 10 +++++-----
 src/main/java/com/odtheking/mixin/mixins/EntityMixin.java                   | 12 +++++++-----
 src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java           |  2 +-
 src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java             |  2 +-
 src/main/java/com/odtheking/mixin/mixins/YggdrasilSignatureIgnoreMixin.java |  4 ++--
 src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt                     |  4 ++--
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt |  5 +++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt  |  4 +++-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/PearlWaypoints.kt   |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt        |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/IrisCompat.kt                      |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderPipelines.kt    | 12 ++++++------
 src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderType.kt         |  3 ---
 src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt             |  5 +----
 src/main/kotlin/com/odtheking/odin/utils/render/RenderUtils.kt              |  4 ++--
 src/main/kotlin/com/odtheking/odin/utils/render/RoundedRectRenderer.kt      |  8 ++++----
 src/main/resources/odin.accesswidener                                       |  3 +--
 18 files changed, 46 insertions(+), 43 deletions(-)
diff --git a/README.md b/README.md
index 8475645..ae087d7 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,6 @@
+#odin-next
+---
+
+This is a branch to add support for latest minecraft version.
+
 # OdinFabric
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 8c7851d..43702f8 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,8 +2,8 @@
 org.gradle.jvmargs=-Xmx4G
 org.gradle.parallel=true
 #Fabric Properties
-minecraft_version=26.2
-minecraft_version_range=~26.2
+minecraft_version=26.3
+minecraft_version_range=~26.3
 loader_version=0.19.3
 loom_version=1.16-SNAPSHOT
 # Mod Properties
@@ -15,11 +15,11 @@ maven_group=com.odtheking
 archives_base_name=odin-fabric
 # Mod Dependencies
 fabric_kotlin_version=1.13.12+kotlin.2.4.0
-fabric_api_version=0.154.2+26.2
-modmenu_version=20.0.0-beta.4
+fabric_api_version=0.161.0+26.3
+modmenu_version=21.0.0
 devauth_version=1.2.2
 commodore_version=1.0.1
-iris=1.11.1+26.2-fabric
+iris=1.11.6+26.3-fabric
 # Other Properties
 kotlin_version=2.4.0
 kotlin.code.style=official
diff --git a/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java b/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
index cca158a..6a3914f 100644
--- a/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
@@ -6,6 +6,7 @@ import com.odtheking.odin.events.EntityEvent;
 import com.odtheking.odin.features.impl.dungeon.Highlight;
 import net.minecraft.world.entity.Entity;
 import net.minecraft.world.entity.InterpolationHandler;
+import net.minecraft.world.entity.PositionPath;
 import net.minecraft.world.phys.Vec3;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
@@ -25,14 +26,15 @@ public abstract class EntityMixin {
     }
 
     @WrapOperation(
-            method = "moveOrInterpolateTo(Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;)V",
-            at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/InterpolationHandler;interpolateTo(Lnet/minecraft/world/phys/Vec3;FF)V")
+            method = "moveOrInterpolateTo(Lnet/minecraft/world/entity/PositionPath;FFZ)V",
+            at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/InterpolationHandler;interpolateTo(Lnet/minecraft/world/entity/PositionPath;FFZ)Z")
     )
-     private void onMoveOrInterpolateTo(InterpolationHandler instance, Vec3 position, float yRot, float xRot, Operation<Void> original) {
-        original.call(instance, position, yRot, xRot);
+     private boolean onMoveOrInterpolateTo(InterpolationHandler instance, PositionPath path, float yRot, float xRot, boolean flag, Operation<Boolean> original) {
+        boolean result = original.call(instance, path, yRot, xRot, flag);
 
         Entity entity = (Entity)(Object)this;
-        new EntityEvent.Move(entity, position, yRot, xRot, entity.onGround()).postAndCatch();
+        new EntityEvent.Move(entity, path.endPosition(), yRot, xRot, entity.onGround()).postAndCatch();
+        return result;
     }
 
     @Inject(method = "setOnGround", at = @At("TAIL"))
diff --git a/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java b/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java
index 74ca475..800f6a4 100644
--- a/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java
@@ -16,7 +16,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 public abstract class EntityRendererMixin<T extends Entity> {
 
     @Inject(method = "shouldRender", at = @At("HEAD"), cancellable = true)
-    private void onRender(T entity, Frustum culler, double camX, double camY, double camZ, CallbackInfoReturnable<Boolean> cir) {
+    private void onRender(T entity, Frustum culler, double camX, double camY, double camZ, float partialTicks, CallbackInfoReturnable<Boolean> cir) {
         if (!HidePlayers.shouldRenderPlayer(entity)) cir.setReturnValue(false);
 
         if (RenderOptimizer.hideEntityDeathAnimation() && entity instanceof LivingEntity livingEntity && livingEntity.isDeadOrDying())
diff --git a/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java b/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java
index 64b5024..fa78f22 100644
--- a/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java
@@ -34,7 +34,7 @@ public class MouseHandlerMixin {
     @Inject(method = "releaseMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getWindow()Lcom/mojang/blaze3d/platform/Window;"))
     private void odin$correctCursorPosition(CallbackInfo ci) {
         if (OdinMod.getMc().gui.screen() instanceof ContainerScreen && NoCursorReset.shouldHookMouse()) {
-            InputConstants.grabOrReleaseMouse(OdinMod.getMc().getWindow(), InputConstants.CURSOR_NORMAL, this.beforeX, this.beforeY);
+            InputConstants.releaseMouse(OdinMod.getMc().getWindow(), this.beforeX, this.beforeY);
             this.xpos = this.beforeX;
             this.ypos = this.beforeY;
         }
diff --git a/src/main/java/com/odtheking/mixin/mixins/YggdrasilSignatureIgnoreMixin.java b/src/main/java/com/odtheking/mixin/mixins/YggdrasilSignatureIgnoreMixin.java
index 060590c..ff7e009 100644
--- a/src/main/java/com/odtheking/mixin/mixins/YggdrasilSignatureIgnoreMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/YggdrasilSignatureIgnoreMixin.java
@@ -1,13 +1,13 @@
 package com.odtheking.mixin.mixins;
 
 import com.mojang.authlib.properties.Property;
-import com.mojang.authlib.yggdrasil.YggdrasilServicesKeyInfo;
+import com.mojang.authlib.services.MinecraftServicesKeyInfo;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
-@Mixin(value = YggdrasilServicesKeyInfo.class, remap = false)
+@Mixin(value = MinecraftServicesKeyInfo.class, remap = false)
 public class YggdrasilSignatureIgnoreMixin {
 
     @Inject(method = "validateProperty", at = @At("HEAD"), cancellable = true, remap = false)
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt b/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt
index 6d6ddae..85573da 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt
@@ -1,5 +1,6 @@
 package com.odtheking.odin.clickgui
 
+import com.mojang.blaze3d.platform.InputConstants
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.clickgui.widget.PanelWidget
 import com.odtheking.odin.clickgui.widget.SearchBarWidget
@@ -17,7 +18,6 @@ import net.minecraft.client.input.KeyEvent
 import net.minecraft.client.input.MouseButtonEvent
 import net.minecraft.network.chat.Component
 import net.minecraft.util.FormattedCharSequence
-import org.lwjgl.glfw.GLFW
 
 object ClickGUI : Screen(Component.literal("Click GUI")) {
 
@@ -129,7 +129,7 @@ object ClickGUI : Screen(Component.literal("Click GUI")) {
     }
 
     override fun keyPressed(event: KeyEvent): Boolean {
-        if (event.key == GLFW.GLFW_KEY_F && event.hasControlDownWithQuirk()) {
+        if (event.key == InputConstants.KEY_F && event.hasControlDownWithQuirk()) {
             focusSearch()
             return true
         }
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt
index 30465af..78697d4 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt
@@ -24,7 +24,7 @@ class KeybindSetting(
     desc: String
 ) : RenderableSetting<InputConstants.Key>(name, desc), Saving {
 
-    constructor(name: String, defaultKeyCode: Int, desc: String = "") : this(name, InputConstants.Type.KEYSYM.getOrCreate(defaultKeyCode), desc)
+    constructor(name: String, defaultKeyCode: Int, desc: String = "") : this(name, InputConstants.Type.KEYBOARD.getOrCreate(defaultKeyCode), desc)
 
     override var value: InputConstants.Key
         get() = mapping?.let { KeyMappingHelper.getBoundKeyOf(it) } ?: pending
@@ -138,6 +138,7 @@ class KeybindSetting(
             }
         }
 
-        fun InputConstants.Key.isDown(): Boolean = InputConstants.isKeyDown(mc.window, value)
+        fun InputConstants.Key.isDown(): Boolean =
+            type == InputConstants.Type.KEYBOARD && value >= 0 && InputConstants.isKeyDown(value)
     }
 }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
index 4f70650..a621ad3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
@@ -101,7 +101,9 @@ fun handleSpawnPacket(particle: ClientboundLevelParticlesPacket) {
         particle.xDist != 2f ||
         particle.yDist != 3f ||
         particle.zDist != 2f ||
-        particle.maxSpeed != 0f ||
+        particle.xMaxSpeed != 0f ||
+        particle.yMaxSpeed != 0f ||
+        particle.zMaxSpeed != 0f ||
         particle.x % 1 != 0.0 ||
         particle.z % 1 != 0.0
     ) return
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/PearlWaypoints.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/PearlWaypoints.kt
index f42bcd9..2756093 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/PearlWaypoints.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/PearlWaypoints.kt
@@ -46,7 +46,7 @@ object PearlWaypoints : Module(
                                 drawFilledBox(AABB.ofSize(result.upAngle, 0.12, 0.12, 0.12), dynamicWaypointsColor)
                                 drawFilledBox(AABB.ofSize(result.flatAngle, 0.12, 0.12, 0.12), dynamicWaypointsColor)
                             }
-                            drawWireFrameBox(AABB(BlockPos(lineup.supply.dropOffSpot.above())), dynamicWaypointsColor)
+                            drawWireFrameBox(AABB(lineup.supply.dropOffSpot.above()), dynamicWaypointsColor)
                         }
                     }
                 }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt
index c267cd3..14d0756 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt
@@ -245,7 +245,7 @@ object Etherwarp : Module(
                 is SugarCaneBlock -> true
                 is MushroomBlock -> true
                 is NetherWartBlock -> true
-                is RedStoneWireBlock, is ComparatorBlock, is RepeaterBlock -> true
+                is RedstoneWireBlock, is ComparatorBlock, is RepeaterBlock -> true
                 is SmallDripleafBlock, is BigDripleafStemBlock -> true
                 is DoublePlantBlock -> true
                 is LeverBlock -> true
diff --git a/src/main/kotlin/com/odtheking/odin/utils/IrisCompat.kt b/src/main/kotlin/com/odtheking/odin/utils/IrisCompat.kt
index 5be4bba..ae62013 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/IrisCompat.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/IrisCompat.kt
@@ -1,6 +1,6 @@
 package com.odtheking.odin.utils
 
-import com.mojang.blaze3d.pipeline.RenderPipeline
+import com.mojang.renderpearl.api.pipeline.RenderPipeline
 import com.odtheking.odin.utils.render.CustomRenderType
 import net.fabricmc.loader.api.FabricLoader
 import net.irisshaders.iris.api.v0.IrisApi
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderPipelines.kt b/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderPipelines.kt
index 74a3049..e73ae7e 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderPipelines.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderPipelines.kt
@@ -1,12 +1,12 @@
 package com.odtheking.odin.utils.render
 
-import com.mojang.blaze3d.PrimitiveTopology
-import com.mojang.blaze3d.pipeline.BlendFunction
-import com.mojang.blaze3d.pipeline.ColorTargetState
-import com.mojang.blaze3d.pipeline.DepthStencilState
-import com.mojang.blaze3d.pipeline.RenderPipeline
-import com.mojang.blaze3d.platform.CompareOp
 import com.mojang.blaze3d.vertex.DefaultVertexFormat
+import com.mojang.renderpearl.api.pipeline.BlendFunction
+import com.mojang.renderpearl.api.pipeline.ColorTargetState
+import com.mojang.renderpearl.api.pipeline.CompareOp
+import com.mojang.renderpearl.api.pipeline.DepthStencilState
+import com.mojang.renderpearl.api.pipeline.PrimitiveTopology
+import com.mojang.renderpearl.api.pipeline.RenderPipeline
 import net.minecraft.client.renderer.RenderPipelines
 import net.minecraft.resources.Identifier
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderType.kt b/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderType.kt
index 57a19d1..027aa9d 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderType.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderType.kt
@@ -1,7 +1,6 @@
 package com.odtheking.odin.utils.render
 
 import net.minecraft.client.renderer.rendertype.LayeringTransform
-import net.minecraft.client.renderer.rendertype.OutputTarget
 import net.minecraft.client.renderer.rendertype.RenderSetup
 import net.minecraft.client.renderer.rendertype.RenderType
 
@@ -13,14 +12,12 @@ object CustomRenderType {
         "lines-esp",
         RenderSetup.builder(CustomRenderPipelines.LINES_ESP)
             .setLayeringTransform(LayeringTransform.VIEW_OFFSET_Z_LAYERING)
-            .setOutputTarget(OutputTarget.ITEM_ENTITY_TARGET)
             .createRenderSetup()
     )
 
     val LINES_TRANSLUCENT_ESP: RenderType = RenderType.create(
         "lines-translucent-esp",
         RenderSetup.builder(CustomRenderPipelines.LINES_TRANSLUCENT_ESP)
-            .setOutputTarget(OutputTarget.ITEM_ENTITY_TARGET)
             .createRenderSetup()
     )
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt b/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
index d430ca0..420ead6 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
@@ -2,9 +2,8 @@ package com.odtheking.odin.utils.render
 
 import com.mojang.blaze3d.platform.Lighting
 import com.mojang.blaze3d.systems.RenderSystem
-import com.mojang.blaze3d.textures.FilterMode
-import com.mojang.blaze3d.textures.GpuTextureView
 import com.mojang.blaze3d.vertex.PoseStack
+import com.mojang.renderpearl.api.textures.FilterMode
 import com.odtheking.odin.OdinMod.mc
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.navigation.ScreenRectangle
@@ -27,11 +26,9 @@ import java.util.*
 class ItemStateRenderer
     : PictureInPictureRenderer<ItemStateRenderer.State>() {
 
-    private var textureView: GpuTextureView? = null
     private var lastState: State? = null
 
     override fun renderToTexture(renderState: State, poseStack: PoseStack, collector: SubmitNodeCollector) {
-        textureView = RenderSystem.outputColorTextureOverride
         lastState = renderState
         poseStack.scale(1f, -1f, -1f)
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/RenderUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/render/RenderUtils.kt
index 18c4646..e354618 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/RenderUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/RenderUtils.kt
@@ -199,8 +199,8 @@ fun RenderExtractEvent.drawText(text: String, pos: Vec3, scale: Float, depth: Bo
     if (displayMode == Font.DisplayMode.SEE_THROUGH) {
         context.submitNodeCollector().submitCustom(SubmitRenderPhases.AFTER_TERRAIN,
             TextFeatureRenderer.Submit(
-                Matrix4f(poseStack.last().pose()), x, 0f, string,
-                true, displayMode, LightCoordsUtil.FULL_BRIGHT, -1, 0, 0
+                Matrix4f(poseStack.last().pose()), displayMode, LightCoordsUtil.FULL_BRIGHT,
+                TextFeatureRenderer.Content.Text(x, 0f, string, true, -1, 0, 0)
             )
         )
     } else context.submitNodeCollector().submitText(poseStack, x, 0f, string, true, displayMode, LightCoordsUtil.FULL_BRIGHT, -1, 0, 0)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/RoundedRectRenderer.kt b/src/main/kotlin/com/odtheking/odin/utils/render/RoundedRectRenderer.kt
index 2f60f72..1178d69 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/RoundedRectRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/RoundedRectRenderer.kt
@@ -1,12 +1,12 @@
 package com.odtheking.odin.utils.render
 
-import com.mojang.blaze3d.GpuFormat
-import com.mojang.blaze3d.pipeline.RenderPipeline
 import com.mojang.blaze3d.systems.RenderSystem
-import com.mojang.blaze3d.textures.FilterMode
 import com.mojang.blaze3d.vertex.DefaultVertexFormat
 import com.mojang.blaze3d.vertex.VertexConsumer
-import com.mojang.blaze3d.vertex.VertexFormat
+import com.mojang.renderpearl.api.GpuFormat
+import com.mojang.renderpearl.api.pipeline.RenderPipeline
+import com.mojang.renderpearl.api.textures.FilterMode
+import com.mojang.renderpearl.api.vertex.VertexFormat
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.utils.render.CustomRenderPipelines.PIPELINE_ROUND_RECT
 import com.odtheking.odin.utils.render.CustomRenderPipelines.PIPELINE_ROUND_RECT_SHADOW
diff --git a/src/main/resources/odin.accesswidener b/src/main/resources/odin.accesswidener
index 38ca284..e6b7f96 100644
--- a/src/main/resources/odin.accesswidener
+++ b/src/main/resources/odin.accesswidener
@@ -5,5 +5,4 @@ accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScr
 accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScreen topPos I
 accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScreen imageWidth I
 accessible field net/minecraft/client/gui/screens/inventory/AbstractContainerScreen imageHeight I
-accessible class com/mojang/blaze3d/opengl/GlDevice
-accessible field com/mojang/blaze3d/systems/GpuDevice backend Lcom/mojang/blaze3d/systems/GpuDeviceBackend;
\ No newline at end of file
+accessible field net/minecraft/client/gui/render/pip/PictureInPictureRenderer textureView Lcom/mojang/renderpearl/api/textures/GpuTextureView;
\ No newline at end of file
26.2 (#162)
* Cleanup rounded rectangle rendering * Started on project FVKGUI mostly functional maybe * 26.2 (only opengl) early * Migrated ClickGUI to the Minecraft backend to support different rendering engines * Fixed missing import * Reworked 3d rendering to work with proper minecraft API * Added animation for settings appearing from dependency hiding * Changed ColorSetting design and added color favorites system * Fix color favorite box positions * A couple fixes * Fixed merge and port * Bump version * Removed forgotten gui scale descale * Fixed import * Fixed BetterPartyFinder not checking the correct floor * Fixed DungeonMap websocket not checking for player names * Save options on main thread (#159) This fixes some warnings about C2ME supposedly automatically redirecting it to main thread. Not sure if it would give a different error or just cause some unsafe behaviour without C2ME. * Cleaned up module settings Made secret interact event higher skull range Fixed beacon texture rendering Fixed SelectorSetting tag position Added 4th color to numbers * Fixed dragon spray notification being wrong --------- Co-authored-by: Mustafa <mustafaoncel92@gmail.com>
odtheking · 2026-09-26 · ae6a199
查看变更
build.gradle.kts                                                                                     |  10 --
 gradle.properties                                                                                    |  11 +-
 src/main/java/com/odtheking/mixin/mixins/ExperienceBarRendererMixin.java                             |   4 +-
 src/main/java/com/odtheking/mixin/mixins/FontMixin.java                                              |   6 +-
 src/main/java/com/odtheking/mixin/mixins/GUIMixin.java                                               |  20 +++
 src/main/java/com/odtheking/mixin/mixins/{GuiMixin.java => HUDMixin.java}                            |   6 +-
 src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java                                         |   9 --
 src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java                                      |   2 +-
 src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java                              |   6 +-
 src/main/kotlin/com/odtheking/odin/OdinMod.kt                                                        |  17 +--
 src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt                                              | 237 ++++++++++++++---------------
 src/main/kotlin/com/odtheking/odin/clickgui/GuiTheme.kt                                              |  30 ++++
 src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt                                            | 124 ++++++++-------
 src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt                                                 | 154 -------------------
 src/main/kotlin/com/odtheking/odin/clickgui/SearchBar.kt                                             |  59 --------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt                                 | 116 --------------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/RenderableSetting.kt                            |  77 ++++++----
 src/main/kotlin/com/odtheking/odin/clickgui/settings/Setting.kt                                      |  61 +++-----
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/ActionSetting.kt                           |  48 +++---
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/BooleanSetting.kt                          |  64 ++------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/ColorSetting.kt                            | 405 +++++++++++++++++++++++++++++--------------------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/DropdownSetting.kt                         |  50 +++----
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/HUDElement.kt                              |  68 +++++++--
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/HUDSetting.kt                              | 118 ++++++---------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt                          | 137 +++++++++++------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/ListSetting.kt                             |   4 +-
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/MapSetting.kt                              |   6 +-
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/NumberSetting.kt                           | 209 +++++++++++++-------------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/SelectorSetting.kt                         | 197 ++++++++++++------------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/StringSetting.kt                           |  90 ++++++-----
 src/main/kotlin/com/odtheking/odin/clickgui/widget/ComponentHelpers.kt                               |  73 +++++++++
 src/main/kotlin/com/odtheking/odin/clickgui/widget/ModuleWidget.kt                                   | 130 ++++++++++++++++
 src/main/kotlin/com/odtheking/odin/clickgui/widget/OdinWidget.kt                                     | 115 ++++++++++++++
 src/main/kotlin/com/odtheking/odin/clickgui/widget/PanelWidget.kt                                    | 152 +++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/clickgui/widget/SearchBarWidget.kt                                |  59 ++++++++
 src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt                                            |   3 +-
 src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt                                |   7 +-
 src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt                                           |   4 +-
 src/main/kotlin/com/odtheking/odin/commands/WaypointCommand.kt                                       |  16 +-
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                                            |   7 +-
 src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt                                         |   8 +-
 src/main/kotlin/com/odtheking/odin/features/Category.kt                                              |  16 +-
 src/main/kotlin/com/odtheking/odin/features/Module.kt                                                |   2 +-
 src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt                                         |  18 +--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt                                  |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt                                |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt                                 |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonPriority.kt                              |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt                                  |  28 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt                           | 101 +++++++++----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt                                  |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt                                 |  26 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt                               |  44 +++---
 src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt                                   |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt                           |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt                              |  71 +++++----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt                               |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTitles.kt                              |   3 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerracottaTimer.kt                             |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt                               |  29 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt                       |  11 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/MelodySim.kt                           |  13 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt                          |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt                            |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt                            |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartGUI.kt                            |  17 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt                          |  11 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt                        |  14 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt                                |  30 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt                                  |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt                            |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt                             |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Highlight.kt                                |   7 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt                       |  32 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt                                 |  80 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MageBeam.kt                                 |   8 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt                                  | 186 +++++++----------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt                       |  15 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/RoomClear.kt                                |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt                            |  13 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt   |  20 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointHud.kt      |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypoints.kt        |  49 ++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/TextPromtScreen.kt         |   4 +-
 .../kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/WaypointPackSelectorScreen.kt   |  14 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt                           |  34 +++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt                          |   8 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt                     |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BeamsSolver.kt                |  10 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt                |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BoulderSolver.kt              |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/IceFillSolver.kt              |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt              |  35 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt                 |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/TPMazeSolver.kt               |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt                |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WeirdosSolver.kt              |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/BuildHelper.kt                               |   6 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/KuudraInfo.kt                                |   6 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/KuudraTracker.kt                             |  29 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Misc.kt                                      |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/PearlWaypoints.kt                            |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt                              |   9 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt                                  |   8 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt                            |  44 +++---
 src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt                                 |  23 +--
 src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt                                  |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/HidePlayers.kt                               |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/PerformanceHUD.kt                            |  10 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt                                |  10 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/RenderOptimizer.kt                           |   8 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/RenderTest.kt                                |  18 +--
 src/main/kotlin/com/odtheking/odin/features/impl/render/Shenanigans.kt                               |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt                                 |  29 +---
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt                            |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt                         |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/NoCursorReset.kt                           |   6 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt                             |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt                           |   6 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt                               |  31 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt                                  |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SpringBoots.kt                             |   4 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt                        |   2 +-
 src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt                                                |   4 +-
 src/main/kotlin/com/odtheking/odin/utils/PlayerUtils.kt                                              |   8 +-
 src/main/kotlin/com/odtheking/odin/utils/Utils.kt                                                    |   8 +-
 src/main/kotlin/com/odtheking/odin/utils/VecUtils.kt                                                 |   4 +-
 src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderPipelines.kt                             |  59 ++++++--
 src/main/kotlin/com/odtheking/odin/utils/render/CustomRenderType.kt                                  |  24 ++-
 src/main/kotlin/com/odtheking/odin/utils/render/DrawContextRenderer.kt                               | 130 ----------------
 src/main/kotlin/com/odtheking/odin/utils/render/DrawContextUtils.kt                                  |  23 ---
 src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt                                      |  16 +-
 src/main/kotlin/com/odtheking/odin/utils/render/RenderUtils.kt                                       | 416 +++++++++++++++++++--------------------------------
 src/main/kotlin/com/odtheking/odin/utils/render/RoundRectPIPRenderer.kt                              | 182 ----------------------
 src/main/kotlin/com/odtheking/odin/utils/render/RoundedRect.kt                                       | 114 ++++++++++++++
 src/main/kotlin/com/odtheking/odin/utils/render/RoundedRectRenderer.kt                               | 172 +++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/utils/render/Scissor.kt                                           |  64 ++++++++
 src/main/kotlin/com/odtheking/odin/utils/skyblock/KuudraUtils.kt                                     |   2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt                                   |   2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt                                   |   4 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt                         |  17 ++-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt                            |   3 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt                 |   8 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/MelodyHandler.kt |   8 +-
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt     |  16 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/PanesHandler.kt  |   3 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/RubixHandler.kt  |   2 +-
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/SelectAllHandler.kt   |   2 +-
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt  |   2 +-
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt    |   7 +-
 src/main/kotlin/com/odtheking/odin/utils/ui/HoverHandler.kt                                          |  23 ---
 src/main/kotlin/com/odtheking/odin/utils/ui/MouseUtils.kt                                            |  20 ---
 src/main/kotlin/com/odtheking/odin/utils/ui/TextInputHandler.kt                                      | 331 ----------------------------------------
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/Animation.kt                                  |  40 -----
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/Animations.kt                                 |  13 ++
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/ColorAnimation.kt                             |  22 ---
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/EaseInOutAnimation.kt                         |  19 ---
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/EaseOutAnimation.kt                           |  16 --
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/Easing.kt                                     |  17 +++
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/Fade.kt                                       |  47 ++++++
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/LinearAnimation.kt                            |  13 --
 src/main/kotlin/com/odtheking/odin/utils/ui/animations/Tween.kt                                      |  35 +++++
 src/main/kotlin/com/odtheking/odin/utils/ui/rendering/Font.kt                                        |  32 ----
 src/main/kotlin/com/odtheking/odin/utils/ui/rendering/Gradient.kt                                    |   6 -
 src/main/kotlin/com/odtheking/odin/utils/ui/rendering/Image.kt                                       |  54 -------
 src/main/kotlin/com/odtheking/odin/utils/ui/rendering/NVGPIPRenderer.kt                              | 102 -------------
 src/main/kotlin/com/odtheking/odin/utils/ui/rendering/NVGRenderer.kt                                 | 403 -------------------------------------------------
 src/main/resources/assets/odin/HueGradient.png                                                       | Bin 229 -> 0 bytes
 src/main/resources/assets/odin/MovementIcon.svg                                                      |   1 -
 src/main/resources/assets/odin/chevron.svg                                                           |   3 -
 src/main/resources/assets/odin/font.ttf                                                              | Bin 343244 -> 0 bytes
 src/main/resources/assets/odin/icon.png                                                              | Bin 807 -> 218 bytes
 src/main/resources/assets/odin/lang/en_us.json                                                       |   3 +
 src/main/resources/assets/odin/map/cross.png                                                         | Bin 250 -> 141 bytes
 src/main/resources/assets/odin/map/green_check.png                                                   | Bin 166 -> 120 bytes
 src/main/resources/assets/odin/map/question.png                                                      | Bin 168 -> 103 bytes
 src/main/resources/assets/odin/map/white_check.png                                                   | Bin 189 -> 120 bytes
 src/main/resources/assets/odin/shaders/core/round_rect.fsh                                           |  48 +-----
 src/main/resources/assets/odin/shaders/core/round_rect.vsh                                           |  29 +++-
 src/main/resources/assets/odin/shaders/core/round_rect_shadow.fsh                                    |  15 ++
 src/main/resources/assets/odin/shaders/core/round_rect_textured.fsh                                  |  17 +++
 src/main/resources/assets/odin/shaders/include/round_rect.glsl                                       |  46 ++++++
 src/main/resources/assets/odin/shaders/pipeline/round_rect.json                                      |   4 -
 src/main/resources/assets/odin/textures/chevron.png                                                  | Bin 0 -> 288 bytes
 src/main/resources/assets/odin/textures/huegradient.png                                              | Bin 0 -> 197 bytes
 src/main/resources/assets/odin/textures/job.png                                                      | Bin 428093 -> 307585 bytes
 src/main/resources/assets/odin/textures/logo.png                                                     | Bin 288721 -> 235849 bytes
 src/main/resources/assets/odin/textures/movementicon.png                                             | Bin 0 -> 3320 bytes
 src/main/resources/odin.mixins.json                                                                  |   3 +-
 189 files changed, 3124 insertions(+), 3820 deletions(-)
diff --git a/build.gradle.kts b/build.gradle.kts
index 366f335..755b1fd 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -32,16 +32,6 @@ dependencies {
 
     compileOnly("com.terraformersmc:modmenu:${property("modmenu_version")}")
 
-    property("minecraft_lwjgl_version").let { lwjglVersion ->
-        implementation("org.lwjgl:lwjgl-nanovg:$lwjglVersion")
-        include("org.lwjgl:lwjgl-nanovg:$lwjglVersion")
-
-        listOf("windows", "linux", "macos", "macos-arm64").forEach { os ->
-            implementation("org.lwjgl:lwjgl-nanovg:$lwjglVersion:natives-$os")
-            include("org.lwjgl:lwjgl-nanovg:$lwjglVersion:natives-$os")
-        }
-    }
-
     compileOnly("maven.modrinth:iris:${property("iris")}")
 }
 
diff --git a/gradle.properties b/gradle.properties
index c9ba482..8c7851d 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -2,8 +2,8 @@
 org.gradle.jvmargs=-Xmx4G
 org.gradle.parallel=true
 #Fabric Properties
-minecraft_version=26.1.2
-minecraft_version_range=~26.1
+minecraft_version=26.2
+minecraft_version_range=~26.2
 loader_version=0.19.3
 loom_version=1.16-SNAPSHOT
 # Mod Properties
@@ -15,12 +15,11 @@ maven_group=com.odtheking
 archives_base_name=odin-fabric
 # Mod Dependencies
 fabric_kotlin_version=1.13.12+kotlin.2.4.0
-fabric_api_version=0.151.0+26.1.2
-modmenu_version=18.0.0-beta.1
+fabric_api_version=0.154.2+26.2
+modmenu_version=20.0.0-beta.4
 devauth_version=1.2.2
 commodore_version=1.0.1
-iris=1.10.9+26.1-fabric
+iris=1.11.1+26.2-fabric
 # Other Properties
 kotlin_version=2.4.0
-minecraft_lwjgl_version=3.4.1
 kotlin.code.style=official
diff --git a/src/main/java/com/odtheking/mixin/mixins/ExperienceBarRendererMixin.java b/src/main/java/com/odtheking/mixin/mixins/ExperienceBarRendererMixin.java
index 2e9e90f..cb62685 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ExperienceBarRendererMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ExperienceBarRendererMixin.java
@@ -4,13 +4,13 @@ import com.odtheking.odin.features.impl.skyblock.OverlayType;
 import com.odtheking.odin.features.impl.skyblock.PlayerDisplay;
 import net.minecraft.client.DeltaTracker;
 import net.minecraft.client.gui.GuiGraphicsExtractor;
-import net.minecraft.client.gui.contextualbar.ExperienceBarRenderer;
+import net.minecraft.client.gui.contextualbar.ExperienceBar;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
-@Mixin(ExperienceBarRenderer.class)
+@Mixin(ExperienceBar.class)
 public class ExperienceBarRendererMixin {
 
     @Inject(method = "extractBackground", at = @At("HEAD"), cancellable = true)
diff --git a/src/main/java/com/odtheking/mixin/mixins/FontMixin.java b/src/main/java/com/odtheking/mixin/mixins/FontMixin.java
index bbbcc0e..23096e0 100644
--- a/src/main/java/com/odtheking/mixin/mixins/FontMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/FontMixin.java
@@ -25,9 +25,9 @@ public class FontMixin {
     }
 
     @ModifyVariable(method = "width(Ljava/lang/String;)I", at = @At("HEAD"), argsOnly = true, name = "str")
-    private String onWidthString(String str) {
-        if (!CustomNameReplacer.isEnabled()) return str;
-        return CustomNameReplacer.replaceStringIfNeeded(str);
+    private String onWidthString(String text) {
+        if (!CustomNameReplacer.isEnabled()) return text;
+        return CustomNameReplacer.replaceStringIfNeeded(text);
     }
 
     @ModifyVariable(method = "width(Lnet/minecraft/network/chat/FormattedText;)I", at = @At("HEAD"), argsOnly = true, name = "text")
diff --git a/src/main/java/com/odtheking/mixin/mixins/GUIMixin.java b/src/main/java/com/odtheking/mixin/mixins/GUIMixin.java
new file mode 100644
index 0000000..a155a0b
--- /dev/null
+++ b/src/main/java/com/odtheking/mixin/mixins/GUIMixin.java
@@ -0,0 +1,20 @@
+package com.odtheking.mixin.mixins;
+
+import com.odtheking.odin.events.ScreenCloseEvent;
+import com.odtheking.odin.events.ScreenEvent;
+import net.minecraft.client.gui.Gui;
+import net.minecraft.client.gui.screens.Screen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(Gui.class)
+public class GUIMixin {
+
+    @Inject(method = "setScreen", at = @At("TAIL"))
+    private void setScreen(Screen screen, CallbackInfo ci) {
+        if (screen != null) new ScreenEvent.Open(screen).postAndCatch();
+        else ScreenCloseEvent.INSTANCE.postAndCatch();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/GuiMixin.java b/src/main/java/com/odtheking/mixin/mixins/HUDMixin.java
similarity index 96%
rename from src/main/java/com/odtheking/mixin/mixins/GuiMixin.java
rename to src/main/java/com/odtheking/mixin/mixins/HUDMixin.java
index e950b2e..fe0646d 100644
--- a/src/main/java/com/odtheking/mixin/mixins/GuiMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/HUDMixin.java
@@ -3,16 +3,16 @@ package com.odtheking.mixin.mixins;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
 import com.odtheking.odin.features.impl.skyblock.OverlayType;
 import com.odtheking.odin.features.impl.skyblock.PlayerDisplay;
-import net.minecraft.client.gui.Gui;
 import net.minecraft.client.gui.GuiGraphicsExtractor;
+import net.minecraft.client.gui.Hud;
 import net.minecraft.world.entity.player.Player;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 
-@Mixin(Gui.class)
-public class GuiMixin {
+@Mixin(Hud.class)
+public class HUDMixin {
 
     @Inject(method = "extractArmor", at = @At("HEAD"), cancellable = true)
     private static void cancelArmorBar(GuiGraphicsExtractor graphics, Player player, int yLineBase, int numHealthRows, int healthRowHeight, int xLeft, CallbackInfo ci) {
diff --git a/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java b/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java
index a4be6ef..01a6329 100644
--- a/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java
@@ -3,12 +3,9 @@ package com.odtheking.mixin.mixins;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
 import com.odtheking.odin.events.BlockInteractEvent;
 import com.odtheking.odin.events.EntityInteractEvent;
-import com.odtheking.odin.events.ScreenCloseEvent;
-import com.odtheking.odin.events.ScreenEvent;
 import com.odtheking.odin.features.impl.boss.TerminalSolver;
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils;
 import net.minecraft.client.Minecraft;
-import net.minecraft.client.gui.screens.Screen;
 import net.minecraft.world.phys.BlockHitResult;
 import net.minecraft.world.phys.EntityHitResult;
 import net.minecraft.world.phys.HitResult;
@@ -43,10 +40,4 @@ public abstract class MinecraftMixin {
         if (TerminalUtils.getCurrentTerm() != null && TerminalSolver.getTermSize() != (Integer) original) return TerminalSolver.getTermSize();
         return original;
     }
-
-    @Inject(method = "setScreen", at = @At(value = "HEAD"))
-    private void onSetScreen(Screen screen, CallbackInfo ci) {
-        if (screen != null) new ScreenEvent.Open(screen).postAndCatch();
-        else ScreenCloseEvent.INSTANCE.postAndCatch();
-    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java b/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java
index 178b32d..64b5024 100644
--- a/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/MouseHandlerMixin.java
@@ -33,7 +33,7 @@ public class MouseHandlerMixin {
 
     @Inject(method = "releaseMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;getWindow()Lcom/mojang/blaze3d/platform/Window;"))
     private void odin$correctCursorPosition(CallbackInfo ci) {
-        if (OdinMod.getMc().screen instanceof ContainerScreen && NoCursorReset.shouldHookMouse()) {
+        if (OdinMod.getMc().gui.screen() instanceof ContainerScreen && NoCursorReset.shouldHookMouse()) {
             InputConstants.grabOrReleaseMouse(OdinMod.getMc().getWindow(), InputConstants.CURSOR_NORMAL, this.beforeX, this.beforeY);
             this.xpos = this.beforeX;
             this.ypos = this.beforeY;
diff --git a/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java b/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java
index d5be233..af7baf2 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java
@@ -2,8 +2,8 @@ package com.odtheking.mixin.mixins;
 
 import com.mojang.blaze3d.vertex.PoseStack;
 import com.odtheking.odin.features.impl.render.RenderOptimizer;
-import net.minecraft.client.renderer.MultiBufferSource;
 import net.minecraft.client.renderer.ScreenEffectRenderer;
+import net.minecraft.client.renderer.SubmitNodeCollector;
 import net.minecraft.client.renderer.texture.TextureAtlasSprite;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
@@ -13,8 +13,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 @Mixin(ScreenEffectRenderer.class)
 public class ScreenEffectRendererMixin {
 
-    @Inject(method = "renderFire", at = @At("HEAD"), cancellable = true)
-    private static void onRenderFireOverlay(PoseStack poseStack, MultiBufferSource bufferSource, TextureAtlasSprite sprite, CallbackInfo ci) {
+    @Inject(method = "submitFire", at = @At("HEAD"), cancellable = true)
+    private static void onRenderFireOverlay(PoseStack poseStack, SubmitNodeCollector submitNodeCollector, TextureAtlasSprite sprite, CallbackInfo ci) {
         if (RenderOptimizer.shouldDisableFireOverlay()) ci.cancel();
     }
 }
diff --git a/src/main/kotlin/com/odtheking/odin/OdinMod.kt b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
index 36d41a4..dc7fb0a 100644
--- a/src/main/kotlin/com/odtheking/odin/OdinMod.kt
+++ b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
@@ -10,13 +10,10 @@ import com.odtheking.odin.utils.IrisCompatability
 import com.odtheking.odin.utils.ServerUtils
 import com.odtheking.odin.utils.handlers.TickTasks
 import com.odtheking.odin.utils.render.ItemStateRenderer
-import com.odtheking.odin.utils.render.RenderBatchManager
-import com.odtheking.odin.utils.render.RoundRectPIPRenderer
 import com.odtheking.odin.utils.skyblock.*
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonListener
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
-import com.odtheking.odin.utils.ui.rendering.NVGPIPRenderer
 import com.odtheking.odin.utils.ui.widget.CustomGUIImpl
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
@@ -72,20 +69,10 @@ object OdinMod : ClientModInitializer {
             ActionBarListener, ServerUtils, EventDispatcher,
             DungeonListener, PartyUtils, TerminalUtils,
             DungeonUtils, SplitsManager, DungeonScan,
-            IrisCompatability, RenderBatchManager,
+            IrisCompatability,
             ModuleManager, CustomGUIImpl, Shenanigans,
         ).forEach { EventBus.subscribe(it) }
 
-        PictureInPictureRendererRegistry.register { context ->
-            NVGPIPRenderer(context.bufferSource())
-        }
-
-        PictureInPictureRendererRegistry.register { context ->
-            RoundRectPIPRenderer(context.bufferSource())
-        }
-
-        PictureInPictureRendererRegistry.register { context ->
-            ItemStateRenderer(context.bufferSource())
-        }
+        PictureInPictureRendererRegistry.register { ItemStateRenderer() }
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt b/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt
index 113da80..6d6ddae 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/ClickGUI.kt
@@ -1,177 +1,162 @@
 package com.odtheking.odin.clickgui
 
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.clickgui.settings.impl.ColorSetting
+import com.odtheking.odin.clickgui.widget.PanelWidget
+import com.odtheking.odin.clickgui.widget.SearchBarWidget
 import com.odtheking.odin.features.Category
 import com.odtheking.odin.features.ModuleManager
 import com.odtheking.odin.features.impl.render.ClickGUIModule
-import com.odtheking.odin.utils.Color
+import com.odtheking.odin.utils.Color.Companion.hsbMax
 import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.ui.HoverHandler
-import com.odtheking.odin.utils.ui.animations.EaseOutAnimation
-import com.odtheking.odin.utils.ui.rendering.NVGPIPRenderer
-import com.odtheking.odin.utils.ui.rendering.NVGRenderer
+import com.odtheking.odin.utils.render.roundedRectOutlined
+import com.odtheking.odin.utils.ui.animations.Animations
 import net.minecraft.client.gui.GuiGraphicsExtractor
+import net.minecraft.client.gui.components.events.GuiEventListener
 import net.minecraft.client.gui.screens.Screen
-import net.minecraft.client.input.CharacterEvent
 import net.minecraft.client.input.KeyEvent
 import net.minecraft.client.input.MouseButtonEvent
 import net.minecraft.network.chat.Component
-import kotlin.math.sign
-import com.odtheking.odin.utils.ui.mouseX as odinMouseX
-import com.odtheking.odin.utils.ui.mouseY as odinMouseY
+import net.minecraft.util.FormattedCharSequence
+import org.lwjgl.glfw.GLFW
 
-/**
- * Renders all the modules.
- */
 object ClickGUI : Screen(Component.literal("Click GUI")) {
 
-    private val panels: ArrayList<Panel> = arrayListOf<Panel>().apply {
-        if (Category.categories.any { (category, _) -> ClickGUIModule.panelSetting[category] == null }) ClickGUIModule.resetPositions()
-        for ((_, category) in Category.categories) add(Panel(category))
+    var searchString = ""
+        private set
+
+    private val panels: MutableList<PanelWidget> by lazy {
+        if (Category.categories.keys.any { ClickGUIModule.panelSetting[it] == null }) ClickGUIModule.resetPositions()
+        Category.categories.values.mapTo(mutableListOf()) { PanelWidget(it) }
     }
 
-    private var openAnim = EaseOutAnimation(500)
-    val gray38 = Color(38, 38, 38)
-    val gray26 = Color(26, 26, 26)
+    private val searchBar by lazy {
+        SearchBarWidget { text ->
+            searchString = text
+            panels.forEach { it.resetScroll() }
+        }
+    }
 
-    override fun extractRenderState(context: GuiGraphicsExtractor, mouseX: Int, mouseY: Int, deltaTicks: Float) {
-        NVGPIPRenderer.draw(context, 0, 0, context.guiWidth(), context.guiHeight()) {
-            val scaledMouseX = odinMouseX / ClickGUIModule.getStandardGuiScale()
-            val scaledMouseY = odinMouseY / ClickGUIModule.getStandardGuiScale()
+    private var scale: Float = 1f
 
-            NVGRenderer.scale(ClickGUIModule.getStandardGuiScale(), ClickGUIModule.getStandardGuiScale())
+    val virtualWidth get() = (mc.window.guiScaledWidth / scale).toInt()
+    val virtualHeight get() = (mc.window.guiScaledHeight / scale).toInt()
 
-            SearchBar.draw(
-                mc.window.screenWidth / (2f * ClickGUIModule.getStandardGuiScale()) - 175f,
-                (mc.window.screenHeight - 110f) / ClickGUIModule.getStandardGuiScale() - 20f,
-                scaledMouseX,
-                scaledMouseY
-            )
+    override fun children(): List<GuiEventListener> = panels.asReversed() + searchBar
 
-            if (openAnim.isAnimating()) {
-                val scale = openAnim.get(0f, 1f)
+    override fun extractRenderState(graphics: GuiGraphicsExtractor, mouseX: Int, mouseY: Int, partialTick: Float) {
+        val scaledX = (mouseX / scale).toInt()
+        val scaledY = (mouseY / scale).toInt()
 
-                val centerX = context.guiWidth().toFloat()
-                val centerY = context.guiHeight().toFloat()
-                NVGRenderer.translate(centerX, centerY)
-                NVGRenderer.scale(scale, scale)
-                NVGRenderer.translate(-centerX, -centerY)
-            }
+        graphics.pose().pushMatrix()
+        graphics.pose().scale(scale, scale)
 
-            val draggedPanel = panels.firstOrNull { it.dragging }
-            for (panel in panels) {
-                if (panel != draggedPanel) panel.draw(scaledMouseX, scaledMouseY)
-            }
+        searchBar.extractRenderState(graphics, scaledX, scaledY, partialTick)
+        for (panel in panels) panel.extractRenderState(graphics, scaledX, scaledY, partialTick)
+        drawDescription(graphics)
 
-            draggedPanel?.draw(scaledMouseX, scaledMouseY)
+        graphics.pose().popMatrix()
+        super.extractRenderState(graphics, mouseX, mouseY, partialTick)
+    }
 
-            desc.render()
-        }
-        super.extractRenderState(context, mouseX, mouseY, deltaTicks)
+    fun setDescription(text: String, x: Int, y: Int) {
+        description = text
+        descriptionX = x
+        descriptionY = y
     }
 
-    override fun mouseScrolled(
-        mouseX: Double,
-        mouseY: Double,
-        horizontalAmount: Double,
-        verticalAmount: Double
-    ): Boolean {
-        val actualAmount = (verticalAmount.sign * 16).toInt()
-        for (i in panels.size - 1 downTo 0) {
-            if (panels[i].handleScroll(actualAmount)) return true
+    private var description = ""
+    private var descriptionX = 0
+    private var descriptionY = 0
+
+    private var lines: List<FormattedCharSequence> = emptyList()
+    private var wrappedText = ""
+    private var boxWidth = 0
+
+    private fun drawDescription(graphics: GuiGraphicsExtractor) {
+        if (description.isEmpty()) return
+
+        if (description != wrappedText) {
+            wrappedText = description
+            lines = font.split(Component.literal(description), DESCRIPTION_WIDTH)
+            boxWidth = (lines.maxOfOrNull { font.width(it) } ?: 0) + DESCRIPTION_PADDING * 2
         }
-        return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
-    }
 
-    override fun mouseClicked(
-        mouseButtonEvent: MouseButtonEvent,
-        bl: Boolean
-    ): Boolean {
-        val scaledMouseX = odinMouseX / ClickGUIModule.getStandardGuiScale()
-        val scaledMouseY = odinMouseY / ClickGUIModule.getStandardGuiScale()
-        SearchBar.mouseClicked(scaledMouseX, scaledMouseY, mouseButtonEvent)
-        for (i in panels.size - 1 downTo 0) {
-            if (panels[i].mouseClicked(scaledMouseX, scaledMouseY, mouseButtonEvent)) return true
+        val boxHeight = lines.size * font.lineHeight + DESCRIPTION_PADDING * 2
+        val boxX = descriptionX.coerceIn(0, (virtualWidth - boxWidth).coerceAtLeast(0))
+        val boxY = descriptionY.coerceIn(0, (virtualHeight - boxHeight).coerceAtLeast(0))
+
+        graphics.roundedRectOutlined(
+            boxX, boxY, boxX + boxWidth, boxY + boxHeight,
+            GuiTheme.surface.rgba, GuiTheme.accent.hsbMax().rgba, 1.5f, GuiTheme.RADIUS
+        )
+        lines.forEachIndexed { index, line ->
+            graphics.text(
+                font, line, boxX + DESCRIPTION_PADDING,
+                boxY + DESCRIPTION_PADDING + index * font.lineHeight, Colors.WHITE.rgba, false
+            )
         }
-        return super.mouseClicked(mouseButtonEvent, bl)
+
+        description = ""
     }
 
-    override fun mouseReleased(mouseButtonEvent: MouseButtonEvent): Boolean {
-        SearchBar.mouseReleased()
-        for (i in panels.size - 1 downTo 0) {
-            panels[i].mouseReleased(mouseButtonEvent)
-        }
-        return super.mouseReleased(mouseButtonEvent)
+    private fun MouseButtonEvent.intoGuiSpace(): MouseButtonEvent =
+        scale.let { MouseButtonEvent(x() / it, y() / it, buttonInfo()) }
+
+    override fun mouseClicked(event: MouseButtonEvent, doubleClick: Boolean): Boolean {
+        val scaled = event.intoGuiSpace()
+        bringToFront(scaled)
+        return super.mouseClicked(scaled, doubleClick)
     }
 
-    override fun charTyped(characterEvent: CharacterEvent): Boolean {
-        SearchBar.keyTyped(characterEvent)
-        for (i in panels.size - 1 downTo 0) {
-            if (panels[i].keyTyped(characterEvent)) return true
-        }
-        return super.charTyped(characterEvent)
+    override fun mouseReleased(event: MouseButtonEvent): Boolean = super.mouseReleased(event.intoGuiSpace())
+
+    override fun mouseDragged(event: MouseButtonEvent, dragX: Double, dragY: Double): Boolean {
+        val factor = scale
+        return super.mouseDragged(event.intoGuiSpace(), dragX / factor, dragY / factor)
     }
 
-    override fun keyPressed(keyEvent: KeyEvent): Boolean {
-        SearchBar.keyPressed(keyEvent)
-        for (i in panels.size - 1 downTo 0) {
-            if (panels[i].keyPressed(keyEvent)) return true
-        }
-        return super.keyPressed(keyEvent)
+    override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean {
+        val factor = scale
+        return super.mouseScrolled(mouseX / factor, mouseY / factor, scrollX, scrollY)
     }
 
     override fun init() {
-        openAnim.start()
-        super.init()
+        Animations.settle()
+
+        scale = ClickGUIModule.clickGuiScale.toFloat() / mc.window.guiScale
+        searchBar.place(virtualWidth / 2 - SearchBarWidget.WIDTH / 2, virtualHeight - SEARCH_BAR_BOTTOM_MARGIN)
+        focusSearch()
     }
 
-    override fun onClose() {
-        for (panel in panels.filter { it.panelSetting.extended }.reversed()) {
-            for (moduleButton in panel.moduleButtons.filter { it.extended }) {
-                for (setting in moduleButton.representableSettings) {
-                    if (setting is ColorSetting) setting.section = null
-                    setting.listening = false
-                }
-            }
+    override fun keyPressed(event: KeyEvent): Boolean {
+        if (event.key == GLFW.GLFW_KEY_F && event.hasControlDownWithQuirk()) {
+            focusSearch()
+            return true
         }
+        return focused?.keyPressed(event) == true || super.keyPressed(event)
+    }
+
+    private fun focusSearch() {
+        setInitialFocus(searchBar)
+        searchBar.selectAll()
+    }
 
+    override fun onClose() {
+        panels.forEach { it.release() }
         ModuleManager.saveConfigurations()
         super.onClose()
     }
 
     override fun isPauseScreen(): Boolean = false
 
-    private var desc = Description("", 0f, 0f, HoverHandler(150))
-
-    /** Sets the description without creating a new data class which isn't optimal */
-    fun setDescription(text: String, x: Float, y: Float, hoverHandler: HoverHandler) {
-        desc.text = text
-        desc.x = x
-        desc.y = y
-        desc.hoverHandler = hoverHandler
-    }
-
-    data class Description(var text: String, var x: Float, var y: Float, var hoverHandler: HoverHandler) {
-
-        fun render() {
-            if (text.isEmpty() || hoverHandler.percent() < 100) return
-            val area = NVGRenderer.wrappedTextBounds(text, 300f, 16f, NVGRenderer.defaultFont)
-            NVGRenderer.rect(x, y, area[2] - area[0] + 16f, area[3] - area[1] + 16f, gray38.rgba, 5f)
-            NVGRenderer.hollowRect(
-                x,
-                y,
-                area[2] - area[0] + 16f,
-                area[3] - area[1] + 16f,
-                1.5f,
-                ClickGUIModule.clickGUIColor.rgba,
-                5f
-            )
-            NVGRenderer.drawWrappedString(text, x + 8f, y + 8f, 300f, 16f, Colors.WHITE.rgba, NVGRenderer.defaultFont)
-        }
+    private fun bringToFront(event: MouseButtonEvent) {
+        val clicked = panels.lastOrNull { it.isMouseOver(event.x(), event.y()) } ?: return
+        if (panels.last() === clicked) return
+        panels.remove(clicked)
+        panels.add(clicked)
     }
 
-    val movementImage = NVGRenderer.createImage("/assets/odin/MovementIcon.svg")
-    val hueImage = NVGRenderer.createImage("/assets/odin/HueGradient.png")
-    val chevronImage = NVGRenderer.createImage("/assets/odin/chevron.svg")
-}
\ No newline at end of file
+    private const val SEARCH_BAR_BOTTOM_MARGIN = 45
+    private const val DESCRIPTION_WIDTH = 200
+    private const val DESCRIPTION_PADDING = 8
+}
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/GuiTheme.kt b/src/main/kotlin/com/odtheking/odin/clickgui/GuiTheme.kt
new file mode 100644
index 0000000..61aaa93
--- /dev/null
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/GuiTheme.kt
@@ -0,0 +1,30 @@
+package com.odtheking.odin.clickgui
+
+import com.odtheking.odin.features.impl.render.ClickGUIModule
+import com.odtheking.odin.utils.Color
+import com.odtheking.odin.utils.Color.Companion.brighter
+import com.odtheking.odin.utils.Colors
+import net.minecraft.util.ARGB
+
+object GuiTheme {
+
+    const val ROW_WIDTH = 160
+    const val ROW_HEIGHT = 21
+    const val RADIUS = 5f
+    const val CAP = 5
+    const val PADDING = 6
+
+    const val PANEL_BLUR = 12f
+
+    fun textY(y: Int, height: Int): Int = y + (height - 8) / 2
+
+    val background: Color get() = Colors.gray26
+    val surface: Color get() = Colors.gray38
+    val accent: Color get() = ClickGUIModule.clickGUIColor
+
+    val shadow: Color = Color(0, 0, 0, 0.4f)
+}
+
+internal fun blend(from: Int, to: Int, progress: Float): Int = ARGB.srgbLerp(progress, from, to)
+internal fun Color.hoverTint(hover: Float, factor: Float = 1.3f): Int =
+    if (hover <= 0f) rgba else blend(rgba, brighter(factor).rgba, hover)
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt b/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt
index 531318f..ef28e49 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt
@@ -2,86 +2,70 @@ package com.odtheking.odin.clickgui
 
 import com.mojang.blaze3d.platform.InputConstants
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.clickgui.settings.impl.HUDSetting
 import com.odtheking.odin.clickgui.settings.impl.HudElement
 import com.odtheking.odin.features.ModuleManager
 import com.odtheking.odin.features.ModuleManager.hudSettingsCache
 import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.ui.animations.Animations
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.Screen
 import net.minecraft.client.input.KeyEvent
 import net.minecraft.client.input.MouseButtonEvent
 import net.minecraft.network.chat.Component
 import kotlin.math.sign
-import com.odtheking.odin.utils.ui.mouseX as odinMouseX
-import com.odtheking.odin.utils.ui.mouseY as odinMouseY
 
 object HudManager : Screen(Component.literal("HUD Manager")) {
 
     private var dragging: HudElement? = null
-
-    private var deltaX = 0f
-    private var deltaY = 0f
+    private var grabX = 0
+    private var grabY = 0
 
     override fun init() {
-        for (hud in hudSettingsCache) {
-            if (hud.isEnabled) {
-                hud.value.x = hud.value.x.coerceIn(0, (mc.window.screenWidth - (hud.value.width * hud.value.scale)).toInt())
-                hud.value.y = hud.value.y.coerceIn(0, (mc.window.screenHeight - (hud.value.height * hud.value.scale)).toInt())
-            }
+        Animations.settle()
+        for (setting in hudSettingsCache) {
+            if (setting.isEnabled) setting.hud.clampToScreen()
         }
-        super.init()
     }
 
     override fun extractRenderState(guiGraphics: GuiGraphicsExtractor, mouseX: Int, mouseY: Int, deltaTicks: Float) {
         super.extractRenderState(guiGraphics, mouseX, mouseY, deltaTicks)
 
-        dragging?.let {
-            it.x = (odinMouseX + deltaX).coerceIn(0f, (mc.window.screenWidth - (it.width * it.scale))).toInt()
-            it.y = (odinMouseY + deltaY).coerceIn(0f, (mc.window.screenHeight - (it.height * it.scale))).toInt()
+        dragging?.let { element ->
+            element.x = mouseX - grabX
+            element.y = mouseY - grabY
+            element.clampToScreen()
         }
 
-        guiGraphics.pose().pushMatrix()
-        val sf = mc.window.guiScale
-        guiGraphics.pose().scale(1f / sf, 1f / sf)
-
-        for (hud in hudSettingsCache) {
-            if (hud.isEnabled) hud.value.draw(guiGraphics, true)
+        for (setting in hudSettingsCache) {
+            if (setting.isEnabled) setting.hud.draw(guiGraphics, true, mouseX, mouseY)
         }
 
-        hudSettingsCache.firstOrNull { it.isEnabled && it.value.isHovered() }?.let { hoveredHud ->
-            guiGraphics.pose().pushMatrix()
-            guiGraphics.pose().translate(
-                (hoveredHud.value.x + hoveredHud.value.width * hoveredHud.value.scale + 10f),
-                hoveredHud.value.y.toFloat(),
+        hovered(mouseX, mouseY)?.let { setting ->
+            val element = setting.hud
+            val labelX = element.x + element.scaledWidth + LABEL_GAP
+            guiGraphics.text(font, setting.name, labelX, element.y, Colors.WHITE.rgba, true)
+            guiGraphics.textWithWordWrap(
+                font, Component.literal(setting.description),
+                labelX, element.y + font.lineHeight + 1, LABEL_WIDTH, Colors.WHITE.rgba
             )
-            guiGraphics.pose().scale(2f, 2f)
-            guiGraphics.text(mc.font, hoveredHud.name, 0, 0, Colors.WHITE.rgba)
-            guiGraphics.textWithWordWrap(mc.font, Component.literal(hoveredHud.description), 0, 10, 150, Colors.WHITE.rgba)
-            guiGraphics.pose().popMatrix()
         }
-
-        guiGraphics.pose().popMatrix()
     }
 
     override fun mouseScrolled(mouseX: Double, mouseY: Double, horizontalAmount: Double, verticalAmount: Double): Boolean {
-        val actualAmount = verticalAmount.sign.toFloat() * 0.2f
-        hudSettingsCache.firstOrNull { it.isEnabled && it.value.isHovered() }?.let { hovered ->
-            hovered.value.scale = (hovered.value.scale + actualAmount).coerceIn(1f, 10f)
-            return true
-        }
-        return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
+        val hovered = hovered(mouseX.toInt(), mouseY.toInt()) ?: return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
+        resize(hovered.hud, verticalAmount.sign.toFloat() * SCROLL_STEP)
+        return true
     }
 
     override fun mouseClicked(mouseButtonEvent: MouseButtonEvent, bl: Boolean): Boolean {
-        hudSettingsCache.firstOrNull { it.isEnabled && it.value.isHovered() }?.let { hovered ->
-            dragging = hovered.value
+        val hovered = hovered(mouseButtonEvent.x().toInt(), mouseButtonEvent.y().toInt())
+            ?: return super.mouseClicked(mouseButtonEvent, bl)
 
-            deltaX = (hovered.value.x - odinMouseX)
-            deltaY = (hovered.value.y - odinMouseY)
-            return true
-        }
-
-        return super.mouseClicked(mouseButtonEvent, bl)
+        dragging = hovered.hud
+        grabX = mouseButtonEvent.x().toInt() - hovered.hud.x
+        grabY = mouseButtonEvent.y().toInt() - hovered.hud.y
+        return true
     }
 
     override fun mouseReleased(mouseButtonEvent: MouseButtonEvent): Boolean {
@@ -90,32 +74,56 @@ object HudManager : Screen(Component.literal("HUD Manager")) {
     }
 
     override fun keyPressed(keyEvent: KeyEvent): Boolean {
-        hudSettingsCache.firstOrNull { it.isEnabled && it.value.isHovered() }?.let { hovered ->
+        val (mouseX, mouseY) = mousePosition()
+        hovered(mouseX, mouseY)?.let { setting ->
+            val element = setting.hud
             when (keyEvent.key) {
-                InputConstants.KEY_EQUALS -> hovered.value.scale = (hovered.value.scale + 0.1f).coerceIn(1f, 10f)
-                InputConstants.KEY_MINUS -> hovered.value.scale = (hovered.value.scale - 0.1f).coerceIn(1f, 10f)
-                InputConstants.KEY_RIGHT -> hovered.value.x += 10
-                InputConstants.KEY_LEFT -> hovered.value.x -= 10
-                InputConstants.KEY_UP -> hovered.value.y -= 10
-                InputConstants.KEY_DOWN -> hovered.value.y += 10
+                InputConstants.KEY_EQUALS -> resize(element, KEY_STEP)
+                InputConstants.KEY_MINUS -> resize(element, -KEY_STEP)
+                InputConstants.KEY_RIGHT -> element.x += NUDGE
+                InputConstants.KEY_LEFT -> element.x -= NUDGE
+                InputConstants.KEY_UP -> element.y -= NUDGE
+                InputConstants.KEY_DOWN -> element.y += NUDGE
+                else -> return super.keyPressed(keyEvent)
             }
+            element.clampToScreen()
+            return true
         }
-
         return super.keyPressed(keyEvent)
     }
 
     override fun onClose() {
+        dragging = null
         ModuleManager.saveConfigurations()
         super.onClose()
     }
 
+    override fun isPauseScreen(): Boolean = false
+
     fun resetHUDS() {
-        hudSettingsCache.forEach {
-            it.value.x = 10
-            it.value.y = 10
-            it.value.scale = 2f
+        for (setting in hudSettingsCache) {
+            setting.hud.x = 10
+            setting.hud.y = 10
+            setting.hud.scale = 1f
         }
     }
 
-    override fun isPauseScreen(): Boolean = false
+    private fun hovered(mouseX: Int, mouseY: Int): HUDSetting? =
+        hudSettingsCache.firstOrNull { it.isEnabled && it.hud.isHovered(mouseX, mouseY) }
+
+    private fun resize(element: HudElement, amount: Float) {
+        element.scale = (element.scale + amount).coerceIn(HudElement.MIN_SCALE, HudElement.MAX_SCALE)
+        element.clampToScreen()
+    }
+
+    private fun mousePosition(): Pair<Int, Int> {
+        val scale = mc.window.guiScale.coerceAtLeast(1)
+        return (mc.mouseHandler.xpos() / scale).toInt() to (mc.mouseHandler.ypos() / scale).toInt()
+    }
+
+    private const val LABEL_GAP = 10
+    private const val LABEL_WIDTH = 150
+    private const val SCROLL_STEP = 0.1f
+    private const val KEY_STEP = 0.1f
+    private const val NUDGE = 5
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt b/src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt
deleted file mode 100644
index 01a7bfb..0000000
--- a/src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt
+++ /dev/null
@@ -1,154 +0,0 @@
-package com.odtheking.odin.clickgui
-
-import com.odtheking.odin.clickgui.ClickGUI.gray26
-import com.odtheking.odin.clickgui.settings.ModuleButton
-import com.odtheking.odin.features.Category
-import com.odtheking.odin.features.ModuleManager
-import com.odtheking.odin.features.impl.render.ClickGUIModule
-import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.ui.isAreaHovered
-import com.odtheking.odin.utils.ui.rendering.NVGRenderer
-import net.minecraft.client.input.CharacterEvent
-import net.minecraft.client.input.KeyEvent
-import net.minecraft.client.input.MouseButtonEvent
-import kotlin.math.floor
-
-/**
- * Renders all the panels.
- *
- * Backend made by Aton, with some changes
- * Design mostly made by Stivais
- *
- * @author Stivais, Aton
- * @see [ModuleButton]
- */
-class Panel(private val category: Category) {
-
-    val panelSetting = ClickGUIModule.panelSetting[category.name] ?: throw IllegalStateException("Panel setting for category $category is not initialized")
-    val moduleButtons = ModuleManager.modulesByCategory[category]
-        ?.sortedByDescending { NVGRenderer.textWidth(it.name, 16f, NVGRenderer.defaultFont) }
-        ?.map { ModuleButton(it, this@Panel) } ?: listOf()
-    private val lastModuleButton by lazy { moduleButtons.lastOrNull() }
-
-    private val textWidth = NVGRenderer.textWidth(category.name, 22f, NVGRenderer.defaultFont)
-    private var previousHeight = 0f
-    private var scrollOffset = 0f
-    var dragging = false
-        private set
-    private var deltaX = 0f
-    private var deltaY = 0f
-
-    fun draw(mouseX: Float, mouseY: Float) {
-        if (dragging) {
-            panelSetting.x = floor(deltaX + mouseX)
-            panelSetting.y = floor(deltaY + mouseY)
-        }
-
-        NVGRenderer.dropShadow(
-            panelSetting.x,
-            panelSetting.y,
-            WIDTH,
-            previousHeight,
-            10f,
-            3f,
-            5f
-        )
-
-        NVGRenderer.drawHalfRoundedRect(panelSetting.x, panelSetting.y, WIDTH, HEIGHT, gray26.rgba, 5f, true)
-        NVGRenderer.text(
-            category.name,
-            panelSetting.x + WIDTH / 2f - textWidth / 2,
-            panelSetting.y + HEIGHT / 2f - 11,
-            22f,
-            Colors.WHITE.rgba,
-            NVGRenderer.defaultFont
-        )
-
-        if (scrollOffset != 0f) NVGRenderer.pushScissor(
-            panelSetting.x,
-            panelSetting.y + HEIGHT,
-            WIDTH,
-            previousHeight - HEIGHT + 10f
-        )
-
-        var startY = scrollOffset + HEIGHT
-        if (panelSetting.extended) {
-            for (button in moduleButtons) {
-                if (!button.module.name.contains(SearchBar.currentSearch, true)) continue
-                startY += button.draw(panelSetting.x, startY + panelSetting.y)
-            }
-        }
-        previousHeight = startY
-
-        NVGRenderer.drawHalfRoundedRect(panelSetting.x, panelSetting.y + startY, WIDTH, 10f, if (lastModuleButton?.module?.enabled == true) ClickGUIModule.clickGUIColor.rgba else gray26.rgba, 5f, false)
-        if (scrollOffset != 0f) NVGRenderer.popScissor()
-    }
-
-    fun handleScroll(amount: Int): Boolean {
-        if (!isMouseOverExtended) return false
-        scrollOffset = (scrollOffset + amount).coerceIn((-previousHeight + scrollOffset + 72f).coerceAtMost(0f), 0f)
-        return true
-    }
-
-    fun mouseClicked(mouseX: Float, mouseY: Float, click: MouseButtonEvent): Boolean {
-        if (isAreaHovered(panelSetting.x, panelSetting.y, WIDTH, HEIGHT, true)) {
-            if (click.button() == 0) {
-                deltaX = (panelSetting.x - mouseX)
-                deltaY = (panelSetting.y - mouseY)
-                dragging = true
-                return true
-            } else if (click.button() == 1) {
-                panelSetting.extended = !panelSetting.extended
-                return true
-            }
-        } else if (isMouseOverExtended) {
-            return moduleButtons.reversed().any {
-                if (!it.module.name.contains(SearchBar.currentSearch, true)) return@any false
-                it.mouseClicked(mouseX, mouseY, click)
-            }
-        }
-        return false
-    }
-
-    fun mouseReleased(click: MouseButtonEvent) {
-        dragging = false
-
-        if (panelSetting.extended)
-            moduleButtons.reversed().forEach {
-                if (!it.module.name.contains(SearchBar.currentSearch, true)) return@forEach
-                it.mouseReleased(click)
-            }
-    }
-
-    fun keyTyped(input: CharacterEvent): Boolean {
-        if (!panelSetting.extended) return false
-
-        return moduleButtons.reversed().any {
-            if (!it.module.name.contains(SearchBar.currentSearch, true)) return@any false
-            it.keyTyped(input)
-        }
-    }
-
-    fun keyPressed(input: KeyEvent): Boolean {
-        if (!panelSetting.extended) return false
-
-        return moduleButtons.reversed().any {
-            if (!it.module.name.contains(SearchBar.currentSearch, true)) return@any false
-            it.keyPressed(input)
-        }
-    }
-
-    private inline val isMouseOverExtended
-        get() = panelSetting.extended && isAreaHovered(
-            panelSetting.x,
-            panelSetting.y,
-            WIDTH,
-            previousHeight.coerceAtLeast(HEIGHT),
-            true
-        )
-
-    companion object {
-        const val WIDTH = 240f
-        const val HEIGHT = 32f
-    }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/SearchBar.kt b/src/main/kotlin/com/odtheking/odin/clickgui/SearchBar.kt
deleted file mode 100644
index e0e94dd..0000000
--- a/src/main/kotlin/com/odtheking/odin/clickgui/SearchBar.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package com.odtheking.odin.clickgui
-
-import com.odtheking.odin.clickgui.ClickGUI.gray38
-import com.odtheking.odin.features.impl.render.ClickGUIModule
-import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.ui.TextInputHandler
-import com.odtheking.odin.utils.ui.rendering.NVGRenderer
-import net.minecraft.client.input.CharacterEvent
-import net.minecraft.client.input.KeyEvent
-import net.minecraft.client.input.MouseButtonEvent
-
-object SearchBar {
-
-    var currentSearch = ""
-        private set(value) {
-            if (value == field || value.length > 16) return
-            field = value
-            searchWidth = NVGRenderer.textWidth(value, 20f, NVGRenderer.defaultFont)
-        }
-
-    private var placeHolderWidth = NVGRenderer.textWidth("Search here...", 20f, NVGRenderer.defaultFont)
-    private var searchWidth = NVGRenderer.textWidth(currentSearch, 20f, NVGRenderer.defaultFont)
-
-    private val textInputHandler = TextInputHandler(
-        textProvider = { currentSearch },
-        textSetter = { currentSearch = it }
-    )
-
-    fun draw(x: Float, y: Float, mouseX: Float, mouseY: Float) {
-        NVGRenderer.dropShadow(x, y, 350f, 40f, 10f, 0.75f, 9f)
-        NVGRenderer.rect(x, y, 350f, 40f, gray38.rgba, 9f)
-        NVGRenderer.hollowRect(x, y, 350f, 40f, 3f, ClickGUIModule.clickGUIColor.rgba, 9f)
-
-        val textY = y + 10f
-
-        if (currentSearch.isEmpty()) NVGRenderer.text("Search here...", x + 175f - placeHolderWidth / 2, textY, 20f, Colors.WHITE.rgba, NVGRenderer.defaultFont)
-        textInputHandler.x = (x + 175f - searchWidth / 2 - if (currentSearch.isEmpty()) placeHolderWidth / 2 + 2f else 0f).coerceAtLeast(x)
-        textInputHandler.y = textY - 1
-        textInputHandler.width = 250f
-        textInputHandler.height = 22f
-        textInputHandler.draw(mouseX, mouseY)
-    }
-
-    fun mouseClicked(mouseX: Float, mouseY: Float, click: MouseButtonEvent): Boolean {
-        return textInputHandler.mouseClicked(mouseX, mouseY, click)
-    }
-
-    fun mouseReleased() {
-        textInputHandler.mouseReleased()
-    }
-
-    fun keyPressed(input: KeyEvent): Boolean {
-        return textInputHandler.keyPressed(input)
-    }
-
-    fun keyTyped(input: CharacterEvent): Boolean {
-        return textInputHandler.keyTyped(input)
-    }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt b/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
deleted file mode 100644
index 334c7f6..0000000
--- a/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-package com.odtheking.odin.clickgui.settings
-
-import com.odtheking.odin.clickgui.ClickGUI
-import com.odtheking.odin.clickgui.ClickGUI.gray26
-import com.odtheking.odin.clickgui.Panel
-import com.odtheking.odin.features.Module
-import com.odtheking.odin.features.impl.rend
…… diff 过长已截断 ……
More accurate rotation axis for the player marker (#161)yuko1101 · 2026-09-24 · 6d8e1bf
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index a9a1864..cf24dc8 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -192,7 +192,10 @@ fun GuiGraphicsExtractor.renderPlayers() {
             fill(-5, -5, 5, 5, DungeonUtils.currentDungeonPlayer.clazz.color.rgba)
             PlayerFaceExtractor.extractRenderState(this, skin, -4, -4, 8)
         }
-    } else blit(RenderPipelines.GUI_TEXTURED, marker, -2, -3, 2f, 0f, 5, 7, 8, 8)
+    } else {
+        pose().translate(-0.5f, -0.5f)
+        blit(RenderPipelines.GUI_TEXTURED, marker, -2, -4, 2f, 0f, 5, 7, 8, 8)
+    }
     pose().popMatrix()
 }
 
Version bumpodtheking · 2026-09-15 · 38ddc1b
查看变更
gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index c9d896a..c9ba482 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ loom_version=1.16-SNAPSHOT
 mod_id=odin
 mod_name=Odin
 mod_description=A Hypixel Skyblock QoL mod
-mod_version=0.3.3
+mod_version=0.3.4
 maven_group=com.odtheking
 archives_base_name=odin-fabric
 # Mod Dependencies
Made core logging keep cores uniqueodtheking · 2026-09-15 · f124942
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt  |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt | 17 ++++++++++++++---
 2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index 34e7a51..551545f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -82,7 +82,7 @@ object DungeonMap : Module(
             val synced = try { gson.fromJson(message, DungeonRoom::class.java) } catch (_: Exception) { return@onMessage }
             if (synced.data == null) return@onMessage
 
-            val room = DungeonScan.rooms.find { it.data == synced.data } ?: return@onMessage
+            val room = DungeonScan.rooms.find { it.data == synced.data || it.tiles == synced.tiles } ?: return@onMessage
 
             if ((room.foundSecrets ?: -1) < (synced.foundSecrets ?: -1)) room.foundSecrets = synced.foundSecrets
             room.walkedInto = true
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
index c0a2cb2..243b726 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
@@ -2,6 +2,7 @@ package com.odtheking.odin.features.impl.dungeon.map
 
 import com.google.gson.reflect.TypeToken
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.OdinMod.scope
 import com.odtheking.odin.events.FloorEnterEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.MapUpdateEvent
@@ -14,6 +15,8 @@ import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.IVec2
 import com.odtheking.odin.utils.JsonResourceLoader.defaultGson
 import com.odtheking.odin.utils.skyblock.dungeon.Floor
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 import java.io.File
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.CopyOnWriteArrayList
@@ -43,10 +46,18 @@ object DungeonScan {
         defaultGson.fromJson<MutableMap<String, MutableList<Map<Int, List<String>>>>>(roomCoresFile.readText(), object : TypeToken<MutableMap<String, MutableList<Map<Int, List<String>>>>>() {}.type)
     }.getOrNull() ?: mutableMapOf()
 
+    private val roomCoresIODispatcher = Dispatchers.IO.limitedParallelism(1)
+
     fun recordRoomCore(name: String, core: Int, blocks: List<String>) {
-        roomCores.getOrPut(name) { mutableListOf() }.add(mapOf(core to blocks))
-        roomCoresFile.parentFile.mkdirs()
-        roomCoresFile.writeText(defaultGson.toJson(roomCores))
+        val cores = roomCores.getOrPut(name) { mutableListOf() }
+        if (cores.any { it.containsKey(core) }) return
+        cores.add(mapOf(core to blocks))
+
+        val json = defaultGson.toJson(roomCores)
+        scope.launch(roomCoresIODispatcher) {
+            roomCoresFile.parentFile.mkdirs()
+            roomCoresFile.writeText(json)
+        }
     }
 
     init {
Merge remote-tracking branch 'origin/main'odtheking · 2026-09-15 · c3475d8
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
More reliable DungeonMap websocket parsingodtheking · 2026-09-15 · 5d02723
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt     | 22 +++++-----------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt | 14 ++------------
 2 files changed, 7 insertions(+), 29 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
index 61afa34..044943f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
@@ -38,8 +38,6 @@ object TerminalTimes : Module(
     private var sectionTimer = 0L
     private var currentTick = 0L
     private var phaseTimer = 0L
-    private var lastSectionTime = 0f
-    private var lastPhaseTime = 0f
 
     init {
         on<TerminalEvent.Solve> {
@@ -47,33 +45,23 @@ object TerminalTimes : Module(
             pbs.time(terminal.type.name, (System.currentTimeMillis() - terminal.timeOpened) / 1000f, "s§7!", "§a${terminal.type.termName}${if (mc.screen is TermSimGUI) " §7(termsim)" else ""} §7solved in §6", sendMessage = terminalTimes)
         }
 
-        on<MessageEvent.Chat> {
+        on<MessageEvent.ModifyChat> {
             if (!terminalSplits) return@on
-            terminalCompleteRegex.find(message)?.destructured?.let { (name, activated, type, current, total) ->
-                lastSectionTime = sectionTimer.seconds
-                lastPhaseTime = phaseTimer.seconds
-
-                if ((current == total && gateBlown) || (current.toIntOrNull() ?: return@on) < completed.first) resetSection()
-                else completed = Pair(current.toIntOrNull() ?: return@on, total.toIntOrNull() ?: return@on)
-                return@on
-            }
 
             when {
                 gateDestroyedRegex.matches(message) -> if (completed.first == completed.second) resetSection() else gateBlown = true
-
                 goldorRegex.matches(message) -> resetSection(true)
-
                 coreOpeningRegex.matches(message) -> {
                     resetSection()
                     modMessage("§bTimes: §a${times.joinToString(" §8| ") { "§a${it}s" }}§8, §bTotal: §a${phaseTimer.seconds}s")
                 }
             }
-        }
 
-        on<MessageEvent.ModifyChat> {
-            if (!terminalSplits) return@on
             terminalCompleteRegex.find(message)?.destructured?.let { (name, activated, type, current, total) ->
-                component = Component.literal("§6$name §a$activated a $type! (§c${current}§a/${total}) §8(§7${lastSectionTime}s §8| §7${lastPhaseTime}s§8)")
+                component = Component.literal("§6$name §a$activated a $type! (§c${current}§a/${total}) §8(§7${sectionTimer.seconds}s §8| §7${phaseTimer.seconds}s§8)")
+
+                if ((current == total && gateBlown) || (current.toIntOrNull() ?: return@on) < completed.first) resetSection()
+                else completed = Pair(current.toIntOrNull() ?: return@on, total.toIntOrNull() ?: return@on)
             }
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index 9a3c8fd..34e7a51 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -82,18 +82,8 @@ object DungeonMap : Module(
             val synced = try { gson.fromJson(message, DungeonRoom::class.java) } catch (_: Exception) { return@onMessage }
             if (synced.data == null) return@onMessage
 
-            val existingRoom = DungeonScan.rooms.find { it.topLeft == synced.topLeft }
-            if (existingRoom?.data != null && existingRoom.data != synced.data) return@onMessage
-
-            val room = existingRoom ?: synced.also { new ->
-                DungeonScan.rooms.add(new)
-                for ((x, z) in new.tiles) {
-                    if (x !in 0..5 || z !in 0..5) continue
-                    DungeonScan.tiles[x + z * 6].room = new
-                }
-            }
-
-            if (room.data == null) room.data = synced.data
+            val room = DungeonScan.rooms.find { it.data == synced.data } ?: return@onMessage
+
             if ((room.foundSecrets ?: -1) < (synced.foundSecrets ?: -1)) room.foundSecrets = synced.foundSecrets
             room.walkedInto = true
         }
m3 ff timer to tick timers (#158)
* added fire freeze to tick timers since skyblockers ig just wasnt tick alligned to server, even though their naming schemes make it seem like it wasted too much time creating a simulation method to account for fall time just for it to be that :/ * removed useless comments and imports and added the detail its for m3 into the desc * reverted back to main so it doesnt get thrown into the pr --------- Co-authored-by: David28121 <ravinthedavin@gmail.com>
David28121 · 2026-09-15 · 522104b
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 84fc902..dc11cd0 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -28,6 +28,7 @@ object TickTimers : Module(
     private val stormEndRegex = Regex("^\\[BOSS] Storm: I should have known that I stood no chance\\.$")
     private val stormStartRegex = Regex("^\\[BOSS] Storm: Pathetic Maxor, just like expected\\.$")
     private val stormPyRegex = Regex("^\\[BOSS] Storm: (ENERGY HEED MY CALL|THUNDER LET ME BE YOUR CATALYST)!$")
+    private val professorFireFreezeRegex = Regex("^\\[BOSS] The Professor: Oh\\? You found my Guardians' one weakness\\?$")
 
     private var necronTime = -1
 
@@ -100,6 +101,17 @@ object TickTimers : Module(
         else 0 to 0
     }
 
+    private var fireFreezeTime = -1
+    private const val FIREFREEZEACTIVATIONTIME = 100
+
+    private val fireFreezeHud by HUD("Fire Freeze Hud", "Displays a timer for when to use fire freeze in M3."){
+        val timeTillFreeze = fireFreezeTime - FIREFREEZEACTIVATIONTIME
+        if (it)                         textDim(formatTimer(50, 106, "Fire Freeze:"), 0, 0, Colors.MINECRAFT_DARK_RED)
+        else if (timeTillFreeze > 0)    textDim(formatTimer(timeTillFreeze, 106, "Fire Freeze:"), 0, 0, Colors.MINECRAFT_DARK_RED)
+        else if (fireFreezeTime >= 0)   textDim("Use Fire Freeze", 0, 0, Colors.MINECRAFT_DARK_RED)
+        else 0 to 0
+    }
+
     init {
         on<MessageEvent.Chat> {
             when {
@@ -124,6 +136,7 @@ object TickTimers : Module(
                     pyTriggered = true
                     pyTickTime = 95
                 }
+                message.matches(professorFireFreezeRegex) -> fireFreezeTime = 206
             }
         }
 
@@ -140,6 +153,7 @@ object TickTimers : Module(
             if (pyTickTime >= 0) pyTickTime--
             if (necronTime >= 0) necronTime--
             if (stormTick >= 0) stormTick++
+            if (fireFreezeTime >= 0) fireFreezeTime--
         }
 
         on<LevelEvent.Load> {
@@ -152,6 +166,7 @@ object TickTimers : Module(
             necronTime = -1
             secretsCounter = 0
             stormTick = -1
+            fireFreezeTime = -1
         }
     }
 
Fixed PositionalMessages throwing NPE with old formatodtheking · 2026-09-15 · 40f19e8
查看变更
src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt                   | 12 ++++++------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt           |  6 ++----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt         |  3 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt     |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt |  8 ++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt     |  7 +++++--
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt      |  9 +++++----
 src/main/kotlin/com/odtheking/odin/utils/network/WebSocketConnection.kt        |  4 ++--
 8 files changed, 28 insertions(+), 25 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
index 38c8b64..d5518d3 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
@@ -35,10 +35,10 @@ val posMsgCommand = Commodore("posmsg") {
                 suggests { listOf("true", "false") }
             }
 
-            runs { x: Double, y: Double, z: Double, delay: Int, distance: Double, color: String, send: Boolean, message: GreedyString ->
+            runs { x: Double, y: Double, z: Double, delay: Int, distance: Double, color: String, dontSend: Boolean, message: GreedyString ->
                 if (posMessageStrings.any { it.message == message.string }) return@runs
                 val color = getColorFromString(color) ?: return@runs modMessage("Unknown color $color")
-                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, null, null, null, delay, distance, color, message.string, send))
+                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, null, null, null, delay, distance, color, message.string, dontSend))
                 modMessage("Message \"${message}\" added at $x, $y, $z, with ${delay}t delay, triggered up to $distance blocks away.")
                 ModuleManager.saveConfigurations()
             }
@@ -52,10 +52,10 @@ val posMsgCommand = Commodore("posmsg") {
                 suggests { listOf("true", "false") }
             }
 
-            runs { x: Double, y: Double, z: Double, x2: Double, y2: Double, z2: Double, delay: Int, color: String, send: Boolean, message: GreedyString ->
+            runs { x: Double, y: Double, z: Double, x2: Double, y2: Double, z2: Double, delay: Int, color: String, dontSend: Boolean, message: GreedyString ->
                 if (posMessageStrings.any { it.message == message.string }) return@runs
                 val color = getColorFromString(color) ?: return@runs modMessage("Unknown color $color")
-                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, x2, y2, z2, delay, null, color, message.string, send))
+                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, x2, y2, z2, delay, null, color, message.string, dontSend))
                 modMessage("Message \"${message}\" added in $x, $y, $z, $x2, $y2, $z2, with ${delay}t delay.")
                 ModuleManager.saveConfigurations()
             }
@@ -71,7 +71,7 @@ val posMsgCommand = Commodore("posmsg") {
                 input
             }
             suggests {
-                posMessageStrings.map { it.message }.distinct()
+                posMessageStrings.mapNotNull { it.message }.distinct()
             }
         }
 
@@ -91,7 +91,7 @@ val posMsgCommand = Commodore("posmsg") {
 
     literal("list").runs {
         val output = posMessageStrings.withIndex().joinToString(separator = "\n") { (index, it) ->
-            "${index + 1}: ${it.x}, ${it.y}, ${it.z}, ${it.x2}, ${it.y2}, ${it.z2}, ${it.delay}, ${it.distance}, ${it.color.hex()}, send=${it.send}, \"${it.message}\""
+            "${index + 1}: ${it.x}, ${it.y}, ${it.z}, ${it.x2}, ${it.y2}, ${it.z2}, ${it.delay}, ${it.distance}, ${it.color.hex()}, send=${it.dontSend}, \"${it.message}\""
         }
         modMessage(if (posMessageStrings.isEmpty()) "Positional Message list is empty!" else "Positonal Message list:\n$output")
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt
index 521719c..1843610 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt
@@ -3,7 +3,6 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.EntityEvent
 import com.odtheking.odin.utils.modMessage
-import net.minecraft.world.entity.EntityType
 import net.minecraft.world.entity.boss.enderdragon.EnderDragon
 import net.minecraft.world.entity.decoration.ArmorStand
 import net.minecraft.world.item.Items
@@ -25,12 +24,11 @@ object DragonCheck {
     }
 
     fun dragonSpawn(event: EntityEvent.Add) {
-        if (event.entity.type == EntityType.ENDER_DRAGON)
-            WitherDragonsEnum.entries.find { it.aabbDimensions.contains(event.entity.position()) }?.setAlive(event.entity.uuid)
+        WitherDragonsEnum.entries.find { it.aabbDimensions.contains(event.entity.position()) }?.setAlive(event.entity.uuid)
     }
 
     fun dragonSprayed(event: EntityEvent.SetItemSlot) {
-        if (event.stack.item == Items.PACKED_ICE ) return
+        if (event.stack.item == Items.PACKED_ICE) return
         val sprayedEntity = event.entity as? ArmorStand ?: return
 
         WitherDragonsEnum.entries.forEach { dragon ->
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
index 74bc4a9..2224b01 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
@@ -17,6 +17,7 @@ import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.skyblock.dungeon.M7Phases
 import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket
+import net.minecraft.world.entity.EntityType
 
 object WitherDragons : Module(
     name = "Wither Dragons",
@@ -72,7 +73,7 @@ object WitherDragons : Module(
         }
 
         on<EntityEvent.Add> {
-            if (DungeonUtils.getF7Phase() == M7Phases.P5) DragonCheck.dragonSpawn(this)
+            if (DungeonUtils.getF7Phase() == M7Phases.P5 && entity.type == EntityType.ENDER_DRAGON) DragonCheck.dragonSpawn(this)
         }
 
         on<EntityEvent.SetData> {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
index b78dcc2..4f70650 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
@@ -38,8 +38,8 @@ enum class WitherDragonsEnum(
     Blue(BlockPos(84, 14, 94), BlockPos(79, 23, 94), AABB(71.5, 13.0, 82.5, 96.5, 26.0, 107.5), 'b', Colors.MINECRAFT_AQUA, 82.0..88.0, 91.0..97.0),
     Purple(BlockPos(56, 14, 125), BlockPos(56, 22, 120), AABB(45.5, 13.0, 113.5, 68.5, 23.0, 136.5), '5', Colors.MINECRAFT_DARK_PURPLE, 53.0..59.0, 122.0..128.0);
 
-    fun setAlive(entityId: UUID?) {
-        if (entityId != null) this.entityUUID = entityId
+    fun setAlive(entityUUID: UUID?) {
+        if (entityUUID != null) this.entityUUID = entityUUID
 
         if (state == WitherDragonState.ALIVE || state != WitherDragonState.SPAWNING) return
         state = WitherDragonState.ALIVE
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
index ce68ccf..e29bd46 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
@@ -30,7 +30,7 @@ object PositionalMessages : Module(
     private val displayMessage by BooleanSetting("Show Message", true, desc = "Whether or not to display the message in the box.").withDependency { showPositions }
     private val messageSize by NumberSetting("Message Size", 1f, 0.1f, 4f, 0.1f, desc = "The size at which to display the message in the box.").withDependency { showPositions && displayMessage }
 
-    data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Int, val distance: Double?, val color: Color, val message: String, val send: Boolean) {
+    data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Int, val distance: Double?, val color: Color, val message: String?, val dontSend: Boolean) {
         @Transient
         private var _center: Vec3? = null
         val center: Vec3
@@ -59,7 +59,7 @@ object PositionalMessages : Module(
         onSend<ServerboundMovePlayerPacket> {
             if (onlyDungeons && !DungeonUtils.inBoss) return@onSend
             posMessageStrings.forEach { posMessage ->
-                if (posMessage.send && posMessage !in sentMessages) posMessage.x2?.let { handleInString(posMessage) } ?: handleAtString(posMessage)
+                if (!posMessage.dontSend && posMessage !in sentMessages) posMessage.x2?.let { handleInString(posMessage) } ?: handleAtString(posMessage)
             }
         }
 
@@ -68,10 +68,10 @@ object PositionalMessages : Module(
             posMessageStrings.forEach { posMessage ->
                 if (posMessage.distance != null) {
                     drawCylinder(posMessage.center, posMessage.distance.toFloat(), cylinderHeight, color = posMessage.color, depth = true)
-                    if (displayMessage) drawText(posMessage.message, Vec3(posMessage.x, posMessage.y + 1, posMessage.z), messageSize, true)
+                    if (displayMessage) posMessage.message?.let { drawText(it, Vec3(posMessage.x, posMessage.y + 1, posMessage.z), messageSize, true) }
                 } else {
                     drawWireFrameBox(posMessage.box ?: return@forEach, posMessage.color, depth = true)
-                    if (displayMessage) drawText(posMessage.message, posMessage.center.add(0.0, 1.0, 0.0), messageSize, true)
+                    if (displayMessage) posMessage.message?.let { drawText(it, posMessage.center.add(0.0, 1.0, 0.0), messageSize, true) }
                 }
             }
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index ab51ae9..9a3c8fd 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -78,11 +78,14 @@ object DungeonMap : Module(
 
     val syncSocket = webSocket {
         onMessage { message ->
-            if (!allowWebsocket) return@onMessage
+            if (!allowWebsocket || !DungeonUtils.inDungeons) return@onMessage
             val synced = try { gson.fromJson(message, DungeonRoom::class.java) } catch (_: Exception) { return@onMessage }
             if (synced.data == null) return@onMessage
 
-            val room = DungeonScan.rooms.find { it.topLeft == synced.topLeft } ?: synced.also { new ->
+            val existingRoom = DungeonScan.rooms.find { it.topLeft == synced.topLeft }
+            if (existingRoom?.data != null && existingRoom.data != synced.data) return@onMessage
+
+            val room = existingRoom ?: synced.also { new ->
                 DungeonScan.rooms.add(new)
                 for ((x, z) in new.tiles) {
                     if (x !in 0..5 || z !in 0..5) continue
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
index cecf212..3d63960 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
@@ -7,10 +7,7 @@ import com.odtheking.odin.clickgui.ClickGUI
 import com.odtheking.odin.clickgui.HudManager
 import com.odtheking.odin.clickgui.settings.AlwaysActive
 import com.odtheking.odin.clickgui.settings.impl.*
-import com.odtheking.odin.events.FloorEnterEvent
-import com.odtheking.odin.events.MessageEvent
-import com.odtheking.odin.events.RoomEnterEvent
-import com.odtheking.odin.events.SecretsUpdateEvent
+import com.odtheking.odin.events.*
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Category
 import com.odtheking.odin.features.Module
@@ -54,6 +51,10 @@ object ClickGUIModule : Module(
             if (room == null) DungeonMap.syncSocket.shutdown()
             else DungeonMap.syncSocket.send(gson.toJson(room))
         }
+
+        on<LevelEvent.Load> {
+            DungeonMap.syncSocket.shutdown()
+        }
     }
 
     val devMessage by BooleanSetting("Developer Message", false, desc = "Sends development related messages to the chat.")
diff --git a/src/main/kotlin/com/odtheking/odin/utils/network/WebSocketConnection.kt b/src/main/kotlin/com/odtheking/odin/utils/network/WebSocketConnection.kt
index b0baf7d..3024c6c 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/network/WebSocketConnection.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/network/WebSocketConnection.kt
@@ -49,7 +49,7 @@ class WebSocketConnection {
                 if (last) {
                     val message = messageBuilder.toString()
                     messageBuilder.clear()
-                    onMessageFunc(message)
+                    if (_webSocket.get() === webSocket) onMessageFunc(message)
                 }
                 webSocket.request(1)
                 return null
@@ -57,7 +57,7 @@ class WebSocketConnection {
 
             override fun onBinary(webSocket: WebSocket, data: ByteBuffer, last: Boolean): CompletionStage<*>? {
                 val text = StandardCharsets.UTF_8.decode(data).toString()
-                onMessageFunc(text)
+                if (_webSocket.get() === webSocket) onMessageFunc(text)
                 webSocket.request(1)
                 return null
             }
Fixed Croesus HUD only appearing sometimesodtheking · 2026-09-15 · 5a6af60
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt | 136 +++++++++++++++++++++++++++++++++++++++++++++++-------------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt |   4 +--
 2 files changed, 78 insertions(+), 62 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
index 448d05e..caa2553 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
@@ -8,7 +8,7 @@ import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.MessageEvent
-import com.odtheking.odin.events.ScreenEvent
+import com.odtheking.odin.events.ScreenCloseEvent
 import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
@@ -88,6 +88,10 @@ object Croesus : Module(
     private var currentChestProfit: Double? = null
     private var mostProfitableSlots = setOf<Int>()
 
+    private val chestSlotData = linkedMapOf<Int, ChestData>()
+    private val chestItemContributions = linkedMapOf<Int, ChestItem>()
+    private val chestCostContributions = linkedMapOf<Int, Double>()
+
     init {
         scope.launch {
             cachedPrices = fetchJson<Map<String, Double>>("https://lb.odtheking.com/averages/7day").getOrElse { OdinMod.logger.error("Failed to fetch lowest bin prices for Croesus module.", it); emptyMap() }
@@ -141,15 +145,18 @@ object Croesus : Module(
             val screenTitle = mc.screen?.title?.string ?: return@on
 
             when {
-                screenTitle.matches(chestNameRegex) -> handleChestContents(menu.items)
-                screenTitle.matches(chestPreviewScreenRegex) -> handleCroesusScreen(menu.items)
+                screenTitle.matches(chestNameRegex) -> handleChestContents(slotIndex, itemStack)
+                screenTitle.matches(chestPreviewScreenRegex) -> handleCroesusScreen(slotIndex, itemStack)
             }
         }
 
-        on<ScreenEvent.Open> {
+        on<ScreenCloseEvent> {
             mostProfitableSlots = emptySet()
             currentChestProfit = null
             chestData = emptyList()
+            chestSlotData.clear()
+            chestItemContributions.clear()
+            chestCostContributions.clear()
         }
 
         onReceive<ClientboundPlayerInfoUpdatePacket> {
@@ -170,35 +177,36 @@ object Croesus : Module(
         }
     }
 
-    private fun handleCroesusScreen(items: List<ItemStack>) {
-        val chests = mutableListOf<ChestData>()
-
-        items.forEachIndexed { index, stack ->
-            if (stack.isEmpty || index > 16 || stack.item != Items.PLAYER_HEAD) return@forEachIndexed
+    private fun handleCroesusScreen(index: Int, stack: ItemStack) {
+        if (index > 16) return
 
+        if (stack.isEmpty || stack.item != Items.PLAYER_HEAD) chestSlotData.remove(index)
+        else {
             val lore = stack.loreString
             val loreStartIndex = lore.indexOfFirst { it == "Contents" } + 1
-            if (loreStartIndex == 0) return@forEachIndexed
 
-            val loreEndIndex = lore.indexOfFirst { it.isEmpty() }
+            if (loreStartIndex == 0) chestSlotData.remove(index)
+            else {
+                val loreEndIndex = lore.indexOfFirst { it.isEmpty() }
 
-            val chestCost = lore.getOrNull(loreEndIndex + 2)?.let { costLine ->
-                chestCostRegex.find(costLine)?.groupValues?.get(1)?.replace(",", "")?.toDoubleOrNull()
-            } ?: 0.0
+                val chestCost = lore.getOrNull(loreEndIndex + 2)?.let { costLine ->
+                    chestCostRegex.find(costLine)?.groupValues?.get(1)?.replace(",", "")?.toDoubleOrNull()
+                } ?: 0.0
 
-            val items = mutableListOf<ChestItem>()
-            var totalValue = 0.0
+                val items = mutableListOf<ChestItem>()
+                var totalValue = 0.0
 
-            lore.subList(loreStartIndex, loreEndIndex).forEach { item ->
-                val price = parseItemValue(item) ?: 0.0
-                totalValue += price
-                items.add(ChestItem(item, price))
-            }
+                lore.subList(loreStartIndex, loreEndIndex).forEach { item ->
+                    val price = parseItemValue(item) ?: 0.0
+                    totalValue += price
+                    items.add(ChestItem(item, price))
+                }
 
-            chests.add(ChestData(stack.hoverName, items, totalValue - chestCost, index))
+                chestSlotData[index] = ChestData(stack.hoverName, items, totalValue - chestCost, index)
+            }
         }
 
-        chestData = chests.sortedByDescending { it.profit }
+        chestData = chestSlotData.values.sortedByDescending { it.profit }
         mostProfitableSlots = chestData.filter { it.profit > 0 }.take(2).map { it.slotIndex }.toSet()
     }
 
@@ -222,60 +230,68 @@ object Croesus : Module(
         return cachedPrices[item.uppercase() .replace("'", "").replace(" -", "").replace(" ", "_")]
     }
 
-    private fun handleChestContents(items: List<ItemStack>) {
-        val chestItems = mutableListOf<ChestItem>()
-        var chestCost = 0.0
-        var profit = 0.0
+    private fun handleChestContents(index: Int, stack: ItemStack) {
+        if (index > 40) return
 
-        items.forEachIndexed { index, stack ->
-            if (stack.isEmpty || index > 40) return@forEachIndexed
+        when {
+            stack.isEmpty -> {
+                chestItemContributions.remove(index)
+                chestCostContributions.remove(index)
+            }
 
-            when (stack.item) {
-                Items.CHEST -> {
-                    stack.loreString.forEach { loreLine ->
-                        chestCostRegex.find(loreLine)?.groupValues?.get(1)?.let { cost ->
-                            chestCost = cost.replace(",", "").toDouble()
-                        }
+            stack.item == Items.CHEST -> {
+                chestItemContributions.remove(index)
+                chestCostContributions.remove(index)
+                stack.loreString.forEach { loreLine ->
+                    chestCostRegex.find(loreLine)?.groupValues?.get(1)?.let { cost ->
+                        chestCostContributions[index] = cost.replace(",", "").toDouble()
                     }
                 }
+            }
 
-                Items.ENCHANTED_BOOK -> {
-                    chestEnchantsRegex.find(stack.customData.get("enchantments").toString())?.destructured?.let { (name, level) ->
-                        cachedPrices["ENCHANTED_BOOK-${name.uppercase()}-$level"]?.let {
-                            chestItems += ChestItem(stack.hoverName.string, it)
-                            profit += it
-                        }
-                    }
-                }
-                else -> {
-                    previewEssenceRegex.find(stack.hoverName.string)?.destructured?.let { (name, quantity) ->
-                        if (!includeEssence) return@forEachIndexed
-                        val price = cachedPrices["ESSENCE_${name.uppercase()}"] ?: return@forEachIndexed
-                        chestItems += ChestItem(stack.hoverName.string, price * quantity.toDouble())
-                        profit += price * quantity.toDouble()
-                    } ?: previewShardRegex.find(stack.hoverName.string)?.destructured?.let { (shardName) ->
-                        cachedPrices["SHARD_${shardName.uppercase().replace(" ", "_").replace("'s", "")}"]?.let {
-                            chestItems += ChestItem(stack.hoverName.string, it)
-                            profit += it
-                        }
-                    } ?: cachedPrices[stack.itemId]?.let {
-                        chestItems += ChestItem(stack.hoverName.string, it)
-                        profit += it
-                    }
+            stack.item == Items.ENCHANTED_BOOK -> {
+                chestCostContributions.remove(index)
+                val contribution = chestEnchantsRegex.find(stack.customData.get("enchantments").toString())?.destructured?.let { (name, level) ->
+                    cachedPrices["ENCHANTED_BOOK-${name.uppercase()}-$level"]?.let { ChestItem(stack.hoverName.string, it) }
                 }
+                if (contribution != null) chestItemContributions[index] = contribution else chestItemContributions.remove(index)
+            }
+
+            else -> {
+                chestCostContributions.remove(index)
+                val contribution = resolveChestItemContribution(stack)
+                if (contribution != null) chestItemContributions[index] = contribution else chestItemContributions.remove(index)
             }
         }
-        currentChestProfit = profit - chestCost
+
+        val profit = chestItemContributions.values.sumOf { it.price }
+        currentChestProfit = profit - chestCostContributions.values.sum()
         chestData = listOf(
             ChestData(
                 Component.literal("§eChest"),
-                chestItems,
+                chestItemContributions.values.toList(),
                 currentChestProfit ?: 0.0,
                 -1
             )
         )
     }
 
+    private fun resolveChestItemContribution(stack: ItemStack): ChestItem? {
+        previewEssenceRegex.find(stack.hoverName.string)?.destructured?.let { (name, quantity) ->
+            if (!includeEssence) return null
+            val price = cachedPrices["ESSENCE_${name.uppercase()}"] ?: return null
+            return ChestItem(stack.hoverName.string, price * quantity.toDouble())
+        }
+
+        previewShardRegex.find(stack.hoverName.string)?.destructured?.let { (shardName) ->
+            cachedPrices["SHARD_${shardName.uppercase().replace(" ", "_").replace("'s", "")}"]?.let {
+                return ChestItem(stack.hoverName.string, it)
+            }
+        }
+
+        return cachedPrices[stack.itemId]?.let { ChestItem(stack.hoverName.string, it) }
+    }
+
     private fun hasStrikeThrough(itemName: String, loreComponents: List<Component>): Boolean =
         loreComponents.any { line ->
             line.siblings.any { sibling ->
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
index 4e59894..c6a659b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
@@ -3,7 +3,7 @@ package com.odtheking.odin.features.impl.nether
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.GuiEvent
-import com.odtheking.odin.events.ScreenEvent
+import com.odtheking.odin.events.ScreenCloseEvent
 import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -79,7 +79,7 @@ object Vesuvius : Module(
             if (slotIndex.equalsOneOf(13, 14) && itemStack.item == Items.PLAYER_HEAD) handleKuudraChest(itemStack)
         }
 
-        on<ScreenEvent.Open> {
+        on<ScreenCloseEvent> {
             currentChest = null
         }
     }
Fixed TerminalSounds Fixed Mimic Message (??) Fixed TerminalTimesodtheking · 2026-09-14 · 9e1c902
查看变更
src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java                                |  8 ++++++++
 src/main/java/com/odtheking/mixin/mixins/EntityMixin.java                                              |  5 -----
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                                              |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt                                | 18 +++---------------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt                                 | 14 ++++++++++++--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt |  3 +--
 6 files changed, 25 insertions(+), 25 deletions(-)
diff --git a/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java b/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
index 85350dc..188cc56 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
@@ -3,10 +3,12 @@ package com.odtheking.mixin.mixins;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
 import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
 import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.odtheking.odin.events.EntityEvent;
 import com.odtheking.odin.events.PacketEvent;
 import net.minecraft.client.multiplayer.ClientPacketListener;
 import net.minecraft.network.PacketListener;
 import net.minecraft.network.protocol.Packet;
+import net.minecraft.world.entity.Entity;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 
@@ -23,4 +25,10 @@ public class ClientPacketListenerMixin {
         if (new PacketEvent.Receive(packet).postAndCatch()) return;
         original.call(packet, listener);
     }
+
+    @WrapOperation(method = "handleEntityEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;handleEntityEvent(B)V"))
+    private void onHandleEntityEvent(Entity instance, byte id, Operation<Void> original) {
+        new EntityEvent.Event(instance, id).postAndCatch();
+        original.call(instance, id);
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java b/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
index 2458ff9..cca158a 100644
--- a/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
@@ -40,9 +40,4 @@ public abstract class EntityMixin {
         Entity entity = (Entity)(Object)this;
         new EntityEvent.Move(entity, Vec3.ZERO, 0f, 0f, onGround).postAndCatch();
     }
-
-    @Inject(method = "handleEntityEvent", at = @At("TAIL"))
-    private void onHandleEntityEvent(byte id, CallbackInfo ci) {
-        new EntityEvent.Event((Entity)(Object)this, id).postAndCatch();
-    }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index ada0c59..ec6229f 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -57,7 +57,7 @@ abstract class TerminalEvent(val terminal: TerminalHandler) : Event {
     class Open(terminal: TerminalHandler) : TerminalEvent(terminal)
     class Close(terminal: TerminalHandler) : TerminalEvent(terminal)
     class Solve(terminal: TerminalHandler) : TerminalEvent(terminal)
-    class Click(terminal: TerminalHandler, val slotIndex: Int, val button: Int) : TerminalEvent(terminal)
+    class Click(terminal: TerminalHandler, val slotIndex: Int, val button: Int, val solution: ArrayList<Int>) : TerminalEvent(terminal)
 }
 
 interface TickEvent : Event {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
index 70cbf32..16bf09c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
@@ -1,10 +1,8 @@
 package com.odtheking.odin.features.impl.boss
 
-import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.events.TerminalEvent
-import com.odtheking.odin.events.core.EventPriority
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
@@ -25,7 +23,6 @@ object TerminalSounds : Module(
     private val clickSoundSettings = createSoundSettings("Click Sound", "entity.blaze.hurt") { clickSounds }
     private val completeSounds by BooleanSetting("Complete Sounds", false, desc = "Plays a sound when you complete a terminal.")
     private val completeSoundSettings = createSoundSettings("Completion Sound", "entity.experience_orb.pickup") { completeSounds }
-    private val cancelLastClick by BooleanSetting("Cancel Last Click", false, desc = "Cancels the last click sound instead of playing both click and completion sound.").withDependency { clickSounds && completeSounds }
 
     private val coreRegex = Regex("^The Core entrance is opening!$")
     private val gateRegex = Regex("^The gate has been destroyed!$")
@@ -37,8 +34,9 @@ object TerminalSounds : Module(
             else if (shouldReplaceSounds && completeSounds && !clickSounds) playSoundSettings(completeSoundSettings())
         }
 
-        on<TerminalEvent.Click> (EventPriority.HIGHEST) {
-            if (shouldReplaceSounds) playSoundForSlot(slotIndex, button)
+        on<TerminalEvent.Click> {
+            if ((solution.isEmpty() || (terminal.type == TerminalTypes.MELODY && slotIndex == 43)) && completeSounds) playSoundSettings(completeSoundSettings())
+            else if (clickSounds) playTerminalSound()
         }
 
         onReceive<ClientboundSoundPacket> {
@@ -55,16 +53,6 @@ object TerminalSounds : Module(
         }
     }
 
-    private fun playSoundForSlot(slot: Int, button: Int) {
-        with(TerminalUtils.currentTerm ?: return) {
-            if (!canClick(slot, button)) return
-            if ((solution.size == 1 || (type == TerminalTypes.MELODY && slot == 43)) && completeSounds) {
-                if (!cancelLastClick) playTerminalSound()
-                playSoundSettings(completeSoundSettings())
-            } else playTerminalSound()
-        }
-    }
-
     private fun playTerminalSound() {
         if (System.currentTimeMillis() - lastPlayed <= 2) return
         playSoundSettings(clickSoundSettings())
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
index e6d7a13..61afa34 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
@@ -38,6 +38,8 @@ object TerminalTimes : Module(
     private var sectionTimer = 0L
     private var currentTick = 0L
     private var phaseTimer = 0L
+    private var lastSectionTime = 0f
+    private var lastPhaseTime = 0f
 
     init {
         on<TerminalEvent.Solve> {
@@ -45,10 +47,11 @@ object TerminalTimes : Module(
             pbs.time(terminal.type.name, (System.currentTimeMillis() - terminal.timeOpened) / 1000f, "s§7!", "§a${terminal.type.termName}${if (mc.screen is TermSimGUI) " §7(termsim)" else ""} §7solved in §6", sendMessage = terminalTimes)
         }
 
-        on<MessageEvent.ModifyChat> {
+        on<MessageEvent.Chat> {
             if (!terminalSplits) return@on
             terminalCompleteRegex.find(message)?.destructured?.let { (name, activated, type, current, total) ->
-                component = Component.literal("§6$name §a$activated a $type! (§c${current}§a/${total}) §8(§7${sectionTimer.seconds}s §8| §7${phaseTimer.seconds}s§8)")
+                lastSectionTime = sectionTimer.seconds
+                lastPhaseTime = phaseTimer.seconds
 
                 if ((current == total && gateBlown) || (current.toIntOrNull() ?: return@on) < completed.first) resetSection()
                 else completed = Pair(current.toIntOrNull() ?: return@on, total.toIntOrNull() ?: return@on)
@@ -67,6 +70,13 @@ object TerminalTimes : Module(
             }
         }
 
+        on<MessageEvent.ModifyChat> {
+            if (!terminalSplits) return@on
+            terminalCompleteRegex.find(message)?.destructured?.let { (name, activated, type, current, total) ->
+                component = Component.literal("§6$name §a$activated a $type! (§c${current}§a/${total}) §8(§7${lastSectionTime}s §8| §7${lastPhaseTime}s§8)")
+            }
+        }
+
         on<TickEvent.Server> {
             if (terminalSplits && !useRealTime) currentTick += 50
         }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
index 6657653..19eec22 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
@@ -56,7 +56,7 @@ abstract class TerminalHandler(val type: TerminalTypes) {
         lastClickTime = System.currentTimeMillis()
 
         if (simulateClick) simulateClick(slotIndex, button)
-
+        TerminalEvent.Click(this, slotIndex, button, solution).postAndCatch()
         mc.screen?.let { screen ->
             if (screen is TermSimGUI) {
                 screen.clickIndex(slotIndex, button)
@@ -64,7 +64,6 @@ abstract class TerminalHandler(val type: TerminalTypes) {
             }
         }
         mc.player?.clickSlot(slotIndex, button, if (button == InputConstants.MOUSE_BUTTON_MIDDLE) ContainerInput.CLONE else ContainerInput.PICKUP)
-        TerminalEvent.Click(this, slotIndex, button).postAndCatch()
     }
 
     open fun canClick(slotIndex: Int, button: Int): Boolean = slotIndex in solution
Reverted inDungeon checks for event listeners in DungeonListener to avoid losing dataodtheking · 2026-09-14 · 8f34a5f
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt | 2 --
 1 file changed, 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
index 8ce446c..743a73d 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
@@ -72,7 +72,6 @@ object DungeonListener {
         }
 
         onReceive<ClientboundPlayerInfoUpdatePacket> {
-            if (!DungeonUtils.inDungeons) return@onReceive
             val tabListEntries = entries().mapNotNull { it.displayName?.string }.ifEmpty { return@onReceive }
             updateDungeonTeammates(tabListEntries)
             updateDungeonStats(tabListEntries)
@@ -81,7 +80,6 @@ object DungeonListener {
         }
 
         onReceive<ClientboundSetPlayerTeamPacket> {
-            if (!DungeonUtils.inDungeons) return@onReceive
             val text = parameters.getOrNull()?.let { it.playerPrefix.string.plus(it.playerSuffix.string).noControlCodes } ?: return@onReceive
 
             floorRegex.find(text)?.groupValues?.get(1)?.let {
Version bumpodtheking · 2026-09-14 · a766b3b
查看变更
gradle.properties | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 5fc82e9..c9d896a 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ loom_version=1.16-SNAPSHOT
 mod_id=odin
 mod_name=Odin
 mod_description=A Hypixel Skyblock QoL mod
-mod_version=0.3.2
+mod_version=0.3.3
 maven_group=com.odtheking
 archives_base_name=odin-fabric
 # Mod Dependencies
Fixed SimonSays checking any block for powered stateodtheking · 2026-09-14 · 4e9dd08
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
index b39d896..3accfa8 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
@@ -83,7 +83,7 @@ object SimonSays : Module(
                 110 ->
                     if (updated.block == Blocks.AIR) {
                         if (grid.count { mc.level?.getBlockState(it)?.isAir == true } > 8) resetSolution()
-                    } else if (old.block == Blocks.STONE_BUTTON && updated.getValue(BlockStateProperties.POWERED)) {
+                    } else if (old.block == Blocks.STONE_BUTTON && updated.block == Blocks.STONE_BUTTON && updated.getValue(BlockStateProperties.POWERED)) {
                         clickNeeded = clickInOrder.indexOf(pos.east()) + 1
                         if (clickNeeded >= clickInOrder.size) {
                             resetSolution()
Changed dungeon Time Elapsed from tab list to scoreboardodtheking · 2026-09-14 · 6ec0794
查看变更
src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt         |  2 +-
 src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt                 |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt          | 18 +++++-------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt | 11 ++++++++---
 4 files changed, 15 insertions(+), 18 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt b/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
index b5c8349..334c7f6 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
@@ -47,7 +47,7 @@ class ModuleButton(val module: Module, val panel: Panel) {
         if (hoverHandler.percent() >= 100 && y >= panel.panelSetting.y + Panel.HEIGHT)
             ClickGUI.setDescription(module.description, x + Panel.WIDTH + 10f, y, hoverHandler)
 
-       NVGRenderer.rect(x, y, Panel.WIDTH, Panel.HEIGHT, color.rgba)
+        NVGRenderer.rect(x, y, Panel.WIDTH, Panel.HEIGHT, color.rgba)
         NVGRenderer.text(module.name, x + Panel.WIDTH / 2 - nameWidth / 2, y + Panel.HEIGHT / 2 - 9f, 18f, Colors.WHITE.rgba, NVGRenderer.defaultFont)
 
         if (representableSettings.isEmpty()) return Panel.HEIGHT
diff --git a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
index 752b4f1..0199a2f 100644
--- a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
@@ -89,7 +89,7 @@ object EventDispatcher {
             val blockState = mc.level?.getBlockState(pos) ?: return@on
             if (blockState.block is SkullBlock) {
                 val distance = mc.player?.eyePosition?.distanceToSqr(Vec3(pos)) ?: return@on
-                if (distance > 20.25) return@on
+                if (distance > 21) return@on
             }
 
             if (isSecret(blockState, pos)) SecretPickupEvent.Interact(pos, blockState).postAndCatch()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
index 172cbca..4e59894 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
@@ -43,9 +43,7 @@ object Vesuvius : Module(
     private val uselessLinesRegex = Regex("^Contents|Cost|Click to open!|FREE|Already opened!|Can't open another chest!|Paid Chest|")
     private val salvageItemsRegex = Regex("Boots|Leggings|Chestplate|Helmet|Cloak|Aurora Staff|Hollow Wand")
 
-    private val ultimateEnchants = setOf(
-        "Fatal Tempo", "Inferno"
-    )
+    private val ultimateEnchants = setOf("Fatal Tempo", "Inferno")
 
     data class Key(val type: String, val coins: Int, val quantity: Int, val tier: Int)
     private data class ChestItem(val name: Component, val price: Double)
@@ -70,9 +68,8 @@ object Vesuvius : Module(
         }
 
         on<GuiEvent.RenderSlot> {
-            if (screen.title.string.containsOneOf("Vesuvius", "Croesus") && slot.item.hoverName.string == "Kuudra's Hollow") {
-                if (hideClaimed && slot.item.loreString.any { it == "No more chests to open!"}) cancel()
-            }
+            if (hideClaimed && screen.title.string.containsOneOf("Vesuvius", "Croesus") &&
+                slot.item.hoverName.string == "Kuudra's Hollow" && slot.item.loreString.any { it == "No more chests to open!"}) cancel()
         }
 
         on<SetSlotEvent> {
@@ -106,9 +103,7 @@ object Vesuvius : Module(
 
         val item = component.string.replace("✪", "").trim()
 
-        if (item.contains("Molten") && useSalvagePrices) {
-            return (cachedPrices["ESSENCE_CRIMSON"] ?: 0.0) * 600.0 * essenceBonus
-        }
+        if (item.contains("Molten") && useSalvagePrices) return (cachedPrices["ESSENCE_CRIMSON"] ?: 0.0) * 600.0 * essenceBonus
 
         previewEnchantedBookRegex.find(item)?.destructured?.let { (name, level) ->
             val ult = if (name in ultimateEnchants) "ULTIMATE_" else ""
@@ -138,7 +133,6 @@ object Vesuvius : Module(
         }
 
         if (useSalvagePrices && salvage) {
-            modMessage("$component}")
             val price = cachedPrices["ESSENCE_CRIMSON"] ?: 0.0
             val quantity = starCountToEssence[starCount] ?: 0.0
             return price * quantity
@@ -164,9 +158,7 @@ object Vesuvius : Module(
         var profit = 0.0
         var chestCost = 0.0
 
-        val lore = item.lore
-
-        lore.forEach { component ->
+        item.lore.forEach { component ->
             val string = component.string
 
             if (string.contains("Kuudra Key")) {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
index 5eadac1..8ce446c 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
@@ -72,6 +72,7 @@ object DungeonListener {
         }
 
         onReceive<ClientboundPlayerInfoUpdatePacket> {
+            if (!DungeonUtils.inDungeons) return@onReceive
             val tabListEntries = entries().mapNotNull { it.displayName?.string }.ifEmpty { return@onReceive }
             updateDungeonTeammates(tabListEntries)
             updateDungeonStats(tabListEntries)
@@ -80,6 +81,7 @@ object DungeonListener {
         }
 
         onReceive<ClientboundSetPlayerTeamPacket> {
+            if (!DungeonUtils.inDungeons) return@onReceive
             val text = parameters.getOrNull()?.let { it.playerPrefix.string.plus(it.playerSuffix.string).noControlCodes } ?: return@onReceive
 
             floorRegex.find(text)?.groupValues?.get(1)?.let {
@@ -96,15 +98,19 @@ object DungeonListener {
                 dungeonStats.percentCleared = it
                 DungeonUtils.updateScore()
             }
+
+            dungeonStats.elapsedTime = timeRegex.find(text)?.groupValues?.get(1) ?: dungeonStats.elapsedTime
         }
 
         onReceive<ClientboundTabListPacket> {
+            if (!DungeonUtils.inDungeons) return@onReceive
             Blessing.entries.forEach { blessing ->
                 blessing.regex.find(footer.string)?.let { blessing.current = romanToInt(it.groupValues[1]) }
             }
         }
 
         on<MessageEvent.Chat> {
+            if (!DungeonUtils.inDungeons) return@on
             if (expectingBloodRegex.matches(message)) expectingBloodUpdate = true
             doorOpenRegex.find(message)?.let { dungeonStats.doorOpener = it.groupValues[1] }
             deathRegex.find(message)?.let { match ->
@@ -139,7 +145,7 @@ object DungeonListener {
         }
 
         on<EntityEvent.Add> {
-            if (entity.type == EntityType.PLAYER && entity.uuid.version() != 4)
+            if (entity.type == EntityType.PLAYER && entity.uuid.version() != 4 && DungeonUtils.inDungeons)
                 DungeonUtils.dungeonTeammates.find { it.entity == null && it.name == entity.name.string }?.entity = entity as? Player
         }
     }
@@ -170,7 +176,6 @@ object DungeonListener {
                 puzzleCount = puzzleCountRegex.find(entry)?.groupValues?.get(1)?.toIntOrNull() ?: puzzleCount
                 deaths = deathsRegex.find(entry)?.groupValues?.get(1)?.toIntOrNull() ?: deaths
                 crypts = cryptRegex.find(entry)?.groupValues?.get(1)?.toIntOrNull() ?: crypts
-                elapsedTime = timeRegex.find(entry)?.groupValues?.get(1) ?: elapsedTime
             }
         }
     }
@@ -194,7 +199,7 @@ object DungeonListener {
     private val doorOpenRegex = Regex("^(?:\\[\\w+] )?(\\w+) opened a (?:WITHER|Blood) door!")
     private val secretPercentRegex = Regex("^ Secrets Found: ([\\d.]+)%$")
     private val deathRegex = Regex("☠ (\\w{1,16}) .* and became a ghost\\.")
-    private val timeRegex = Regex("^ Time: ((?:\\d+h ?)?(?:\\d+m ?)?\\d+s)$")
+    private val timeRegex = Regex("^Time Elapsed: ((?:\\d+h ?)?(?:\\d+m ?)?\\d+s)$")
     private val completedRoomsRegex = Regex("^ Completed Rooms: (\\d+)$")
     private val clearedRegex = Regex("^Cleared: (\\d+)% \\(\\d+\\)$")
     private val secretCountRegex = Regex("^ Secrets Found: (\\d+)$")
fix playersize translate (#156)Cezqr · 2026-09-11 · 3316286
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
index f770088..6b2b077 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
@@ -65,7 +65,7 @@ object PlayerSize : Module(
         if (!randoms.containsKey(gameProfile.id)) return
         if (!devSize && gameProfile.name == mc.player?.gameProfile?.name) return
         val random = randoms[gameProfile.id] ?: return
-        if (random.scale[1] < 0) matrix.translate(0f, random.scale[1] * 2, 1f)
+        if (random.scale[1] < 0) matrix.translate(0f, random.scale[1] * 2, 0f)
         matrix.scale(random.scale[0], random.scale[1], random.scale[2])
     }
 
kuudra profit tracker (#149)
* fix vesuvius not working at all * vitality rework compat * Add lava leech and kuudra pet bonus to profit calculation * better detection * should finally be done :sob: * should finally be done :sob: * profit tracker for kuudra * maybe dont leave in the debug ig * fixed stuff * forgot some item colors * Cleanup merge --------- Co-authored-by: odtheking <78278562+odtheking@users.noreply.github.com> Co-authored-by: odtheking <odtheking1@gmail.com>
TurtleOnFire2 · 2026-09-11 · 360bf84
查看变更
src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt             |   1 +
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt      |   6 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/KuudraTracker.kt | 272 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt      |  22 +++----
 4 files changed, 286 insertions(+), 15 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
index c4130bb..330ede9 100644
--- a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
@@ -77,6 +77,7 @@ object ModuleManager {
 
             // nether
             SupplyHelper, BuildHelper, RemovePerks, NoPre, PearlWaypoints, FreshTools, KuudraInfo, Misc, Vesuvius,
+            KuudraTracker,
 
             RenderTest,
         )
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
index a9c18f0..448d05e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
@@ -22,7 +22,6 @@ import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import kotlinx.coroutines.launch
 import net.minecraft.client.gui.GuiGraphicsExtractor
-import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
 import net.minecraft.network.chat.Component
 import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket
 import net.minecraft.world.item.ItemStack
@@ -140,11 +139,10 @@ object Croesus : Module(
 
         on<SetSlotEvent> {
             val screenTitle = mc.screen?.title?.string ?: return@on
-            val items = (mc.screen as? AbstractContainerScreen<*>)?.menu?.items ?: return@on
 
             when {
-                screenTitle.matches(chestNameRegex) -> handleChestContents(items)
-                screenTitle.matches(chestPreviewScreenRegex) -> handleCroesusScreen(items)
+                screenTitle.matches(chestNameRegex) -> handleChestContents(menu.items)
+                screenTitle.matches(chestPreviewScreenRegex) -> handleCroesusScreen(menu.items)
             }
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/KuudraTracker.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/KuudraTracker.kt
new file mode 100644
index 0000000..a010f6b
--- /dev/null
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/KuudraTracker.kt
@@ -0,0 +1,272 @@
+package com.odtheking.odin.features.impl.nether
+
+import com.odtheking.odin.clickgui.settings.impl.ActionSetting
+import com.odtheking.odin.clickgui.settings.impl.ListSetting
+import com.odtheking.odin.clickgui.settings.impl.MapSetting
+import com.odtheking.odin.clickgui.settings.impl.NumberSetting
+import com.odtheking.odin.events.GuiEvent
+import com.odtheking.odin.events.MessageEvent
+import com.odtheking.odin.events.core.on
+import com.odtheking.odin.events.core.onSend
+import com.odtheking.odin.features.Module
+import com.odtheking.odin.features.ModuleManager
+import com.odtheking.odin.features.impl.nether.Vesuvius.getPriceOfKey
+import com.odtheking.odin.features.impl.nether.Vesuvius.keys
+import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.equalsOneOf
+import com.odtheking.odin.utils.lore
+import com.odtheking.odin.utils.render.text
+import net.minecraft.ChatFormatting
+import net.minecraft.client.gui.GuiGraphicsExtractor
+import net.minecraft.network.chat.Component
+import net.minecraft.network.chat.MutableComponent
+import net.minecraft.network.protocol.game.ServerboundContainerClickPacket
+import net.minecraft.world.item.Items
+
+object KuudraTracker : Module(
+    name = "Kuudra Tracker",
+    description = "Tracks your Kuudra runs."
+) {
+    private val lineAmount by NumberSetting("Lines", 10, 0,40, 1, "The amount of items to display in the profit tracker.")
+    private val reset by ActionSetting("Reset Profit", "Resets the Profit Tracker") {
+        totalKeys = mutableListOf(0, 0, 0, 0, 0)
+        singleItems.clear()
+        multiItems.clear()
+        paid = 0
+        free = 0
+        last = LastAdded(mutableListOf(), mutableListOf(), 0)
+
+        updateDisplay()
+        ModuleManager.saveConfigurations()
+    }
+
+    private val chestRegex = Regex("(Free|Paid) Chest")
+    private val hudRegex = Regex("^((Free|Paid) Chest)|(Kuudra - .+)|(.+)?Vesuvius$")
+    private val uselessLinesRegex = Regex("^Contents|Cost|Click to open!|FREE|Already opened!|Can't open another chest!|Paid Chest$")
+    private val amountRegex = Regex("(.+)? x(\\d+)$")
+
+    private val lightPurpleRegex = Regex("Fatal Tempo|Inferno|Crimson Essence")
+    private val whiteRegex = Regex("Book")
+    private val darkPurpleRegex = Regex("Kuudra Teeth|Fire Eel Shard|Kuudra Mandible|Barbarian Duke X Shard|XYZ Shard|Hellstorm Wand|Tentacle Dye")
+    private val blueRegex = Regex("Kada Knight Shard|Wheel of Fate|Ananke Feather|Burning Kuudra Core|Matcho Shard|Kuudra Tentacle|Lava Flame Shard|Wither Spectre Shard")
+    private val goldRegex = Regex("Boots|Leggings|Chestplate|Helmet|Cloak|Aurora Staff|Hollow Wand|Heavy Pearl|Kraken Shard|Hellwisp Shard|Ananke Shard|Moltenfish Shard|Tormentor")
+
+    val profitHud by HUD("Kuudra Tracker HUD", "Displays the profit of all your Kudura chests.") {
+        if (!it) return@HUD 0 to 0
+        drawOverlay(true)
+    }
+
+    private var singleItems by MapSetting("Single Items",mutableMapOf<String, Int>())
+    private var multiItems by MapSetting("Multi Items", mutableMapOf<String, Int>())
+
+    private var paid by NumberSetting("Paid Chests", 0, 0, Int.MAX_VALUE, 1, "Amount of Paid Chests opened.").hide()
+    private var free by NumberSetting("Free Chests", 0, 0, Int.MAX_VALUE, 1, "Amount of Paid Chests opened").hide()
+
+    private var toDisplay = mutableListOf<Pair<MutableComponent, Double>>()
+    private var totalKeys by ListSetting("Total Keys", mutableListOf(0, 0, 0, 0, 0)).hide() //Tier 1-5
+
+    private var last = LastAdded(mutableListOf(), mutableListOf(), 0)
+
+    init {
+        updateDisplay()
+
+        on<GuiEvent.DrawTooltip> {
+            val title = screen.title.string
+            if (enabled && title.matches(hudRegex)) {
+                guiGraphics.pose().pushMatrix()
+                val sf = mc.window.guiScale
+                guiGraphics.pose().scale(1f / sf, 1f / sf)
+                guiGraphics.pose().translate(profitHud.x.toFloat(), profitHud.y.toFloat())
+                guiGraphics.pose().scale(profitHud.scale)
+
+                guiGraphics.drawOverlay(false)
+
+                guiGraphics.pose().popMatrix()
+            }
+        }
+
+        on<MessageEvent.Chat> {
+            val title = mc.screen?.title?.string ?: return@on
+            if (message.equalsOneOf("You cannot afford this!", "Whoa! Slow down there!") && title.matches(chestRegex)) {
+                for (single in last.single) {
+                    singleItems[single] = (singleItems.getOrDefault(single, 0) - 1).coerceAtLeast(0)
+                }
+                for ((itemComponent, amount) in last.multi) {
+                    multiItems[itemComponent] = (multiItems.getOrDefault(itemComponent, 0) - amount).coerceAtLeast(0)
+                }
+                totalKeys[last.key - 1]--
+                paid--
+
+                updateDisplay()
+                ModuleManager.saveConfigurations()
+            }
+        }
+
+        onSend<ServerboundContainerClickPacket> {
+            val title = mc.screen?.title?.string ?: return@onSend
+            if (!title.matches(chestRegex)) return@onSend
+
+            val cursorStack = mc.player?.containerMenu?.carried ?: return@onSend
+
+            if (!cursorStack.`is`(Items.CHEST)) return@onSend
+
+            val loreLines = cursorStack.lore
+
+            updateDisplay()
+
+            if (loreLines.any { it.string.equalsOneOf("Already opened!", "Can't open another chest!")}) return@onSend
+
+            last = LastAdded(mutableListOf(), mutableListOf(), 0)
+
+            for (lore in loreLines) {
+                if (lore.string.matches(uselessLinesRegex)) continue
+
+                if (lore.string.contains("Kuudra Key")) {
+                    keys.find { it.type == lore.string }?.let { key ->
+                        totalKeys[key.tier - 1]++
+                        last.key = key.tier
+                        continue
+                    }
+                }
+
+                amountRegex.find(lore.string)?.destructured?.let { (name, amount) ->
+                    multiItems[name] = multiItems.getOrDefault(name, 0) + (amount.toIntOrNull() ?: 0)
+                    continue
+                }
+
+                singleItems[lore.string] = singleItems.getOrDefault(lore.string, 0) + 1
+            }
+
+            if (title == "Paid Chest") paid++
+            if (title == "Free Chest") free++
+
+            updateDisplay()
+            ModuleManager.saveConfigurations()
+        }
+    }
+
+    private fun updateDisplay() {
+        toDisplay.clear()
+
+        singleItems.forEach { (name, amount) ->
+            if (amount == 0) return@forEach
+
+            val price = Vesuvius.parseItemValue(Component.literal(name))?.times(amount) ?: return@forEach
+
+            toDisplay.add(Component.literal(name).withStyle(getColor(name)).append(Component.literal(" x$amount").withStyle(ChatFormatting.DARK_GRAY)) to price)
+        }
+
+        multiItems.forEach { (name, amount) ->
+            if (amount == 0) return@forEach
+
+            val built = Component.literal(name).append(Component.literal(" x$amount").withStyle(ChatFormatting.DARK_GRAY))
+
+            val price = Vesuvius.parseItemValue(built) ?: return@forEach
+
+            toDisplay.add(built.withStyle(getColor(name)) to price)
+        }
+    }
+
+    private fun getKeyPrices(): Double {
+        return getPriceOfKey("Kuudra Key") * totalKeys[0] +
+                getPriceOfKey("Hot Kuudra Key") * totalKeys[1] +
+                getPriceOfKey("Burning Kuudra Key") * totalKeys[2] +
+                getPriceOfKey("Fiery Kuudra Key") * totalKeys[3] +
+                getPriceOfKey("Infernal Kuudra Key") * totalKeys[4]
+    }
+
+    private fun GuiGraphicsExtractor.drawOverlay(isEditing: Boolean): Pair<Int, Int> {
+        var yOffset = 0
+        val maxWidth = 300
+
+        var profit = 0.0
+
+        val display = if (isEditing) { sampleProfitData } else toDisplay
+
+        for ((index, pair) in display
+            .sortedByDescending { it.second }
+            .withIndex()
+        ) {
+            val price = "%,.0f".format(pair.second)
+
+            profit += pair.second
+
+            if (index >= lineAmount) continue
+
+            text(mc.font, pair.first, 0, yOffset, -1)
+            text(mc.font, price, maxWidth - mc.font.width(price), yOffset, Colors.MINECRAFT_DARK_GRAY.rgba)
+
+            yOffset += 9
+        }
+
+        yOffset += 6
+
+        val keyPrices = if (isEditing) getPriceOfKey("Infernal Kuudra Key").toString() else "%,.0f".format(getKeyPrices())
+
+        text(mc.font, "§cTotal Key Costs:", 0, yOffset, -1)
+        text(mc.font, keyPrices, maxWidth - mc.font.width(keyPrices), yOffset, Colors.MINECRAFT_RED.rgba)
+
+        yOffset += 12
+
+        val profitStr = "%,.0f".format(profit - getKeyPrices())
+
+        text("§aProfit:", 0, yOffset)
+        text(profitStr, maxWidth - mc.font.width(profitStr), yOffset, Colors.MINECRAFT_GREEN)
+
+        yOffset += 15
+
+        val freeChests = if (isEditing) 0 else free
+        val paidChests = if (isEditing) 1 else paid
+
+        val avg = "%,.0f".format((profit - getKeyPrices()) / (freeChests + paidChests))
+
+        text(mc.font, "Total Chests: Free [$freeChests], Paid [$paidChests]", 0, yOffset, Colors.MINECRAFT_YELLOW.rgba)
+        text(mc.font, "Avg [$avg]", maxWidth - mc.font.width("Avg [$avg]"), yOffset, Colors.MINECRAFT_GOLD.rgba)
+
+        yOffset += 9
+
+        return maxWidth to yOffset
+    }
+
+    private fun getColor(name: String): ChatFormatting {
+        if (name.contains(whiteRegex)) return ChatFormatting.WHITE
+        if (name.contains(goldRegex)) return ChatFormatting.GOLD
+        if (name.contains(darkPurpleRegex)) return ChatFormatting.DARK_PURPLE
+        if (name.contains(lightPurpleRegex)) return ChatFormatting.LIGHT_PURPLE
+        if (name.contains(blueRegex)) return ChatFormatting.BLUE
+
+        return ChatFormatting.WHITE
+    }
+
+    data class LastAdded(var single: MutableList<String>, var multi: MutableList<Multi>, var key: Int)
+    data class Multi(val component: String, val amount: Int)
+
+    private val sampleProfitData = listOf(
+        Pair(
+            Component.literal("Fervor Helmet")
+                .withStyle(ChatFormatting.GOLD),
+            748000.0
+        ),
+        Pair(
+            Component.literal("Enchanted Book (").withStyle(ChatFormatting.WHITE)
+                .append(Component.literal("Ferocious Mana V").withStyle(ChatFormatting.LIGHT_PURPLE))
+                .append(Component.literal(")").withStyle(ChatFormatting.WHITE)),
+            118500000.0
+        ),
+        Pair(
+            Component.literal("Crimson Essence").withStyle(ChatFormatting.LIGHT_PURPLE)
+                .append(Component.literal(" x2000").withStyle(ChatFormatting.DARK_GRAY)),
+            2420000.0
+        ),
+        Pair(
+            Component.literal("Kuudra Teeth").withStyle(ChatFormatting.DARK_PURPLE)
+                .append(Component.literal(" x4").withStyle(ChatFormatting.DARK_GRAY)),
+            35480.0
+        ),
+        Pair(
+            Component.literal("Kraken Shard").withStyle(ChatFormatting.GOLD)
+                .append(Component.literal(" x2").withStyle(ChatFormatting.DARK_GRAY)),
+            821226.0
+        )
+    )
+}
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
index 153d3e0..172cbca 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
@@ -41,13 +41,13 @@ object Vesuvius : Module(
     private val pearlRegex = Regex("^Heavy Pearl x(\\d+)$")
     private val chestRegex = Regex("^((Free|Paid) Chest)|(Kuudra - .+)$")
     private val uselessLinesRegex = Regex("^Contents|Cost|Click to open!|FREE|Already opened!|Can't open another chest!|Paid Chest|")
-    private val salvageItemsRegex = Regex("^Boots|Chestplate|Helmet|Cloak|Aurora Staff|Hollow Wand")
+    private val salvageItemsRegex = Regex("Boots|Leggings|Chestplate|Helmet|Cloak|Aurora Staff|Hollow Wand")
 
     private val ultimateEnchants = setOf(
         "Fatal Tempo", "Inferno"
     )
 
-    private data class Key(val type: String, val coins: Int, val quantity: Int)
+    data class Key(val type: String, val coins: Int, val quantity: Int, val tier: Int)
     private data class ChestItem(val name: Component, val price: Double)
     private data class ChestData(val items: List<ChestItem>, val cost: Double, val profit: Double)
 
@@ -87,8 +87,7 @@ object Vesuvius : Module(
         }
     }
 
-    private fun parseItemValue(component: Component): Double? {
-
+    fun parseItemValue(component: Component): Double? {
         var starCount = 0
         val salvage = salvageItemsRegex.containsMatchIn(component.string)
 
@@ -139,6 +138,7 @@ object Vesuvius : Module(
         }
 
         if (useSalvagePrices && salvage) {
+            modMessage("$component}")
             val price = cachedPrices["ESSENCE_CRIMSON"] ?: 0.0
             val quantity = starCountToEssence[starCount] ?: 0.0
             return price * quantity
@@ -149,7 +149,7 @@ object Vesuvius : Module(
         return cachedPrices[item.uppercase().replace(" ", "_")]
     }
 
-    private fun getPriceOfKey(key: String): Double {
+    fun getPriceOfKey(key: String): Double {
         keys.find { it.type == key }?.let {
             val material = minOf(cachedPrices["ENCHANTED_RED_SAND"] ?: 0.0, cachedPrices["ENCHANTED_MYCELIUM"] ?: 0.0)
             val star = (cachedPrices["CORRUPTED_NETHER_STAR"] ?: 0.0)
@@ -211,12 +211,12 @@ object Vesuvius : Module(
         return maxWidth to yOffset
     }
 
-    private val keys = listOf<Key>(
-        Key("Kuudra Key", 155200, 2),
-        Key("Hot Kuudra Key", 310400, 4),
-        Key("Burning Kuudra Key", 582000, 16),
-        Key("Fiery Kuudra Key", 1164000, 40),
-        Key("Infernal Kuudra Key", 2328000, 80)
+    val keys = listOf<Key>(
+        Key("Kuudra Key", 155200, 2, 1),
+        Key("Hot Kuudra Key", 310400, 4, 2),
+        Key("Burning Kuudra Key", 582000, 16, 3),
+        Key("Fiery Kuudra Key", 1164000, 40, 4),
+        Key("Infernal Kuudra Key", 2328000, 80, 5)
     )
 
     private val itemReplacements = mapOf(
Rotate the dwp AABB to match the room's rotation (#155)yuko1101 · 2026-09-11 · 131078e
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt  |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt                   | 15 +++++++++++++++
 src/main/kotlin/com/odtheking/odin/utils/VecUtils.kt                                               | 24 ++++++++++++++++++++----
 4 files changed, 38 insertions(+), 7 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt
index ed604aa..7f38e88 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt
@@ -56,7 +56,7 @@ internal fun DungeonWaypoints.handleEditorInput(event: InputEvent) {
         return
     }
     if (allowTextEdit && mc.player?.isCrouching == true) {
-        openWaypointTitlePrompt(room, blockPos, relativeAabbAt(pos), editableWaypoints)
+        openWaypointTitlePrompt(room, blockPos, room.getRelativeAABB(relativeAabbAt(pos)), editableWaypoints)
         return
     }
     if (editableWaypoints.removeIf { it.blockPos == blockPos }) {
@@ -65,7 +65,7 @@ internal fun DungeonWaypoints.handleEditorInput(event: InputEvent) {
         OdinMod.scope.launch { saveWaypoints() }
         return
     }
-    editableWaypoints.add(createWaypoint(blockPos, relativeAabbAt(pos)))
+    editableWaypoints.add(createWaypoint(blockPos, room.getRelativeAABB(relativeAabbAt(pos))))
     devMessage("Added waypoint at $blockPos")
     syncRoomToActive(room)
     OdinMod.scope.launch { saveWaypoints() }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
index 30d9049..972301a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
@@ -84,7 +84,7 @@ fun DungeonRoom.setWaypoints() {
     val name = data?.name ?: return
     waypoints = DungeonWaypoints.allActiveWaypoints[name]
         ?.mapTo(mutableSetOf()) { waypoint ->
-            waypoint.copy(blockPos = getRealCoords(waypoint.blockPos))
+            waypoint.copy(blockPos = getRealCoords(waypoint.blockPos), aabb = getRealAABB(waypoint.aabb))
         } ?: mutableSetOf()
 }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
index f13338c..9d37f9d 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
@@ -5,6 +5,7 @@ import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoint
 import com.odtheking.odin.utils.*
 import net.minecraft.core.BlockPos
 import net.minecraft.world.level.block.Blocks
+import net.minecraft.world.phys.AABB
 
 class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData? = null) {
     val tiles: ArrayList<IVec2> = ArrayList(4)
@@ -181,4 +182,18 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
         val rot = rotation ?: return BlockPos.ZERO
         return pos.rotateAroundNorth(rot).offset(clay.x, 0, clay.z)
     }
+
+    fun getRelativeAABB(aabb: AABB): AABB {
+        val rot = rotation ?: return aabb
+        val minPos = aabb.minPosition.add(-0.5, -0.5, -0.5).rotateToNorth(rot).add(0.5, 0.5, 0.5)
+        val maxPos = aabb.maxPosition.add(-0.5, -0.5, -0.5).rotateToNorth(rot).add(0.5, 0.5, 0.5)
+        return AABB(minPos, maxPos)
+    }
+
+    fun getRealAABB(aabb: AABB): AABB {
+        val rot = rotation ?: return aabb
+        val minPos = aabb.maxPosition.add(-0.5, -0.5, -0.5).rotateAroundNorth(rot).add(0.5, 0.5, 0.5)
+        val maxPos = aabb.minPosition.add(-0.5, -0.5, -0.5).rotateAroundNorth(rot).add(0.5, 0.5, 0.5)
+        return AABB(minPos, maxPos)
+    }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/utils/VecUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/VecUtils.kt
index bc6358b..4f5c410 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/VecUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/VecUtils.kt
@@ -26,9 +26,9 @@ fun Vec3.addVec(x: Number = 0.0, y: Number = 0.0, z: Number = 0.0): Vec3 =
     Vec3(this.x + x.toDouble(), this.y + y.toDouble(), this.z + z.toDouble())
 
 /**
- * Rotates a Vec3 around the given rotation.
+ * Rotates a BlockPos around the given rotation.
  * @param rotation The rotation to rotate around
- * @return The rotated Vec3
+ * @return The rotated BlockPos
  */
 fun BlockPos.rotateAroundNorth(rotation: RoomRotation): BlockPos =
     when (rotation) {
@@ -38,10 +38,18 @@ fun BlockPos.rotateAroundNorth(rotation: RoomRotation): BlockPos =
         RoomRotation.EAST ->  BlockPos(this.z, this.y, -this.x)
     }
 
+fun Vec3.rotateAroundNorth(rotation: RoomRotation): Vec3 =
+    when (rotation) {
+        RoomRotation.NORTH -> Vec3(-this.x, this.y, -this.z)
+        RoomRotation.WEST ->  Vec3(-this.z, this.y, this.x)
+        RoomRotation.SOUTH -> Vec3(this.x, this.y, this.z)
+        RoomRotation.EAST ->  Vec3(this.z, this.y, -this.x)
+    }
+
 /**
- * Rotates a Vec3 to the given rotation.
+ * Rotates a BlockPos to the given rotation.
  * @param rotation The rotation to rotate to
- * @return The rotated Vec3
+ * @return The rotated BlockPos
  */
 fun BlockPos.rotateToNorth(rotation: RoomRotation): BlockPos =
     when (rotation) {
@@ -51,6 +59,14 @@ fun BlockPos.rotateToNorth(rotation: RoomRotation): BlockPos =
         RoomRotation.EAST ->  BlockPos(-this.z, this.y, this.x)
     }
 
+fun Vec3.rotateToNorth(rotation: RoomRotation): Vec3 =
+    when (rotation) {
+        RoomRotation.NORTH -> Vec3(-this.x, this.y, -this.z)
+        RoomRotation.WEST ->  Vec3(this.z, this.y, -this.x)
+        RoomRotation.SOUTH -> Vec3(this.x, this.y, this.z)
+        RoomRotation.EAST ->  Vec3(-this.z, this.y, this.x)
+    }
+
 fun isXZInterceptable(box: AABB, range: Double, pos: Vec3, yaw: Float, pitch: Float): Boolean {
     val start = pos.addVec(y = (mc.player?.eyeY ?: 0.0))
     val goal = start.add(getLook(yaw, pitch).multiply(range, range, range))
Fixed PositionalMessages data class constructor never firing issueodtheking · 2026-09-11 · af8e180
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt | 23 ++++++++++++++++++++---
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt      | 24 +++++++++++++-----------
 2 files changed, 33 insertions(+), 14 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
index b836e73..ce68ccf 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
@@ -31,9 +31,26 @@ object PositionalMessages : Module(
     private val messageSize by NumberSetting("Message Size", 1f, 0.1f, 4f, 0.1f, desc = "The size at which to display the message in the box.").withDependency { showPositions && displayMessage }
 
     data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Int, val distance: Double?, val color: Color, val message: String, val send: Boolean) {
-        val center: Vec3 get() = Vec3((x + (x2 ?: x)) / 2, (y + (y2 ?: y)) / 2, (z + (z2 ?: z)) / 2)
-        val box: AABB? get() = if (x2 != null && y2 != null && z2 != null) AABB(x, y, z, x2, y2, z2) else null
-        val radiusSquared: Double? get() = distance?.let { it * it }
+        @Transient
+        private var _center: Vec3? = null
+        val center: Vec3
+            get() = _center ?: Vec3((x + (x2 ?: x)) / 2, (y + (y2 ?: y)) / 2, (z + (z2 ?: z)) / 2).also { _center = it }
+
+        @Transient
+        private var _box: AABB? = null
+        val box: AABB?
+            get() {
+                if (_box == null && x2 != null && y2 != null && z2 != null) _box = AABB(x, y, z, x2, y2, z2)
+                return _box
+            }
+
+        @Transient
+        private var _radiusSquared: Double? = null
+        val radiusSquared: Double?
+            get() {
+                if (_radiusSquared == null) distance?.let { _radiusSquared = it * it }
+                return _radiusSquared
+            }
     }
     val posMessageStrings by ListSetting("Pos Messages", mutableListOf<PosMessage>())
     private val sentMessages = mutableSetOf<PosMessage>()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
index 0838abf..cecf212 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
@@ -128,31 +128,33 @@ object ClickGUIModule : Module(
 
             modMessage(
                 Component.literal(getChatBreak())
-                    .append(Component.literal("§3Odin update available: §f$latestVersionNumber\n\n"))
+                    .append("\n")
+                    .append(Component.literal("§3Odin update available: §f$latestVersionNumber\n"))
+                    .append(Component.literal("§7Click the links below to download the latest version.\n\n"))
                     .append(
-                        Component.literal("§bGitHub link").withStyle {
-                            it.withClickEvent(ClickEvent.OpenUrl(URI(RELEASE_LINK))).withHoverEvent(
-                                HoverEvent.ShowText(Component.literal(RELEASE_LINK))
+                        Component.literal("    §a§nModrinth Link").withStyle {
+                            it.withClickEvent(ClickEvent.OpenUrl(URI(MODRINTH_LINK))).withHoverEvent(
+                                HoverEvent.ShowText(Component.literal(MODRINTH_LINK))
                             )
                         }
                     )
-                    .append(Component.literal("\n"))
+                    .append(Component.literal("        "))
                     .append(
-                        Component.literal("§bModrinth Link").withStyle {
-                            it.withClickEvent(ClickEvent.OpenUrl(URI(MODRINTH_LINK))).withHoverEvent(
-                                HoverEvent.ShowText(Component.literal(MODRINTH_LINK))
+                        Component.literal("§b§nGitHub link").withStyle {
+                            it.withClickEvent(ClickEvent.OpenUrl(URI(RELEASE_LINK))).withHoverEvent(
+                                HoverEvent.ShowText(Component.literal(RELEASE_LINK))
                             )
                         }
                     )
-                    .append(Component.literal("\n"))
+                    .append(Component.literal("        "))
                     .append(
-                        Component.literal("§9Discord link").withStyle {
+                        Component.literal("§9§nDiscord link").withStyle {
                             it.withClickEvent(ClickEvent.OpenUrl(URI("https://discord.gg/2nCbC9hkxT"))).withHoverEvent(
                                 HoverEvent.ShowText(Component.literal("https://discord.gg/2nCbC9hkxT"))
                             )
                         }
                     )
-                    .append(Component.literal("\n\n${getChatBreak()}§r")),
+                    .append(Component.literal("\n${getChatBreak()}§r")),
                 ""
             )
 
Fixed BloodCamp estimate going wild due to event changeodtheking · 2026-09-11 · 1900687
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt                                | 15 +++++++++------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt                         |  7 +++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt                                  |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt                         |  8 ++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt                         |  6 +++---
 src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt                                    |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt                                    | 33 ++++++++++++++++-----------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt  |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt |  2 +-
 9 files changed, 40 insertions(+), 39 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index 264451f..93002bd 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -26,19 +26,22 @@ object TerminalSolver : Module(
     val customTermSize by NumberSetting("Term Size", 2f, 1f, 3f, 0.1f, desc = "The size of the custom terminal GUI.").withDependency { renderType == 2 }
     val roundness by NumberSetting("Roundness", 5, 0f, 15f, 1f, desc = "The roundness of the custom terminal gui.").withDependency { renderType == 2 }
     val gap by NumberSetting("Slot gap", 2, 0, 8, 1, desc = "The gap between the slots in the custom terminal gui.").withDependency { renderType == 2 }
+
     private val solverSettings by DropdownSetting("Solver Functionality")
+    val clickPrediction by BooleanSetting("Client Prediction", true, desc = "Visually predicts the server state before the gui update is sent to the client.").withDependency { solverSettings }
+    val terminalReloadThreshold by NumberSetting("Resolve timeout", 600, 300, 1200, 10, unit = "ms", desc = "The amount of time before the terminal reloads after a click wasn't registered while using click prediction.").withDependency { clickPrediction && solverSettings }
     private val cancelMelodySolver by BooleanSetting("Stop Melody Solver", false, desc = "Stops rendering the melody solver.").withDependency { solverSettings }
     val melodyTermSize by NumberSetting("Melody Size", 1.5f, 1f, 3f, 0.1f, desc = "The size of the melody terminal GUI.").withDependency { !cancelMelodySolver && solverSettings && renderType == 2 }
-    val showNumbers by BooleanSetting("Show Numbers", true, desc = "Shows numbers in the order terminal.").withDependency { solverSettings }
-    val rubixMode by SelectorSetting("Rubix Mode", "Left Clicks Only", arrayListOf("Fewest Clicks", "Left Clicks Only"), desc = "Whether the rubix solver should mix in right clicks for the fewest clicks overall, or stick to left clicks only.").withDependency { solverSettings }
-    val hideClicked by BooleanSetting("Client Prediction", true, desc = "Visually predicts the server state before the gui update is sent to the client.").withDependency { solverSettings }
-    val terminalReloadThreshold by NumberSetting("Resolve timeout", 600, 300, 1200, 10, unit = "ms", desc = "The amount of time before the terminal reloads after a click wasn't registered while using hide clicked.").withDependency { hideClicked && solverSettings }
+    val rubixMode by SelectorSetting("Rubix Mode", "Fewest Clicks", arrayListOf("Fewest Clicks", "Left Clicks Only"), desc = "Whether the rubix solver should mix in right clicks for the fewest clicks overall, or stick to left clicks only.").withDependency { solverSettings }
+
     private val firstClickProtSettings by DropdownSetting("First Click Prot Dropdown")
     val firstClickProt by NumberSetting("First Click Prot", 500, 350, 800, 10, unit = "ms", desc = "The amount of time after opening a terminal where clicks are blocked to prevent bans (recommended value is 500 minus your ping).").withDependency { firstClickProtSettings }
     val shouldFirstClickProtWithTicks by BooleanSetting("Account For Server Lag", false, desc = "Prevents bans from clicking when the server lags after opening the terminal (disabled in singleplayer").withDependency { firstClickProtSettings }
     val firstClickProtTicks by NumberSetting("Lag Protection Ticks", 8, 7, 16, unit = "ticks", desc = "Each tick = 50ms (recommended value is 8)").withDependency { shouldFirstClickProtWithTicks && firstClickProtSettings }
+
     private val showColors by DropdownSetting("Color Settings")
     val backgroundColor by ColorSetting("Background", Colors.gray26, true, desc = "Background color of the terminal solver.").withDependency { showColors }
+    val textColor by ColorSetting("Text", Colors.WHITE, true, desc = "Text color of the terminal solver.").withDependency { showColors }
 
     val panesColor by ColorSetting("Panes", Colors.MINECRAFT_GREEN, true, desc = "Color of the panes terminal solver.").withDependency { showColors }
 
@@ -67,7 +70,7 @@ object TerminalSolver : Module(
     init {
         on<GuiEvent.SlotClick> {
             TerminalUtils.currentTerm?.let {
-                it.click(slotIndex, button, hideClicked)
+                it.click(slotIndex, button, clickPrediction)
                 cancel()
             }
         }
@@ -86,7 +89,7 @@ object TerminalSolver : Module(
             if (slot.index <= currentTerm.type.windowSize - 1) {
                 currentTerm.getSlotRendering(slot.index)?.let { (color, text) ->
                     guiGraphics.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, color.rgba)
-                    text?.let { guiGraphics.centeredText(screen.font, it, slot.x + 8, slot.y + 4, Colors.WHITE.rgba) }
+                    text?.let { guiGraphics.centeredText(screen.font, it, slot.x + 8, slot.y + 4, textColor.rgba) }
                     cancel()
                 }
                 if (renderType == 0) cancel()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
index d4fedb7..2897f64 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
@@ -6,7 +6,6 @@ import com.odtheking.odin.events.ScreenEvent
 import com.odtheking.odin.features.impl.boss.TerminalSolver
 import com.odtheking.odin.features.impl.boss.TerminalSolver.renderDebug
 import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.roundedFill
 import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
@@ -50,13 +49,13 @@ abstract class TermGui {
                 grid?.let { g ->
                     val (bx, by) = g.toBase(click.x(), click.y(), guiScale)
                     hoveredSlotIndex = g.slots.firstOrNull { it.containsBase(bx, by) }?.slotIndex
-                    hoveredSlotIndex?.let {  TerminalUtils.currentTerm?.click(it, click.button(), TerminalSolver.hideClicked) }
+                    hoveredSlotIndex?.let {  TerminalUtils.currentTerm?.click(it, click.button(), TerminalSolver.clickPrediction) }
                 }; return true
             },
             key = fun ScreenEvent.KeyPress.(): Any {
                 if (!isTerminalOverrideKey(input)) return false
                 hoveredSlotIndex?.let {
-                    TerminalUtils.currentTerm?.click(it, if (!input.hasControlDown()) InputConstants.MOUSE_BUTTON_LEFT else InputConstants.MOUSE_BUTTON_RIGHT, TerminalSolver.hideClicked)
+                    TerminalUtils.currentTerm?.click(it, if (!input.hasControlDown()) InputConstants.MOUSE_BUTTON_LEFT else InputConstants.MOUSE_BUTTON_RIGHT, TerminalSolver.clickPrediction)
                 }; return true
             })
         )
@@ -127,7 +126,7 @@ fun simpleTermGui(rows: Int, cols: Int, startRow: Int, startCol: Int): TermGui =
         override fun buildTerminal(screen: AbstractContainerScreen<*>) =
             buildTerminalGrid(screen, rows, cols, startRow, startCol) { slotIndex ->
                 SlotVisual({ TerminalUtils.currentTerm?.getSlotRendering(slotIndex) }) { x, y, w, h ->
-                    TerminalUtils.currentTerm?.getSlotRendering(slotIndex)?.second?.let { renderSlotText(it, x, y, w, h, Colors.WHITE) }
+                    TerminalUtils.currentTerm?.getSlotRendering(slotIndex)?.second?.let { renderSlotText(it, x, y, w, h, TerminalSolver.textColor) }
                 }
             }
     }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt
index c2013ef..1aa9d57 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt
@@ -63,7 +63,7 @@ object BloodCamp : Module(
 
     init {
         on<EntityEvent.Move> {
-            if (newPos == Vec3.ZERO || !DungeonUtils.inClear) return@on
+            if (newPos == Vec3.ZERO || newPos == entity.position() || !DungeonUtils.inClear) return@on
 
             val entity = entity as? ArmorStand ?: return@on
             if (currentWatcherEntity?.let {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
index abf5ff7..e74d9f3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
@@ -78,9 +78,9 @@ object InvincibilityTimer : Module(
     private val cooldownRegex = Regex("^Cooldown: (\\d+)s$")
     
     private val itemCat by DropdownSetting("Cooldown On Item")
-    private val showOnItem by BooleanSetting("Show On Item", true, "Renders the cooldown on the spirit mask and bonzo mask items").withDependency { itemCat }
+    private val showOnItem by BooleanSetting("Show On Item", false, "Renders the cooldown on the spirit mask and bonzo mask items").withDependency { itemCat }
     private val durability by BooleanSetting("Display As Durability", false, "True: durability, False: colored vertical slide").withDependency { itemCat }
-    private val cdColor by ColorSetting("Cooldown Color", Colors.gray38, true, "Color of the cooldown").withDependency { itemCat }
+    private val cdColor by ColorSetting("Cooldown Color", Colors.gray38, false, "Color of the cooldown").withDependency { itemCat }
 
     init {
         on<TickEvent.Server> {
@@ -112,8 +112,8 @@ object InvincibilityTimer : Module(
                 "SPIRIT_MASK","STARRED_SPIRIT_MASK" -> InvincibilityType.SPIRIT.currentCooldown.toDouble() / (InvincibilityType.SPIRIT.maxCooldownTime * 20)
                 else -> return@on
             }
-            if(percent <= 0) return@on
-            if(durability) {
+            if (percent <= 0) return@on
+            if (durability) {
                 guiGraphics.fakeItem(slot.item, slot.x, slot.y)
                 guiGraphics.fill(slot.x + 2, slot.y + 13, slot.x + 14, slot.y + 15, Colors.BLACK.rgba)
                 guiGraphics.fill(slot.x + 2, slot.y + 13, slot.x + 14 - ((1 - percent) * 12).toInt(), slot.y + 14,cdColor.rgba)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
index 64947db..b836e73 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
@@ -31,9 +31,9 @@ object PositionalMessages : Module(
     private val messageSize by NumberSetting("Message Size", 1f, 0.1f, 4f, 0.1f, desc = "The size at which to display the message in the box.").withDependency { showPositions && displayMessage }
 
     data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Int, val distance: Double?, val color: Color, val message: String, val send: Boolean) {
-        val center: Vec3 by lazy { Vec3((x + (x2 ?: x)) / 2, (y + (y2 ?: y)) / 2, (z + (z2 ?: z)) / 2) }
-        val box: AABB? by lazy { if (x2 != null && y2 != null && z2 != null) AABB(x, y, z, x2, y2, z2) else null }
-        val radiusSquared by lazy { distance?.let { it * it } }
+        val center: Vec3 get() = Vec3((x + (x2 ?: x)) / 2, (y + (y2 ?: y)) / 2, (z + (z2 ?: z)) / 2)
+        val box: AABB? get() = if (x2 != null && y2 != null && z2 != null) AABB(x, y, z, x2, y2, z2) else null
+        val radiusSquared: Double? get() = distance?.let { it * it }
     }
     val posMessageStrings by ListSetting("Pos Messages", mutableListOf<PosMessage>())
     private val sentMessages = mutableSetOf<PosMessage>()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt
index 2a5edaf..7e79502 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt
@@ -19,9 +19,9 @@ object GyroWand : Module(
     name = "Gyro Wand",
     description = "Shows area of effect and cooldown of the Gyrokinetic Wand."
 ) {
-    private val color by ColorSetting("Color", Colors.MINECRAFT_DARK_PURPLE.withAlpha(0.5f), allowAlpha = true, desc = "The color of the Gyrokinetic Wand range.")
+    private val color by ColorSetting("Color", Colors.MINECRAFT_DARK_PURPLE.withAlpha(0.5f), true, desc = "The color of the Gyrokinetic Wand range.")
     private val showCooldown by BooleanSetting("Show Cooldown", true, desc = "Shows the cooldown of the Gyrokinetic Wand.")
-    private val cooldownColor by ColorSetting("Cooldown Color", Colors.MINECRAFT_RED.withAlpha(0.5f), allowAlpha = true, desc = "The color of the cooldown of the Gyrokinetic Wand.").withDependency { showCooldown }
+    private val cooldownColor by ColorSetting("Cooldown Color", Colors.MINECRAFT_RED.withAlpha(0.5f), true, desc = "The color of the cooldown of the Gyrokinetic Wand.").withDependency { showCooldown }
     private val depthCheck by BooleanSetting("Depth Check", true, desc = "Whether or not the cylinder should have a depth check.")
 
     private val gravityStormRegex = Regex("(?s)(.*(-\\d+ Mana \\(Gravity Storm\\)).*)")
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
index b3e62b7..7413979 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
@@ -8,6 +8,7 @@ import com.odtheking.odin.utils.PersonalBest
 import com.odtheking.odin.utils.formatTime
 import com.odtheking.odin.utils.render.getStringWidth
 import com.odtheking.odin.utils.render.text
+import com.odtheking.odin.utils.skyblock.SplitRow
 import com.odtheking.odin.utils.skyblock.SplitsManager.currentRows
 import com.odtheking.odin.utils.skyblock.floor7SplitGroup
 import com.odtheking.odin.utils.toFixed
@@ -40,34 +41,32 @@ object Splits : Module(
         val labelWidth = segments.maxOfOrNull { getStringWidth(it.name) } ?: 0
         val totalWidth = labelWidth + 4 + timeWidth + 2
 
+        val displayRows = mutableListOf<SplitRow>()
         segments.forEachIndexed { index, row ->
-            if (row.time == 0L && !show0Time) return@forEachIndexed
-            val time = formatTime(row.time)
-            text(row.name, 0, index * 9, Colors.WHITE)
-
-            val displayText = if (showTickTime) "$time §8(§7${(row.tickTime / 20f).toFixed()}§8)" else time
-
-            val timeX = if (fixedWidth) labelWidth + 4 + timeWidth - getStringWidth(displayText)
-            else labelWidth + 4
-
-            text(displayText, timeX, index * 9, Colors.WHITE)
+            displayRows.add(row)
+            if (bossEntrySplit && index == 2 && rows.size > 3) {
+                val bossTime = segments.take(3).sumOf { it.time }
+                val bossTickTime = segments.take(3).sumOf { it.tickTime }
+                displayRows.add(SplitRow("§9Boss Entry", bossTime, bossTickTime, isCurrent = false))
+            }
         }
 
-        if (bossEntrySplit && rows.size > 3) {
-            val y = segments.size * 9
-            text("§9Boss Entry", 0, y, Colors.WHITE)
+        var y = 0
+        displayRows.forEach { row ->
+            if (row.time == 0L && !show0Time) return@forEach
+            val time = formatTime(row.time)
+            text(row.name, 0, y, Colors.WHITE)
 
-            val totalTime = formatTime(segments.take(3).sumOf { it.time })
-            val displayText = if (showTickTime) "$totalTime §8(§7${(segments.take(3).sumOf { it.tickTime } / 20f).toFixed()}§8)"
-            else totalTime
+            val displayText = if (showTickTime) "$time §8(§7${(row.tickTime / 20f).toFixed()}§8)" else time
 
             val timeX = if (fixedWidth) labelWidth + 4 + timeWidth - getStringWidth(displayText)
             else labelWidth + 4
 
             text(displayText, timeX, y, Colors.WHITE)
+            y += 9
         }
 
-        totalWidth to 9 * (rows.size + (if (bossEntrySplit) 1 else 0))
+        totalWidth to y
     }
 
     private val currentSplitHud by HUD("Current Split HUD", "Shows only the current split and its tick time.") { example ->
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt
index df54803..33aee9e 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt
@@ -28,6 +28,6 @@ class NumbersHandler: TerminalHandler(TerminalTypes.NUMBERS) {
             1 -> TerminalSolver.orderColor2
             2 -> TerminalSolver.orderColor3
             else -> Colors.TRANSPARENT
-        } to if (TerminalSolver.showNumbers) (abs((solution.size - 14) - solutionIndex) + 1).toString() else null
+        } to (abs((solution.size - 14) - solutionIndex) + 1).toString()
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
index c4116cf..6657653 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
@@ -34,7 +34,7 @@ abstract class TerminalHandler(val type: TerminalTypes) {
 
         solution.clear()
         solution.addAll(solve(event.slots.subList(0, type.windowSize - 9), event.slotIndex))
-        if (TerminalSolver.hideClicked) clickedSlots.forEach { (index, button) -> simulateClick(index, button) }
+        if (TerminalSolver.clickPrediction) clickedSlots.forEach { (index, button) -> simulateClick(index, button) }
     }
 
     protected abstract fun renderSlot(slotIndex: Int): Pair<Color, String?>?
fixed invincibility timer on items (#151)
* fixed invincibility timer on item and reorganized settings * formatting * formatting v2
poisonous888 · 2026-09-11 · ea4462a
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt | 50 +++++++++++++++++++++++++-------------------------
 1 file changed, 25 insertions(+), 25 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
index fe0e0da..abf5ff7 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
@@ -3,6 +3,7 @@ package com.odtheking.odin.features.impl.dungeon
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
+import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.LevelEvent
@@ -22,21 +23,16 @@ object InvincibilityTimer : Module(
 ) {
     private val invincibilityAlert by BooleanSetting("Invincibility Alert", true, desc = "Plays a sound when you get invincibility.")
     private val invincibilityAnnounce by BooleanSetting("Announce Invincibility", true, desc = "Announces when you get invincibility.")
-    private val showWhen by SelectorSetting("Show", "Always", listOf("Always", "Any", "When Active", "On Cooldown"), "Controls when invincibility items are shown.")
-    private val equippedMaskColor by ColorSetting("Equipped Mask", Colors.MINECRAFT_DARK_PURPLE, desc = "Color of the equipped mask in the HUD. (Bonzo/Spirit)")
-
-    private val showSpirit by BooleanSetting("Show Spirit Mask", true, desc = "Shows the Spirit Mask in the HUD.")
-    private val showBonzo by BooleanSetting("Show Bonzo Mask", true, desc = "Shows the Bonzo Mask in the HUD.")
-    private val showPhoenix by BooleanSetting("Show Phoenix Pet", true, desc = "Shows the Phoenix Pet in the HUD.")
-
-    private val onlyInDungeons by BooleanSetting("Only In Dungeons", true, "Only proc in dungeons")
-    private val showOnItem by BooleanSetting("Show On Item", true, "Renders the cooldown on the spirit mask and bonzo mask items")
-    private val showPhoenixOnTopLeft by BooleanSetting("Phoenix As Top Left", false, "Displays the cooldown for phoenix pet on the top-left most slot in your inventory")
-    private val durability by BooleanSetting("Display As Durability", false, "True: durability, False: colored vertical slide").withDependency { showOnItem }
-    private val cdColor by ColorSetting("Cooldown Color", Colors.gray38, true, "Color of the cooldown").withDependency { showOnItem }
-
+    private val onlyInDungeons by BooleanSetting("Only In Dungeons", true, "Only proc invincibility timers while in a dungeon")
+    
+    private val maskCat by DropdownSetting("Displayed Items")
+    private val showSpirit by BooleanSetting("Show Spirit Mask", true, desc = "Shows the Spirit Mask in the HUD.").withDependency { maskCat }
+    private val showBonzo by BooleanSetting("Show Bonzo Mask", true, desc = "Shows the Bonzo Mask in the HUD.").withDependency { maskCat }
+    private val showPhoenix by BooleanSetting("Show Phoenix Pet", true, desc = "Shows the Phoenix Pet in the HUD.").withDependency { maskCat }
+    
+    private val hudCat by DropdownSetting("Invincibility Hud")
     private val hud by HUD(name, "Shows the invincibility time in the HUD.") { example ->
-        if ((!DungeonUtils.inDungeons && !example) || (showOnlyInBoss && !DungeonUtils.inBoss && !example)) return@HUD 0 to 0
+        if(!example && (onlyInDungeons && !DungeonUtils.inDungeons) || (showOnlyInBoss && !DungeonUtils.inBoss)) return@HUD 0 to 0
 
         val visibleTypes = InvincibilityType.entries.filter { type ->
             (when (type) {
@@ -75,9 +71,16 @@ object InvincibilityTimer : Module(
         }
 
         width + 20 to visibleTypes.size * 14
-    }
-    private val showOnlyInBoss by BooleanSetting("Show In Boss", false, desc = "Only shows invincibility timers during dungeon boss fights.")
+    }.withDependency { hudCat }
+    private val showOnlyInBoss by BooleanSetting("Show In Boss", false, desc = "Only shows invincibility timers during dungeon boss fights.").withDependency { hudCat }
+    private val showWhen by SelectorSetting("Show", "Always", listOf("Always", "Any", "When Active", "On Cooldown"), "Controls when invincibility items are shown.").withDependency { hudCat }
+    private val equippedMaskColor by ColorSetting("Equipped Mask", Colors.MINECRAFT_DARK_PURPLE, desc = "Color of the equipped mask in the HUD. (Bonzo/Spirit)").withDependency { hudCat }
     private val cooldownRegex = Regex("^Cooldown: (\\d+)s$")
+    
+    private val itemCat by DropdownSetting("Cooldown On Item")
+    private val showOnItem by BooleanSetting("Show On Item", true, "Renders the cooldown on the spirit mask and bonzo mask items").withDependency { itemCat }
+    private val durability by BooleanSetting("Display As Durability", false, "True: durability, False: colored vertical slide").withDependency { itemCat }
+    private val cdColor by ColorSetting("Cooldown Color", Colors.gray38, true, "Color of the cooldown").withDependency { itemCat }
 
     init {
         on<TickEvent.Server> {
@@ -105,18 +108,15 @@ object InvincibilityTimer : Module(
         on<GuiEvent.RenderSlot> {
             if (!showOnItem) return@on
             val percent = when(slot.item.itemId) {
-                "BONZO_MASK", "STARRED_BONZO_MASK" -> InvincibilityType.BONZO.currentCooldown.toDouble() / InvincibilityType.BONZO.maxCooldownTime
-                "SPIRIT_MASK", "STARRED_SPIRIT_MASK" -> InvincibilityType.SPIRIT.currentCooldown.toDouble() / InvincibilityType.SPIRIT.maxCooldownTime
-                else -> {
-                    if(slot.containerSlot != 9 || !showPhoenixOnTopLeft) return@on
-                    InvincibilityType.PHOENIX.currentCooldown.toDouble() / InvincibilityType.PHOENIX.maxCooldownTime
-                }
+                "BONZO_MASK","STARRED_BONZO_MASK" -> InvincibilityType.BONZO.currentCooldown.toDouble() / (InvincibilityType.BONZO.maxCooldownTime * 20)
+                "SPIRIT_MASK","STARRED_SPIRIT_MASK" -> InvincibilityType.SPIRIT.currentCooldown.toDouble() / (InvincibilityType.SPIRIT.maxCooldownTime * 20)
+                else -> return@on
             }
-            if (percent !in 0.0..1.0) return@on
-            if (durability && percent > 0) {
+            if(percent <= 0) return@on
+            if(durability) {
                 guiGraphics.fakeItem(slot.item, slot.x, slot.y)
                 guiGraphics.fill(slot.x + 2, slot.y + 13, slot.x + 14, slot.y + 15, Colors.BLACK.rgba)
-                guiGraphics.fill(slot.x + 2, slot.y + 13, slot.x + 14 - ((1 - percent) * 12).toInt(), slot.y + 14, cdColor.rgba)
+                guiGraphics.fill(slot.x + 2, slot.y + 13, slot.x + 14 - ((1 - percent) * 12).toInt(), slot.y + 14,cdColor.rgba)
                 cancel()
             } else {
                 guiGraphics.fakeItem(slot.item, slot.x, slot.y)
Update mixin compatibility level to JAVA_25 (#154)
Very small change, was forgotten during 26.1 port I guess. Minecraft 26.1 requires Java 25.
Mustafa · 2026-09-11 · 697dc46
查看变更
src/main/resources/odin.mixins.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/resources/odin.mixins.json b/src/main/resources/odin.mixins.json
index 116ac8a..6a9fb94 100644
--- a/src/main/resources/odin.mixins.json
+++ b/src/main/resources/odin.mixins.json
@@ -2,7 +2,7 @@
   "required": true,
   "minVersion": "0.8",
   "package": "com.odtheking.mixin",
-  "compatibilityLevel": "JAVA_21",
+  "compatibilityLevel": "JAVA_25",
   "injectors": {
     "defaultRequire": 1
   },
@@ -34,4 +34,4 @@
     "mixins.SynchedEntityDataMixin",
     "mixins.YggdrasilSignatureIgnoreMixin"
   ]
-}
\ No newline at end of file
+}
Added QuiverDisplay Cleaned up Positionalmessagesodtheking · 2026-09-09 · a8284e7
查看变更
src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt                   | 40 +++++++++++++++++++++-------------------
 src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt                   |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt     |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt       |  3 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt | 74 ++++++++++++++++++++++++++------------------------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt      |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/QuiverDisplay.kt     | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt                   |  7 ++++++-
 src/main/kotlin/com/odtheking/odin/utils/render/DrawContextUtils.kt            |  9 +++++++--
 9 files changed, 116 insertions(+), 75 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
index f5b5fb4..38c8b64 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
@@ -12,6 +12,10 @@ import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.modMessage
 import kotlin.math.floor
 
+private val colorSuggestions = listOf("darkblue", "darkgreen", "darkaqua", "darkred", "darkpurple",
+    "gold", "gray", "darkgray", "blue", "green", "aqua", "red",
+    "lightpurple", "yellow", "white", "black")
+
 val posMsgCommand = Commodore("posmsg") {
     literal("add") {
         literal("at").executable {
@@ -25,17 +29,16 @@ val posMsgCommand = Commodore("posmsg") {
                 suggests { listOfNotNull(mc.player?.z?.let { floor(it).toString() }) }
             }
             param("color") {
-                suggests {
-                    listOf("darkblue", "darkgreen", "darkaqua", "darkred", "darkpurple",
-                        "gold", "gray", "darkgray", "blue", "green", "aqua", "red",
-                        "lightpurple", "yellow", "white", "black")
-                }
+                suggests { colorSuggestions }
+            }
+            param("send") {
+                suggests { listOf("true", "false") }
             }
 
-            runs { x: Double, y: Double, z: Double, delay: Int, distance: Double, color: String, message: GreedyString? ->
-                if (posMessageStrings.any { it.message == message?.string }) return@runs
+            runs { x: Double, y: Double, z: Double, delay: Int, distance: Double, color: String, send: Boolean, message: GreedyString ->
+                if (posMessageStrings.any { it.message == message.string }) return@runs
                 val color = getColorFromString(color) ?: return@runs modMessage("Unknown color $color")
-                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, null, null, null, delay, distance, color, message?.string))
+                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, null, null, null, delay, distance, color, message.string, send))
                 modMessage("Message \"${message}\" added at $x, $y, $z, with ${delay}t delay, triggered up to $distance blocks away.")
                 ModuleManager.saveConfigurations()
             }
@@ -43,17 +46,16 @@ val posMsgCommand = Commodore("posmsg") {
 
         literal("in").executable {
             param("color") {
-                suggests {
-                    listOf("darkblue", "darkgreen", "darkaqua", "darkred", "darkpurple",
-                        "gold", "gray", "darkgray", "blue", "green", "aqua", "red",
-                        "lightpurple", "yellow", "white", "black")
-                }
+                suggests { colorSuggestions }
+            }
+            param("send") {
+                suggests { listOf("true", "false") }
             }
 
-            runs { x: Double, y: Double, z: Double, x2: Double, y2: Double, z2: Double, delay: Int, color: String, message: GreedyString? ->
-                if (posMessageStrings.any { it.message == message?.string }) return@runs
+            runs { x: Double, y: Double, z: Double, x2: Double, y2: Double, z2: Double, delay: Int, color: String, send: Boolean, message: GreedyString ->
+                if (posMessageStrings.any { it.message == message.string }) return@runs
                 val color = getColorFromString(color) ?: return@runs modMessage("Unknown color $color")
-                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, x2, y2, z2, delay, null, color, message?.string))
+                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, x2, y2, z2, delay, null, color, message.string, send))
                 modMessage("Message \"${message}\" added in $x, $y, $z, $x2, $y2, $z2, with ${delay}t delay.")
                 ModuleManager.saveConfigurations()
             }
@@ -69,7 +71,7 @@ val posMsgCommand = Commodore("posmsg") {
                 input
             }
             suggests {
-                posMessageStrings.mapNotNull { it.message }.distinct()
+                posMessageStrings.map { it.message }.distinct()
             }
         }
 
@@ -88,8 +90,8 @@ val posMsgCommand = Commodore("posmsg") {
     }
 
     literal("list").runs {
-        val output = posMessageStrings.joinToString(separator = "\n") {
-            "${posMessageStrings.indexOf(it) + 1}: ${it.x}, ${it.y}, ${it.z}, ${it.x2}, ${it.y2}, ${it.z2}, ${it.delay}, ${it.distance}, ${it.color.hex()}, \"${it.message}\""
+        val output = posMessageStrings.withIndex().joinToString(separator = "\n") { (index, it) ->
+            "${index + 1}: ${it.x}, ${it.y}, ${it.z}, ${it.x2}, ${it.y2}, ${it.z2}, ${it.delay}, ${it.distance}, ${it.color.hex()}, send=${it.send}, \"${it.message}\""
         }
         modMessage(if (posMessageStrings.isEmpty()) "Positional Message list is empty!" else "Positonal Message list:\n$output")
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
index ac7baac..c4130bb 100644
--- a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
@@ -73,7 +73,7 @@ object ModuleManager {
 
             //skyblock
             ChatCommands, NoCursorReset, Ragnarock, SpringBoots, WardrobeKeybinds, PetKeybinds, AutoSprint,
-            CommandKeybinds, SlotBinds, Splits, LoadoutKeybinds,
+            CommandKeybinds, SlotBinds, Splits, LoadoutKeybinds, QuiverDisplay,
 
             // nether
             SupplyHelper, BuildHelper, RemovePerks, NoPre, PearlWaypoints, FreshTools, KuudraInfo, Misc, Vesuvius,
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
index fcd9e23..9fbd10f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
@@ -6,7 +6,7 @@ import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.itemId
-import com.odtheking.odin.utils.loreString
+import com.odtheking.odin.utils.lore
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 
@@ -27,7 +27,7 @@ object BreakerDisplay : Module(
     init {
          on<SetSlotEvent> {
             if (!DungeonUtils.inDungeons || itemStack.itemId != "DUNGEONBREAKER") return@on
-            itemStack.loreString.firstNotNullOfOrNull { chargesRegex.find(it) }?.let { match ->
+            itemStack.lore.firstNotNullOfOrNull { chargesRegex.find(it.string) }?.let { match ->
                 charges = match.groupValues[1].toIntOrNull() ?: 0
                 maxCharges = match.groupValues[2].toIntOrNull() ?: 0
             }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
index 1eb71c0..56e1224 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
@@ -29,6 +29,7 @@ object DungeonQueue : Module(
     private val disablePartyLeave by BooleanSetting("Disable on leave/kick", true, desc = "Disables the requeue on party leave message.").withDependency { autoRequeue }
 
     private val enterRegex = Regex("^-*\\n\\[[^]]+] (\\w+) entered (?:MM )?\\w+ Catacombs, Floor (\\w+)!\\n-*$")
+    private val timeoutExeceptionRegex = Regex("^Exception Connecting:ReadTimeoutException : null$")
     private val kickedInstanceRegex = Regex("^You are no longer allowed to access this instance!$")
     private val kickedJoiningRegex = Regex("^You were kicked while joining that server!$")
     private val extraStatsRegex = Regex(" {29}> EXTRA STATS <")
@@ -39,7 +40,7 @@ object DungeonQueue : Module(
     init {
         on<MessageEvent.Chat> {
             when {
-                announceKick && (message.matches(kickedJoiningRegex) || message.matches(kickedInstanceRegex)) -> sendCommand("pc I was kicked!")
+                announceKick && (message.matches(kickedJoiningRegex) || message.matches(kickedInstanceRegex) || message.matches(timeoutExeceptionRegex)) -> sendCommand("pc I was kicked!")
                 message.matches(enterRegex) -> warpTimer = System.currentTimeMillis() + 30_000L
                 autoRequeue && message.matches(extraStatsRegex) -> {
                     if (disableRequeue.also { disableRequeue = false }) return@on
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
index 86bd0df..64947db 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
@@ -25,80 +25,58 @@ object PositionalMessages : Module(
     description = "Sends a message when you're near a certain position. /posmsg"
 ) {
     private val onlyDungeons by BooleanSetting("Only in Dungeons", true, desc = "Only sends messages when you're in a dungeon.")
-    private val oncePerWorld by BooleanSetting("Once Per World", false, desc = "Whether or not to only send each message once per world.")
     private val showPositions by BooleanSetting("Show Positions", true, desc = "Draws boxes/lines around the positions.")
-    private val cylinderHeight by NumberSetting("Height", 0.2, 0.1, 5.0, 0.1, desc = "Height of the cylinder for in messages.").withDependency { showPositions }
-    private val depthCheck by BooleanSetting("Depth Check", true, desc = "Whether or not the boxes should be seen through walls. False = Through walls.").withDependency { showPositions }
+    private val cylinderHeight by NumberSetting("Height", 0.2f, 0.1, 5.0, 0.1, desc = "Height of the cylinder for in messages.").withDependency { showPositions }
     private val displayMessage by BooleanSetting("Show Message", true, desc = "Whether or not to display the message in the box.").withDependency { showPositions }
-    private val messageSize by NumberSetting("Message Size", 1f, 0.1f, 4f, 0.1f, desc = "Whether or not to display the message size in the box.").withDependency { showPositions && displayMessage }
+    private val messageSize by NumberSetting("Message Size", 1f, 0.1f, 4f, 0.1f, desc = "The size at which to display the message in the box.").withDependency { showPositions && displayMessage }
 
-    data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Int, val distance: Double?, val color: Color, val message: String?)
+    data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Int, val distance: Double?, val color: Color, val message: String, val send: Boolean) {
+        val center: Vec3 by lazy { Vec3((x + (x2 ?: x)) / 2, (y + (y2 ?: y)) / 2, (z + (z2 ?: z)) / 2) }
+        val box: AABB? by lazy { if (x2 != null && y2 != null && z2 != null) AABB(x, y, z, x2, y2, z2) else null }
+        val radiusSquared by lazy { distance?.let { it * it } }
+    }
     val posMessageStrings by ListSetting("Pos Messages", mutableListOf<PosMessage>())
-    private val sentMessages = mutableMapOf<PosMessage, Boolean>()
+    private val sentMessages = mutableSetOf<PosMessage>()
 
     init {
         onSend<ServerboundMovePlayerPacket> {
-            posMessageSend()
+            if (onlyDungeons && !DungeonUtils.inBoss) return@onSend
+            posMessageStrings.forEach { posMessage ->
+                if (posMessage.send && posMessage !in sentMessages) posMessage.x2?.let { handleInString(posMessage) } ?: handleAtString(posMessage)
+            }
         }
 
         on<RenderEvent.Extract> {
-            if (!showPositions || (onlyDungeons && !DungeonUtils.inDungeons)) return@on
-            val player = mc.player ?: return@on
+            if (!showPositions || (onlyDungeons && !DungeonUtils.inBoss)) return@on
             posMessageStrings.forEach { posMessage ->
-                val distanceToMessage = if (posMessage.distance != null)
-                    player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z)
-                else {
-                    val centerX = (posMessage.x + (posMessage.x2 ?: posMessage.x)) / 2
-                    val centerY = (posMessage.y + (posMessage.y2 ?: posMessage.y)) / 2
-                    val centerZ = (posMessage.z + (posMessage.z2 ?: posMessage.z)) / 2
-                    player.distanceToSqr(centerX, centerY, centerZ)
-                }
-                if (distanceToMessage > 1024) return@forEach
-
                 if (posMessage.distance != null) {
-                    drawCylinder(Vec3(posMessage.x, posMessage.y, posMessage.z), posMessage.distance.toFloat(), cylinderHeight.toFloat(), color = posMessage.color, depth = depthCheck)
-                    if (displayMessage) posMessage.message?.let { drawText(it, Vec3(posMessage.x, posMessage.y + 1, posMessage.z), messageSize, depthCheck) }
+                    drawCylinder(posMessage.center, posMessage.distance.toFloat(), cylinderHeight, color = posMessage.color, depth = true)
+                    if (displayMessage) drawText(posMessage.message, Vec3(posMessage.x, posMessage.y + 1, posMessage.z), messageSize, true)
                 } else {
-                    val box = AABB(posMessage.x, posMessage.y, posMessage.z, posMessage.x2 ?: return@forEach, posMessage.y2 ?: return@forEach,posMessage.z2  ?: return@forEach)
-                    drawWireFrameBox(box, posMessage.color, depth = depthCheck)
-                    if (!displayMessage) return@forEach
-                    val center = Vec3((posMessage.x + posMessage.x2) / 2, (posMessage.y + posMessage.y2) / 2, (posMessage.z + posMessage.z2) / 2)
-                    posMessage.message?.let { drawText(it, center.add(0.0, 1.0, 0.0), messageSize, depthCheck) }
+                    drawWireFrameBox(posMessage.box ?: return@forEach, posMessage.color, depth = true)
+                    if (displayMessage) drawText(posMessage.message, posMessage.center.add(0.0, 1.0, 0.0), messageSize, true)
                 }
             }
         }
 
         on<LevelEvent.Load> {
-            if (oncePerWorld) sentMessages.forEach { (message) -> sentMessages[message] = false }
-        }
-    }
-
-    private fun posMessageSend() {
-        if (onlyDungeons && !DungeonUtils.inDungeons) return
-        posMessageStrings.forEach { message ->
-            message.x2?.let { handleInString(message) } ?: handleAtString(message)
+            sentMessages.clear()
         }
     }
 
     private fun handleAtString(posMessage: PosMessage) {
-        val msgSent = sentMessages.getOrDefault(posMessage, false)
         val player = mc.player ?: return
-        val radius = posMessage.distance ?: return
-        if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= radius * radius) {
-            if (!msgSent && player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= radius * radius)
-                schedule(posMessage.delay) { sendCommand("pc ${posMessage.message}") }
-            sentMessages[posMessage] = true
-        } else if (!oncePerWorld) sentMessages[posMessage] = false
+        val radiusSquared = posMessage.radiusSquared ?: return
+        if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) > radiusSquared) return
+        sentMessages.add(posMessage)
+        schedule(posMessage.delay) { sendCommand("pc ${posMessage.message}") }
     }
 
     private fun handleInString(posMessage: PosMessage) {
+        val aabb = posMessage.box ?: return
         val position = mc.player?.position() ?: return
-        val msgSent = sentMessages.getOrDefault(posMessage, false)
-        val aabb = AABB(posMessage.x, posMessage.y, posMessage.z, posMessage.x2 ?: return, posMessage.y2 ?: return, posMessage.z2 ?: return)
-        if (aabb.contains(position)) {
-            if (!msgSent && aabb.contains(position))
-                schedule(posMessage.delay) { sendCommand("pc ${posMessage.message}") }
-            sentMessages[posMessage] = true
-        } else if (!oncePerWorld) sentMessages[posMessage] = false
+        if (!aabb.contains(position)) return
+        sentMessages.add(posMessage)
+        schedule(posMessage.delay) { sendCommand("pc ${posMessage.message}") }
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
index 76e267d..3684582 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
@@ -214,7 +214,7 @@ object ChatCommands : Module(
     }
 
     private fun findPartyMember(partialName: String): String =
-        PartyUtils.members.find { it.contains(partialName, true) } ?: partialName
+        PartyUtils.members.find { it.startsWith(partialName, true) } ?: partialName
 
     private fun channelMessage(message: Any, name: String, channel: ChatChannel) {
         when (channel) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/QuiverDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/QuiverDisplay.kt
new file mode 100644
index 0000000..f1232b2
--- /dev/null
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/QuiverDisplay.kt
@@ -0,0 +1,50 @@
+package com.odtheking.odin.features.impl.skyblock
+
+import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
+import com.odtheking.odin.clickgui.settings.impl.ColorSetting
+import com.odtheking.odin.events.SetSlotEvent
+import com.odtheking.odin.events.core.on
+import com.odtheking.odin.features.Module
+import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.lore
+import com.odtheking.odin.utils.render.textDim
+import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
+import net.minecraft.network.chat.Component
+import net.minecraft.network.chat.Style
+import net.minecraft.network.chat.TextColor
+import net.minecraft.world.item.Items
+
+object QuiverDisplay : Module(
+    name = "Quiver Display",
+    description = "Displays the number of arrows in your quiver."
+) {
+    private val showName by BooleanSetting("Show Name", true, desc = "Shows the arrow type's name before the count.")
+    private val countColor by ColorSetting("Count Color", Colors.MINECRAFT_GREEN, true, desc = "The color of the arrow count.")
+
+    private val quiverHud by HUD("Quiver HUD", "Displays the number of arrows in your quiver.", false) { example ->
+        val text = when {
+            example -> buildText(Component.literal("Flint Arrow"), "100")
+            arrows != null -> arrows?.let { buildText(it.first, it.second) }
+            else -> null
+        } ?: return@HUD 0 to 0
+        return@HUD textDim(text, 0, 0)
+    }
+
+    private val arrowRegex = Regex("^Arrows Remaining: ([\\d,]+)$")
+    private var arrows: Pair<Component, String>? = null
+
+    private fun buildText(name: Component, count: String) =
+        (if (showName) name.copy().append("§8x") else Component.literal(""))
+            .append(Component.literal(count).withStyle(Style.EMPTY.withColor(TextColor.fromRgb(countColor.rgba and 0xFFFFFF))))
+            .visualOrderText
+
+    init {
+        on<SetSlotEvent> {
+            if (slotIndex != (if (DungeonUtils.inClear) 9 else 44) || (itemStack.item != Items.FEATHER && itemStack.item != Items.ARROW)) return@on
+
+            arrows = itemStack.hoverName to (itemStack.lore.find { arrowRegex.matches(it.string) }?.let {
+                arrowRegex.find(it.string)?.groupValues?.get(1)
+            } ?: return@on)
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
index a422bd5..6e402d7 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
@@ -3,6 +3,7 @@ package com.odtheking.odin.utils.network
 import com.google.gson.Gson
 import com.google.gson.GsonBuilder
 import com.google.gson.JsonObject
+import com.odtheking.odin.OdinMod
 import com.odtheking.odin.OdinMod.logger
 import kotlinx.coroutines.suspendCancellableCoroutine
 import java.io.InputStream
@@ -55,6 +56,7 @@ object WebUtils {
             .uri(URI.create(url))
             .header("Accept", "application/json")
             .header("User-Agent", USER_AGENT)
+            .header("X-Mod-Version", OdinMod.version.friendlyString)
             .GET()
             .timeout(Duration.ofSeconds(10))
             .build()
@@ -79,7 +81,10 @@ object WebUtils {
                 if (!cont.isActive) return@whenComplete
 
                 if (response.statusCode() in 200..299) cont.resume(Result.success(response))
-                else cont.resume(Result.failure(InputStreamException(response.body(), request.uri().toString())))
+                else {
+                    cont.resume(Result.failure(InputStreamException(response.body(), request.uri().toString())))
+                    logger.error("Request to ${request.uri()} failed with status code ${response.statusCode()}: $response")
+                }
             }
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/DrawContextUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/render/DrawContextUtils.kt
index 44f7484..6910396 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/DrawContextUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/DrawContextUtils.kt
@@ -14,13 +14,18 @@ fun GuiGraphicsExtractor.text(text: String, x: Int, y: Int, color: Color = Color
     text(mc.font, text, x, y, color.rgba, shadow)
 }
 
+fun GuiGraphicsExtractor.text(text: FormattedCharSequence, x: Int, y: Int, color: Color = Colors.WHITE, shadow: Boolean = true) {
+    text(mc.font, text, x, y, color.rgba, shadow)
+}
+
 fun GuiGraphicsExtractor.textDim(text: String, x: Int, y: Int, color: Color = Colors.WHITE, shadow: Boolean = true): Pair<Int, Int> {
     text(text, x, y, color, shadow)
     return mc.font.width(text) to mc.font.lineHeight
 }
 
-fun GuiGraphicsExtractor.text(text: FormattedCharSequence, x: Int, y: Int, color: Color = Colors.WHITE, shadow: Boolean = true) {
-    text(mc.font, text, x, y, color.rgba, shadow)
+fun GuiGraphicsExtractor.textDim(text: FormattedCharSequence, x: Int, y: Int, color: Color = Colors.WHITE, shadow: Boolean = true): Pair<Int, Int> {
+    text(text, x, y, color, shadow)
+    return mc.font.width(text) to mc.font.lineHeight
 }
 
 fun getStringWidth(text: String): Int = mc.font.width(text)
Moved some events from network thread to main threadodtheking · 2026-09-06 · e3cb25e
查看变更
src/main/java/com/odtheking/mixin/mixins/ChatListenerMixin.java                                      | 17 --------------
 src/main/java/com/odtheking/mixin/mixins/EntityMixin.java                                            | 28 ++++++++++++++++++++++++
 src/main/java/com/odtheking/mixin/mixins/LivingEntityMixin.java                                      | 20 +++++++++++++++++
 src/main/java/com/odtheking/mixin/mixins/SynchedEntityDataMixin.java                                 | 27 +++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt                                            | 14 ++++++------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt                          | 13 +++--------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/NumberSetting.kt                           |  6 ++---
 src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt                                            | 15 ++-----------
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                                            | 27 ++++++++++++++++++-----
 src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt                                         | 47 +++++++++++++++++++++------------------
 src/main/kotlin/com/odtheking/odin/features/Module.kt                                                |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt                                  |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt                                | 17 +++++---------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt                                 | 26 +++++++++-------------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt                                  | 27 +++++++++++------------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt                           | 14 ++++++------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt                                  | 31 ++++++++++----------------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt                                 | 12 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt                               |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt                                   |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt                              |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt                              |  8 +++----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt                               | 17 +++++++-------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt                                  | 18 +++++++--------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt                               | 13 +++++------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt                       |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt                        |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt                                | 62 +++++++++++++++++++++-------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt                                  |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt                            | 25 ++++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt                             | 10 ++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Highlight.kt                                |  5 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt                       |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt                                 | 29 ++++++++++++------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt                                  |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt                                    | 61 ++++++++++++++++++---------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt                            | 12 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretsCounter.kt                           | 41 ++++++++++++++--------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt   |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypoints.kt        |  7 +++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/TextPromtScreen.kt         |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt                            | 14 ++++++------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/RoomData.kt                        |  3 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BeamsSolver.kt                |  5 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BoulderSolver.kt              |  9 ++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/IceFillSolver.kt              | 11 ++++------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt              | 16 +++++---------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt                 |  5 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt                |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/nether/FreshTools.kt                                |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Misc.kt                                      |  8 +++----
 src/main/kotlin/com/odtheking/odin/features/impl/nether/NoPre.kt                                     | 12 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt                              | 10 ++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt                                  |  5 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt                            | 10 ++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt                                  |  9 +++-----
 src/main/kotlin/com/odtheking/odin/features/impl/render/HidePlayers.kt                               | 12 ++++++----
 src/main/kotlin/com/odtheking/odin/features/impl/render/RenderOptimizer.kt                           | 42 +++++++++++++++--------------------
 src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt                                 | 12 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt                            |  8 +++----
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt                         | 18 +++++++--------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt                         | 30 ++++++++++++-------------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt                             | 26 +++++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt                           | 29 ++++++++++++------------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Ragnarock.kt                               |  6 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt                               |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt                        | 26 +++++++++++-----------
 src/main/kotlin/com/odtheking/odin/utils/JsonResourceLoader.kt                                       | 13 +++++------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt                               | 21 +++++++-----------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/KuudraUtils.kt                                     | 10 ++++-----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt                                   |  3 +--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/PartyUtils.kt                                      | 40 ++++++++++++++++-----------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt                                   |  6 ++---
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt                         | 25 +++++++++++----------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt                            | 26 ++++++----------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt                 |  4 ++--
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt    |  6 ++---
 src/main/kotlin/com/odtheking/odin/utils/ui/TextInputHandler.kt                                      | 36 ++++++++++++++----------------
 src/main/resources/assets/odin/puzzles/{boulderSolutions.json => boulder-solutions.json}             |  0
 src/main/resources/assets/odin/puzzles/{creeperBeamsSolutions.json => creeper-beams-solutions.json}  |  0
 src/main/resources/assets/odin/puzzles/{iceFillFloors.json => ice-fill-floors.json}                  |  0
 src/main/resources/assets/odin/puzzles/{quizAnswers.json => quiz-answers.json}                       |  0
 src/main/resources/assets/odin/puzzles/{waterSolutions.json => water-solutions.json}                 |  0
 src/main/resources/odin.mixins.json                                                                  |  3 ++-
 84 files changed, 599 insertions(+), 645 deletions(-)
diff --git a/src/main/java/com/odtheking/mixin/mixins/ChatListenerMixin.java b/src/main/java/com/odtheking/mixin/mixins/ChatListenerMixin.java
deleted file mode 100644
index 94b7fbd..0000000
--- a/src/main/java/com/odtheking/mixin/mixins/ChatListenerMixin.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.odtheking.mixin.mixins;
-
-import com.odtheking.odin.features.impl.skyblock.PlayerDisplay;
-import net.minecraft.client.multiplayer.chat.ChatListener;
-import net.minecraft.network.chat.Component;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.ModifyArg;
-
-@Mixin(ChatListener.class)
-public class ChatListenerMixin {
-
-    @ModifyArg(method = "handleOverlay", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/Gui;setOverlayMessage(Lnet/minecraft/network/chat/Component;Z)V"), index = 0)
-    private Component modifyOverlayMessage(Component component) {
-        return PlayerDisplay.modifyText(component);
-    }
-}
diff --git a/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java b/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
index 5d25b89..2458ff9 100644
--- a/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/EntityMixin.java
@@ -1,10 +1,16 @@
 package com.odtheking.mixin.mixins;
 
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.odtheking.odin.events.EntityEvent;
 import com.odtheking.odin.features.impl.dungeon.Highlight;
 import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.InterpolationHandler;
+import net.minecraft.world.phys.Vec3;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
 import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
 @Mixin(Entity.class)
@@ -17,4 +23,26 @@ public abstract class EntityMixin {
         Integer color = Highlight.getTeammateColor(self);
         if (color != null) cir.setReturnValue(color);
     }
+
+    @WrapOperation(
+            method = "moveOrInterpolateTo(Ljava/util/Optional;Ljava/util/Optional;Ljava/util/Optional;)V",
+            at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/InterpolationHandler;interpolateTo(Lnet/minecraft/world/phys/Vec3;FF)V")
+    )
+     private void onMoveOrInterpolateTo(InterpolationHandler instance, Vec3 position, float yRot, float xRot, Operation<Void> original) {
+        original.call(instance, position, yRot, xRot);
+
+        Entity entity = (Entity)(Object)this;
+        new EntityEvent.Move(entity, position, yRot, xRot, entity.onGround()).postAndCatch();
+    }
+
+    @Inject(method = "setOnGround", at = @At("TAIL"))
+    private void onSetOnGround(boolean onGround, CallbackInfo ci) {
+        Entity entity = (Entity)(Object)this;
+        new EntityEvent.Move(entity, Vec3.ZERO, 0f, 0f, onGround).postAndCatch();
+    }
+
+    @Inject(method = "handleEntityEvent", at = @At("TAIL"))
+    private void onHandleEntityEvent(byte id, CallbackInfo ci) {
+        new EntityEvent.Event((Entity)(Object)this, id).postAndCatch();
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/LivingEntityMixin.java b/src/main/java/com/odtheking/mixin/mixins/LivingEntityMixin.java
new file mode 100644
index 0000000..7f0639a
--- /dev/null
+++ b/src/main/java/com/odtheking/mixin/mixins/LivingEntityMixin.java
@@ -0,0 +1,20 @@
+package com.odtheking.mixin.mixins;
+
+import com.odtheking.odin.events.EntityEvent;
+import net.minecraft.world.entity.EquipmentSlot;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.item.ItemStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(LivingEntity.class)
+public abstract class LivingEntityMixin {
+
+    @Inject(method = "setItemSlot", at = @At("TAIL"))
+    private void onSetItemSlot(EquipmentSlot slot, ItemStack itemStack, CallbackInfo ci) {
+        LivingEntity self = (LivingEntity)(Object)this;
+        if (self != null) new EntityEvent.SetItemSlot(self, slot, itemStack).postAndCatch();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/SynchedEntityDataMixin.java b/src/main/java/com/odtheking/mixin/mixins/SynchedEntityDataMixin.java
new file mode 100644
index 0000000..d1c7bca
--- /dev/null
+++ b/src/main/java/com/odtheking/mixin/mixins/SynchedEntityDataMixin.java
@@ -0,0 +1,27 @@
+package com.odtheking.mixin.mixins;
+
+import com.odtheking.odin.events.EntityEvent;
+import net.minecraft.network.syncher.SyncedDataHolder;
+import net.minecraft.network.syncher.SynchedEntityData;
+import net.minecraft.world.entity.Entity;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.List;
+
+@Mixin(SynchedEntityData.class)
+public class SynchedEntityDataMixin {
+    @Final
+    @Shadow
+    private SyncedDataHolder entity;
+
+    @Inject(method = "assignValues", at = @At("TAIL"))
+    private void onAssignValues(List<SynchedEntityData.DataValue<?>> items, CallbackInfo ci) {
+        Entity self = (Entity) this.entity;
+        if (self != null) new EntityEvent.SetData(self, items).postAndCatch();
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt b/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt
index 4b592b1..531318f 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/HudManager.kt
@@ -1,5 +1,6 @@
 package com.odtheking.odin.clickgui
 
+import com.mojang.blaze3d.platform.InputConstants
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.clickgui.settings.impl.HudElement
 import com.odtheking.odin.features.ModuleManager
@@ -10,7 +11,6 @@ import net.minecraft.client.gui.screens.Screen
 import net.minecraft.client.input.KeyEvent
 import net.minecraft.client.input.MouseButtonEvent
 import net.minecraft.network.chat.Component
-import org.lwjgl.glfw.GLFW
 import kotlin.math.sign
 import com.odtheking.odin.utils.ui.mouseX as odinMouseX
 import com.odtheking.odin.utils.ui.mouseY as odinMouseY
@@ -92,12 +92,12 @@ object HudManager : Screen(Component.literal("HUD Manager")) {
     override fun keyPressed(keyEvent: KeyEvent): Boolean {
         hudSettingsCache.firstOrNull { it.isEnabled && it.value.isHovered() }?.let { hovered ->
             when (keyEvent.key) {
-                GLFW.GLFW_KEY_EQUAL -> hovered.value.scale = (hovered.value.scale + 0.1f).coerceIn(1f, 10f)
-                GLFW.GLFW_KEY_MINUS -> hovered.value.scale = (hovered.value.scale - 0.1f).coerceIn(1f, 10f)
-                GLFW.GLFW_KEY_RIGHT -> hovered.value.x += 10
-                GLFW.GLFW_KEY_LEFT -> hovered.value.x -= 10
-                GLFW.GLFW_KEY_UP -> hovered.value.y -= 10
-                GLFW.GLFW_KEY_DOWN -> hovered.value.y += 10
+                InputConstants.KEY_EQUALS -> hovered.value.scale = (hovered.value.scale + 0.1f).coerceIn(1f, 10f)
+                InputConstants.KEY_MINUS -> hovered.value.scale = (hovered.value.scale - 0.1f).coerceIn(1f, 10f)
+                InputConstants.KEY_RIGHT -> hovered.value.x += 10
+                InputConstants.KEY_LEFT -> hovered.value.x -= 10
+                InputConstants.KEY_UP -> hovered.value.y -= 10
+                InputConstants.KEY_DOWN -> hovered.value.y += 10
             }
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt
index 8325918..52f6a64 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/KeybindSetting.kt
@@ -14,7 +14,6 @@ import com.odtheking.odin.utils.ui.isAreaHovered
 import com.odtheking.odin.utils.ui.rendering.NVGRenderer
 import net.minecraft.client.input.KeyEvent
 import net.minecraft.client.input.MouseButtonEvent
-import org.lwjgl.glfw.GLFW
 
 class KeybindSetting(
     name: String,
@@ -71,8 +70,8 @@ class KeybindSetting(
         if (!listening) return false
 
         when (input.key) {
-            GLFW.GLFW_KEY_ESCAPE, GLFW.GLFW_KEY_BACKSPACE -> key = InputConstants.UNKNOWN
-            GLFW.GLFW_KEY_ENTER -> listening = false
+            InputConstants.KEY_ESCAPE, InputConstants.KEY_BACKSPACE -> key = InputConstants.UNKNOWN
+            InputConstants.KEY_RETURN -> listening = false
             else -> key = InputConstants.getKey(input)
         }
 
@@ -85,8 +84,6 @@ class KeybindSetting(
         return this
     }
 
-
-
     override val isHovered: Boolean
         get() =
             isAreaHovered(lastX + width - 20 - keyNameWidth, lastY + getHeight() / 2f - 10f, keyNameWidth + 12f, 22f, true)
@@ -102,10 +99,6 @@ class KeybindSetting(
     }
 
     companion object {
-        fun InputConstants.Key.isDown(): Boolean {
-            val window = mc.window
-            return if (value > 7) InputConstants.isKeyDown(window, value)
-            else GLFW.glfwGetMouseButton(window.handle(), value) == GLFW.GLFW_PRESS
-        }
+        fun InputConstants.Key.isDown(): Boolean = InputConstants.isKeyDown(mc.window, value)
     }
 }
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/NumberSetting.kt b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/NumberSetting.kt
index b40f6f1..cf22eff 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/NumberSetting.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/NumberSetting.kt
@@ -3,6 +3,7 @@ package com.odtheking.odin.clickgui.settings.impl
 import com.google.gson.Gson
 import com.google.gson.JsonElement
 import com.google.gson.JsonPrimitive
+import com.mojang.blaze3d.platform.InputConstants
 import com.odtheking.odin.clickgui.ClickGUI.gray38
 import com.odtheking.odin.clickgui.Panel
 import com.odtheking.odin.clickgui.settings.RenderableSetting
@@ -15,7 +16,6 @@ import com.odtheking.odin.utils.ui.isAreaHovered
 import com.odtheking.odin.utils.ui.rendering.NVGRenderer
 import net.minecraft.client.input.KeyEvent
 import net.minecraft.client.input.MouseButtonEvent
-import org.lwjgl.glfw.GLFW
 import kotlin.math.floor
 import kotlin.math.round
 import kotlin.math.roundToInt
@@ -136,8 +136,8 @@ class NumberSetting<E>(
         if (!isHovered) return false
 
         val amount = when (input.key) {
-            GLFW.GLFW_KEY_RIGHT, GLFW.GLFW_KEY_EQUAL -> incrementDouble
-            GLFW.GLFW_KEY_LEFT, GLFW.GLFW_KEY_MINUS -> -incrementDouble
+            InputConstants.KEY_RIGHT, InputConstants.KEY_EQUALS -> incrementDouble
+            InputConstants.KEY_LEFT, InputConstants.KEY_MINUS -> -incrementDouble
             else -> return false
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
index 430f07d..2eef65b 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
@@ -4,7 +4,7 @@ import com.github.stivais.commodore.Commodore
 import com.github.stivais.commodore.utils.GreedyString
 import com.odtheking.odin.OdinMod
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.ChatMessageEvent
+import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.features.ModuleManager
 import com.odtheking.odin.features.impl.boss.MelodyMessage.melodyWebSocket
 import com.odtheking.odin.features.impl.boss.WitherDragonState
@@ -16,10 +16,7 @@ import com.odtheking.odin.features.impl.dungeon.map.WorldScan
 import com.odtheking.odin.features.impl.nether.NoPre
 import com.odtheking.odin.features.impl.render.ClickGUIModule.webSocketUrl
 import com.odtheking.odin.features.impl.render.PlayerSize
-import com.odtheking.odin.features.impl.render.PlayerSize.DEV_SERVER
-import com.odtheking.odin.features.impl.render.PlayerSize.buildDevBody
 import com.odtheking.odin.utils.*
-import com.odtheking.odin.utils.network.WebUtils.postData
 import com.odtheking.odin.utils.skyblock.KuudraUtils
 import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.PartyUtils
@@ -53,7 +50,7 @@ val devCommand = Commodore("oddev") {
     }
 
     literal("simulate").runs { greedyString: GreedyString ->
-        ChatMessageEvent(greedyString.string, Component.literal(greedyString.string)).postAndCatch()
+        MessageEvent.Chat(greedyString.string, Component.literal(greedyString.string)).postAndCatch()
         modMessage("§8Simulated message: ${greedyString.string}")
     }
 
@@ -67,14 +64,6 @@ val devCommand = Commodore("oddev") {
         PlayerSize.clearCustomProperties()
     }
 
-    literal("adddev").runs { name: String, password: String, xSize: Float?, ySize: Float?, zSize: Float? ->
-        val devBody = buildDevBody(name, xSize ?: 0.6f, ySize ?: 0.6f, zSize ?: 0.6f, " ", password)
-        modMessage("Sending $devBody")
-        OdinMod.scope.launch {
-            modMessage(postData(DEV_SERVER, devBody).getOrNull())
-        }
-    }
-
     literal("generatefeaturelist").runs {
         fun generateFeatureList(): String {
             val featureList = StringBuilder()
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index c261b0b..ada0c59 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -15,10 +15,12 @@ import net.minecraft.core.BlockPos
 import net.minecraft.network.chat.Component
 import net.minecraft.network.protocol.Packet
 import net.minecraft.network.protocol.game.ClientboundSoundPacket
+import net.minecraft.network.syncher.SynchedEntityData
 import net.minecraft.world.BossEvent
 import net.minecraft.world.InteractionHand
 import net.minecraft.world.InteractionResult
 import net.minecraft.world.entity.Entity
+import net.minecraft.world.entity.EquipmentSlot
 import net.minecraft.world.entity.item.ItemEntity
 import net.minecraft.world.inventory.AbstractContainerMenu
 import net.minecraft.world.inventory.Slot
@@ -35,9 +37,14 @@ class BlockInteractEvent(val pos: BlockPos) : CancellableEvent()
 class EntityInteractEvent(val pos: Vec3, val entity: Entity) : CancellableEvent()
 class UseItemOnPostEvent(val hand: InteractionHand, val hitResult: BlockHitResult, val interactionResult: InteractionResult) : Event
 
-class ChatMessageEvent(val value: String, val component: Component) : CancellableEvent()
-class MessageSentEvent(val message: String) : CancellableEvent()
+open class MessageEvent(val message: String, open val component: Component) : CancellableEvent() {
+    class Chat(value: String, component: Component) : MessageEvent(value, component)
+    class Overlay(value: String, component: Component) : MessageEvent(value, component)
+    class ModifyChat(value: String, override var component: Component) : MessageEvent(value, component)
+    class ModifyOverlay(value: String, override var component: Component) : MessageEvent(value, component)
+}
 
+class MessageSentEvent(val message: String) : CancellableEvent()
 class RenderBossBarEvent(val bossBar: BossEvent) : CancellableEvent()
 
 interface SecretPickupEvent : Event { // all are currently packet based but can probably use mixins
@@ -46,7 +53,7 @@ interface SecretPickupEvent : Event { // all are currently packet based but can
     class Bat(val packet: ClientboundSoundPacket) : SecretPickupEvent
 }
 
-abstract class TerminalEvent(val terminal: TerminalHandler) : Event { // first 2 are packet based can use mixins
+abstract class TerminalEvent(val terminal: TerminalHandler) : Event {
     class Open(terminal: TerminalHandler) : TerminalEvent(terminal)
     class Close(terminal: TerminalHandler) : TerminalEvent(terminal)
     class Solve(terminal: TerminalHandler) : TerminalEvent(terminal)
@@ -54,8 +61,7 @@ abstract class TerminalEvent(val terminal: TerminalHandler) : Event { // first 2
 }
 
 interface TickEvent : Event {
-    class Start(val world: ClientLevel) : TickEvent
-    class End(val world: ClientLevel) : TickEvent
+    class End(val level: ClientLevel) : TickEvent
     object Server : TickEvent
 }
 
@@ -87,4 +93,13 @@ class SecretsUpdateEvent(val room: DungeonRoom, val foundSecrets: Int) : Event
 
 object LocationChangeEvent : Event
 object ScreenCloseEvent : Event
-class SetSlotEvent(val slotIndex: Int, val itemStack: ItemStack, val slots: List<Slot>, val menu: AbstractContainerMenu) : Event
\ No newline at end of file
+class SetSlotEvent(val slotIndex: Int, val itemStack: ItemStack, val slots: List<Slot>, val menu: AbstractContainerMenu) : Event
+
+abstract class EntityEvent(val entity: Entity) : Event {
+    class Add(entity: Entity) : EntityEvent(entity)
+    class Remove(entity: Entity) : EntityEvent(entity)
+    class Move(entity: Entity, val newPos: Vec3, val yRot: Float, val xRot: Float, val onGround: Boolean) : EntityEvent(entity)
+    class SetItemSlot(entity: Entity, val slot: EquipmentSlot, val stack: ItemStack) : EntityEvent(entity)
+    class SetData(entity: Entity, val synchedDataValues: List<SynchedEntityData.DataValue<*>>) : EntityEvent(entity)
+    class Event(entity: Entity, val id: Byte) : EntityEvent(entity)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
index ce61343..752b4f1 100644
--- a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
@@ -1,14 +1,15 @@
 package com.odtheking.odin.events
 
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
-import com.odtheking.odin.events.core.onSend
 import com.odtheking.odin.utils.containsOneOf
 import com.odtheking.odin.utils.equalsOneOf
 import com.odtheking.odin.utils.noControlCodes
 import com.odtheking.odin.utils.render.RenderBatchManager
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils.isSecret
+import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientEntityEvents
 import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
 import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents
 import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents
@@ -16,12 +17,9 @@ import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents
 import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents
 import net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents
 import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents
-import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket
 import net.minecraft.network.protocol.game.ClientboundSoundPacket
 import net.minecraft.network.protocol.game.ClientboundTakeItemEntityPacket
-import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket
 import net.minecraft.sounds.SoundEvents
-import net.minecraft.world.InteractionHand
 import net.minecraft.world.entity.item.ItemEntity
 import net.minecraft.world.level.block.SkullBlock
 import net.minecraft.world.phys.Vec3
@@ -32,7 +30,6 @@ object EventDispatcher {
         ClientPlayConnectionEvents.JOIN.register { _, _, _ -> LevelEvent.Load.postAndCatch() }
         ClientPlayConnectionEvents.DISCONNECT.register { _, _ -> LevelEvent.Unload.postAndCatch() }
 
-        ClientTickEvents.START_LEVEL_TICK.register { world -> TickEvent.Start(world).postAndCatch() }
         ClientTickEvents.END_LEVEL_TICK.register { world -> TickEvent.End(world).postAndCatch() }
 
         LevelRenderEvents.AFTER_TRANSLUCENT_TERRAIN.register {
@@ -52,25 +49,33 @@ object EventDispatcher {
             }
         }
 
+        ClientEntityEvents.ENTITY_LOAD.register { entity, _ -> EntityEvent.Add(entity).postAndCatch() }
+        ClientEntityEvents.ENTITY_UNLOAD.register { entity, _ -> EntityEvent.Remove(entity).postAndCatch() }
+
+        ClientReceiveMessageEvents.MODIFY_GAME.register { message, overlay ->
+            if (overlay) MessageEvent.ModifyOverlay(message.string.noControlCodes, message).apply { postAndCatch() }.component
+            else MessageEvent.ModifyChat(message.string.noControlCodes, message).apply { postAndCatch() }.component
+        }
+
         ClientReceiveMessageEvents.ALLOW_GAME.register { message, overlay ->
-            if (overlay) return@register true
-            !ChatMessageEvent(message.string.noControlCodes, message).postAndCatch()
+            if (overlay) !MessageEvent.Overlay(message.string.noControlCodes, message).postAndCatch()
+            else !MessageEvent.Chat(message.string.noControlCodes, message).postAndCatch()
         }
 
         onReceive<ClientboundTakeItemEntityPacket> {
-            if (mc.player == null || !DungeonUtils.inClear) return@onReceive
+            if (!DungeonUtils.inClear) return@onReceive
             val itemEntity = mc.level?.getEntity(itemId) as? ItemEntity ?: return@onReceive
             if (itemEntity.item.hoverName.string.containsOneOf(dungeonItemDrops, true) && itemEntity.distanceTo(mc.player ?: return@onReceive) <= 6)
                 SecretPickupEvent.Item(itemEntity).postAndCatch()
         }
 
-        onReceive<ClientboundRemoveEntitiesPacket> {
-            if (mc.player == null || !DungeonUtils.inClear) return@onReceive
-            entityIds.forEach { id ->
-                val entity = mc.level?.getEntity(id) as? ItemEntity ?: return@forEach
-                if (entity.item.hoverName.string.containsOneOf(dungeonItemDrops, true) && entity.distanceTo(mc.player ?: return@onReceive) <= 6)
-                    SecretPickupEvent.Item(entity).postAndCatch()
-            }
+        on<EntityEvent.Remove> {
+            if (!DungeonUtils.inClear) return@on
+            val entity = entity as? ItemEntity ?: return@on
+            if (
+                entity.item.hoverName.string.containsOneOf(dungeonItemDrops, true) &&
+                entity.distanceTo(mc.player ?: return@on) <= 6
+            ) SecretPickupEvent.Item(entity).postAndCatch()
         }
 
         onReceive<ClientboundSoundPacket> {
@@ -79,15 +84,15 @@ object EventDispatcher {
                 SecretPickupEvent.Bat(this).postAndCatch()
         }
 
-        onSend<ServerboundUseItemOnPacket> {
-            if (!DungeonUtils.inDungeons || hand == InteractionHand.OFF_HAND) return@onSend
-            val blockState = mc.level?.getBlockState(hitResult.blockPos) ?: return@onSend
+        on<BlockInteractEvent> {
+            if (!DungeonUtils.inDungeons) return@on
+            val blockState = mc.level?.getBlockState(pos) ?: return@on
             if (blockState.block is SkullBlock) {
-                val distance = mc.player?.eyePosition?.distanceToSqr(Vec3(hitResult.blockPos)) ?: return@onSend
-                if (distance > 20.25) return@onSend
+                val distance = mc.player?.eyePosition?.distanceToSqr(Vec3(pos)) ?: return@on
+                if (distance > 20.25) return@on
             }
 
-            if (isSecret(blockState, hitResult.blockPos)) SecretPickupEvent.Interact(hitResult.blockPos, blockState).postAndCatch()
+            if (isSecret(blockState, pos)) SecretPickupEvent.Interact(pos, blockState).postAndCatch()
         }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/Module.kt b/src/main/kotlin/com/odtheking/odin/features/Module.kt
index 3fc7a43..d305362 100644
--- a/src/main/kotlin/com/odtheking/odin/features/Module.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/Module.kt
@@ -1,5 +1,6 @@
 package com.odtheking.odin.features
 
+import com.mojang.blaze3d.platform.InputConstants
 import com.odtheking.odin.OdinMod
 import com.odtheking.odin.clickgui.settings.AlwaysActive
 import com.odtheking.odin.clickgui.settings.DevModule
@@ -9,7 +10,6 @@ import com.odtheking.odin.events.core.EventBus
 import com.odtheking.odin.features.impl.render.ClickGUIModule
 import com.odtheking.odin.utils.modMessage
 import net.minecraft.client.gui.GuiGraphicsExtractor
-import org.lwjgl.glfw.GLFW
 import kotlin.reflect.full.hasAnnotation
 
 /**
@@ -18,7 +18,7 @@ import kotlin.reflect.full.hasAnnotation
  */
 abstract class Module(
     val name: String,
-    val key: Int? = GLFW.GLFW_KEY_UNKNOWN,
+    val key: Int? = InputConstants.UNKNOWN.value,
     category: Category? = null,
     @Transient var description: String,
     toggled: Boolean = false,
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt
index 2ce447a..b099a73 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt
@@ -1,5 +1,6 @@
 package com.odtheking.odin.features.impl.boss
 
+import com.mojang.blaze3d.platform.InputConstants
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.KeybindSetting
@@ -19,14 +20,13 @@ import com.odtheking.odin.utils.skyblock.dungeon.M7Phases
 import net.minecraft.core.BlockPos
 import net.minecraft.world.entity.decoration.ItemFrame
 import net.minecraft.world.item.Items
-import org.lwjgl.glfw.GLFW
 
 object ArrowAlign : Module(
     name = "Arrow Align",
     description = "Shows the solution for the Arrow Align device."
 ) {
     private val blockWrong by BooleanSetting("Block Wrong Clicks", true, desc = "Blocks wrong clicks, shift will override this.")
-    private val preventKey by KeybindSetting("Prevent Blocking", GLFW.GLFW_KEY_LEFT_SHIFT, desc = "While holding this key, wrong clicks won't be blocked, even if you aren't sneaking.").withDependency { blockWrong }
+    private val preventKey by KeybindSetting("Prevent Blocking", InputConstants.KEY_LSHIFT, desc = "While holding this key, wrong clicks won't be blocked, even if you aren't sneaking.").withDependency { blockWrong }
     private val invertKey by BooleanSetting("Invert Key", false, desc = "Inverts the behavior of the prevent key. Wrong clicks will be blocked while holding the prevent key, and allowed otherwise.").withDependency { blockWrong }
 
     private val recentClickTimestamps = mutableMapOf<Int, Long>()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
index e5418ef..3c91204 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
@@ -4,12 +4,8 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.ActionSetting
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.events.BlockUpdateEvent
-import com.odtheking.odin.events.ChatMessageEvent
-import com.odtheking.odin.events.LevelEvent
-import com.odtheking.odin.events.RenderEvent
+import com.odtheking.odin.events.*
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
@@ -19,7 +15,6 @@ import com.odtheking.odin.utils.render.drawFilledBox
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.skyblock.dungeon.M7Phases
 import net.minecraft.core.BlockPos
-import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
 import net.minecraft.world.level.block.Blocks
 import net.minecraft.world.phys.AABB
 import net.minecraft.world.phys.Vec3
@@ -102,14 +97,14 @@ object ArrowsDevice : Module(
             optimalAimPositions = emptyList()
         }
 
-        on<ChatMessageEvent> {
+        on<MessageEvent.Chat> {
             if (DungeonUtils.getF7Phase() != M7Phases.P3 || !isPlayerInRoom || isDeviceComplete) return@on
-            if (deviceCompleteRegex.find(value)?.groupValues?.get(1) == mc.player?.name?.string) onComplete("Chat")
+            if (deviceCompleteRegex.find(message)?.groupValues?.get(1) == mc.player?.name?.string) onComplete("Chat")
         }
 
-        onReceive<ClientboundSetEntityDataPacket> {
-            if (DungeonUtils.getF7Phase() != M7Phases.P3 || !isPlayerInRoom || isDeviceComplete) return@onReceive
-            if (mc.level?.getEntity(id)?.name?.string == "Active") onComplete("Entity")
+        on<EntityEvent.SetData> {
+            if (DungeonUtils.getF7Phase() != M7Phases.P3 || !isPlayerInRoom || isDeviceComplete) return@on
+            if (entity.name.string == "Active") onComplete("Entity")
         }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt
index 113c0f6..521719c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/DragonCheck.kt
@@ -1,15 +1,12 @@
 package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.events.EntityEvent
 import com.odtheking.odin.utils.modMessage
-import net.minecraft.network.protocol.game.ClientboundAddEntityPacket
-import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
-import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket
 import net.minecraft.world.entity.EntityType
 import net.minecraft.world.entity.boss.enderdragon.EnderDragon
 import net.minecraft.world.entity.decoration.ArmorStand
 import net.minecraft.world.item.Items
-import net.minecraft.world.phys.Vec3
 import java.util.*
 
 object DragonCheck {
@@ -17,9 +14,9 @@ object DragonCheck {
     val dragonHealthMap = mutableMapOf<UUID, Pair<EnderDragon, Float>>()
     var lastDragonDeath: WitherDragonsEnum? = null
 
-    fun dragonUpdate(packet: ClientboundSetEntityDataPacket) {
-        val entity = mc.level?.getEntity(packet.id) as? EnderDragon ?: return
-        val dragonHealth = (packet.packedItems.find { it.id == 9 }?.value as? Float) ?: return
+    fun dragonUpdate(event: EntityEvent.SetData) {
+        val entity = event.entity as? EnderDragon ?: return
+        val dragonHealth = (event.synchedDataValues.find { it.id == 9 }?.value as? Float) ?: return
         dragonHealthMap[entity.uuid] = Pair(entity, dragonHealth)
 
         WitherDragonsEnum.entries.firstOrNull { it.entityUUID == entity.uuid }?.let {
@@ -27,17 +24,14 @@ object DragonCheck {
         }
     }
 
-    fun dragonSpawn(packet: ClientboundAddEntityPacket) {
-        if (packet.type == EntityType.ENDER_DRAGON)
-            WitherDragonsEnum.entries.find {
-                it.aabbDimensions.contains(Vec3(packet.x, packet.y, packet.z))
-            }?.setAlive(packet.uuid)
+    fun dragonSpawn(event: EntityEvent.Add) {
+        if (event.entity.type == EntityType.ENDER_DRAGON)
+            WitherDragonsEnum.entries.find { it.aabbDimensions.contains(event.entity.position()) }?.setAlive(event.entity.uuid)
     }
 
-    fun dragonSprayed(packet: ClientboundSetEquipmentPacket) {
-        if (packet.slots.none { it.second.item == Items.PACKED_ICE }) return
-
-        val sprayedEntity = mc.level?.getEntity(packet.entity) as? ArmorStand ?: return
+    fun dragonSprayed(event: EntityEvent.SetItemSlot) {
+        if (event.stack.item == Items.PACKED_ICE ) return
+        val sprayedEntity = event.entity as? ArmorStand ?: return
 
         WitherDragonsEnum.entries.forEach { dragon ->
             val entity = mc.level?.getEntity(dragon.entityUUID ?: return@forEach) as? EnderDragon ?: return@forEach
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
index df3fec1..f5e6133 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
@@ -2,8 +2,8 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
-import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.*
@@ -55,64 +55,63 @@ object ExtraStats : Module(
     )
 
     init {
-        on<ChatMessageEvent> {
+        on<MessageEvent.Chat> {
             if (!DungeonUtils.inDungeons) return@on
 
-            if (cancelRegexes.any { it.matches(value) }) cancel()
+            if (extraStatsRegex.matches(message)) return@on sendCommand("showextrastats")
+            if (cancelRegexes.any { it.matches(message) }) cancel()
 
-            if (extraStatsRegex.matches(value)) return@on sendCommand("showextrastats")
-
-            terminalCompleteRegex.find(value)?.let {
+            terminalCompleteRegex.find(message)?.let {
                 extraStats.timePB = it.groupValues[3].isNotEmpty()
                 extraStats.bossKilled = it.groupValues[1]
                 return@on
             }
 
-            teamScoreRegex.find(value)?.let {
+            teamScoreRegex.find(message)?.let {
                 extraStats.score = it.groupValues[1].toIntOrNull() ?: 0
                 extraStats.scorePB = it.groupValues[3].isNotEmpty()
                 extraStats.scoreLetter = it.groupValues[2]
                 return@on
             }
 
-            expRegex.find(value)?.let {
+            expRegex.find(message)?.let {
                 extraStats.xp.add("§3${it.groupValues[1].replace("Experience", "EXP").replace("Catacombs", "Cata")}")
                 return@on
             }
 
-            bitsRegex.find(value)?.let {
+            bitsRegex.find(message)?.let {
                 extraStats.bits = it.groupValues[1]
                 return@on
             }
 
-            damageRegex.find(value)?.let {
+            damageRegex.find(message)?.let {
                 extraStats.damagePB = it.groupValues[3].isNotEmpty()
                 extraStats.damage = formatNumber(it.groupValues[2]) + if (extraStats.damagePB) "(NEW PB!)" else ""
                 extraStats.combatStats.add("§e${it.groupValues[1]}")
                 return@on
             }
 
-            healRegex.find(value)?.let {
+            healRegex.find(message)?.let {
                 extraStats.healPB = it.groupValues[3].isNotEmpty()
                 extraStats.heal = formatNumber(it.groupValues[2]) + if (extraStats.healPB) "(NEW PB!)" else ""
                 extraStats.combatStats.add("§a${it.groupValues[1]}")
                 return@on
             }
 
-            enemyKillRegex.find(value)?.let {
+            enemyKillRegex.find(message)?.let {
                 extraStats.enemyKillPB = it.groupValues[3].isNotEmpty()
                 extraStats.enemyKill = (it.groupValues[2].toIntOrNull() ?: 0).toString() + if (extraStats.enemyKillPB) "(NEW PB!)" else ""
                 extraStats.combatStats.add(1, "§b${it.groupValues[1]}")
                 return@on
             }
 
-            deathsRegex.find(value)?.let {
+            deathsRegex.find(message)?.let {
                 extraStats.deaths = it.groupValues[2].toIntOrNull() ?: 0
                 if (teamStats.equalsOneOf(1, 3)) extraStats.skillStats.add("§c${it.groupValues[1]}")
                 return@on
             }
 
-            secretsRegex.find(value)?.let {
+            secretsRegex.find(message)?.let {
                 extraStats.secretsFound = it.groupValues[2].toIntOrNull() ?: 0
                 if (teamStats.equalsOneOf(1, 3)) extraStats.skillStats.add(0, "§b${it.groupValues[1]}")
                 if (teamStats == 3) extraStats.skillStats.add("")
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
index e2c377a..54e0702 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
@@ -2,8 +2,8 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -67,12 +67,12 @@ object InactiveWaypoints : Module(
             }?.toSet().orEmpty()
         }
 
-        on<ChatMessageEvent> {
+        on<MessageEvent.Chat> {
             if (!DungeonUtils.inBoss) return@on
 
             when {
-                completedRegex.matches(value) -> {
-                    val it = completedRegex.find(value) ?: return@on
+                completedRegex.matches(message) -> {
+                    val it = completedRegex.find(message) ?: return@on
                     val completed = (it.groupValues[4].toIntOrNull() ?: 0).apply { if (this == 1) firstInSection = true }
 
                     if (completed == (it.groupValues[5].toIntOrNull() ?: 0)) {
@@ -88,18 +88,18 @@ object InactiveWaypoints : Module(
                     lastCompleted = completed
                 }
 
-                gateRegex.matches(value) -> {
+                gateRegex.matches(message) -> {
                     gate = true
                     if (isComplete) newSection()
                 }
 
-                goldorRegex.matches(value) -> {
+                goldorRegex.matches(message) -> {
                     shouldRender = true
                     resetState()
                     section = 1
                 }
 
-                coreOpeningRegex.matches(value) -> {
+                coreOpeningRegex.matches(message) -> {
                     shouldRender = false
                     resetState()
                 }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
index a830c79..6e6cf93 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
@@ -2,13 +2,8 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.ChatMessageEvent
-import com.odtheking.odin.events.LevelEvent
-import com.odtheking.odin.events.RenderEvent
-import com.odtheking.odin.events.TickEvent
+import com.odtheking.odin.events.*
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
-import com.odtheking.odin.events.core.onSend
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.*
 import com.odtheking.odin.utils.render.drawCustomBeacon
@@ -16,9 +11,6 @@ import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.skyblock.dungeon.M7Phases
 import net.minecraft.core.BlockPos
-import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket
-import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket
-import net.minecraft.world.InteractionHand
 import net.minecraft.world.item.Items
 import net.minecraft.world.level.block.Blocks
 
@@ -47,18 +39,18 @@ object KingRelics : Module(
     private val relicPBs = PersonalBest(this, "Relics")
 
     init {
-        on<ChatMessageEvent> {
-            if (DungeonUtils.getF7Phase() != M7Phases.P5 || !relicPickupRegex.matches(value)) return@on
+        on<MessageEvent.Chat> {
+            if (DungeonUtils.getF7Phase() != M7Phases.P5 || !relicPickupRegex.matches(message)) return@on
             relicPlaceTick = serverTickCounter
             relicTicksToSpawn = relicSpawnTicks
             hasAnnouncedSpawn = false
         }
 
-        onSend<ServerboundUseItemOnPacket> {
-            if (DungeonUtils.getF7Phase() != M7Phases.P5 || relicPlaceTick == 0L || hand == InteractionHand.OFF_HAND) return@onSend
+        on<BlockInteractEvent> {
+            if (DungeonUtils.getF7Phase() != M7Phases.P5 || relicPlaceTick == 0L) return@on
 
-            val block = mc.level?.getBlockState(hitResult.blockPos)?.block
-            if (!block.equalsOneOf(Blocks.CAULDRON, Blocks.ANVIL)) return@onSend
+            val block = mc.level?.getBlockState(pos)?.block
+            if (!block.equalsOneOf(Blocks.CAULDRON, Blocks.ANVIL)) return@on
 
             Relic.entries.find { it.id == currentRelic?.id }?.let {
                 relicPBs.time(it.name, (serverTickCounter - relicPlaceTick) / 20f, message = "§${it.colorCode}${it.name} relic §7placed in §6", sendMessage = relicAnnounceTime)
@@ -66,12 +58,11 @@ object KingRelics : Module(
             }
         }
 
-        onReceive<ClientboundSetEquipmentPacket> {
-            if (DungeonUtils.getF7Phase() != M7Phases.P5 || currentRelic == null) return@onReceive
+        on<EntityEvent.SetItemSlot> {
+            if (DungeonUtils.getF7Phase() != M7Phases.P5 || currentRelic == null) return@on
 
-            val equipmentSlot = slots.find { it.second.item== Items.PLAYER_HEAD } ?: return@onReceive
-
-            Relic.entries.find { it.id == equipmentSlot.second.itemId }?.let { relic ->
+            if (stack.item != Items.PLAYER_HEAD) return@on
+            Relic.entries.find { it.id == stack.itemId }?.let { relic ->
                 if (relicPlaceTick > 0 && !hasAnnouncedSpawn) {
                     modMessage("§${relic.colorCode}${relic.name} relic §7spawned in §6${(serverTickCounter - relicPlaceTick) / 20f}s")
                     hasAnnouncedSpawn = true
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt
index e80cdea..768f1d1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt
@@ -3,7 +3,6 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.events.*
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Colors
@@ -13,7 +12,6 @@ import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.renderBoundingBox
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.core.BlockPos
-import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
 import net.minecraft.world.effect.MobEffects
 import net.minecraft.world.entity.player.Player
 import net.minecraft.world.level.block.Block
@@ -41,9 +39,9 @@ object LividSolver : Module(
     private var invulnTime = 0
 
     init {
-        on<ChatMessageEvent> {
+        on<MessageEvent.Chat> {
             if (!DungeonUtils.inDungeons || !DungeonUtils.isFloor(5)) return@on
-            if (value.matches(lividStartRegex)) invulnTime = 390
+            if (message.matches(lividStartRegex)) invulnTime = 390
         }
 
         on<BlockUpdateEvent> {
@@ -52,9 +50,9 @@ object LividSolver : Module(
             modMessage("Found Livid: §${currentLivid.colorCode}${currentLivid.entityName}")
         }
 
-        onReceive<ClientboundSetEntityDataPacket> {
-            if (!DungeonUtils.inBoss || !DungeonUtils.isFloor(5)) return@onReceive
-            currentLivid.entity = (mc.level?.getEntity(id) as? Player)?.takeIf { it.name.string == "${currentLivid.entityName} Livid" } ?: return@onReceive
+        on<EntityEvent.SetData> {
+            if (!DungeonUtils.inBoss || !DungeonUtils.isFloor(5)) return@on
+            currentLivid.entity = (entity as? Player)?.takeIf { it.name.string == "${currentLivid.entityName} Livid" } ?: return@on
         }
 
         on<RenderEvent.Extract> {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
index 46b2a50..7938bb7 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
@@ -4,8 +4,8 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
 import com.odtheking.odin.clickgui.settings.impl.StringSetting
-import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.core.on
@@ -92,8 +92,8 @@ object MelodyMessage : Module(
             if (melodySendCoords) sendCommand("od sendcoords")
         }
 
-        on<ChatMessageEvent> {
-            if (broadcast || melodyProgress) onChatMessage(value)
+        on<MessageEvent.Chat> {
+            if (broadcast || melodyProgress) onChatMessage(message)
         }
 
         on<SetSlotEvent> {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
index 47bd726..b39d896 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
@@ -52,8 +52,8 @@ object SimonSays : Module(
             startClickCounter = 0
         }
 
-        on<ChatMessageEvent> {
-            if (value == "[BOSS] Goldor: Who dares trespass into my domain?") startClickCounter = 0
+        on<MessageEvent.Chat> {
+            if (message == "[BOSS] Goldor: Who dares trespass into my domain?") startClickCounter = 0
         }
 
         on<BlockUpdateEvent> {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index f774e96..264451f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -99,11 +99,11 @@ object TerminalSolver : Module(
         }
 
         on<TerminalEvent.Open> {
-            if (renderType == 0 || renderType == 1) mc.execute { mc.resizeGui() }
+            if (renderType == 0 || renderType == 1) mc.resizeGui()
         }
 
         on<TerminalEvent.Close> {
-            if (renderType == 0 || renderType == 1) mc.execute { mc.resizeGui() }
+            if (renderType == 0 || renderType == 1) mc.resizeGui()
         }
 
         on<TickEvent.Server> {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
index 115aec9..70cbf32 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatMessageEvent
+import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.core.EventPriority
 import com.odtheking.odin.events.core.on
@@ -46,11 +46,11 @@ object TerminalSounds : Module(
                 it.cancel()
         }
 
-        on<ChatMessageEvent> {
+        on<MessageEvent.Chat> {
             if (!DungeonUtils.inDungeons || !shouldReplaceSounds) return@on
             when {
-                value.matches(gateRegex) -> playSoundAtPlayer(SoundEvents.NOTE_BLOCK_PLING.value())
-                value.matches(coreRegex) -> playSoundAtPlayer(SoundEvents.NOTE_BLOCK_PLING.value())
+                message.matches(gateRegex) -> playSoundAtPlayer(SoundEvents.NOTE_BLOCK_PLING.value())
+                message.matches(coreRegex) -> playSoundAtPlayer(SoundEvents.NOTE_BLOCK_PLING.value())
             }
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
index 2384c7b..e6d7a13 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
@@ -2,8 +2,8 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.ActionSetting
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
@@ -11,6 +11,7 @@ import com.odtheking.odin.features.Module
 import com.odtheking.odin.features.impl.boss.termsim.TermSimGUI
 import com.odtheking.odin.utils.PersonalBest
 import com.odtheking.odin.utils.modMessage
+import net.minecraft.network.chat.Component
 
 object TerminalTimes : Module(
     name = "Terminal Times",
@@ -44,12 +45,10 @@ object TerminalTimes : Module(
             pbs.time(terminal.type.name, (System.currentTimeMillis() - terminal.timeOpened) / 1000f, "s§7!", "§a${terminal.type.termName}${if (mc.screen is TermSimGUI) " §7(termsim)" else ""} §7solved in §6", sendMessage = terminalTimes)
         }
 
-        on<ChatMessageEvent> {
+        on<MessageEvent.ModifyChat> {
             if (!terminalSplits) return@on
-            terminalCompleteRegex.find(value)?.destructured?.let { (name, activated, type, current, total) ->
-                cancel()
-
-                modMessage("§6$name §a$activated a $type! (§c${current}§a/${total}) §8(§7${sectionTimer.seconds}s §8| §7${phaseTimer.seconds}s§8)", "")
+            terminalCompleteRegex.find(message)?.destructured?.let { (name, activated, type, current, total) ->
+                component = Component.literal("§6$name §a$activated a $type! (§c${current}§a/${total}) §8(§7${sectionTimer.seconds}s §8| §7${phaseTimer.seconds}s§8)")
 
                 if ((current == total && gateBlown) || (current.toIntOrNull() ?: return@on) < completed.first) resetSection()
                 else completed = Pair(current.toIntOrNull() ?: return@on, total.toIntOrNull() ?: return@on)
@@ -57,11 +56,11 @@ object TerminalTimes : Module(
             }
 
             when {
-                gateDestroyedRegex.matches(value) -> if (completed.first == completed.second) resetSection() else gateBlown = true
+                gateDestroyedRegex.matches(message) -> if (completed.first == completed.second) resetSection() else gateBlown = true
 
-                goldorRegex.matches(value) -> resetSection(true)
+                goldorRegex.matches(message) -> resetSection(true)
 
-                coreOpeningRegex.matches(value) -> {
+                coreOpeningRegex.matches(message) -> {
                     resetSection()
                     modMessage("§bTimes: §a${times.joinToString(" §8| ") { "§a${it}s" }}§8, §bTotal: §a${phaseTimer.seconds}s")
                 }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 1d84d89..84fc902 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -3,8 +3,8 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.HudElement
-import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.MessageEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -101,26 +101,26 @@ object TickTimers : Module(
     }
 
     init {
-        on<ChatMessageEvent> {
+        on<MessageEvent.Chat> {
             when {
-                value.matches(MORT_REGEX) -> secretsCounter = 0
-                value.matches(necronRegex) -> necronTime = 60
-                value.matches(goldorRegex) -> goldorTickTime = 60
-                value.matches(coreOpeningRegex) -> {
+                message.matches(MORT_REGEX) -> secretsCounter = 0
+                message.matches(necronRegex) -> necronTime = 60
+                message.matches(goldorRegex) -> goldorTickTime = 60
+                message.matches(coreOpeningRegex) -> {
                     goldorStartTime = -1
                     goldorTickTime = -1
                 }
-                value.matches(stormEndRegex) -> {
+                message.matches(stormEndRegex) -> {
                     goldorStartTime = 104
                     padTickTime = -1
                     stormTick = -1
                 }
-                value.matches(stormStartRegex) -> {
+                message.matches(stormStartRegex) -> {
                     padTickTime = 20
                     lightningTickTime = 560
                     stormTick = 0
                 }
-                !pyTriggered && value.matches(stormPyRegex) -> {
+                !pyTriggered && message.matches(stormPyRegex) -> {
                     pyTriggered = true
                     pyTickTime = 95
                 }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
index f9d2e9f..74bc4a9 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
+++ b/s
…… diff 过长已截断 ……
Fixed core logging causing CMEodtheking · 2026-09-05 · 1133291
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
index e56fbe6..f154071 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
@@ -1,8 +1,8 @@
 package com.odtheking.odin.features.impl.dungeon.map
 
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.events.FloorEnterEvent
 import com.odtheking.odin.events.LevelEvent
-import com.odtheking.odin.events.LocationChangeEvent
 import com.odtheking.odin.events.RoomEnterEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
@@ -70,19 +70,20 @@ object WorldScan {
         }
 
         ClientChunkEvents.CHUNK_LOAD.register { _, chunk ->
-            if (!DungeonUtils.inDungeons) chunksToScan.add(IVec2(chunk.pos.x, chunk.pos.z))
+            if (DungeonUtils.floor == null) chunksToScan.add(IVec2(chunk.pos.x, chunk.pos.z))
             else scanChunk(chunk)
         }
+
         ClientChunkEvents.CHUNK_UNLOAD.register { _, chunk ->
             if (!DungeonUtils.inDungeons) chunksToScan.remove(IVec2(chunk.pos.x, chunk.pos.z))
         }
 
-        on<LocationChangeEvent> {
-            if (DungeonUtils.inDungeons) {
-                val level = mc.level ?: return@on
+        on<FloorEnterEvent> {
+            mc.execute {
+                val level = mc.level ?: return@execute
                 for (position in chunksToScan) scanChunk(level.getChunk(position.x, position.z))
+                chunksToScan.clear()
             }
-            chunksToScan.clear()
         }
     }
 
Allow chat commands to work in coop chat (#152)CloodDev · 2026-09-05 · f1ab18f
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
index 5ac6a02..8d0ce8e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
@@ -28,6 +28,7 @@ object ChatCommands : Module(
     private val partyChatCommands by BooleanSetting("Party Commands", true, "Enables party chat commands.")
     private val guildChatCommands by BooleanSetting("Guild Commands", false, "Enables guild chat commands.")
     private val privateChatCommands by BooleanSetting("Private Commands", true, "Enables private chat commands.")
+    private val coopChatCommands by BooleanSetting("Co-op Commands", true, "Enables co-op chat commands.")
 
     private val showSettings by DropdownSetting("Show Settings", false)
     private val partyWarp by BooleanSetting("Warp", true, desc = "Executes the /party warp command.").withDependency { showSettings }
@@ -57,7 +58,7 @@ object ChatCommands : Module(
     private val holding by BooleanSetting("Holding", true, desc = "Sends the item you are holding.").withDependency { showSettings }
 
     // https://regex101.com/r/joY7dm/1
-    private val messageRegex = Regex("^(?:Party > (\\[[^]]*?])? ?(\\w{1,16})(?: [ቾ⚒])?: ?(.+)$|Guild > (\\[[^]]*?])? ?(\\w{1,16})(?: \\[([^]]*?)])?: ?(.+)$|From (\\[[^]]*?])? ?(\\w{1,16}): ?(.+)$)")
+    private val messageRegex = Regex("^(?:Party > (\\[[^]]*?])? ?(\\w{1,16})(?: [ቾ⚒])?: ?(.+)$|Guild > (\\[[^]]*?])? ?(\\w{1,16})(?: \\[([^]]*?)])?: ?(.+)$|From (\\[[^]]*?])? ?(\\w{1,16}): ?(.+)$|Co-op > (\\[[^]]*?])? ?(\\w{1,16})(?: [ቾ⚒])?: ?(.+)$)")
     private val endRunRegex = Regex(" {29}> EXTRA STATS <|^\\[NPC] Elle: Good job everyone. A hard fought battle come to an end. Let's get out of here before we run into any more trouble!$")
     private val dtReason = mutableListOf<Pair<String, String>>()
 
@@ -74,15 +75,18 @@ object ChatCommands : Module(
             }
 
             val result = messageRegex.find(value) ?: return@on
-            val channel = when(result.value.split(" ")[0]) {
+
+            val channelStr = result.value.split(" ")[0]
+            val channel = when(channelStr) {
                 "From" -> if (!privateChatCommands) return@on else ChatChannel.PRIVATE
                 "Party" -> if (!partyChatCommands)  return@on else ChatChannel.PARTY
                 "Guild" -> if (!guildChatCommands)  return@on else ChatChannel.GUILD
+                "Co-op" -> if (!coopChatCommands) return@on else ChatChannel.COOP
                 else -> return@on
             }
-
-            val ign = result.groups[2]?.value ?: result.groups[5]?.value ?: result.groups[9]?.value ?: return@on
-            val msg = result.groups[3]?.value ?: result.groups[7]?.value ?: result.groups[10]?.value ?: return@on
+            
+            val ign = result.groups[2]?.value ?: result.groups[5]?.value ?: result.groups[9]?.value ?: result.groups[12]?.value ?: return@on
+            val msg = result.groups[3]?.value ?: result.groups[7]?.value ?: result.groups[10]?.value ?: result.groups[13]?.value ?: return@on
 
             if (!msg.startsWith("!")) return@on
 
@@ -92,7 +96,7 @@ object ChatCommands : Module(
         }
 
         on<MessageSentEvent> {
-            if (!chatEmotes || (message.startsWith("/") && !listOf("/pc", "/ac", "/gc", "/msg", "/w", "/r").any { message.startsWith(it) })) return@on
+            if (!chatEmotes || (message.startsWith("/") && !listOf("/pc", "/ac", "/gc", "/cc", "/msg", "/w", "/r").any { message.startsWith(it) })) return@on
 
             var replaced = false
             val words = message.split(" ").toMutableList()
@@ -120,6 +124,7 @@ object ChatCommands : Module(
             )
             ChatChannel.GUILD -> mapOf("coords" to coords, "odin" to odin, "boop" to boop, "cf" to coinFlip, "8ball" to eightBall, "dice" to dice, "racism" to racism, "ping" to ping, "tps" to tps, "time" to time)
             ChatChannel.PRIVATE -> mapOf("coords" to coords, "odin" to odin, "boop" to boop, "cf" to coinFlip, "8ball" to eightBall, "dice" to dice, "racism" to racism, "ping" to ping, "tps" to tps, "invite" to invite, "time" to time)
+            ChatChannel.COOP -> mapOf("coords" to coords, "odin" to odin, "boop" to boop, "cf" to coinFlip, "8ball" to eightBall, "dice" to dice, "racism" to racism, "ping" to ping, "tps" to tps, "time" to time)
         }
 
         val words = message.drop(1).split(" ").map { it.lowercase() }
@@ -216,6 +221,7 @@ object ChatCommands : Module(
             ChatChannel.GUILD -> sendCommand("gc $message")
             ChatChannel.PARTY -> sendCommand("pc $message")
             ChatChannel.PRIVATE -> sendCommand("msg $name $message")
+            ChatChannel.COOP -> sendCommand("cc $message")
         }
     }
 
@@ -263,6 +269,6 @@ object ChatCommands : Module(
     )
 
     private enum class ChatChannel {
-        PARTY, GUILD, PRIVATE
+        PARTY, GUILD, PRIVATE, COOP
     }
 }
Fixed Phoenix pet's cooldown was incorrect when wearing Spirit/Bonzo mask (#153)yuko1101 · 2026-09-05 · f23f088
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
index 1e28d47..b5f457e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
@@ -87,8 +87,10 @@ object InvincibilityTimer : Module(
         on<ChatMessageEvent> {
             if (onlyInDungeons && !DungeonUtils.inDungeons) return@on
             InvincibilityType.entries.firstOrNull { type -> value.matches(type.regex) }?.let { type ->
-                val seconds = mc.player?.getItemBySlot(EquipmentSlot.HEAD)?.loreString?.reversed()
-                    ?.firstNotNullOfOrNull { cooldownRegex.matchEntire(it)?.groupValues?.get(1)?.toIntOrNull() }
+                val seconds = if (type == InvincibilityType.BONZO) {
+                    mc.player?.getItemBySlot(EquipmentSlot.HEAD)?.loreString?.reversed()
+                        ?.firstNotNullOfOrNull { cooldownRegex.matchEntire(it)?.groupValues?.get(1)?.toIntOrNull() }
+                } else null
                 type.proc(seconds)
                 val usedMasks = InvincibilityType.entries.count { it.currentCooldown > 0 }
                 if (invincibilityAnnounce) sendCommand("pc ${type.name.lowercase().capitalizeFirst()} Procced! ($usedMasks/${InvincibilityType.entries.size})")
Made DungeonMap websocket share entire roomodtheking · 2026-09-05 · 318578f
查看变更
src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt                             | 16 +++-------------
 src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt             |  9 ++-------
 src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt                            |  2 --
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt              |  5 ++++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt       | 52 ++++++++++++++++++----------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt      |  1 +
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt          |  1 +
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt        | 30 ++++++++++++++++++++++--------
 9 files changed, 52 insertions(+), 66 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt b/src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt
index ff68757..01a7bfb 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/Panel.kt
@@ -48,7 +48,7 @@ class Panel(private val category: Category) {
             panelSetting.x,
             panelSetting.y,
             WIDTH,
-            (previousHeight + if (ClickGUIModule.roundedPanelBottom) 10f else 0f).coerceAtLeast(HEIGHT),
+            previousHeight,
             10f,
             3f,
             5f
@@ -75,22 +75,12 @@ class Panel(private val category: Category) {
         if (panelSetting.extended) {
             for (button in moduleButtons) {
                 if (!button.module.name.contains(SearchBar.currentSearch, true)) continue
-                startY += button.draw(panelSetting.x, startY + panelSetting.y, button == lastModuleButton)
+                startY += button.draw(panelSetting.x, startY + panelSetting.y)
             }
         }
         previousHeight = startY
 
-        if (ClickGUIModule.roundedPanelBottom) {
-            NVGRenderer.drawHalfRoundedRect(
-                panelSetting.x,
-                panelSetting.y + startY,
-                WIDTH,
-                10f,
-                if (lastModuleButton?.module?.enabled == true) ClickGUIModule.clickGUIColor.rgba else gray26.rgba,
-                5f,
-                false
-            )
-        }
+        NVGRenderer.drawHalfRoundedRect(panelSetting.x, panelSetting.y + startY, WIDTH, 10f, if (lastModuleButton?.module?.enabled == true) ClickGUIModule.clickGUIColor.rgba else gray26.rgba, 5f, false)
         if (scrollOffset != 0f) NVGRenderer.popScissor()
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt b/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
index 2738a02..b5c8349 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/settings/ModuleButton.kt
@@ -40,19 +40,14 @@ class ModuleButton(val module: Module, val panel: Panel) {
     private val hover = HoverHandler(250)
     var extended = false
 
-    fun draw(x: Float, y: Float, lastModule: Boolean = false): Float {
+    fun draw(x: Float, y: Float): Float {
         hoverHandler.handle(x, y, Panel.WIDTH, Panel.HEIGHT - 1, true)
         hover.handle(x, y, Panel.WIDTH, Panel.HEIGHT - 1, true)
 
         if (hoverHandler.percent() >= 100 && y >= panel.panelSetting.y + Panel.HEIGHT)
             ClickGUI.setDescription(module.description, x + Panel.WIDTH + 10f, y, hoverHandler)
 
-        if (!ClickGUIModule.roundedPanelBottom && lastModule) {
-            NVGRenderer.rect(x, y, Panel.WIDTH, Panel.HEIGHT - 10f, color.rgba)
-            NVGRenderer.drawHalfRoundedRect(x, y + Panel.HEIGHT - 10f, Panel.WIDTH, 10f, color.rgba, 5f, false)
-        } else {
-            NVGRenderer.rect(x, y, Panel.WIDTH, Panel.HEIGHT, color.rgba)
-        }
+       NVGRenderer.rect(x, y, Panel.WIDTH, Panel.HEIGHT, color.rgba)
         NVGRenderer.text(module.name, x + Panel.WIDTH / 2 - nameWidth / 2, y + Panel.HEIGHT / 2 - 9f, 18f, Colors.WHITE.rgba, NVGRenderer.defaultFont)
 
         if (representableSettings.isEmpty()) return Panel.HEIGHT
diff --git a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
index 158572f..2d5dfb6 100644
--- a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
@@ -13,7 +13,5 @@ abstract class GuiEvent(val screen: Screen) : CancellableEvent() {
 
     class RenderSlot(screen: Screen, val guiGraphics: GuiGraphicsExtractor, val slot: Slot) : GuiEvent(screen)
 
-    class CustomTermGuiClick(screen: Screen, val slot: Int, val button: Int) : GuiEvent(screen)
-
     class DrawTooltip(screen: Screen, val guiGraphics: GuiGraphicsExtractor, val mouseX: Int, val mouseY: Int) : GuiEvent(screen)
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index 673fede..cc388a1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -176,7 +176,10 @@ object MapInfo : Module(
         if ((!DungeonUtils.inClear) && !it) return@HUD 0 to 0
 
         val secrets = if (it) 0 to 2 else DungeonUtils.currentRoom?.foundSecrets?.let { found ->
-            DungeonUtils.currentRoom?.data?.maxSecrets?.let { max -> found to max }
+            DungeonUtils.currentRoom?.data?.maxSecrets?.let { max ->
+                if (max == 0) return@HUD 0 to 0
+                found to max
+            }
         } ?: return@HUD 0 to 0
         val color = when {
             secrets.first * 2 < secrets.second -> "§c"
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index af67ecc..ab51ae9 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -2,22 +2,15 @@ package com.odtheking.odin.features.impl.dungeon.map
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
-import com.odtheking.odin.events.FloorEnterEvent
-import com.odtheking.odin.events.RoomEnterEvent
-import com.odtheking.odin.events.SecretsUpdateEvent
-import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
-import com.odtheking.odin.features.impl.render.ClickGUIModule
+import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.IVec2
-import com.odtheking.odin.utils.devMessage
 import com.odtheking.odin.utils.network.WebUtils.gson
 import com.odtheking.odin.utils.network.webSocket
 import com.odtheking.odin.utils.render.hollowFill
-import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 
 object DungeonMap : Module(
@@ -34,7 +27,7 @@ object DungeonMap : Module(
     private val playerDropdown by DropdownSetting("Player Settings")
     val playerNamesScaling by NumberSetting("Player Names Scaling", 0.75f, 0.1f, 2f, 0.05f, desc = "Scale of player name labels.").withDependency { playerDropdown }
     val playerNameColor by ColorSetting("Player Name Color", Color(70, 70, 70), true, desc = "Color of player name labels.").withDependency { playerDropdown }
-    val playerHead by BooleanSetting("Own Player Head", false, desc = "Shows the player head on the map.").withDependency { playerDropdown }
+    val playerHead by BooleanSetting("Own Player Head", desc = "Shows the player head on the map.").withDependency { playerDropdown }
 
     private val roomDropdown by DropdownSetting("Room Settings")
     val normalRoomColor by ColorSetting("Normal Room", Color(107, 58, 17), true, desc = "Color of normal rooms.").withDependency { roomDropdown }
@@ -47,15 +40,14 @@ object DungeonMap : Module(
     val unknownRoomColor by ColorSetting("Unknown Room", Color(40, 40, 40), true, desc = "Color of unknown rooms hinted by a door with no discovered room on the other side.").withDependency { roomDropdown }
 
     private val doorDropdown by DropdownSetting("Door Settings")
-    val normalDoorColor by ColorSetting("Normal Door", Color(107, 58, 17).darker(), desc = "Color of normal doors.").withDependency { doorDropdown }
+    val normalDoorColor by ColorSetting("Normal Door", Color(107, 58, 17).darker(), true, desc = "Color of normal doors.").withDependency { doorDropdown }
     val witherDoorColor by ColorSetting("Wither Door", Colors.BLACK, true, desc = "Color of wither doors.").withDependency { doorDropdown }
     val bloodDoorColor by ColorSetting("Blood Door", Color(255, 0, 0), true, desc = "Color of blood room doors.").withDependency { doorDropdown }
     val fairyDoorColor by ColorSetting("Fairy Door", Color(224, 0, 255).darker(), true, desc = "Color of fairy room doors.").withDependency { doorDropdown }
     val unknownDoorColor by ColorSetting("Unknown Door", Color(40, 40, 40).darker(), true, desc = "Color of doors with no discovered room on the other side.").withDependency { doorDropdown }
 
     val disablePred by BooleanSetting("Disable Prediction", false, desc = "Disables special-column room type prediction.")
-
-    private val allowWebsocket by BooleanSetting("Websocket", true, desc = "Shares information in your room with the rest of your dungeon party.")
+    private val allowWebsocket by BooleanSetting("Parse map websocket", true, desc = "Parses the websocket data from your teammates update dungeon secrets in real time.")
 
     private val exampleRooms by lazy { buildExampleRooms() }
     private val exampleDoors by lazy { buildExampleDoors() }
@@ -86,29 +78,21 @@ object DungeonMap : Module(
 
     val syncSocket = webSocket {
         onMessage { message ->
-            val (roomName, foundSecrets, position) = try { gson.fromJson(message, RoomSync::class.java) } catch (_: Exception) { return@onMessage }
-            val room = DungeonScan.rooms.find { it.name == roomName && it.topLeft == position } ?: return@onMessage
-            if ((room.foundSecrets ?: -1) < (foundSecrets ?: -1)) room.foundSecrets = foundSecrets
+            if (!allowWebsocket) return@onMessage
+            val synced = try { gson.fromJson(message, DungeonRoom::class.java) } catch (_: Exception) { return@onMessage }
+            if (synced.data == null) return@onMessage
+
+            val room = DungeonScan.rooms.find { it.topLeft == synced.topLeft } ?: synced.also { new ->
+                DungeonScan.rooms.add(new)
+                for ((x, z) in new.tiles) {
+                    if (x !in 0..5 || z !in 0..5) continue
+                    DungeonScan.tiles[x + z * 6].room = new
+                }
+            }
+
+            if (room.data == null) room.data = synced.data
+            if ((room.foundSecrets ?: -1) < (synced.foundSecrets ?: -1)) room.foundSecrets = synced.foundSecrets
             room.walkedInto = true
         }
     }
-
-    init {
-        on<SecretsUpdateEvent> {
-            if (!allowWebsocket) return@on
-            room.name?.let { syncSocket.send(gson.toJson(RoomSync(it, foundSecrets, room.topLeft))) }
-        }
-
-        on<FloorEnterEvent> {
-            if (!allowWebsocket) return@on
-            LocationUtils.lobbyId?.let { syncSocket.connect("${ClickGUIModule.webSocketUrl}$it") } ?: devMessage("Failed to connect to dungeon websocket, lobbyId is null.")
-        }
-
-        on<RoomEnterEvent> {
-            if (room == null) syncSocket.shutdown()
-            else if (allowWebsocket) room.name?.let { syncSocket.send(gson.toJson(RoomSync(it, room.foundSecrets, room.topLeft))) }
-        }
-    }
-
-    private data class RoomSync(val roomName: String, val foundSecrets: Int?, val position: IVec2)
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index 306b443..a9a1864 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -71,6 +71,7 @@ fun GuiGraphicsExtractor.renderDoors(doors: Collection<DungeonDoor>) {
 
 fun GuiGraphicsExtractor.renderPathHints(pathHints: Collection<DungeonTile>) {
     for ((position, room) in pathHints) {
+        if (room?.walkedInto == true) continue
         val (x, y) = position.x * MAP_ROOM_GAP to position.z * MAP_ROOM_GAP
 
         val colors = if (room?.type == RoomType.BLOOD) arrayOf(DungeonMap.bloodRoomColor.darker(0.5f))
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
index d6a9c3d..bb83675 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
@@ -208,6 +208,7 @@ object MapScan {
 
             if (newCheckmark != null && newCheckmark != room.checkmark) {
                 room.checkmark = newCheckmark
+                if (newCheckmark == MapCheckmark.GREEN) room.foundSecrets = room.data?.maxSecrets ?: room.foundSecrets
                 CheckmarkUpdateEvent(room, newCheckmark).postAndCatch()
             }
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
index af2c698..f13338c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
@@ -20,7 +20,7 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
     var walkedInto: Boolean = false
     var clayPos: BlockPos? = null
     var highestBlock: Int? = null
-    var waypoints: MutableSet<DungeonWaypoints.DungeonWaypoint> = mutableSetOf()
+    @Transient var waypoints: MutableSet<DungeonWaypoints.DungeonWaypoint> = mutableSetOf()
 
     var checkmark: MapCheckmark = MapCheckmark.UNDISCOVERED
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
index 9d02e47..a68ee49 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
@@ -7,16 +7,19 @@ import com.odtheking.odin.clickgui.HudManager
 import com.odtheking.odin.clickgui.settings.AlwaysActive
 import com.odtheking.odin.clickgui.settings.impl.*
 import com.odtheking.odin.events.ChatMessageEvent
+import com.odtheking.odin.events.FloorEnterEvent
+import com.odtheking.odin.events.RoomEnterEvent
+import com.odtheking.odin.events.SecretsUpdateEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Category
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.features.ModuleManager
-import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.alert
-import com.odtheking.odin.utils.getChatBreak
-import com.odtheking.odin.utils.modMessage
+import com.odtheking.odin.features.impl.dungeon.map.DungeonMap
+import com.odtheking.odin.utils.*
 import com.odtheking.odin.utils.network.WebUtils.fetchJson
+import com.odtheking.odin.utils.network.WebUtils.gson
 import com.odtheking.odin.utils.network.WebUtils.postData
+import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.ui.rendering.NVGRenderer
 import kotlinx.coroutines.launch
 import net.minecraft.network.chat.ClickEvent
@@ -35,13 +38,24 @@ object ClickGUIModule : Module(
 ) {
     val enableNotification by BooleanSetting("Chat notifications", true, desc = "Sends a message when you toggle a module with a keybind")
     val clickGUIColor by ColorSetting("Color", Color(50, 150, 220), desc = "The color of the Click GUI.")
-
-    val roundedPanelBottom by BooleanSetting("Rounded Panel Bottoms", true, desc = "Whether to extend panels to make them rounded at the bottom.")
+    private val action by ActionSetting("Open HUD Editor", desc = "Opens the HUD editor when clicked.") { mc.setScreen(HudManager) }
 
     val hypixelApiUrl by StringSetting("API URL", "https://api.odtheking.com/hypixel/", 128, "The Hypixel API server to connect to.").hide()
     val webSocketUrl by StringSetting("Socket URL", "wss://ws.odtheking.com/", 128, "The Websocket server to connect to.").hide()
 
-    private val action by ActionSetting("Open HUD Editor", desc = "Opens the HUD editor when clicked.") { mc.setScreen(HudManager) }
+    init {
+        on<SecretsUpdateEvent> { DungeonMap.syncSocket.send(gson.toJson(room)) }
+
+        on<FloorEnterEvent> {
+            LocationUtils.lobbyId?.let { DungeonMap.syncSocket.connect("${webSocketUrl}$it") } ?: devMessage("Failed to connect to dungeon websocket, lobbyId is null.")
+        }
+
+        on<RoomEnterEvent> {
+            if (room == null) DungeonMap.syncSocket.shutdown()
+            else DungeonMap.syncSocket.send(gson.toJson(room))
+        }
+    }
+
     val devMessage by BooleanSetting("Developer Message", false, desc = "Sends development related messages to the chat.")
     val dungeonCoresLogging by BooleanSetting("Core loggings", false, desc = "")
 
@@ -78,7 +92,7 @@ object ClickGUIModule : Module(
     init {
         OdinMod.scope.launch {
             latestVersionNumber = checkNewerVersion(OdinMod.version.toString())
-            val name = OdinMod.mc.user.name.takeIf { !it.matches(Regex("Player\\d{2,3}")) } ?: return@launch
+            val name = mc.user.name.takeIf { !it.matches(Regex("Player\\d{2,3}")) } ?: return@launch
             postData("https://api.odtheking.com/tele/", """{"username": "$name", "version": "Fabric ${OdinMod.version}"}""")
         }
 
Rubix stuffodtheking · 2026-09-04 · 8aeb5b7
查看变更
src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                                              |  1 +
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt                                | 31 ++++++++++---------------------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt                                |  7 +------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt                         | 33 +++++++++------------------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/RubixHandler.kt    | 48 ++++++++++++++++++++----------------------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt | 23 +++++++++++++----------
 6 files changed, 54 insertions(+), 89 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index 293c088..c261b0b 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -50,6 +50,7 @@ abstract class TerminalEvent(val terminal: TerminalHandler) : Event { // first 2
     class Open(terminal: TerminalHandler) : TerminalEvent(terminal)
     class Close(terminal: TerminalHandler) : TerminalEvent(terminal)
     class Solve(terminal: TerminalHandler) : TerminalEvent(terminal)
+    class Click(terminal: TerminalHandler, val slotIndex: Int, val button: Int) : TerminalEvent(terminal)
 }
 
 interface TickEvent : Event {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index 9a34826..f774e96 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -16,7 +16,6 @@ import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
 import net.minecraft.network.chat.Component
 import net.minecraft.world.item.Items
-import org.lwjgl.glfw.GLFW
 
 object TerminalSolver : Module(
     name = "Terminal Solver",
@@ -28,17 +27,15 @@ object TerminalSolver : Module(
     val roundness by NumberSetting("Roundness", 5, 0f, 15f, 1f, desc = "The roundness of the custom terminal gui.").withDependency { renderType == 2 }
     val gap by NumberSetting("Slot gap", 2, 0, 8, 1, desc = "The gap between the slots in the custom terminal gui.").withDependency { renderType == 2 }
     private val solverSettings by DropdownSetting("Solver Functionality")
-    private val cancelToolTip by BooleanSetting("Stop Tooltips", true, desc = "Stops rendering tooltips in terminals.").withDependency { (renderType == 0 || renderType == 1) && solverSettings }
-    private val middleClickGUI by BooleanSetting("Middle Click GUI", true, desc = "Replaces right click with middle click in terminals.").withDependency { (renderType == 0 || renderType == 1) && solverSettings }
-    private val blockIncorrectClicks by BooleanSetting("Block Incorrect Clicks", true, desc = "Blocks incorrect clicks in terminals.").withDependency { (renderType == 0 || renderType == 1) && solverSettings }
     private val cancelMelodySolver by BooleanSetting("Stop Melody Solver", false, desc = "Stops rendering the melody solver.").withDependency { solverSettings }
     val melodyTermSize by NumberSetting("Melody Size", 1.5f, 1f, 3f, 0.1f, desc = "The size of the melody terminal GUI.").withDependency { !cancelMelodySolver && solverSettings && renderType == 2 }
     val showNumbers by BooleanSetting("Show Numbers", true, desc = "Shows numbers in the order terminal.").withDependency { solverSettings }
+    val rubixMode by SelectorSetting("Rubix Mode", "Left Clicks Only", arrayListOf("Fewest Clicks", "Left Clicks Only"), desc = "Whether the rubix solver should mix in right clicks for the fewest clicks overall, or stick to left clicks only.").withDependency { solverSettings }
     val hideClicked by BooleanSetting("Client Prediction", true, desc = "Visually predicts the server state before the gui update is sent to the client.").withDependency { solverSettings }
     val terminalReloadThreshold by NumberSetting("Resolve timeout", 600, 300, 1200, 10, unit = "ms", desc = "The amount of time before the terminal reloads after a click wasn't registered while using hide clicked.").withDependency { hideClicked && solverSettings }
     private val firstClickProtSettings by DropdownSetting("First Click Prot Dropdown")
     val firstClickProt by NumberSetting("First Click Prot", 500, 350, 800, 10, unit = "ms", desc = "The amount of time after opening a terminal where clicks are blocked to prevent bans (recommended value is 500 minus your ping).").withDependency { firstClickProtSettings }
-    val shouldFirstClickProtWithTicks by BooleanSetting("Account For Server Lag",  false, desc = "Prevents bans from clicking when the server lags after opening the terminal (disabled in singleplayer").withDependency { firstClickProtSettings }
+    val shouldFirstClickProtWithTicks by BooleanSetting("Account For Server Lag", false, desc = "Prevents bans from clicking when the server lags after opening the terminal (disabled in singleplayer").withDependency { firstClickProtSettings }
     val firstClickProtTicks by NumberSetting("Lag Protection Ticks", 8, 7, 16, unit = "ticks", desc = "Each tick = 50ms (recommended value is 8)").withDependency { shouldFirstClickProtWithTicks && firstClickProtSettings }
     private val showColors by DropdownSetting("Color Settings")
     val backgroundColor by ColorSetting("Background", Colors.gray26, true, desc = "Background color of the terminal solver.").withDependency { showColors }
@@ -68,23 +65,11 @@ object TerminalSolver : Module(
     private val renderMelody get() = !(cancelMelodySolver && TerminalUtils.currentTerm?.type == TerminalTypes.MELODY)
 
     init {
-        on<TickEvent.Server> {
-            TerminalUtils.currentTerm?.ticksOpened++
-        }
-
         on<GuiEvent.SlotClick> {
-            val term = TerminalUtils.currentTerm ?: return@on
-
-            if (blockIncorrectClicks && !term.canClick(slotIndex, button)) return@on cancel()
-
-            if (term.shouldProtect()) return@on cancel()
-
-            if (middleClickGUI) {
-                term.click(slotIndex, if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button, hideClicked)
-                return@on cancel()
+            TerminalUtils.currentTerm?.let {
+                it.click(slotIndex, button, hideClicked)
+                cancel()
             }
-
-            if (hideClicked) term.simulateClick(slotIndex, button)
         }
 
         on<GuiEvent.Render> {
@@ -109,7 +94,7 @@ object TerminalSolver : Module(
         }
 
         on<GuiEvent.DrawTooltip> {
-            if (cancelToolTip && TerminalUtils.currentTerm != null) cancel()
+            if (TerminalUtils.currentTerm != null) cancel()
             this.guiGraphics.renderDebug()
         }
 
@@ -120,6 +105,10 @@ object TerminalSolver : Module(
         on<TerminalEvent.Close> {
             if (renderType == 0 || renderType == 1) mc.execute { mc.resizeGui() }
         }
+
+        on<TickEvent.Server> {
+            TerminalUtils.currentTerm?.ticksOpened++
+        }
     }
 
     fun GuiGraphicsExtractor.renderDebug() {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
index 34d7215..115aec9 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
@@ -3,7 +3,6 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.events.ChatMessageEvent
-import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.core.EventPriority
 import com.odtheking.odin.events.core.on
@@ -38,14 +37,10 @@ object TerminalSounds : Module(
             else if (shouldReplaceSounds && completeSounds && !clickSounds) playSoundSettings(completeSoundSettings())
         }
 
-        on<GuiEvent.SlotClick> (EventPriority.HIGHEST) {
+        on<TerminalEvent.Click> (EventPriority.HIGHEST) {
             if (shouldReplaceSounds) playSoundForSlot(slotIndex, button)
         }
 
-        on<GuiEvent.CustomTermGuiClick> (EventPriority.HIGHEST) {
-            if (shouldReplaceSounds) playSoundForSlot(slot, button)
-        }
-
         onReceive<ClientboundSoundPacket> {
             if (sound.value() == SoundEvents.NOTE_BLOCK_PLING.value() && volume == 8f && pitch == 4.047619f && shouldReplaceSounds)
                 it.cancel()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
index 56066a5..1a17b2b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
@@ -1,10 +1,8 @@
 package com.odtheking.odin.features.impl.boss.termGUI
 
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.ScreenEvent
 import com.odtheking.odin.features.impl.boss.TerminalSolver
-import com.odtheking.odin.features.impl.boss.TerminalSolver.hideClicked
 import com.odtheking.odin.features.impl.boss.TerminalSolver.renderDebug
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Colors
@@ -52,13 +50,13 @@ abstract class TermGui {
                 grid?.let { g ->
                     val (bx, by) = g.toBase(click.x(), click.y(), guiScale)
                     hoveredSlotIndex = g.slots.firstOrNull { it.containsBase(bx, by) }?.slotIndex
-                    hoveredSlotIndex?.let { customTerminalClick(it, click.button()) }
+                    hoveredSlotIndex?.let {  TerminalUtils.currentTerm?.click(it, click.button(), TerminalSolver.hideClicked) }
                 }; return true
             },
             key = fun ScreenEvent.KeyPress.(): Any {
                 if (!isTerminalOverrideKey(input)) return false
                 hoveredSlotIndex?.let {
-                    customTerminalClick(it, if (!input.hasControlDown()) GLFW.GLFW_MOUSE_BUTTON_1 else GLFW.GLFW_MOUSE_BUTTON_2)
+                    TerminalUtils.currentTerm?.click(it, if (!input.hasControlDown()) GLFW.GLFW_MOUSE_BUTTON_1 else GLFW.GLFW_MOUSE_BUTTON_2, TerminalSolver.hideClicked)
                 }; return true
             })
         )
@@ -66,9 +64,8 @@ abstract class TermGui {
 
     private fun currentTermScreen() = mc.screen as? AbstractContainerScreen<*>
 
-    private fun isActiveTermScreen(): Boolean =
-        !(!TerminalSolver.customGuiEnabled || TerminalUtils.currentTerm == null || currentTermScreen() == null) &&
-                TerminalUtils.currentTerm?.type?.getGUI() === this
+    private fun isActiveTermScreen() =
+        TerminalSolver.customGuiEnabled && TerminalUtils.currentTerm?.type?.getGUI() === this && currentTermScreen() != null
 
     private fun isTerminalOverrideKey(event: KeyEvent) =
         mc.options.keyDrop.matches(event) || mc.options.keyHotbarSlots.any { it.matches(event) }
@@ -97,22 +94,6 @@ abstract class TermGui {
         )
     }
 
-    protected fun createSlotVisualFromRendering(slotIndex: Int) = SlotVisual(
-        resolve = { TerminalUtils.currentTerm?.getSlotRendering(slotIndex) }
-    ) { x, y, w, h ->
-        TerminalUtils.currentTerm?.getSlotRendering(slotIndex)?.second?.let { renderSlotText(it, x, y, w, h, Colors.WHITE) }
-    }
-
-    fun customTerminalClick(slotIndex: Int, button: Int) {
-        TerminalUtils.currentTerm?.let { term ->
-            val screen = mc.screen ?: return@let
-            val btn = if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button
-            if (term.shouldProtect()) return@let
-            if (!GuiEvent.CustomTermGuiClick(screen, slotIndex, btn).postAndCatch() && term.canClick(slotIndex, btn))
-                term.click(slotIndex, btn, hideClicked)
-        }
-    }
-
     private fun render(guiGraphics: GuiGraphicsExtractor, mouseX: Int, mouseY: Int) {
         val g       = grid ?: return
         val scale   = guiScale
@@ -144,5 +125,9 @@ abstract class TermGui {
 fun simpleTermGui(rows: Int, cols: Int, startRow: Int, startCol: Int): TermGui =
     object : TermGui() {
         override fun buildTerminal(screen: AbstractContainerScreen<*>) =
-            buildTerminalGrid(screen, rows, cols, startRow, startCol) { createSlotVisualFromRendering(it) }
+            buildTerminalGrid(screen, rows, cols, startRow, startCol) { slotIndex ->
+                SlotVisual({ TerminalUtils.currentTerm?.getSlotRendering(slotIndex) }) { x, y, w, h ->
+                    TerminalUtils.currentTerm?.getSlotRendering(slotIndex)?.second?.let { renderSlotText(it, x, y, w, h, Colors.WHITE) }
+                }
+            }
     }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/RubixHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/RubixHandler.kt
index 81b202a..6910749 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/RubixHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/RubixHandler.kt
@@ -8,61 +8,53 @@ import net.minecraft.world.item.BlockItem
 import net.minecraft.world.item.DyeColor
 import net.minecraft.world.item.ItemStack
 import net.minecraft.world.level.block.StainedGlassPaneBlock
+import kotlin.math.abs
 
 class RubixHandler : TerminalHandler(TerminalTypes.RUBIX) {
 
     private var lockedColor: DyeColor? = null
 
+    private val rightClickSlots = HashSet<Int>()
+
     override fun solve(slots: List<Slot>, updatedIndex: Int): List<Int> {
         val panes = slots.mapIndexedNotNull { index, slot ->
             slot.item.paneColor?.takeUnless { it == DyeColor.BLACK }?.let { index to it }
         }
 
-        return if (updatedIndex == LAST_PANE_SLOT && lockedColor == null) {
-            val (best, clicks) = rubixColorOrder
-                .map { it to clicksFor(it, panes) }
-                .minBy { (_, clicks) -> getRealSize(clicks) }
+        if (updatedIndex == LAST_PANE_SLOT && lockedColor == null)
+            lockedColor = rubixColorOrder.minBy { goal -> clicksFor(goal, panes).values.sumOf { abs(it) } }
+
+        val clicks = lockedColor?.let { clicksFor(it, panes) }.orEmpty()
 
-            lockedColor = best
-            clicks
-        } else lockedColor?.let { clicksFor(it, panes) } ?: emptyList()
+        rightClickSlots.clear()
+        clicks.forEach { (slotIndex, count) -> if (count < 0) rightClickSlots.add(slotIndex) }
+        return clicks.flatMap { (slotIndex, count) -> List(abs(count)) { slotIndex } }
     }
 
-    private fun clicksFor(goal: DyeColor, panes: List<Pair<Int, DyeColor>>): List<Int> {
+    private fun clicksFor(goal: DyeColor, panes: List<Pair<Int, DyeColor>>): Map<Int, Int> {
         val goalIndex = rubixColorOrder.indexOf(goal)
-        return panes.flatMap { (slotIndex, color) ->
-            List(dist(rubixColorOrder.indexOf(color), goalIndex)) { slotIndex }
-        }
+        return panes.associate { (slotIndex, color) ->
+            val forward = dist(rubixColorOrder.indexOf(color), goalIndex)
+            slotIndex to if (forward > 2 && TerminalSolver.rubixMode != 1) forward - rubixColorOrder.size else forward
+        }.filterValues { it != 0 }
     }
 
     private val ItemStack.paneColor: DyeColor?
         get() = ((item as? BlockItem)?.block as? StainedGlassPaneBlock)?.color
 
-    override fun simulateClick(slotIndex: Int, clickType: Int) {
-        if (clickType == 1) solution.add(slotIndex)
-        else solution.remove(slotIndex)
-        if (solution.count { it == slotIndex } >= 5) solution.removeAll { it == slotIndex }
-    }
-
-    override fun canClick(slotIndex: Int, button: Int): Boolean {
-        val needed = solution.count { it == slotIndex }
-        return (needed >= 3 && button == 1) || (needed in 1..<3 && button != 1)
-    }
-
-    private fun getRealSize(clicks: List<Int>): Int =
-        clicks.groupingBy { it }.eachCount().values.sumOf { if (it >= 3) 5 - it else it }
+    override fun canClick(slotIndex: Int, button: Int): Boolean =
+        slotIndex in solution && (button == 1) == (slotIndex in rightClickSlots)
 
     private fun dist(pane: Int, most: Int): Int =
         if (pane > most) (most + rubixColorOrder.size) - pane else most - pane
 
     override fun renderSlot(slotIndex: Int): Pair<Color, String?>? {
-        val amount = solution.count { it == slotIndex }
-        val clicksRequired = if (amount < 3) amount else amount - 5
-        if (clicksRequired == 0) return null
+        val remaining = solution.count { it == slotIndex }.takeIf { it > 0 } ?: return null
+        val clicksRequired = if (slotIndex in rightClickSlots) -remaining else remaining
         return when (clicksRequired) {
             1 -> TerminalSolver.rubixColor1
             2 -> TerminalSolver.rubixColor2
-            -1 -> TerminalSolver.oppositeRubixColor1
+            -1, 4 -> TerminalSolver.oppositeRubixColor1
             else -> TerminalSolver.oppositeRubixColor2
         } to clicksRequired.toString()
     }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
index 644b8a1..61ff49c 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
@@ -2,6 +2,7 @@ package com.odtheking.odin.utils.skyblock.dungeon.terminals.terminalhandler
 
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.SetSlotEvent
+import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.features.impl.boss.TerminalSimulator
 import com.odtheking.odin.features.impl.boss.TerminalSolver
 import com.odtheking.odin.features.impl.boss.TerminalSolver.firstClickProt
@@ -15,12 +16,10 @@ import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 import net.minecraft.world.inventory.ContainerInput
 import net.minecraft.world.inventory.Slot
-import net.minecraft.world.item.Item
 import net.minecraft.world.item.Items
 import org.lwjgl.glfw.GLFW
 
 abstract class TerminalHandler(val type: TerminalTypes) {
-    private val lastSeenItems = HashMap<Int, Item>()
     val clickedSlots = ArrayList<Pair<Int, Int>>()
     val timeOpened = System.currentTimeMillis()
     val solution = ArrayList<Int>()
@@ -30,10 +29,8 @@ abstract class TerminalHandler(val type: TerminalTypes) {
     open fun updateSlot(event: SetSlotEvent) {
         if (event.slots.isEmpty() || event.slotIndex !in 0 until type.windowSize - 9 || event.itemStack.item == Items.BLACK_STAINED_GLASS_PANE) return
 
-        if (lastSeenItems.put(event.slotIndex, event.itemStack.item) != event.itemStack.item) {
-            val index = clickedSlots.indexOfFirst { it.first == event.slotIndex }
-            if (index >= 0) clickedSlots.subList(0, index + 1).clear()
-        }
+        val index = clickedSlots.indexOfFirst { it.first == event.slotIndex }
+        if (index >= 0) clickedSlots.subList(0, index + 1).clear()
 
         solution.clear()
         solution.addAll(solve(event.slots.subList(0, type.windowSize - 9), event.slotIndex))
@@ -52,16 +49,22 @@ abstract class TerminalHandler(val type: TerminalTypes) {
     abstract fun solve(slots: List<Slot>, updatedIndex: Int): List<Int>
 
     open fun click(slotIndex: Int, button: Int, simulateClick: Boolean) {
-        val screen = mc.screen ?: return
+        if (!canClick(slotIndex, button) || shouldProtect()) return
+
+        val button = if (button == 1 && type == TerminalTypes.RUBIX) GLFW.GLFW_MOUSE_BUTTON_2 else GLFW.GLFW_MOUSE_BUTTON_3
         clickedSlots.add(slotIndex to button)
         lastClickTime = System.currentTimeMillis()
+
         if (simulateClick) simulateClick(slotIndex, button)
 
-        if (screen is TermSimGUI) {
-            screen.clickIndex(slotIndex, button)
-            return
+        mc.screen?.let { screen ->
+            if (screen is TermSimGUI) {
+                screen.clickIndex(slotIndex, button)
+                return
+            }
         }
         mc.player?.clickSlot(slotIndex, button, if (button == GLFW.GLFW_MOUSE_BUTTON_3) ContainerInput.CLONE else ContainerInput.PICKUP)
+        TerminalEvent.Click(this, slotIndex, button).postAndCatch()
     }
 
     open fun canClick(slotIndex: Int, button: Int): Boolean = slotIndex in solution
Made blaze accept any mob levelodtheking · 2026-09-01 · edbeb9a
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
index 6e973ed..38db42a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
@@ -19,7 +19,7 @@ object BlazeSolver {
     private var blazes = mutableListOf<ArmorStand>()
     private var roomType = 0
     private var lastBlazeCount = 10
-    private val blazeHealthRegex = Regex("^\\[Lv15]  Blaze [\\d,]+/([\\d,]+)❤$")
+    private val blazeHealthRegex = Regex("^\\[Lv+\\d+]  Blaze [\\d,]+/([\\d,]+)❤$")
 
     fun getBlaze() {
         if (!DungeonUtils.inDungeons || DungeonUtils.currentRoom?.data?.name?.equalsOneOf("Lower Blaze", "Higher Blaze") == false) return
Improved visuals for storm tick timer and for Melody progress HUDodtheking · 2026-09-01 · 5bf7249
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt                | 26 +++++++++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt                   |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt |  2 +-
 3 files changed, 17 insertions(+), 13 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
index 2acdad7..46b2a50 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
@@ -16,7 +16,7 @@ import com.odtheking.odin.utils.equalsOneOf
 import com.odtheking.odin.utils.network.WebUtils.gson
 import com.odtheking.odin.utils.network.webSocket
 import com.odtheking.odin.utils.render.getStringWidth
-import com.odtheking.odin.utils.render.textDim
+import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.sendCommand
 import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonClass
@@ -45,7 +45,7 @@ object MelodyMessage : Module(
         fun track(data: MelodyData, index: Int, name: String) {
             rows = maxOf(rows, index + 1)
             val clay = data.clay ?: return
-            val label = melodyLabel(data, name) ?: return
+            val label = melodyLabel(data, name) ?: ""
             labelWidth = maxOf(labelWidth, getStringWidth("$clay $label"))
         }
 
@@ -63,7 +63,7 @@ object MelodyMessage : Module(
             }
         }
 
-        (width * 5 + 2 + labelWidth) to (width * 2 * rows)
+        (width * 5 + 2 + labelWidth) to (width * rows)
     }.withDependency { broadcast }
 
     private val showPlayer by SelectorSetting("Show Player", "None", arrayListOf("None", "Class", "Name", "Class & Name"), desc = "How player details should be rendered in the Melody GUI.").withDependency { broadcast }
@@ -172,22 +172,26 @@ object MelodyMessage : Module(
     private val width by lazy { getStringWidth("§d■") }
 
     private fun melodyLabel(data: MelodyData, playerName: String): String? = when (showPlayer) {
-        1 -> data.dungeonClass.toString()
-        2 -> playerName
-        3 -> "$playerName (${data.dungeonClass})"
+        1 -> "§${data.dungeonClass.colorCode}${data.dungeonClass.name.lowercase()}"
+        2 -> "§6$playerName"
+        3 -> "§6$playerName §8(§${data.dungeonClass.colorCode}${data.dungeonClass.name.lowercase()}§8)"
         else -> null
     }
 
     private fun GuiGraphicsExtractor.drawMelody(data: MelodyData, index: Int, playerName: String) {
-        val y = width * 2 * index
+        val y = width * index - 1
 
         repeat(5) {
-            if (data.purple == it) textDim("§d■", width * it, y)
-            textDim("${if (data.pane == it) "§a" else "§f"}■", width * it, y + width)
+            val color = when (it) {
+                data.pane -> "§a"
+                data.purple -> "§d"
+                else -> "§f"
+            }
+            text("$color■", width * it, y)
         }
 
-        val label = melodyLabel(data, playerName) ?: return
-        data.clay?.let { textDim("$it $label", width * 5 + 2, y + width / 2) }
+        val label = melodyLabel(data, playerName) ?: ""
+        data.clay?.let { text("$it $label", width * 5 + 2, y) }
     }
 
     private data class UpdateMessage(val username: String, val type: Int, val slot: Int)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 311b2c3..1d84d89 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -79,7 +79,7 @@ object TickTimers : Module(
     private val stormTickHud by HUD("Storm Tick Hud", "Displays a timer for Storm's second phase, optionally counting down to the crush window.") {
         if (it) {
             val (time, max, prefix) = Triple(200, 620, "§bStorm:")
-            textDim(formatTimer(time, max, prefix), 0, 0, Colors.MINECRAFT_DARK_RED)
+            textDim(formatTimer(time, max, prefix, "§a"), 0, 0, Colors.MINECRAFT_DARK_RED)
         } else if (stormTick >= 0) textDim(formatTimer(stormTick, 620, "§bStorm:"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else 0 to 0
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
index 282d819..6e973ed 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
@@ -19,7 +19,7 @@ object BlazeSolver {
     private var blazes = mutableListOf<ArmorStand>()
     private var roomType = 0
     private var lastBlazeCount = 10
-    private val blazeHealthRegex = Regex("^\\[Lv11]  Blaze [\\d,]+/([\\d,]+)❤$")
+    private val blazeHealthRegex = Regex("^\\[Lv15]  Blaze [\\d,]+/([\\d,]+)❤$")
 
     fun getBlaze() {
         if (!DungeonUtils.inDungeons || DungeonUtils.currentRoom?.data?.name?.equalsOneOf("Lower Blaze", "Higher Blaze") == false) return
Fixed Numbers and Melody CustomTermGui renderingodtheking · 2026-09-01 · 49c0933
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/MelodyHandler.kt  | 1 -
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt | 2 +-
 2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/MelodyHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/MelodyHandler.kt
index 1066a42..6e158b3 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/MelodyHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/MelodyHandler.kt
@@ -17,7 +17,6 @@ class MelodyHandler: TerminalHandler(TerminalTypes.MELODY) {
         return buildList {
             add(greenPane)
             add(magentaPane)
-            add(magentaPane + 36)
 
             if (greenPane % 9 == magentaPane % 9) add(greenClay)
         }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt
index eebaa34..df54803 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt
@@ -28,6 +28,6 @@ class NumbersHandler: TerminalHandler(TerminalTypes.NUMBERS) {
             1 -> TerminalSolver.orderColor2
             2 -> TerminalSolver.orderColor3
             else -> Colors.TRANSPARENT
-        } to if (TerminalSolver.showNumbers) (abs((solution.size - 10) - solutionIndex) + 1).toString() else null
+        } to if (TerminalSolver.showNumbers) (abs((solution.size - 14) - solutionIndex) + 1).toString() else null
     }
 }
\ No newline at end of file
Code cleanupodtheking · 2026-09-01 · 4f914b5
查看变更
src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                     |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt           |  3 +--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt        | 10 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt       |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt        |  3 +--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt   |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt     |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt     |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt       |  3 +--
 src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt                       | 17 -----------------
 11 files changed, 14 insertions(+), 34 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index 32a1567..293c088 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -35,7 +35,7 @@ class BlockInteractEvent(val pos: BlockPos) : CancellableEvent()
 class EntityInteractEvent(val pos: Vec3, val entity: Entity) : CancellableEvent()
 class UseItemOnPostEvent(val hand: InteractionHand, val hitResult: BlockHitResult, val interactionResult: InteractionResult) : Event
 
-class ChatMessageEvent(val value: String, val component: Component) : Event // mixin instead of packet (still needs to run before vanilla processing for hideMessage()
+class ChatMessageEvent(val value: String, val component: Component) : CancellableEvent()
 class MessageSentEvent(val message: String) : CancellableEvent()
 
 class RenderBossBarEvent(val bossBar: BossEvent) : CancellableEvent()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
index aadca87..df3fec1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
@@ -7,7 +7,6 @@ import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.*
-import com.odtheking.odin.utils.ChatManager.hideMessage
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.network.chat.ClickEvent
 import net.minecraft.network.chat.Component
@@ -59,7 +58,7 @@ object ExtraStats : Module(
         on<ChatMessageEvent> {
             if (!DungeonUtils.inDungeons) return@on
 
-            if (cancelRegexes.any { it.matches(value) }) hideMessage()
+            if (cancelRegexes.any { it.matches(value) }) cancel()
 
             if (extraStatsRegex.matches(value)) return@on sendCommand("showextrastats")
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
index f2cd288..2acdad7 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
@@ -97,7 +97,7 @@ object MelodyMessage : Module(
         }
 
         on<SetSlotEvent> {
-            if (broadcast || melodyProgress) onSlotUpdate(this)
+            if (broadcast || melodyProgress) onSlotUpdate()
         }
 
         on<LevelEvent.Load> {
@@ -125,13 +125,13 @@ object MelodyMessage : Module(
         }
     }
 
-    private fun onSlotUpdate(event: SetSlotEvent) {
+    private fun SetSlotEvent.onSlotUpdate() {
         val term = TerminalUtils.currentTerm ?: return
         if (DungeonUtils.getF7Phase() != M7Phases.P3 || term.type != TerminalTypes.MELODY || mc.screen is TermSimGUI) return
 
-        val item = event.itemStack.item
+        val item = itemStack.item
         if (item == Items.LIME_TERRACOTTA) {
-            val position = event.slotIndex / 9
+            val position = slotIndex / 9
             if (lastSent.clay == position) return
             if (broadcast) melodyWebSocket.send(update(1, position))
             if (melodyProgress) clayProgress[position]?.let { sendCommand("pc $it") }
@@ -139,7 +139,7 @@ object MelodyMessage : Module(
             return
         }
         if (!broadcast || !item.equalsOneOf(Items.MAGENTA_STAINED_GLASS_PANE, Items.LIME_STAINED_GLASS_PANE)) return
-        val index = mapToRange(event.slotIndex) ?: return
+        val index = mapToRange(slotIndex) ?: return
         val meta = when (item) {
             Items.MAGENTA_STAINED_GLASS_PANE -> {
                 if (lastSent.purple == index) return
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index a5207d1..9a34826 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -147,7 +147,7 @@ object TerminalSolver : Module(
                 textWithWordWrap(mc.font, Component.literal(line), 5, 20 + (index * 10), 300, Colors.WHITE.rgba)
             }
 
-            menu.items.forEachIndexed { index, stack ->
+            items.forEachIndexed { index, stack ->
                 item(stack, 5 + (index % 9) * 18, 250 + (index / 9) * 18)
                 itemDecorations(mc.font, stack, 5 + (index % 9) * 18, 250 + (index / 9) * 18)
             }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
index 5f431ca..2384c7b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
@@ -9,7 +9,6 @@ import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.features.impl.boss.termsim.TermSimGUI
-import com.odtheking.odin.utils.ChatManager.hideMessage
 import com.odtheking.odin.utils.PersonalBest
 import com.odtheking.odin.utils.modMessage
 
@@ -48,7 +47,7 @@ object TerminalTimes : Module(
         on<ChatMessageEvent> {
             if (!terminalSplits) return@on
             terminalCompleteRegex.find(value)?.destructured?.let { (name, activated, type, current, total) ->
-                hideMessage()
+                cancel()
 
                 modMessage("§6$name §a$activated a $type! (§c${current}§a/${total}) §8(§7${sectionTimer.seconds}s §8| §7${phaseTimer.seconds}s§8)", "")
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt
index 0bde66a..2a36f9f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt
@@ -14,7 +14,7 @@ object NumbersSim : TermSimGUI(
 ) {
     override fun create() {
         val used = (1..14).shuffled().toMutableList()
-        setSlots  {
+        setSlots {
             if (floor(it.index / 9f) in 1f..2f && it.index % 9 in 1..7) ItemStack(Items.RED_STAINED_GLASS_PANE, used.first()).apply { set(DataComponents.CUSTOM_NAME, Component.literal("§a${used.removeFirst()}")) }
             else blackPane
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt
index 8e53866..134a7cd 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt
@@ -16,7 +16,7 @@ object PanesSim : TermSimGUI(
     private val redPane   get() = ItemStack(Items.RED_STAINED_GLASS_PANE).apply { set(DataComponents.CUSTOM_NAME, Component.literal("")) }
 
     override fun create() {
-        setSlots  {
+        setSlots {
             if (floor(it.index / 9f) in 1f..3f && it.index % 9 in 2..6) if (Math.random() > 0.75) greenPane else redPane else blackPane
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt
index fe2e5b3..605ee7d 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt
@@ -20,7 +20,7 @@ object RubixSim : TermSimGUI(
     private val indices = listOf(12, 13, 14, 21, 22, 23, 30, 31, 32)
 
     override fun create() {
-        setSlots  {
+        setSlots {
             if (floor(it.index / 9f) in 1f..3f && it.index % 9 in 3..5) getPane()
             else blackPane
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt
index fc444f2..b6b3fe6 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt
@@ -22,7 +22,7 @@ class SelectAllSim(
 ) {
     override fun create() {
         val guaranteed = (10..16).plus(19..25).plus(28..34).plus(37..43).random()
-        setSlots  { slot ->
+        setSlots { slot ->
             if (floor(slot.index / 9.0) in 1.0..4.0 && slot.index % 9 in 1..7) {
                 val item = ItemStack(getPossibleItems(color).random())
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt
index 76a3836..14be809 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt
@@ -7,7 +7,6 @@ import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
-import com.odtheking.odin.utils.ChatManager.hideMessage
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.equalsOneOf
 import com.odtheking.odin.utils.formatTime
@@ -46,7 +45,7 @@ object SupplyHelper : Module(
                     if (KuudraUtils.phase != 1) return@on
                     val (name, current, total) = supplyPickUpRegex.find(value)?.destructured ?: return@on
                     modMessage("§6$name §a§lrecovered a supply in ${formatTime(System.currentTimeMillis() - startRun)}! §r§8($current/$total)", "")
-                    hideMessage()
+                    cancel()
                 }
             }
         }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt b/src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt
deleted file mode 100644
index a4d05d7..0000000
--- a/src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package com.odtheking.odin.utils
-
-import com.odtheking.odin.events.ChatMessageEvent
-import net.minecraft.network.chat.Component
-import java.util.concurrent.ConcurrentLinkedQueue
-
-object ChatManager {
-    private val cancelQueue = ConcurrentLinkedQueue<Component>()
-
-    fun ChatMessageEvent.hideMessage() {
-        cancelQueue.add(component)
-    }
-
-    internal fun shouldCancelMessage(message: Component): Boolean {
-        return cancelQueue.remove(message)
-    }
-}
\ No newline at end of file
Ported terminal gui changesodtheking · 2026-09-01 · 05ae50f
查看变更
src/main/java/com/odtheking/mixin/mixins/AbstractContainerMenuMixin.java                             | 26 ++++++++++++++++++
 src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java                           |  2 +-
 src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java                              | 13 ---------
 src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java                                         |  9 +++++++
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                                            |  7 ++++-
 src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt                                         |  4 ---
 src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt                                                |  7 +----
 src/main/kotlin/com/odtheking/odin/events/ScreenEvent.kt                                             |  2 --
 src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt                               | 49 +++++++++++++++++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt                              | 23 ++++++++--------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt                              |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt                       |  9 +++----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/MelodySim.kt                           | 19 +++++++------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt                          |  7 ++---
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt                            |  4 +--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt                            |  9 ++-----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt                        |  8 +++---
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartGUI.kt                            |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartsWithSim.kt                       | 19 +++++++++----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt                          | 24 +++++------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt                           |  8 +++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt                                  |  3 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt                                 | 11 ++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt                |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/RemovePerks.kt                               |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt                                  | 30 ++++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt                         |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt                             |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt                               |  5 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt                        |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/Utils.kt                                                    |  4 +--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalTypes.kt                 | 10 +++----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt                 | 62 +++++++++++++------------------------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/MelodyHandler.kt | 26 +++++++++---------
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/NumbersHandler.kt     | 13 +++++----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/PanesHandler.kt  |  9 +++----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/RubixHandler.kt  | 73 ++++++++++++++++++++------------------------------
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/SelectAllHandler.kt   | 10 +++----
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt  | 41 +++++++++++-----------------
 .../kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt    | 75 ++++++++++++++++++++--------------------------------
 src/main/resources/odin.mixins.json                                                                  |  1 +
 41 files changed, 295 insertions(+), 341 deletions(-)
diff --git a/src/main/java/com/odtheking/mixin/mixins/AbstractContainerMenuMixin.java b/src/main/java/com/odtheking/mixin/mixins/AbstractContainerMenuMixin.java
new file mode 100644
index 0000000..3dbd650
--- /dev/null
+++ b/src/main/java/com/odtheking/mixin/mixins/AbstractContainerMenuMixin.java
@@ -0,0 +1,26 @@
+package com.odtheking.mixin.mixins;
+
+import com.odtheking.odin.events.SetSlotEvent;
+import net.minecraft.core.NonNullList;
+import net.minecraft.world.inventory.AbstractContainerMenu;
+import net.minecraft.world.inventory.Slot;
+import net.minecraft.world.item.ItemStack;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(AbstractContainerMenu.class)
+public class AbstractContainerMenuMixin {
+
+    @Shadow
+    @Final
+    public NonNullList<Slot> slots;
+
+    @Inject(method = "setItem", at = @At("TAIL"))
+    private void postSetSlot(int slot, int stateId, ItemStack itemStack, CallbackInfo ci) {
+        new SetSlotEvent(slot, itemStack, slots, (AbstractContainerMenu) (Object) this).postAndCatch();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java b/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java
index 617e9f3..016caa2 100644
--- a/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java
@@ -26,7 +26,7 @@ public class AbstractContainerScreenMixin {
 
     @Inject(method = "slotClicked", at = @At("HEAD"), cancellable = true)
     private void onMouseClickedSlot(Slot slot, int slotId, int buttonNum, ContainerInput containerInput, CallbackInfo ci) {
-        if (new GuiEvent.SlotClick((Screen) (Object) this, slot, slotId, buttonNum, containerInput).postAndCatch()) ci.cancel();
+        if (new GuiEvent.SlotClick((Screen) (Object) this, slotId, buttonNum).postAndCatch()) ci.cancel();
     }
 
     @Inject(method = "extractTooltip", at = @At("HEAD"), cancellable = true)
diff --git a/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java b/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
index 1acb34a..85350dc 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
@@ -3,19 +3,12 @@ package com.odtheking.mixin.mixins;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
 import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
 import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
-import com.odtheking.odin.events.GuiEvent;
 import com.odtheking.odin.events.PacketEvent;
-import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
 import net.minecraft.client.multiplayer.ClientPacketListener;
 import net.minecraft.network.PacketListener;
 import net.minecraft.network.protocol.Packet;
-import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
 import org.spongepowered.asm.mixin.Mixin;
 import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
-
-import static com.odtheking.odin.OdinMod.getMc;
 
 @Mixin(ClientPacketListener.class)
 public class ClientPacketListenerMixin {
@@ -25,12 +18,6 @@ public class ClientPacketListenerMixin {
         return true;
     }
 
-    @Inject(method = "handleContainerSetSlot", at = @At("TAIL"))
-    private void onSetSlot(ClientboundContainerSetSlotPacket packet, CallbackInfo ci) {
-        if (getMc().screen instanceof AbstractContainerScreen<?> container)
-            new GuiEvent.SlotUpdate(getMc().screen, packet, container.getMenu()).postAndCatch();
-    }
-
     @WrapOperation(method = "handleBundlePacket", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/protocol/Packet;handle(Lnet/minecraft/network/PacketListener;)V"))
     private void wrapPacketHandle(Packet<?> packet, PacketListener listener, Operation<Void> original) {
         if (new PacketEvent.Receive(packet).postAndCatch()) return;
diff --git a/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java b/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java
index 01a6329..a4be6ef 100644
--- a/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/MinecraftMixin.java
@@ -3,9 +3,12 @@ package com.odtheking.mixin.mixins;
 import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
 import com.odtheking.odin.events.BlockInteractEvent;
 import com.odtheking.odin.events.EntityInteractEvent;
+import com.odtheking.odin.events.ScreenCloseEvent;
+import com.odtheking.odin.events.ScreenEvent;
 import com.odtheking.odin.features.impl.boss.TerminalSolver;
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils;
 import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.Screen;
 import net.minecraft.world.phys.BlockHitResult;
 import net.minecraft.world.phys.EntityHitResult;
 import net.minecraft.world.phys.HitResult;
@@ -40,4 +43,10 @@ public abstract class MinecraftMixin {
         if (TerminalUtils.getCurrentTerm() != null && TerminalSolver.getTermSize() != (Integer) original) return TerminalSolver.getTermSize();
         return original;
     }
+
+    @Inject(method = "setScreen", at = @At(value = "HEAD"))
+    private void onSetScreen(Screen screen, CallbackInfo ci) {
+        if (screen != null) new ScreenEvent.Open(screen).postAndCatch();
+        else ScreenCloseEvent.INSTANCE.postAndCatch();
+    }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index bef5a68..32a1567 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -20,6 +20,9 @@ import net.minecraft.world.InteractionHand
 import net.minecraft.world.InteractionResult
 import net.minecraft.world.entity.Entity
 import net.minecraft.world.entity.item.ItemEntity
+import net.minecraft.world.inventory.AbstractContainerMenu
+import net.minecraft.world.inventory.Slot
+import net.minecraft.world.item.ItemStack
 import net.minecraft.world.level.block.state.BlockState
 import net.minecraft.world.phys.BlockHitResult
 import net.minecraft.world.phys.Vec3
@@ -81,4 +84,6 @@ data object MapUpdateEvent : Event
 class CheckmarkUpdateEvent(val room: DungeonRoom, val checkmark: MapCheckmark) : Event
 class SecretsUpdateEvent(val room: DungeonRoom, val foundSecrets: Int) : Event
 
-object LocationChangeEvent : Event
\ No newline at end of file
+object LocationChangeEvent : Event
+object ScreenCloseEvent : Event
+class SetSlotEvent(val slotIndex: Int, val itemStack: ItemStack, val slots: List<Slot>, val menu: AbstractContainerMenu) : Event
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
index 46e3601..ce61343 100644
--- a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
@@ -40,11 +40,7 @@ object EventDispatcher {
             RenderEvent.Last(context).postAndCatch()
         }
 
-        ScreenEvents.AFTER_INIT.register { _, screen, _, _ -> ScreenEvent.Open(screen).postAndCatch() }
         ScreenEvents.BEFORE_INIT.register { _, screen, _, _ ->
-            ScreenEvents.remove(screen).register {
-                ScreenEvent.Close(screen).postAndCatch()
-            }
             ScreenMouseEvents.allowMouseClick(screen).register { screen, event ->
                 !ScreenEvent.MouseClick(screen, event).postAndCatch()
             }
diff --git a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
index aebbc3b..158572f 100644
--- a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
@@ -3,18 +3,13 @@ package com.odtheking.odin.events
 import com.odtheking.odin.events.core.CancellableEvent
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.Screen
-import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket
-import net.minecraft.world.inventory.AbstractContainerMenu
-import net.minecraft.world.inventory.ContainerInput
 import net.minecraft.world.inventory.Slot
 
 abstract class GuiEvent(val screen: Screen) : CancellableEvent() {
 
     class Render(screen: Screen, val guiGraphics: GuiGraphicsExtractor, val mouseX: Int, val mouseY: Int) : GuiEvent(screen)
 
-    class SlotClick(screen: Screen, val slot: Slot?, val slotId: Int, val button: Int, val containerInput: ContainerInput) : GuiEvent(screen)
-
-    class SlotUpdate(screen: Screen, val packet: ClientboundContainerSetSlotPacket, val menu: AbstractContainerMenu) : GuiEvent(screen)
+    class SlotClick(screen: Screen, val slotIndex: Int, val button: Int) : GuiEvent(screen)
 
     class RenderSlot(screen: Screen, val guiGraphics: GuiGraphicsExtractor, val slot: Slot) : GuiEvent(screen)
 
diff --git a/src/main/kotlin/com/odtheking/odin/events/ScreenEvent.kt b/src/main/kotlin/com/odtheking/odin/events/ScreenEvent.kt
index 2efdb46..b0f30fd 100644
--- a/src/main/kotlin/com/odtheking/odin/events/ScreenEvent.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/ScreenEvent.kt
@@ -10,8 +10,6 @@ abstract class ScreenEvent(val screen: Screen) : CancellableEvent() {
 
     class Open(screen: Screen) : ScreenEvent(screen)
 
-    class Close(screen: Screen) : ScreenEvent(screen)
-
     class MouseClick(screen: Screen, val click: MouseButtonEvent) : ScreenEvent(screen)
 
     class MouseRelease(screen: Screen, val click: MouseButtonEvent) : ScreenEvent(screen)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
index c0697d6..f2cd288 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
@@ -5,8 +5,8 @@ import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
 import com.odtheking.odin.clickgui.settings.impl.StringSetting
 import com.odtheking.odin.events.ChatMessageEvent
-import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -39,18 +39,34 @@ object MelodyMessage : Module(
 
     private val broadcast by BooleanSetting("Broadcast Progress", true, desc = "Broadcasts melody progress to all other odin users in the party.")
     private val melodyGui by HUD("Progress GUI", "Shows a gui with the progress of broadcasting odin users in melody.", true) {
-        if (it) drawMelody(MelodyData(3, 1, 2, DungeonClass.ARCHER), 0, mc.user.name)
+        var rows = 0
+        var labelWidth = 0
+
+        fun track(data: MelodyData, index: Int, name: String) {
+            rows = maxOf(rows, index + 1)
+            val clay = data.clay ?: return
+            val label = melodyLabel(data, name) ?: return
+            labelWidth = maxOf(labelWidth, getStringWidth("$clay $label"))
+        }
+
+        if (it) {
+            val example = MelodyData(3, 1, 2, DungeonClass.ARCHER)
+            drawMelody(example, 0, mc.user.name)
+            track(example, 0, mc.user.name)
+        }
 
         if (broadcast && melodyWebSocket.connected) {
             melodies.entries.forEachIndexed { i, (name, data) ->
                 if (showPlayer == 0 && name == mc.user.name) return@forEachIndexed
                 drawMelody(data, i, name)
+                track(data, i, name)
             }
         }
-        40 to 15
+
+        (width * 5 + 2 + labelWidth) to (width * 2 * rows)
     }.withDependency { broadcast }
 
-    private val showPlayer by SelectorSetting("Show Player", "Name", arrayListOf("None", "Class", "Name", "Class & Name"), desc = "How player details should be rendered in the Melody GUI.").withDependency { broadcast }
+    private val showPlayer by SelectorSetting("Show Player", "None", arrayListOf("None", "Class", "Name", "Class & Name"), desc = "How player details should be rendered in the Melody GUI.").withDependency { broadcast }
 
     val melodyWebSocket = webSocket {
         onMessage { message ->
@@ -80,7 +96,7 @@ object MelodyMessage : Module(
             if (broadcast || melodyProgress) onChatMessage(value)
         }
 
-        on<GuiEvent.SlotUpdate> {
+        on<SetSlotEvent> {
             if (broadcast || melodyProgress) onSlotUpdate(this)
         }
 
@@ -109,13 +125,13 @@ object MelodyMessage : Module(
         }
     }
 
-    private fun onSlotUpdate(event: GuiEvent.SlotUpdate) {
+    private fun onSlotUpdate(event: SetSlotEvent) {
         val term = TerminalUtils.currentTerm ?: return
         if (DungeonUtils.getF7Phase() != M7Phases.P3 || term.type != TerminalTypes.MELODY || mc.screen is TermSimGUI) return
 
-        val item = event.packet.item.item
+        val item = event.itemStack.item
         if (item == Items.LIME_TERRACOTTA) {
-            val position = event.packet.slot / 9
+            val position = event.slotIndex / 9
             if (lastSent.clay == position) return
             if (broadcast) melodyWebSocket.send(update(1, position))
             if (melodyProgress) clayProgress[position]?.let { sendCommand("pc $it") }
@@ -123,7 +139,7 @@ object MelodyMessage : Module(
             return
         }
         if (!broadcast || !item.equalsOneOf(Items.MAGENTA_STAINED_GLASS_PANE, Items.LIME_STAINED_GLASS_PANE)) return
-        val index = mapToRange(event.packet.slot) ?: return
+        val index = mapToRange(event.slotIndex) ?: return
         val meta = when (item) {
             Items.MAGENTA_STAINED_GLASS_PANE -> {
                 if (lastSent.purple == index) return
@@ -155,6 +171,13 @@ object MelodyMessage : Module(
 
     private val width by lazy { getStringWidth("§d■") }
 
+    private fun melodyLabel(data: MelodyData, playerName: String): String? = when (showPlayer) {
+        1 -> data.dungeonClass.toString()
+        2 -> playerName
+        3 -> "$playerName (${data.dungeonClass})"
+        else -> null
+    }
+
     private fun GuiGraphicsExtractor.drawMelody(data: MelodyData, index: Int, playerName: String) {
         val y = width * 2 * index
 
@@ -163,13 +186,7 @@ object MelodyMessage : Module(
             textDim("${if (data.pane == it) "§a" else "§f"}■", width * it, y + width)
         }
 
-        val label = when (showPlayer) {
-            1 -> data.dungeonClass
-            2 -> playerName
-            3 -> "$playerName (${data.dungeonClass})"
-            else -> return
-        }
-
+        val label = melodyLabel(data, playerName) ?: return
         data.clay?.let { textDim("$it $label", width * 5 + 2, y + width / 2) }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index 11d443a..a5207d1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -15,6 +15,7 @@ import net.minecraft.client.Options
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
 import net.minecraft.network.chat.Component
+import net.minecraft.world.item.Items
 import org.lwjgl.glfw.GLFW
 
 object TerminalSolver : Module(
@@ -33,13 +34,12 @@ object TerminalSolver : Module(
     private val cancelMelodySolver by BooleanSetting("Stop Melody Solver", false, desc = "Stops rendering the melody solver.").withDependency { solverSettings }
     val melodyTermSize by NumberSetting("Melody Size", 1.5f, 1f, 3f, 0.1f, desc = "The size of the melody terminal GUI.").withDependency { !cancelMelodySolver && solverSettings && renderType == 2 }
     val showNumbers by BooleanSetting("Show Numbers", true, desc = "Shows numbers in the order terminal.").withDependency { solverSettings }
-    private val firstClickProtSettings by DropdownSetting("First Click Protect Dropdown")
-    val firstClickProt by NumberSetting("First Click Protection", 500, 350, 800, 10, unit = "ms", desc = "The amount of time after opening a terminal where clicks are blocked to prevent bans (recommended value is 500 minus your ping).").withDependency { firstClickProtSettings }
+    val hideClicked by BooleanSetting("Client Prediction", true, desc = "Visually predicts the server state before the gui update is sent to the client.").withDependency { solverSettings }
+    val terminalReloadThreshold by NumberSetting("Resolve timeout", 600, 300, 1200, 10, unit = "ms", desc = "The amount of time before the terminal reloads after a click wasn't registered while using hide clicked.").withDependency { hideClicked && solverSettings }
+    private val firstClickProtSettings by DropdownSetting("First Click Prot Dropdown")
+    val firstClickProt by NumberSetting("First Click Prot", 500, 350, 800, 10, unit = "ms", desc = "The amount of time after opening a terminal where clicks are blocked to prevent bans (recommended value is 500 minus your ping).").withDependency { firstClickProtSettings }
     val shouldFirstClickProtWithTicks by BooleanSetting("Account For Server Lag",  false, desc = "Prevents bans from clicking when the server lags after opening the terminal (disabled in singleplayer").withDependency { firstClickProtSettings }
     val firstClickProtTicks by NumberSetting("Lag Protection Ticks", 8, 7, 16, unit = "ticks", desc = "Each tick = 50ms (recommended value is 8)").withDependency { shouldFirstClickProtWithTicks && firstClickProtSettings }
-    val hideClicked by BooleanSetting("Hide Clicked", false, desc = "Visually hides your first click before a gui updates instantly to improve perceived response time. Does not affect actual click time.").withDependency { solverSettings }
-    val terminalReloadThreshold by NumberSetting("Resolve timeout", 600, 300, 1000, 10, unit = "ms", desc = "The amount of time before the terminal reloads after a click wasn't registered while using hide clicked.").withDependency { hideClicked && solverSettings }
-    private val debug by BooleanSetting("Debug", false, desc = "Shows debug terminals.").withDependency { solverSettings }
     private val showColors by DropdownSetting("Color Settings")
     val backgroundColor by ColorSetting("Background", Colors.gray26, true, desc = "Background color of the terminal solver.").withDependency { showColors }
 
@@ -61,6 +61,7 @@ object TerminalSolver : Module(
     val melodyColumColor by ColorSetting("Melody Column", Colors.MINECRAFT_DARK_PURPLE, true, desc = "Color of the column indicator for melody.").withDependency { showColors && !cancelMelodySolver }
     val melodyPointerColor by ColorSetting("Melody Pointer", Colors.MINECRAFT_GREEN, true, desc = "Color of the location for pressing for melody.").withDependency { showColors && !cancelMelodySolver }
     val melodyBackgroundColor by ColorSetting("Melody Background", Colors.gray38, true, desc = "Color of the background slot in melody.").withDependency { showColors && !cancelMelodySolver }
+    private val debug by BooleanSetting("Debug", false, desc = "Shows debug terminals.").withDependency { showColors }
 
     @JvmStatic val termSize get() = if (enabled && (renderType == 0 || renderType == 1) && TerminalUtils.currentTerm != null) if (normalTermSize == 6) Options.AUTO_GUI_SCALE else normalTermSize else 1
     val customGuiEnabled get() = enabled && renderType == 2 && renderMelody
@@ -74,16 +75,16 @@ object TerminalSolver : Module(
         on<GuiEvent.SlotClick> {
             val term = TerminalUtils.currentTerm ?: return@on
 
-            if (blockIncorrectClicks && !term.canClick(slotId, button)) return@on cancel()
+            if (blockIncorrectClicks && !term.canClick(slotIndex, button)) return@on cancel()
 
             if (term.shouldProtect()) return@on cancel()
 
             if (middleClickGUI) {
-                term.click(slotId, if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button, hideClicked && !term.isClicked)
+                term.click(slotIndex, if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button, hideClicked)
                 return@on cancel()
             }
 
-            if (hideClicked && !term.isClicked) term.simulateClick(slotId, button)
+            if (hideClicked) term.simulateClick(slotIndex, button)
         }
 
         on<GuiEvent.Render> {
@@ -130,9 +131,8 @@ object TerminalSolver : Module(
                 "§7Container ID: §f${menu.containerId}",
                 "§7Time Open: §f${System.currentTimeMillis() - term.timeOpened}ms",
                 "§7Ticks Open: §f${term.ticksOpened}",
-                "§7Is Clicked: §f${term.isClicked}",
-                "§7Window Count: §f${term.windowCount}",
                 "§7Solution: §f${term.solution.joinToString(", ")}",
+                "§7Clicked Slots: §f${term.clickedSlots}",
             )
 
             pose().pushMatrix()
@@ -140,7 +140,8 @@ object TerminalSolver : Module(
             pose().scale(1f / sf, 1f / sf)
             pose().scale(3f)
 
-            textWithWordWrap(mc.font, Component.literal(menu.items.filter { !it.isEmpty }.map { stack -> stack.hoverName.string  }.toString()), 400, 0, 300, Colors.WHITE.rgba)
+            val items = menu.items.subList(0, term.type.windowSize)
+            textWithWordWrap(mc.font, Component.literal(items.filter { !it.isEmpty && it.item != Items.BLACK_STAINED_GLASS_PANE }.map { stack -> stack.hoverName.string }.toString()), 400, 0, 300, Colors.WHITE.rgba)
 
             debugInfo.forEachIndexed { index, line ->
                 textWithWordWrap(mc.font, Component.literal(line), 5, 20 + (index * 10), 300, Colors.WHITE.rgba)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
index a82e482..34d7215 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
@@ -39,7 +39,7 @@ object TerminalSounds : Module(
         }
 
         on<GuiEvent.SlotClick> (EventPriority.HIGHEST) {
-            if (shouldReplaceSounds) playSoundForSlot(slotId, button)
+            if (shouldReplaceSounds) playSoundForSlot(slotIndex, button)
         }
 
         on<GuiEvent.CustomTermGuiClick> (EventPriority.HIGHEST) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
index 9a2c4f4..56066a5 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
@@ -66,10 +66,9 @@ abstract class TermGui {
 
     private fun currentTermScreen() = mc.screen as? AbstractContainerScreen<*>
 
-    private fun isActiveTermScreen(): Boolean {
-        if (!TerminalSolver.customGuiEnabled || TerminalUtils.currentTerm == null || currentTermScreen() == null) return false
-        return TerminalUtils.currentTerm?.type?.getGUI() === this
-    }
+    private fun isActiveTermScreen(): Boolean =
+        !(!TerminalSolver.customGuiEnabled || TerminalUtils.currentTerm == null || currentTermScreen() == null) &&
+                TerminalUtils.currentTerm?.type?.getGUI() === this
 
     private fun isTerminalOverrideKey(event: KeyEvent) =
         mc.options.keyDrop.matches(event) || mc.options.keyHotbarSlots.any { it.matches(event) }
@@ -110,7 +109,7 @@ abstract class TermGui {
             val btn = if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button
             if (term.shouldProtect()) return@let
             if (!GuiEvent.CustomTermGuiClick(screen, slotIndex, btn).postAndCatch() && term.canClick(slotIndex, btn))
-                term.click(slotIndex, btn, hideClicked && !term.isClicked)
+                term.click(slotIndex, btn, hideClicked)
         }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/MelodySim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/MelodySim.kt
index f229d94..35db4ce 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/MelodySim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/MelodySim.kt
@@ -28,7 +28,7 @@ object MelodySim : TermSimGUI(
         magentaColumn = (1..5).random()
         limeColumn = 1
         limeDirection = 1
-        createNewGui { it.generateItemStack() }
+        setSlots { it.generateItemStack() }
     }
 
     private var counter = 0
@@ -37,25 +37,26 @@ object MelodySim : TermSimGUI(
         if (counter++ % 10 != 0) return
         limeColumn += limeDirection
         if (limeColumn == 1 || limeColumn == 5) limeDirection *= -1
-        updateGui()
+        updateRow(currentRow)
         super.containerTick()
     }
 
     override fun slotClick(slot: Slot, button: Int) {
         if (slot.index % 9 != 7 || limeColumn != magentaColumn || slot.index / 9 != currentRow) return
 
+        val oldRow = currentRow
         magentaColumn = (1 until 5).random()
         currentRow++
-        updateGui()
+        updateRow(0)
+        updateRow(oldRow)
+        if (currentRow <= 4) updateRow(currentRow)
 
         if (currentRow >= 5) TerminalUtils.lastTermOpened?.onComplete()
         super.slotClick(slot, button)
     }
 
-    private fun updateGui() {
-        guiInventorySlots.forEachIndexed { index, currentStack ->
-            currentStack?.setSlot(guiInventorySlots.map { it.generateItemStack() }.getOrNull(index)?.takeIf { it != currentStack.item } ?: return@forEachIndexed)
-        }
+    private fun updateRow(row: Int) {
+        for (col in 0..8) guiInventorySlots[row * 9 + col].let { it.setSlot(it.generateItemStack()) }
     }
 
     private fun Slot.generateItemStack(): ItemStack {
@@ -69,6 +70,4 @@ object MelodySim : TermSimGUI(
             else -> blackPane
         }
     }
-}
-
-
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt
index 54e1570..0bde66a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/NumbersSim.kt
@@ -14,7 +14,7 @@ object NumbersSim : TermSimGUI(
 ) {
     override fun create() {
         val used = (1..14).shuffled().toMutableList()
-        createNewGui {
+        setSlots  {
             if (floor(it.index / 9f) in 1f..2f && it.index % 9 in 1..7) ItemStack(Items.RED_STAINED_GLASS_PANE, used.first()).apply { set(DataComponents.CUSTOM_NAME, Component.literal("§a${used.removeFirst()}")) }
             else blackPane
         }
@@ -22,10 +22,7 @@ object NumbersSim : TermSimGUI(
 
     override fun slotClick(slot: Slot, button: Int) {
         if (guiInventorySlots.minByOrNull { if (it.item.item == Items.RED_STAINED_GLASS_PANE) it.item.count else 1000 } != slot) return
-        createNewGui {
-            if (it == slot) ItemStack(Items.LIME_STAINED_GLASS_PANE, slot.item.count).apply { set(DataComponents.CUSTOM_NAME, Component.literal("")) }
-            else it.item
-        }
+        slot.setSlot(ItemStack(Items.LIME_STAINED_GLASS_PANE, slot.item.count).apply { set(DataComponents.CUSTOM_NAME, Component.literal("")) })
 
         if (guiInventorySlots.none { it?.item?.item == Items.RED_STAINED_GLASS_PANE })
             TerminalUtils.lastTermOpened?.onComplete()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt
index ff0c18c..8e53866 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/PanesSim.kt
@@ -16,13 +16,13 @@ object PanesSim : TermSimGUI(
     private val redPane   get() = ItemStack(Items.RED_STAINED_GLASS_PANE).apply { set(DataComponents.CUSTOM_NAME, Component.literal("")) }
 
     override fun create() {
-        createNewGui {
+        setSlots  {
             if (floor(it.index / 9f) in 1f..3f && it.index % 9 in 2..6) if (Math.random() > 0.75) greenPane else redPane else blackPane
         }
     }
 
     override fun slotClick(slot: Slot, button: Int) {
-        createNewGui { if (it == slot) { if (slot.item.item == Items.RED_STAINED_GLASS_PANE) greenPane else redPane } else it.item }
+        slot.setSlot(if (slot.item.item == Items.RED_STAINED_GLASS_PANE) greenPane else redPane)
 
         if (guiInventorySlots.none { it?.item?.item == Items.RED_STAINED_GLASS_PANE })
             TerminalUtils.lastTermOpened?.onComplete()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt
index b027653..fe2e5b3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/RubixSim.kt
@@ -20,7 +20,7 @@ object RubixSim : TermSimGUI(
     private val indices = listOf(12, 13, 14, 21, 22, 23, 30, 31, 32)
 
     override fun create() {
-        createNewGui {
+        setSlots  {
             if (floor(it.index / 9f) in 1f..3f && it.index % 9 in 3..5) getPane()
             else blackPane
         }
@@ -28,12 +28,7 @@ object RubixSim : TermSimGUI(
 
     override fun slotClick(slot: Slot, button: Int) {
         val current = order.find { it == ((slot.item.item as? BlockItem)?.block as? StainedGlassPaneBlock)?.color } ?: return
-        createNewGui {
-            if (it == slot) {
-                if (button == 1) genStack(order.indexOf(current) - 1)
-                else genStack((order.indexOf(current) + 1) % order.size)
-            } else it.item
-        }
+        slot.setSlot(if (button == 1) genStack(order.indexOf(current) - 1) else genStack((order.indexOf(current) + 1) % order.size))
 
         if (indices.all { guiInventorySlots[it]?.item?.item == guiInventorySlots[12]?.item?.item })
             TerminalUtils.lastTermOpened?.onComplete()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt
index 2ae636a..fc444f2 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/SelectAllSim.kt
@@ -22,7 +22,7 @@ class SelectAllSim(
 ) {
     override fun create() {
         val guaranteed = (10..16).plus(19..25).plus(28..34).plus(37..43).random()
-        createNewGui { slot ->
+        setSlots  { slot ->
             if (floor(slot.index / 9.0) in 1.0..4.0 && slot.index % 9 in 1..7) {
                 val item = ItemStack(getPossibleItems(color).random())
 
@@ -53,11 +53,9 @@ class SelectAllSim(
     override fun slotClick(slot: Slot, button: Int) {
         val possibleItems = getPossibleItems(color)
         if (!possibleItems.contains(slot.item.item)) return modMessage("§cThat item is not: ${color.name.uppercase()}!")
+        if (slot.item.hasGlint()) return modMessage("§cAlready selected!")
 
-        createNewGui {
-            if (it == slot) slot.item.apply { set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, true) }
-            else it.item
-        }
+        slot.setSlot(slot.item.apply { set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, true) })
 
         if (guiInventorySlots.none { !it.item.hasGlint() && possibleItems.contains(it.item.item) })
             TerminalUtils.lastTermOpened?.onComplete()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartGUI.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartGUI.kt
index 43a6e69..23c146d 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartGUI.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartGUI.kt
@@ -41,7 +41,7 @@ object StartGUI : TermSimGUI(
 
     override fun create() {
         val termItems = buildTermItems()
-        createNewGui {
+        setSlots {
             when (it.index) {
                 4 -> resetButton
                 13  -> randomButton
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartsWithSim.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartsWithSim.kt
index 2d7ede7..f89a29c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartsWithSim.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/StartsWithSim.kt
@@ -15,8 +15,10 @@ class StartsWithSim(private val letter: String = listOf("A", "B", "C", "G", "D",
     "What starts with: \'$letter\'?",
     TerminalTypes.STARTS_WITH.windowSize
 ) {
+    private val clickedOverrides = ArrayList<Int>()
+
     override fun create() {
-        createNewGui {
+        setSlots {
             when {
                 floor(it.index / 9f) !in 1f..3f || it.index % 9 !in 1..7 -> blackPane
                 it.index == (10..16).random() -> getLetterItemStack()
@@ -26,12 +28,14 @@ class StartsWithSim(private val letter: String = listOf("A", "B", "C", "G", "D",
         }
     }
 
-    override fun slotClick(slot: Slot, button: Int) = with(slot.item) {
-        if (!hoverName.string.startsWith(letter, true) || hasGlint()) return@with modMessage("§cThat item does not start with: \'$letter\'!")
+    override fun slotClick(slot: Slot, button: Int) {
+        if (!slot.item.hoverName.string.startsWith(letter, true)) return modMessage("§cThat item does not start with: \'$letter\'!")
+        if (slot.item.hasRealGlint() || slot.index in clickedOverrides) return modMessage("§cAlready selected!")
 
-        createNewGui { if (it == slot) apply { set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, false) } else it.item }
+        if (slot.item.item in enchantOverrides) clickedOverrides.add(slot.index)
+        slot.setSlot(slot.item.apply { set(DataComponents.ENCHANTMENT_GLINT_OVERRIDE, true) })
 
-        if (guiInventorySlots.none { it.item.hoverName.string.startsWith(letter, true) && !it.item.hasGlint() })
+        if (guiInventorySlots.all { !it.item.hoverName.string.startsWith(letter, true) || it.item.hasRealGlint() || it.index in clickedOverrides })
             TerminalUtils.lastTermOpened?.onComplete()
 
         super.slotClick(slot, button)
@@ -46,4 +50,9 @@ class StartsWithSim(private val letter: String = listOf("A", "B", "C", "G", "D",
 
         return ItemStack(matchingItem)
     }
+
+    private companion object {
+        fun ItemStack.hasRealGlint(): Boolean = hasGlint() && item !in enchantOverrides
+        val enchantOverrides = BuiltInRegistries.ITEM.filter { it.components().has(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) } + Items.GOLDEN_APPLE
+    }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
index 0d85254..50a6928 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.boss.termsim
 
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.GuiEvent
-import com.odtheking.odin.events.PacketEvent
+import com.odtheking.odin.events.ScreenCloseEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.features.impl.boss.TerminalSounds
 import com.odtheking.odin.utils.handlers.schedule
@@ -11,9 +11,6 @@ import com.odtheking.odin.utils.skyblock.dungeon.terminals.terminalhandler.Termi
 import net.minecraft.client.gui.screens.inventory.ContainerScreen
 import net.minecraft.core.component.DataComponents
 import net.minecraft.network.chat.Component
-import net.minecraft.network.protocol.game.ClientboundContainerClosePacket
-import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket
-import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket
 import net.minecraft.sounds.SoundEvents
 import net.minecraft.world.SimpleContainer
 import net.minecraft.world.entity.player.Inventory
@@ -44,12 +41,10 @@ open class TermSimGUI(
 ) {
     val blackPane = ItemStack(Items.BLACK_STAINED_GLASS_PANE).apply { set(DataComponents.CUSTOM_NAME, Component.literal("")) }
     protected val guiInventorySlots get() = menu.slots.subList(0, size)
-    private var doesAcceptClick = true
     protected var ping = 0L
-    private var syncId = 0
 
     open fun create() {
-        guiInventorySlots.forEach { it.setSlot(blackPane) }
+        setSlots { blackPane }
     }
 
     fun open(terminalPing: Long = 0L) {
@@ -63,18 +58,17 @@ open class TermSimGUI(
     }
 
     internal fun TerminalHandler.onComplete() {
-        PacketEvent.Receive(ClientboundContainerClosePacket(-2)).postAndCatch()
+        ScreenCloseEvent.postAndCatch()
         TerminalEvent.Solve(this).postAndCatch()
         StartGUI.open(ping)
     }
 
     override fun onClose() {
-        doesAcceptClick = true
         super.onClose()
     }
 
     override fun slotClicked(slot: Slot, i: Int, j: Int, clickType: ContainerInput) {
-        if (GuiEvent.SlotClick(this, slot, i, j, clickType).postAndCatch()) return
+        if (GuiEvent.SlotClick(this, i, j).postAndCatch()) return
         slot?.let { delaySlotClick(it, j) }
     }
 
@@ -85,23 +79,19 @@ open class TermSimGUI(
 
     private fun delaySlotClick(slot: Slot, button: Int) {
         if (mc.screen == StartGUI) return slotClick(slot, button)
-        if (!doesAcceptClick || slot.container != inv || slot.item.item == Items.BLACK_STAINED_GLASS_PANE) return
+        if (slot.container != inv || slot.item.item == Items.BLACK_STAINED_GLASS_PANE) return
         if (ping <= 0L) return slotClick(slot, button)
-        doesAcceptClick = false
         schedule((ping / 50).toInt().coerceAtLeast(0)) {
-            doesAcceptClick = true
             if (mc.screen == this) slotClick(slot, button)
         }
     }
 
-    protected fun createNewGui(block: (Slot) -> ItemStack) {
-        PacketEvent.Receive(ClientboundOpenScreenPacket(syncId++, MenuType.GENERIC_9x3, Component.literal(name))).postAndCatch()
+    protected fun setSlots(block: (Slot) -> ItemStack) {
         guiInventorySlots.forEach { it.setSlot(block(it)) }
     }
 
     protected fun Slot.setSlot(stack: ItemStack) {
-        GuiEvent.SlotUpdate(mc.screen ?: return, ClientboundContainerSetSlotPacket(-2, 0, index, stack), menu).postAndCatch()
-        set(stack)
+        menu.setItem(index, menu.incrementStateId(), stack)
     }
 
     protected fun playTermSimSound() {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
index bda98ea..fcd9e23 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
@@ -1,7 +1,7 @@
 package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.GuiEvent
+import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
@@ -25,9 +25,9 @@ object BreakerDisplay : Module(
     private val renderFull by BooleanSetting("Render full", false, desc = "Renders the HUD even when the Dungeon Breaker is full.")
 
     init {
-        on<GuiEvent.SlotUpdate> {
-            if (!DungeonUtils.inDungeons || packet.item.itemId != "DUNGEONBREAKER") return@on
-            packet.item.loreString.firstNotNullOfOrNull { chargesRegex.find(it) }?.let { match ->
+         on<SetSlotEvent> {
+            if (!DungeonUtils.inDungeons || itemStack.itemId != "DUNGEONBREAKER") return@on
+            itemStack.loreString.firstNotNullOfOrNull { chargesRegex.find(it) }?.let { match ->
                 charges = match.groupValues[1].toIntOrNull() ?: 0
                 maxCharges = match.groupValues[2].toIntOrNull() ?: 0
             }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
index c58eb32..845af6b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
@@ -9,6 +9,7 @@ import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.ScreenEvent
+import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
@@ -137,7 +138,7 @@ object Croesus : Module(
             }
         }
 
-        on<GuiEvent.SlotUpdate> {
+        on<SetSlotEvent> {
             val screenTitle = mc.screen?.title?.string ?: return@on
             val items = (mc.screen as? AbstractContainerScreen<*>)?.menu?.items ?: return@on
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
index 5f9d1c4..8548dcc 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
@@ -6,6 +6,7 @@ import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.ScreenEvent
+import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.*
@@ -172,11 +173,11 @@ object LeapMenu : Module(
                 leapedRegex.find(value)?.groupValues?.get(1)?.let { sendCommand("pc Leaped to ${it}!") }
         }
 
-        on<GuiEvent.SlotUpdate> {
+        on<SetSlotEvent> {
             currentLeapScreen()?.let {
-                if (packet.item.isEmpty || packet.item.item != Items.PLAYER_HEAD) return@on
-                val (name) = playerNameRegex.find(packet.item.hoverName.string)?.destructured ?: return@on
-                leapIndex[name] = packet.slot
+                if (itemStack.isEmpty || itemStack.item != Items.PLAYER_HEAD) return@on
+                val (name) = playerNameRegex.find(itemStack.hoverName.string)?.destructured ?: return@on
+                leapIndex[name] = slotIndex
             }
         }
 
@@ -198,7 +199,7 @@ object LeapMenu : Module(
         val index = screenHandler.menu.slots.subList(11, 16).firstOrNull {
             it.item.hoverName.string.substringAfter(' ').equals(name.noControlCodes, true)
         }?.index ?: return
-        mc.player?.clickSlot(screenHandler.menu.containerId, index)
+        mc.player?.clickSlot(index)
         modMessage("Teleporting to $name.")
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
index 6e973ed..282d819 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
@@ -19,7 +19,7 @@ object BlazeSolver {
     private var blazes = mutableListOf<ArmorStand>()
     private var roomType = 0
     private var lastBlazeCount = 10
-    private val blazeHealthRegex = Regex("^\\[Lv15]  Blaze [\\d,]+/([\\d,]+)❤$")
+    private val blazeHealthRegex = Regex("^\\[Lv11]  Blaze [\\d,]+/([\\d,]+)❤$")
 
     fun getBlaze() {
         if (!DungeonUtils.inDungeons || DungeonUtils.currentRoom?.data?.name?.equalsOneOf("Lower Blaze", "Higher Blaze") == false) return
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/RemovePerks.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/RemovePerks.kt
index a1b6c40..69c0c5b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/RemovePerks.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/RemovePerks.kt
@@ -21,7 +21,7 @@ object RemovePerks : Module(
         }
 
         on<GuiEvent.SlotClick> {
-            if (screen is AbstractContainerScreen<*> && screen.title.string == "Perk Menu" && slotCheck(screen.menu.getSlot(slotId).item.hoverName.string))
+            if (screen is AbstractContainerScreen<*> && screen.title.string == "Perk Menu" && slotCheck(screen.menu.getSlot(slotIndex).item.hoverName.string))
                 cancel()
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
index 247fe8c..96d53d3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
@@ -3,8 +3,9 @@ package com.odtheking.odin.features.impl.nether
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.GuiEvent
-import com.odtheking.odin.events.ScreenEvent
+import com.odtheking.odin.events.SetSlotEvent
 import com.odtheking.odin.events.core.on
+import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.features.impl.dungeon.Croesus.cachedPrices
 import com.odtheking.odin.utils.*
@@ -14,6 +15,7 @@ import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.network.chat.Component
 import net.minecraft.network.chat.Style
 import net.minecraft.network.chat.TextColor
+import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket
 import net.minecraft.world.item.ItemStack
 import net.minecraft.world.item.Items
 import java.util.*
@@ -74,16 +76,14 @@ object Vesuvius : Module(
             }
         }
 
-        on<GuiEvent.SlotUpdate> {
-            if (!screen.title.string.matches(chestRegex)) return@on
+        on<SetSlotEvent> {
+            if (mc.screen?.title?.string?.matches(chestRegex) != true) return@on
 
-            val item = packet.item
-
-            if (packet.slot == 31 && item.item == Items.CHEST) handleKuudraChest(item)
-            if (packet.slot.equalsOneOf(13, 14) && item.item == Items.PLAYER_HEAD) handleKuudraChest(item)
+            if (slotIndex == 31 && itemStack.item == Items.CHEST) handleKuudraChest(itemStack)
+            if (slotIndex.equalsOneOf(13, 14) && itemStack.item == Items.PLAYER_HEAD) handleKuudraChest(itemStack)
         }
 
-        on<ScreenEvent.Open> {
+        onReceive<ClientboundOpenScreenPacket> {
             currentChest = null
         }
     }
@@ -101,10 +101,10 @@ object Vesuvius : Module(
                 when (style.color) {
                     TextColor.fromLegacyFormat(ChatFormatting.GOLD) -> starCount += count
                     TextColor.fromLegacyFormat(ChatFormatting.LIGHT_PURPLE) -> starCount += count * 2
-                    }
                 }
+            }
             Optional.empty<Unit>()
-            }, Style.EMPTY)
+        }, Style.EMPTY)
 
         val item = component.string.replace("✪", "").trim()
 
@@ -178,9 +178,9 @@ object Vesuvius : Module(
             if (string.matches(uselessLinesRegex)) return@forEach
 
             val price = parseItemValue(component) ?: 0.0
-                profit += price
-                chestItems.add(ChestItem(component, price))
-            }
+            profit += price
+            chestItems.add(ChestItem(component, price))
+        }
         currentChest = ChestData(chestItems, chestCost, (profit - chestCost))
     }
 
@@ -193,7 +193,7 @@ object Vesuvius : Module(
         val profit = "%,.0f".format(dataToDisplay?.profit)
 
         dataToDisplay?.items?.forEach { item ->
-            val price: String = "%,.0f".format(item.price)
+            val price = "%,.0f".format(item.price)
 
             text(mc.font,item.name, 0, yOffset, -1)
             text(price, maxWidth - mc.font.width(price), yOffset, Colors.MINECRAFT_GRAY)
@@ -258,7 +258,7 @@ object Vesuvius : Module(
             ),
             ChestItem(
                 Component.literal("Crimson Essence").withStyle(ChatFormatting.LIGHT_PURPLE)
-                        .append(Component.literal(" x2000").withStyle(ChatFormatting.DARK_GRAY)),
+                    .append(Component.literal(" x2000").withStyle(ChatFormatting.DARK_GRAY)),
                 2420000.0
             ),
             ChestItem(
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
index 82ca16e..27ae14f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
@@ -68,7 +68,7 @@ object LoadoutKeybinds : Module(
             }
         }
 
-        mc.player?.clickSlot(screen.menu.containerId, index)
+        mc.player?.clickSlot(index)
         return true
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
index c00401f..3fb6b63 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
@@ -74,7 +74,7 @@ object PetKeybinds : Module(
             else if (nounequip) return false
         }
 
-        mc.player?.clickSlot(screen.menu.containerId, index)
+        mc.player?.clickSlot(index)
         return true
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
index 05a96c1..e359784 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
@@ -2,6 +2,7 @@ package com.odtheking.odin.features.impl.skyblock
 
 import com.odtheking.odin.clickgui.settings.impl.*
 import com.odtheking.odin.events.GuiEvent
+import com.odtheking.odin.events.ScreenCloseEvent
 import com.odtheking.odin.events.ScreenEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -48,7 +49,7 @@ object SlotBinds : Module(
                 else -> return@on
             }
 
-            mc.player?.clickSlot(screen.menu.containerId, from, to % 36, ContainerInput.SWAP)
+            mc.player?.clickSlot(from, to % 36, ContainerInput.SWAP)
             cancel()
         }
 
@@ -99,7 +100,7 @@ object SlotBinds : Module(
             guiGraphics.drawLine(startX.toFloat(), startY.toFloat(), endX.toFloat(), endY.toFloat(), lineColor, lineWidth)
         }
 
-        on<ScreenEvent.Close> {
+        on<ScreenCloseEvent> {
             previousSlot = null
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt
index c87bffc..fe7f6f4 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt
@@ -67,7 +67,7 @@ object WardrobeKeybinds : Module(
             }
         }
         if (disallowUnequippingEquipped && screen.menu.slots[index].item.isEmpty) return false
-        mc.player?.clickSlot(screen.menu.containerId, index)
+        mc.player?.clickSlot(index)
         return true
     }
 }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/Utils.kt b/src/main/kotlin/com/odtheking/odin/utils/Utils.kt
index 4021341..599306f 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/Utils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/Utils.kt
@@ -171,8 +171,8 @@ fun BlockPos.getBlockBounds() =
         level.getBlockState(this).getShape(level, this).singleEncompassing().takeIf { !it.isEmpty }?.bounds()
     }
 
-fun Player.clickSlot(containerId: Int, slotIndex: Int, button: Int = 0, containerInput: ContainerInput = ContainerInput.PICKUP) {
-    mc.gameMode?.handleContainerInput(containerId, slotIndex, button, containerInput, this)
+fun Player.clickSlot(slotIndex: Int, button: Int = 0, containerInput: ContainerInput = ContainerInput.PICKUP) {
+    mc.gameMode?.handleContainerInput(containerMenu.containerId, slotIndex, button, containerInput, this)
 }
 
 fun formatNumber(numStr: String): String {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalTypes.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalTypes.kt
index e3ec727..25271e0 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalTypes.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalTypes.kt
@@ -46,25 +46,25 @@ enum class TerminalTypes(
         override fun getGUI() = gui
     };
 
-    fun openHandler(guiName: String): TerminalHandler? {
-        return when (this) {
+    companion object {
+        fun openHandler(guiName: String): TerminalHandler? = when (entries.find { it.regex.matches(guiName) }) {
             PANES -> PanesHandler()
             RUBIX -> RubixHandler()
             NUMBERS -> NumbersHandler()
-            STARTS_WITH -> StartsWithHandler(regex.find(guiName)?.groupValues?.get(1) ?: run {
+            STARTS_WITH -> StartsWithHandler(STARTS_WITH.regex.find(guiName)?.groupValues?.get(1) ?: run {
                 modMessage("Failed to find letter, please report this!")
                 return null
             })
             SELECT -> {
                 SelectAllHandler(DyeColor.entries.find {
-                    it.name.replace("_", " ")
-                        .equals(regex.find(guiName)?.groupValues?.get(1)?.replace("SILVER", "LIGHT GRAY"), true)
+                    it.name.replace("_", " ").equals(SELECT.regex.find(guiName)?.groupValues?.get(1)?.replace("SILVER", "LIGHT GRAY"), true)
                 } ?: run {
                     modMessage("Failed to find letter, please report this!")
                     return null
                 })
             }
             MELODY -> MelodyHandler()
+            null -> return null
         }
     }
 }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt
index 7bb297c..7b6342e 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt
@@ -1,10 +1,7 @@
 package com.odtheking.odin.utils.skyblock.dungeon.terminals
 
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.ChatMessageEvent
-import com.odtheking.odin.events.GuiEvent
-import com.odtheking.odin.events.TerminalEvent
-import com.odtheking.odin.events.TickEvent
+import com.odtheking.odin.events.*
 import com.odtheking.odin.events.core.EventPriority
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
@@ -14,14 +11,14 @@ import com.odtheking.odin.features.impl.boss.termsim.TermSimGUI
 import com.odtheking.odin.utils.devMessage
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.terminalhandler.TerminalHandler
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
-import net.minecraft.network.protocol.game.*
+import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket
+import net.minecraft.network.protocol.game.ServerboundContainerClickPacket
 import net.minecraft.world.inventory.ContainerInput
 import net.minecraft.world.item.ItemStack
 
 object TerminalUtils {
 
     private val termSolverRegex = Regex("^(.{1,16}) activated a terminal! \\((\\d)/(\\d)\\)$")
-    private var lastClickTime = 0L
 
     @JvmStatic var currentTerm: TerminalHandler? = null
         private set
@@ -29,42 +26,34 @@ object TerminalUtils {
         private set
 
     init {
-        onReceive<ClientboundOpenScreenPacket> (EventPriority.HIGHEST) {
-            val windowName = title.string
-            currentTerm?.let { if (!it.isClicked && it.windowCount <= 2) leftTerm() }
-            val newType = TerminalTypes.entries.find { it.regex.matches(windowName) } ?: return@onReceive
-
-            if (newType != currentTerm?.type) newType.openHandler(windowName)?.let {
+        on<ScreenEvent.Open> (EventPriority.HIGHEST) {
+            TerminalTypes.openHandler(screen.title.string)?.let {
                 devMessage("§aNew terminal: §6${it.type.name}")
                 currentTerm = it
                 TerminalEvent.Open(it).postAndCatch()
                 lastTermOpened = it
             }
-            currentTerm?.openScreen()
         }
 
-        on<GuiEvent.SlotUpdate> {
-            currentTerm?.updateSlot(this)
+        on<ScreenCloseEvent> {
+            currentTerm?.let {
+                devMessage("§cLeft terminal: §6${it.type.name}")
+                currentTerm = null
+                TerminalEvent.Close(it).postAndCatch()
+            }
         }
 
-        onReceive<ClientboundContainerClosePacket> { leftTerm() }
-        onSend<ServerboundContainerClosePacket> { leftTerm() }
-
-        onSend<ServerboundContainerClickPacket> {
-            lastClickTime = System.currentTimeMillis()
-            currentTerm?.isClicked = true
+        on<SetSlotEvent> {
+            if (menu !== (mc.screen as? AbstractContainerScreen<*>)?.menu) return@on
+            currentTerm?.updateSlot(this)
         }
 
         on<TickEvent.End> {
-            val term = currentTerm ?: return@on
-            if (System.currentTimeMillis() - lastClickTime >= TerminalSolver.terminalReloadThreshold && term.isClicked) {
-                val screen = (mc.screen as? AbstractContainerScreen<*>) ?: return@on
-                GuiEvent.SlotUpdate(
-                    screen,
-                    ClientboundContainerSetSlotPacket(screen.menu.containerId, 0, term.type.windowSize - 1, ItemStack.EMPTY),
-                    screen.menu
-                ).postAndCatch()
-                term.isClicked = false
+            currentTerm?.let { term ->
+                if (term.clickedSlots.isNotEmpty() && System.currentTimeMillis() - term.lastClickTime >= TerminalSolver.terminalReloadThreshold) {
+                    term.clickedSlots.clear()
+                    (mc.screen as? AbstractContainerScreen<*>)?.menu?.let { SetSlotEvent(0, ItemStack.EMPTY, it.slots, it).postAndCatch() }
+                }
             }
         }
 
@@ -76,11 +65,6 @@ object TerminalUtils {
             }
         }
 
-        on<GuiEvent.SlotClick> (EventPriority.HIGH) {
-            lastClickTime = System.currentTimeMillis()
-            currentTerm?.isClicked = true
-        }
-
         onSend<ServerboundContainerClickPacket> (EventPriority.LOW) {
             val termSimScreen = mc.screen as? TermSimGUI ?: return@onSend
             if (containerInput != ContainerInput.PICKUP_ALL) termSimScreen.clickIndex(slotNum.toInt(), buttonNum.toInt())
@@ -94,12 +78,4 @@ object TerminalUtils {
             it.cancel()
         }
     }
-
-    private fun left
…… diff 过长已截断 ……
Added dungeon core logging for future useodtheking · 2026-08-31 · e7719c8
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt | 15 +++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt   |  6 ++++++
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt   |  1 +
 3 files changed, 22 insertions(+)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
index 929402e..c0a2cb2 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
@@ -1,5 +1,7 @@
 package com.odtheking.odin.features.impl.dungeon.map
 
+import com.google.gson.reflect.TypeToken
+import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.FloorEnterEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.MapUpdateEvent
@@ -10,7 +12,9 @@ import com.odtheking.odin.features.impl.dungeon.map.tile.*
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.IVec2
+import com.odtheking.odin.utils.JsonResourceLoader.defaultGson
 import com.odtheking.odin.utils.skyblock.dungeon.Floor
+import java.io.File
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.CopyOnWriteArrayList
 
@@ -34,6 +38,17 @@ object DungeonScan {
 
     val directions = arrayOf(1 to 0, -1 to 0, 0 to 1, 0 to -1)
 
+    private val roomCoresFile = File(mc.gameDirectory, "config/odin/room-cores.json")
+    val roomCores: MutableMap<String, MutableList<Map<Int, List<String>>>> = runCatching {
+        defaultGson.fromJson<MutableMap<String, MutableList<Map<Int, List<String>>>>>(roomCoresFile.readText(), object : TypeToken<MutableMap<String, MutableList<Map<Int, List<String>>>>>() {}.type)
+    }.getOrNull() ?: mutableMapOf()
+
+    fun recordRoomCore(name: String, core: Int, blocks: List<String>) {
+        roomCores.getOrPut(name) { mutableListOf() }.add(mapOf(core to blocks))
+        roomCoresFile.parentFile.mkdirs()
+        roomCoresFile.writeText(defaultGson.toJson(roomCores))
+    }
+
     init {
         on<LevelEvent.Load> { reset() }
         on<FloorEnterEvent> { initClient(floor) }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
index 1d63a86..e56fbe6 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
@@ -9,6 +9,7 @@ import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
 import com.odtheking.odin.features.impl.dungeon.map.tile.RoomData
 import com.odtheking.odin.features.impl.dungeon.map.tile.RoomShape
+import com.odtheking.odin.features.impl.render.ClickGUIModule
 import com.odtheking.odin.utils.IVec2
 import com.odtheking.odin.utils.devMessage
 import com.odtheking.odin.utils.equalsOneOf
@@ -106,6 +107,8 @@ object WorldScan {
             else return devMessage("Unknown room data for core: $core $chunkPosition")
         }
 
+        if (ClickGUIModule.dungeonCoresLogging) DungeonScan.recordRoomCore(data.name, core, getRoomCoreBlocks(chunk, (chunkPosition * 16) + 7))
+
         val tilePosition = (chunkPosition / 2) + 6
         val tile = DungeonScan.tiles.getOrNull(tilePosition.x + (tilePosition.z * 6)) ?: return
 
@@ -166,6 +169,9 @@ object WorldScan {
         return stringBuilder.toString().hashCode() to highestBlock
     }
 
+    fun getRoomCoreBlocks(chunk: LevelChunk, position: IVec2): List<String> =
+        (160 downTo 12).map { y -> chunk.getBlockState(position.x, y, position.z).block.descriptionId }
+
     private fun LevelChunk.getBlockState(x: Int, y: Int, z: Int): BlockState {
         val sectionIndex = getSectionIndex(y)
         if (sectionIndex >= 0 && sectionIndex < sections.size) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
index 1c5d9f2..9d02e47 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
@@ -43,6 +43,7 @@ object ClickGUIModule : Module(
 
     private val action by ActionSetting("Open HUD Editor", desc = "Opens the HUD editor when clicked.") { mc.setScreen(HudManager) }
     val devMessage by BooleanSetting("Developer Message", false, desc = "Sends development related messages to the chat.")
+    val dungeonCoresLogging by BooleanSetting("Core loggings", false, desc = "")
 
     private var firstJoin by BooleanSetting("First join", true, "").hide()
 
Moved some events to main threadodtheking · 2026-08-31 · 499a7dd
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt | 10 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt        | 11 +++++------
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt        |  5 ++---
 3 files changed, 12 insertions(+), 14 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
index a9ffa25..bda98ea 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BreakerDisplay.kt
@@ -1,14 +1,14 @@
 package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.core.onReceive
+import com.odtheking.odin.events.GuiEvent
+import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.itemId
 import com.odtheking.odin.utils.loreString
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket
 
 object BreakerDisplay : Module(
     name = "Breaker Display",
@@ -25,9 +25,9 @@ object BreakerDisplay : Module(
     private val renderFull by BooleanSetting("Render full", false, desc = "Renders the HUD even when the Dungeon Breaker is full.")
 
     init {
-        onReceive<ClientboundContainerSetSlotPacket> {
-            if (!DungeonUtils.inDungeons || item.itemId != "DUNGEONBREAKER") return@onReceive
-            item.loreString.firstNotNullOfOrNull { chargesRegex.find(it) }?.let { match ->
+        on<GuiEvent.SlotUpdate> {
+            if (!DungeonUtils.inDungeons || packet.item.itemId != "DUNGEONBREAKER") return@on
+            packet.item.loreString.firstNotNullOfOrNull { chargesRegex.find(it) }?.let { match ->
                 charges = match.groupValues[1].toIntOrNull() ?: 0
                 maxCharges = match.groupValues[2].toIntOrNull() ?: 0
             }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
index 408f17e..c58eb32 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
@@ -8,6 +8,7 @@ import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
+import com.odtheking.odin.events.ScreenEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
@@ -22,8 +23,6 @@ import kotlinx.coroutines.launch
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
 import net.minecraft.network.chat.Component
-import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket
-import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket
 import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket
 import net.minecraft.world.item.ItemStack
 import net.minecraft.world.item.Items
@@ -138,9 +137,9 @@ object Croesus : Module(
             }
         }
 
-        onReceive<ClientboundContainerSetSlotPacket> {
-            val screenTitle = mc.screen?.title?.string ?: return@onReceive
-            val items = (mc.screen as? AbstractContainerScreen<*>)?.menu?.items ?: return@onReceive
+        on<GuiEvent.SlotUpdate> {
+            val screenTitle = mc.screen?.title?.string ?: return@on
+            val items = (mc.screen as? AbstractContainerScreen<*>)?.menu?.items ?: return@on
 
             when {
                 screenTitle.matches(chestNameRegex) -> handleChestContents(items)
@@ -148,7 +147,7 @@ object Croesus : Module(
             }
         }
 
-        onReceive<ClientboundOpenScreenPacket> {
+        on<ScreenEvent.Open> {
             mostProfitableSlots = emptySet()
             currentChestProfit = null
             chestData = emptyList()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
index cecd313..247fe8c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
@@ -3,8 +3,8 @@ package com.odtheking.odin.features.impl.nether
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.GuiEvent
+import com.odtheking.odin.events.ScreenEvent
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.features.impl.dungeon.Croesus.cachedPrices
 import com.odtheking.odin.utils.*
@@ -14,7 +14,6 @@ import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.network.chat.Component
 import net.minecraft.network.chat.Style
 import net.minecraft.network.chat.TextColor
-import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket
 import net.minecraft.world.item.ItemStack
 import net.minecraft.world.item.Items
 import java.util.*
@@ -84,7 +83,7 @@ object Vesuvius : Module(
             if (packet.slot.equalsOneOf(13, 14) && item.item == Items.PLAYER_HEAD) handleKuudraChest(item)
         }
 
-        onReceive<ClientboundOpenScreenPacket> {
+        on<ScreenEvent.Open> {
             currentChest = null
         }
     }
Change Chat message event to fabric's dispatcherodtheking · 2026-08-31 · 31fe79a
查看变更
src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt                               |  4 ++--
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                               |  2 +-
 src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt                            | 14 ++++++--------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt                   |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt                     |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt              |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt                     |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt                    |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt                  |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt                      |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt                 |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt                  |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt                     |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt                  |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt           |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt                   |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt                     |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt               |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt                |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt          |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt                    |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt                       |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt               |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretsCounter.kt              |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/nether/FreshTools.kt                   |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/nether/Misc.kt                         |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/nether/NoPre.kt                        |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt                 |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt               |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt                    |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt               |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Ragnarock.kt                  |  4 ++--
 src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt                                 |  4 ++--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/KuudraUtils.kt                        |  4 ++--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/PartyUtils.kt                         |  4 ++--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt                      |  4 ++--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt            |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt    |  4 ++--
 39 files changed, 75 insertions(+), 77 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
index 5ad900a..430f07d 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
@@ -4,7 +4,7 @@ import com.github.stivais.commodore.Commodore
 import com.github.stivais.commodore.utils.GreedyString
 import com.odtheking.odin.OdinMod
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.features.ModuleManager
 import com.odtheking.odin.features.impl.boss.MelodyMessage.melodyWebSocket
 import com.odtheking.odin.features.impl.boss.WitherDragonState
@@ -53,7 +53,7 @@ val devCommand = Commodore("oddev") {
     }
 
     literal("simulate").runs { greedyString: GreedyString ->
-        ChatPacketEvent(greedyString.string, Component.literal(greedyString.string)).postAndCatch()
+        ChatMessageEvent(greedyString.string, Component.literal(greedyString.string)).postAndCatch()
         modMessage("§8Simulated message: ${greedyString.string}")
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index 5a2404a..bef5a68 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -32,7 +32,7 @@ class BlockInteractEvent(val pos: BlockPos) : CancellableEvent()
 class EntityInteractEvent(val pos: Vec3, val entity: Entity) : CancellableEvent()
 class UseItemOnPostEvent(val hand: InteractionHand, val hitResult: BlockHitResult, val interactionResult: InteractionResult) : Event
 
-class ChatPacketEvent(val value: String, val component: Component) : Event // mixin instead of packet (still needs to run before vanilla processing for hideMessage()
+class ChatMessageEvent(val value: String, val component: Component) : Event // mixin instead of packet (still needs to run before vanilla processing for hideMessage()
 class MessageSentEvent(val message: String) : CancellableEvent()
 
 class RenderBossBarEvent(val bossBar: BossEvent) : CancellableEvent()
diff --git a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
index aa16c18..46e3601 100644
--- a/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/EventDispatcher.kt
@@ -3,7 +3,6 @@ package com.odtheking.odin.events
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.events.core.onSend
-import com.odtheking.odin.utils.ChatManager
 import com.odtheking.odin.utils.containsOneOf
 import com.odtheking.odin.utils.equalsOneOf
 import com.odtheking.odin.utils.noControlCodes
@@ -17,7 +16,10 @@ import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderEvents
 import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents
 import net.fabricmc.fabric.api.client.screen.v1.ScreenKeyboardEvents
 import net.fabricmc.fabric.api.client.screen.v1.ScreenMouseEvents
-import net.minecraft.network.protocol.game.*
+import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket
+import net.minecraft.network.protocol.game.ClientboundSoundPacket
+import net.minecraft.network.protocol.game.ClientboundTakeItemEntityPacket
+import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket
 import net.minecraft.sounds.SoundEvents
 import net.minecraft.world.InteractionHand
 import net.minecraft.world.entity.item.ItemEntity
@@ -54,9 +56,9 @@ object EventDispatcher {
             }
         }
 
-        ClientReceiveMessageEvents.ALLOW_GAME.register { text, overlay ->
+        ClientReceiveMessageEvents.ALLOW_GAME.register { message, overlay ->
             if (overlay) return@register true
-            !ChatManager.shouldCancelMessage(text)
+            !ChatMessageEvent(message.string.noControlCodes, message).postAndCatch()
         }
 
         onReceive<ClientboundTakeItemEntityPacket> {
@@ -91,10 +93,6 @@ object EventDispatcher {
 
             if (isSecret(blockState, hitResult.blockPos)) SecretPickupEvent.Interact(hitResult.blockPos, blockState).postAndCatch()
         }
-
-        onReceive<ClientboundSystemChatPacket> {
-            if (!overlay) ChatPacketEvent(content.string.noControlCodes, content).postAndCatch()
-        }
     }
 
     private val dungeonItemDrops = listOf(
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
index b3bf60e..e5418ef 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
@@ -5,7 +5,7 @@ import com.odtheking.odin.clickgui.settings.impl.ActionSetting
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.events.BlockUpdateEvent
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
@@ -102,7 +102,7 @@ object ArrowsDevice : Module(
             optimalAimPositions = emptyList()
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (DungeonUtils.getF7Phase() != M7Phases.P3 || !isPlayerInRoom || isDeviceComplete) return@on
             if (deviceCompleteRegex.find(value)?.groupValues?.get(1) == mc.player?.name?.string) onComplete("Chat")
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
index 5fc2aa0..aadca87 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -56,7 +56,7 @@ object ExtraStats : Module(
     )
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!DungeonUtils.inDungeons) return@on
 
             if (cancelRegexes.any { it.matches(value) }) hideMessage()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
index 5b06ed4..e2c377a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
@@ -67,7 +67,7 @@ object InactiveWaypoints : Module(
             }?.toSet().orEmpty()
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!DungeonUtils.inBoss) return@on
 
             when {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
index 9bbac68..a830c79 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.TickEvent
@@ -47,7 +47,7 @@ object KingRelics : Module(
     private val relicPBs = PersonalBest(this, "Relics")
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (DungeonUtils.getF7Phase() != M7Phases.P5 || !relicPickupRegex.matches(value)) return@on
             relicPlaceTick = serverTickCounter
             relicTicksToSpawn = relicSpawnTicks
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt
index 8aad876..e80cdea 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/LividSolver.kt
@@ -41,7 +41,7 @@ object LividSolver : Module(
     private var invulnTime = 0
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!DungeonUtils.inDungeons || !DungeonUtils.isFloor(5)) return@on
             if (value.matches(lividStartRegex)) invulnTime = 390
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
index fe8829d..c0697d6 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
@@ -4,7 +4,7 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
 import com.odtheking.odin.clickgui.settings.impl.StringSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.TerminalEvent
@@ -76,7 +76,7 @@ object MelodyMessage : Module(
             if (melodySendCoords) sendCommand("od sendcoords")
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (broadcast || melodyProgress) onChatMessage(value)
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
index 70b8f5f..47bd726 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SimonSays.kt
@@ -52,7 +52,7 @@ object SimonSays : Module(
             startClickCounter = 0
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (value == "[BOSS] Goldor: Who dares trespass into my domain?") startClickCounter = 0
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
index 97afc12..a82e482 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSounds.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.core.EventPriority
@@ -51,7 +51,7 @@ object TerminalSounds : Module(
                 it.cancel()
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!DungeonUtils.inDungeons || !shouldReplaceSounds) return@on
             when {
                 value.matches(gateRegex) -> playSoundAtPlayer(SoundEvents.NOTE_BLOCK_PLING.value())
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
index ec50781..5f431ca 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.impl.ActionSetting
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.TickEvent
@@ -45,7 +45,7 @@ object TerminalTimes : Module(
             pbs.time(terminal.type.name, (System.currentTimeMillis() - terminal.timeOpened) / 1000f, "s§7!", "§a${terminal.type.termName}${if (mc.screen is TermSimGUI) " §7(termsim)" else ""} §7solved in §6", sendMessage = terminalTimes)
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!terminalSplits) return@on
             terminalCompleteRegex.find(value)?.destructured?.let { (name, activated, type, current, total) ->
                 hideMessage()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 1cf0f45..311b2c3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -3,7 +3,7 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.HudElement
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
@@ -101,7 +101,7 @@ object TickTimers : Module(
     }
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             when {
                 value.matches(MORT_REGEX) -> secretsCounter = 0
                 value.matches(necronRegex) -> necronTime = 60
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
index f29c765..f9d2e9f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
@@ -87,7 +87,7 @@ object WitherDragons : Module(
                 WitherDragonsEnum.entries.find { it.statuePos == pos }?.setDead(false)
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (DungeonUtils.getF7Phase() != M7Phases.P5 || !witherKingRegex.matches(value)) return@on
             (DragonCheck.lastDragonDeath ?: WitherDragonsEnum.entries.find { it.state != WitherDragonState.DEAD })
                 ?.apply {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt
index a3bed0a..a97e24a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt
@@ -7,7 +7,7 @@ import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
 import com.odtheking.odin.commands.fetchAndDisplayCataStats
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.formatNumber
@@ -47,7 +47,7 @@ object BetterPartyFinder : Module(
     private val kickedList = mutableSetOf<String>()
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!statsDisplay && !autoKickToggle) return@on
             val (name) = pfRegex.find(value)?.destructured ?: return@on
             if (name == mc.player?.name?.string) return@on
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt
index d944758..a02e452 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BloodCamp.kt
@@ -100,7 +100,7 @@ object BloodCamp : Module(
             }
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!DungeonUtils.inClear) return@on
             if (BLOOD_START_REGEX.matches(value)) startTime = currentTickTime
             else if (BLOOD_MOVE_REGEX.matches(value)) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
index 7585479..408f17e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
@@ -6,7 +6,7 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.ActionSetting
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
@@ -164,7 +164,7 @@ object Croesus : Module(
             }
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (DungeonUtils.inBoss && value.matches(extraStatsRegex)) {
                 currentChestCount++
                 if (currentChestCount > chestWarning) alert("§cChest limit reached!")
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
index ce60465..f162426 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
@@ -45,7 +45,7 @@ object DoorHighlight : Module(
     private val bloodDoorOpenRegex = Regex("^The BLOOD DOOR has been opened!$")
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!DungeonUtils.inClear) return@on
             when {
                 witherKeyObtainRegex.matches(value) || witherKeyPickedUpRegex.matches(value) -> witherKeys++
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
index 7ec5cad..1a5c33b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
@@ -3,7 +3,7 @@ package com.odtheking.odin.features.impl.dungeon
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.PartyEvent
 import com.odtheking.odin.events.core.on
@@ -37,7 +37,7 @@ object DungeonQueue : Module(
     var disableRequeue = false
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             when {
                 announceKick && (value.matches(kickedJoiningRegex) || value.matches(kickedInstanceRegex)) -> sendCommand("pc I was kicked!")
                 value.matches(enterRegex) -> warpTimer = System.currentTimeMillis() + 30_000L
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
index 80d7769..1e28d47 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
@@ -4,7 +4,7 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.TickEvent
@@ -84,7 +84,7 @@ object InvincibilityTimer : Module(
             InvincibilityType.entries.forEach { it.tick() }
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (onlyInDungeons && !DungeonUtils.inDungeons) return@on
             InvincibilityType.entries.firstOrNull { type -> value.matches(type.regex) }?.let { type ->
                 val seconds = mc.player?.getItemBySlot(EquipmentSlot.HEAD)?.loreString?.reversed()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
index c7c7509..5f9d1c4 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.ScreenEvent
@@ -167,7 +167,7 @@ object LeapMenu : Module(
             })
         )
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (leapAnnounce && DungeonUtils.inDungeons)
                 leapedRegex.find(value)?.groupValues?.get(1)?.let { sendCommand("pc Leaped to ${it}!") }
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
index 1c02e1c..ed997fe 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.clickgui.settings.impl.ActionSetting
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
@@ -40,7 +40,7 @@ object Mimic : Module(
             mimicKilled()
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (value.matches(princeRegex)) princeKilled()
             if (value.matches(batRegex)) batKilled()
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt
index b69969b..3d73014 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.SecretPickupEvent
@@ -59,7 +59,7 @@ object SecretClicked : Module(
             secretChime()
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (value == "That chest is locked!") clickedSecretsList.lastOrNull()?.locked = true
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretsCounter.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretsCounter.kt
index 703c677..872576b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretsCounter.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretsCounter.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.OdinMod.scope
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -32,7 +32,7 @@ object SecretsCounter : Module(
             snapshotDone = false
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (dungeonEndRegex.containsMatchIn(value)) schedule(30) { fetchAndDisplay() }
 
             if (!dungeonStartRegex.containsMatchIn(value) || !secretsEnabled || snapshotDone) return@on
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index f96139b..fb832b0 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -150,7 +150,7 @@ object PuzzleSolvers : Module(
             if (tpMaze) TPMazeSolver.tpPacket(this)
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!DungeonUtils.inClear) return@on
             if (draftPrompt && isInPuzzle) failRegex.find(value)?.destructured?.let {
                 modMessage("§7Click §ehere §7to fetch architect's draft", chatStyle = Style.EMPTY
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/FreshTools.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/FreshTools.kt
index 3a981c9..72dfb1e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/FreshTools.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/FreshTools.kt
@@ -1,7 +1,7 @@
 package com.odtheking.odin.features.impl.nether
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.render.text
@@ -43,7 +43,7 @@ object FreshTools : Module(
     private val ownFreshRegex = Regex("^Your Fresh Tools Perk bonus doubles your building speed for the next 10 seconds!$")
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (notifyFresh && KuudraUtils.inKuudra && ownFreshRegex.matches(value))
                 sendCommand("pc FRESH")
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Misc.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Misc.kt
index ad498a5..e3d50c0 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Misc.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Misc.kt
@@ -3,7 +3,7 @@ package com.odtheking.odin.features.impl.nether
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.handlers.schedule
@@ -22,7 +22,7 @@ object Misc : Module(
     private val endStoneRegex = Regex("^Used Extreme Focus! \\((\\d+) Mana\\)$")
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (manaDrain) endStoneRegex.find(value)?.groupValues?.getOrNull(1)?.let { mana ->
                 val players =
                     mc.level?.players()?.filter { it.distanceToSqr(mc.player ?: return@on) < 49 && it.uuid.version() == 4 } ?: return@on
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/NoPre.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/NoPre.kt
index 2eeeefe..83b0da1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/NoPre.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/NoPre.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.nether
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -29,7 +29,7 @@ object NoPre : Module(
     var missing = Supply.None
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!KuudraUtils.inKuudra) return@on
 
             when {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt
index 0fb14e9..76a3836 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/SupplyHelper.kt
@@ -3,7 +3,7 @@ package com.odtheking.odin.features.impl.nether
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -36,7 +36,7 @@ object SupplyHelper : Module(
     private var startRun = 0L
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!KuudraUtils.inKuudra || !sendSupplyTime) return@on
 
             when {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
index eca3571..1c5d9f2 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
@@ -6,7 +6,7 @@ import com.odtheking.odin.clickgui.ClickGUI
 import com.odtheking.odin.clickgui.HudManager
 import com.odtheking.odin.clickgui.settings.AlwaysActive
 import com.odtheking.odin.clickgui.settings.impl.*
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Category
 import com.odtheking.odin.features.Module
@@ -81,7 +81,7 @@ object ClickGUIModule : Module(
             postData("https://api.odtheking.com/tele/", """{"username": "$name", "version": "Fabric ${OdinMod.version}"}""")
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!profileRegex.matches(value)) return@on
 
             if (firstJoin) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt
index ff44c6a..f3cb31f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt
@@ -5,7 +5,7 @@ import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
 import com.odtheking.odin.clickgui.settings.impl.KeybindSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
@@ -49,7 +49,7 @@ object Waypoints : Module(
     private val temporaryWaypoints = mutableListOf<Waypoint>()
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             val (name, x, y, z) = when {
                 fromParty && partyRegex.matches(value) -> partyRegex.find(value)?.destructured
                 fromAll && allRegex.matches(value) -> allRegex.find(value)?.destructured
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
index acef565..5ac6a02 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
@@ -3,7 +3,7 @@ package com.odtheking.odin.features.impl.skyblock
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.MessageSentEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -62,7 +62,7 @@ object ChatCommands : Module(
     private val dtReason = mutableListOf<Pair<String, String>>()
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (value.matches(endRunRegex)) {
                 if (!dt || dtReason.isEmpty()) return@on
                 schedule(30) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Ragnarock.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Ragnarock.kt
index 6e36e1d..2145f9c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Ragnarock.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Ragnarock.kt
@@ -2,7 +2,7 @@ package com.odtheking.odin.features.impl.skyblock
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
@@ -22,7 +22,7 @@ object Ragnarock : Module(
     private val cancelRegex = Regex("Ragnarock was cancelled due to (?:being hit|taking damage)!")
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (cancelAlert && value.matches(cancelRegex)) alert("§cRagnarock Cancelled!")
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt b/src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt
index 5e9447e..a4d05d7 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/ChatManager.kt
@@ -1,13 +1,13 @@
 package com.odtheking.odin.utils
 
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import net.minecraft.network.chat.Component
 import java.util.concurrent.ConcurrentLinkedQueue
 
 object ChatManager {
     private val cancelQueue = ConcurrentLinkedQueue<Component>()
 
-    fun ChatPacketEvent.hideMessage() {
+    fun ChatMessageEvent.hideMessage() {
         cancelQueue.add(component)
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/KuudraUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/KuudraUtils.kt
index 0b55c96..abb3214 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/KuudraUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/KuudraUtils.kt
@@ -1,7 +1,7 @@
 package com.odtheking.odin.utils.skyblock
 
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
@@ -84,7 +84,7 @@ object KuudraUtils {
             }
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (!inKuudra) return@on
 
             when (value) {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/PartyUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/PartyUtils.kt
index ebda0e1..1bb794a 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/PartyUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/PartyUtils.kt
@@ -1,7 +1,7 @@
 package com.odtheking.odin.utils.skyblock
 
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.PartyEvent
 import com.odtheking.odin.events.core.on
 
@@ -45,7 +45,7 @@ object PartyUtils {
         private set
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             joinedOther.find(value)?.let { return@on addMember(it.groupValues[2]) }
 
             joinedSelf.find(value)?.let {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
index 4d3578d..b72d3c7 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
@@ -1,6 +1,6 @@
 package com.odtheking.odin.utils.skyblock
 
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
@@ -21,7 +21,7 @@ object SplitsManager {
     private var tickCounter: Long = 0L
 
     init {
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (value == "Starting in 1 second.") startRun() else onSplitMessage(value)
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
index 74134f6..8c009fb 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
@@ -102,7 +102,7 @@ object DungeonListener {
             }
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             if (expectingBloodRegex.matches(value)) expectingBloodUpdate = true
             doorOpenRegex.find(value)?.let { dungeonStats.doorOpener = it.groupValues[1] }
             deathRegex.find(value)?.let { match ->
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt
index 3f1f87b..7bb297c 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/TerminalUtils.kt
@@ -1,7 +1,7 @@
 package com.odtheking.odin.utils.skyblock.dungeon.terminals
 
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.ChatMessageEvent
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.TickEvent
@@ -68,7 +68,7 @@ object TerminalUtils {
             }
         }
 
-        on<ChatPacketEvent> {
+        on<ChatMessageEvent> {
             termSolverRegex.find(value)?.let { message ->
                 if (message.groupValues[1] == mc.player?.name?.string) lastTermOpened?.let {
                     TerminalEvent.Solve(it).postAndCatch()
Fixed PuzzleTimer min timeodtheking · 2026-08-31 · 33e23ed
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt | 13 +++++++++----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonEnums.kt               | 23 ++++++++++++-----------
 2 files changed, 21 insertions(+), 15 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index 267c8b1..f96139b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -13,6 +13,7 @@ import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.handlers.TickTask
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
+import com.odtheking.odin.utils.skyblock.dungeon.Puzzle
 import net.minecraft.core.BlockPos
 import net.minecraft.network.chat.ClickEvent
 import net.minecraft.network.chat.Component
@@ -200,10 +201,14 @@ object PuzzleSolvers : Module(
     private val puzzlePBs = PersonalBest(this, "PuzzlePBs")
 
     fun onPuzzleComplete(puzzleName: String) {
-        puzzleTimersMap[puzzleName]?.let {
-            if (it.sentMessage || (System.currentTimeMillis() - it.timeEntered) / 1000f < 5) return
-            puzzlePBs.time(puzzleName, (System.currentTimeMillis() - it.timeEntered) / 1000f, "s§7!", "§a${puzzleName} §7solved in §6")
-            it.sentMessage = true
+        puzzleTimersMap[puzzleName]?.let { puzzleTimer ->
+            if (puzzleTimer.sentMessage) return
+
+            val puzzleTime = (System.currentTimeMillis() - puzzleTimer.timeEntered) / 1000f
+            if (puzzleTime < (Puzzle.entries.find { it.displayName == puzzleName }?.timeToBeat ?: 0)) return@onPuzzleComplete
+
+            puzzlePBs.time(puzzleName, puzzleTime, "s§7!", "§a${puzzleName} §7solved in §6")
+            puzzleTimer.sentMessage = true
         }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonEnums.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonEnums.kt
index f3e1517..2ee59ad 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonEnums.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonEnums.kt
@@ -36,20 +36,21 @@ data class DungeonPlayer(
  */
 enum class Puzzle(
     val displayName: String,
+    val timeToBeat: Int,
     var status: PuzzleStatus? = null,
     var player: String? = null
 ) {
-    UNKNOWN("???"),
-    BLAZE("Higher Or Lower"),
-    BEAMS("Creeper Beams"),
-    WEIRDOS("Three Weirdos"),
-    TTT("Tic Tac Toe"),
-    WATER_BOARD("Water Board"),
-    TP_MAZE("Teleport Maze"),
-    BOULDER("Boulder"),
-    ICE_FILL("Ice Fill"),
-    ICE_PATH("Ice Path"),
-    QUIZ("Quiz");
+    UNKNOWN("???", 0),
+    BLAZE("Higher Or Lower", 6),
+    BEAMS("Creeper Beams", 6),
+    WEIRDOS("Three Weirdos", 2),
+    TTT("Tic Tac Toe", 0),
+    WATER_BOARD("Water Board", 10),
+    TP_MAZE("Teleport Maze", 6),
+    BOULDER("Boulder", 6),
+    ICE_FILL("Ice Fill", 10),
+    ICE_PATH("Ice Path", 0),
+    QUIZ("Quiz", 10);
 }
 
 enum class PuzzleStatus {
Added Storm Tick Hud Bump versionodtheking · 2026-08-29 · 2e371e6
查看变更
gradle.properties                                                          |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt        |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt        |  56 ++++++++++++-------------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt        |  41 ++++++++-----------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt         | 172 +++++++++++++++++++++++++++++++++++++++++++----------------------------------
 6 files changed, 143 insertions(+), 132 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index 944c612..5fc82e9 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ loom_version=1.16-SNAPSHOT
 mod_id=odin
 mod_name=Odin
 mod_description=A Hypixel Skyblock QoL mod
-mod_version=0.3.1
+mod_version=0.3.2
 maven_group=com.odtheking
 archives_base_name=odin-fabric
 # Mod Dependencies
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt
index 0d00ccb..2ce447a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowAlign.kt
@@ -25,7 +25,7 @@ object ArrowAlign : Module(
     name = "Arrow Align",
     description = "Shows the solution for the Arrow Align device."
 ) {
-    private val blockWrong by BooleanSetting("Block Wrong Clicks", false, desc = "Blocks wrong clicks, shift will override this.")
+    private val blockWrong by BooleanSetting("Block Wrong Clicks", true, desc = "Blocks wrong clicks, shift will override this.")
     private val preventKey by KeybindSetting("Prevent Blocking", GLFW.GLFW_KEY_LEFT_SHIFT, desc = "While holding this key, wrong clicks won't be blocked, even if you aren't sneaking.").withDependency { blockWrong }
     private val invertKey by BooleanSetting("Invert Key", false, desc = "Inverts the behavior of the prevent key. Wrong clicks will be blocked while holding the prevent key, and allowed otherwise.").withDependency { blockWrong }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt
index cf3a28e..ba5802a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt
@@ -13,7 +13,7 @@ object TerminalSimulator : Module(
     description = "Simulates a floor 7 terminal from phase 3."
 ) {
     private val ping by NumberSetting("Ping", 0, 0, 500, 50, desc = "Ping of the terminal.")
-    val disableFirstClickProtection by BooleanSetting("First Click Protection", false, desc = "")
+    val disableFirstClickProtection by BooleanSetting("First Click Prot", false, desc = "Disables First click protection while solving termsim.")
 
     val termSimPBs = PersonalBest(this, "TermsimPBs")
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 63f22fe..1cf0f45 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -7,14 +7,12 @@ import com.odtheking.odin.events.ChatPacketEvent
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.MORT_REGEX
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.toFixed
-import net.minecraft.network.protocol.game.ClientboundSetTimePacket
 
 object TickTimers : Module(
     name = "Tick Timers",
@@ -27,17 +25,20 @@ object TickTimers : Module(
     private val necronRegex = Regex("^\\[BOSS] Necron: I'm afraid, your journey ends now\\.$")
     private val goldorRegex = Regex("^\\[BOSS] Goldor: Who dares trespass into my domain\\?$")
     private val coreOpeningRegex = Regex("^The Core entrance is opening!$")
-    private val stormStartRegex = Regex("^\\[BOSS] Storm: I should have known that I stood no chance\\.$")
-    private val stormPadRegex = Regex("^\\[BOSS] Storm: Pathetic Maxor, just like expected\\.$")
+    private val stormEndRegex = Regex("^\\[BOSS] Storm: I should have known that I stood no chance\\.$")
+    private val stormStartRegex = Regex("^\\[BOSS] Storm: Pathetic Maxor, just like expected\\.$")
     private val stormPyRegex = Regex("^\\[BOSS] Storm: (ENERGY HEED MY CALL|THUNDER LET ME BE YOUR CATALYST)!$")
 
+    private var necronTime = -1
+
     private val necronHud by HUD("Necron Hud", "Displays a timer for Necron's drop.") {
         if (it)                   textDim(formatTimer(35, 60, "§4Necron dropping in"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else if (necronTime >= 0) textDim(formatTimer(necronTime, 60, "§4Necron dropping in"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else 0 to 0
     }
 
-    private var necronTime = -1
+    private var goldorTickTime = -1
+    private var goldorStartTime = -1
 
     private val goldorHud: HudElement by HUD("Goldor Hud", "Displays a timer for Goldor's Core entrance opening.") {
         if (it) textDim(formatTimer(35, 60, "§7Tick:"), 0, 0, Colors.MINECRAFT_DARK_RED)
@@ -48,8 +49,7 @@ object TickTimers : Module(
     }
     private val startTimer by BooleanSetting("Start timer", false, desc = "Displays a timer counting down until devices/terms are able to be activated/completed.").withDependency { goldorHud.enabled }
 
-    private var goldorTickTime = -1
-    private var goldorStartTime = -1
+    private var padTickTime = -1
 
     private val stormHud by HUD("Storm Pad Hud", "Displays a timer for Storm's Pad.") {
         if (it)                    textDim(formatTimer(15, 20, "§bPad:"), 0, 0, Colors.MINECRAFT_DARK_RED)
@@ -57,36 +57,38 @@ object TickTimers : Module(
         else 0 to 0
     }
 
+    private var lightningTickTime = -1
+
     private val lightningHud by HUD("Storm Lightning Hud", "Displays a timer for Storm's Lightning.") {
         if (it)                          textDim(formatTimer(560, 560, "§bLightning:"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else if (lightningTickTime >= 0) textDim(formatTimer(lightningTickTime, 560, "§bLightning:"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else 0 to 0
     }
 
+    private var pyTriggered = false
+    private var pyTickTime = -1
+
     private val pyHud by HUD("Storm PY Hud", "Displays a timer for when to crush storm under the purple pillar.") {
         if (it)                   textDim(formatTimer(95, 95, "§bPY:"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else if (pyTickTime >= 0) textDim(formatTimer(pyTickTime, 95, "§bPY:"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else 0 to 0
     }
 
-    private var padTickTime = -1
-    private var lightningTickTime = -1
-    private var pyTriggered = false
-    private var pyTickTime = -1
+    private var stormTick = -1
 
-    private val outboundsHud by HUD("Outbounds Hud", "Displays a timer for out of bounds death ticks.") {
-        if (it)                      textDim(formatTimer(15, 20, "§8Outbounds:"), 0, 0, Colors.MINECRAFT_DARK_RED)
-        else if (outboundsTime >= 0) textDim(formatTimer(outboundsTime, 20, "§8Outbounds:"), 0, 0, Colors.MINECRAFT_DARK_RED)
+    private val stormTickHud by HUD("Storm Tick Hud", "Displays a timer for Storm's second phase, optionally counting down to the crush window.") {
+        if (it) {
+            val (time, max, prefix) = Triple(200, 620, "§bStorm:")
+            textDim(formatTimer(time, max, prefix), 0, 0, Colors.MINECRAFT_DARK_RED)
+        } else if (stormTick >= 0) textDim(formatTimer(stormTick, 620, "§bStorm:"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else 0 to 0
     }
 
-    private var outboundsTime = -1
     private var secretsCounter = 0
 
     private val secretsHud by HUD("Secrets Hud", "Displays a timer for secret spawn ticks.") {
         if (it) textDim(formatTimer(15, 20, "§7Secret:", overrideColor = "§c"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else if (DungeonUtils.openRoomCount != 0 && !DungeonUtils.inBoss) {
-            // Color change at < 10 because that's when items are picked up.
             val time = 20 - secretsCounter % 20
             val color = when {
                 time < 5 -> "§a"
@@ -108,13 +110,15 @@ object TickTimers : Module(
                     goldorStartTime = -1
                     goldorTickTime = -1
                 }
-                value.matches(stormStartRegex) -> {
-                    if (goldorHud.enabled) goldorStartTime = 104
-                    if (stormHud.enabled) padTickTime = -1
+                value.matches(stormEndRegex) -> {
+                    goldorStartTime = 104
+                    padTickTime = -1
+                    stormTick = -1
                 }
-                value.matches(stormPadRegex) -> {
+                value.matches(stormStartRegex) -> {
                     padTickTime = 20
                     lightningTickTime = 560
+                    stormTick = 0
                 }
                 !pyTriggered && value.matches(stormPyRegex) -> {
                     pyTriggered = true
@@ -125,8 +129,6 @@ object TickTimers : Module(
 
         on<TickEvent.Server> {
             if (!DungeonUtils.inDungeons) return@on
-            if (outboundsTime == 0) outboundsTime = 40
-            if (outboundsTime >= 0) outboundsTime--
             secretsCounter++
             if (!DungeonUtils.inBoss) return@on
             if (goldorTickTime == 0 && goldorStartTime <= 0 && goldorHud.enabled) goldorTickTime = 60
@@ -137,13 +139,7 @@ object TickTimers : Module(
             if (lightningTickTime >= 0) lightningTickTime--
             if (pyTickTime >= 0) pyTickTime--
             if (necronTime >= 0) necronTime--
-        }
-
-        onReceive<ClientboundSetTimePacket> {
-            if (!DungeonUtils.inClear) return@onReceive
-            if (DungeonUtils.openRoomCount == 0) {
-                if (outboundsHud.enabled) outboundsTime = 40 - (gameTime % 40).toInt()
-            } else outboundsTime = -1
+            if (stormTick >= 0) stormTick++
         }
 
         on<LevelEvent.Load> {
@@ -154,8 +150,8 @@ object TickTimers : Module(
             pyTickTime = -1
             pyTriggered = false
             necronTime = -1
-            outboundsTime = -1
             secretsCounter = 0
+            stormTick = -1
         }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
index de6e4b8..b3e62b7 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
@@ -8,8 +8,7 @@ import com.odtheking.odin.utils.PersonalBest
 import com.odtheking.odin.utils.formatTime
 import com.odtheking.odin.utils.render.getStringWidth
 import com.odtheking.odin.utils.render.text
-import com.odtheking.odin.utils.skyblock.SplitsManager.currentSplits
-import com.odtheking.odin.utils.skyblock.SplitsManager.getAndUpdateSplitsTimes
+import com.odtheking.odin.utils.skyblock.SplitsManager.currentRows
 import com.odtheking.odin.utils.skyblock.floor7SplitGroup
 import com.odtheking.odin.utils.toFixed
 
@@ -34,19 +33,19 @@ object Splits : Module(
             return@HUD totalWidth to 9 * floor7SplitGroup.size
         }
 
-        val (times, tickTimes, current) = getAndUpdateSplitsTimes(currentSplits)
-        if (currentSplits.splits.isEmpty()) return@HUD 0 to 0
-        val splits = currentSplits.splits.dropLast(1)
+        val rows = currentRows()
+        if (rows.isEmpty()) return@HUD 0 to 0
+        val segments = rows.dropLast(1)
 
-        val labelWidth = splits.maxOfOrNull { getStringWidth(it.name) } ?: 0
+        val labelWidth = segments.maxOfOrNull { getStringWidth(it.name) } ?: 0
         val totalWidth = labelWidth + 4 + timeWidth + 2
 
-        splits.forEachIndexed { index, split ->
-            val time = formatTime(if (index >= times.size) 0 else times[index])
-            text(split.name, 0, index * 9, Colors.WHITE)
+        segments.forEachIndexed { index, row ->
+            if (row.time == 0L && !show0Time) return@forEachIndexed
+            val time = formatTime(row.time)
+            text(row.name, 0, index * 9, Colors.WHITE)
 
-            val displayText = if (showTickTime && index < tickTimes.size) "$time §8(§7${(tickTimes[index] / 20f).toFixed()}§8)"
-            else time
+            val displayText = if (showTickTime) "$time §8(§7${(row.tickTime / 20f).toFixed()}§8)" else time
 
             val timeX = if (fixedWidth) labelWidth + 4 + timeWidth - getStringWidth(displayText)
             else labelWidth + 4
@@ -54,12 +53,12 @@ object Splits : Module(
             text(displayText, timeX, index * 9, Colors.WHITE)
         }
 
-        if (bossEntrySplit && currentSplits.splits.size > 3) {
-            val y = splits.size * 9
+        if (bossEntrySplit && rows.size > 3) {
+            val y = segments.size * 9
             text("§9Boss Entry", 0, y, Colors.WHITE)
 
-            val totalTime = formatTime(times.take(3).sum())
-            val displayText = if (showTickTime) "$totalTime §8(§7${(tickTimes.take(3).sum() / 20f).toFixed()}§8)"
+            val totalTime = formatTime(segments.take(3).sumOf { it.time })
+            val displayText = if (showTickTime) "$totalTime §8(§7${(segments.take(3).sumOf { it.tickTime } / 20f).toFixed()}§8)"
             else totalTime
 
             val timeX = if (fixedWidth) labelWidth + 4 + timeWidth - getStringWidth(displayText)
@@ -68,7 +67,7 @@ object Splits : Module(
             text(displayText, timeX, y, Colors.WHITE)
         }
 
-        totalWidth to 9 * (currentSplits.splits.size + (if (bossEntrySplit) 1 else 0))
+        totalWidth to 9 * (rows.size + (if (bossEntrySplit) 1 else 0))
     }
 
     private val currentSplitHud by HUD("Current Split HUD", "Shows only the current split and its tick time.") { example ->
@@ -79,13 +78,9 @@ object Splits : Module(
             return@HUD w to 9
         }
 
-        val splits = currentSplits.splits
-        if (splits.isEmpty()) return@HUD 0 to 0
+        val current = currentRows().find { it.isCurrent } ?: return@HUD 0 to 0
 
-        val (_, tickTimes, current) = getAndUpdateSplitsTimes(currentSplits)
-        if (current !in splits.indices || current >= tickTimes.size) return@HUD 0 to 0
-
-        val displayText = "§7${(tickTimes[current] / 20f).toFixed()}s"
+        val displayText = "§7${(current.tickTime / 20f).toFixed()}s"
         val w = getStringWidth(displayText) + 2
         text(displayText, -w / 2, 0, Colors.WHITE)
         w to 9
@@ -93,7 +88,7 @@ object Splits : Module(
 
     private val fixedWidth by BooleanSetting("Fixed Width", true, desc = "Always use a fixed HUD width, right-aligning the times.")
     private val bossEntrySplit by BooleanSetting("Boss Entry Split", true, desc = "Split for boss entry.")
-    val sendSplits by BooleanSetting("Send Splits", true, desc = "Send splits to chat.")
+    private val show0Time by BooleanSetting("Show 0 splits", false, desc = "Shows splits which have their time at 0.")
     val showTickTime by BooleanSetting("Show Tick Time", true, desc = "Show tick-based time alongside real time.")
     val splitLocation by SelectorSetting("Split Location", "Both", listOf("Both", "Dungeons Only", "Kuudra Only"), desc = "Which areas to show splits in.")
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
index 1c07559..4d3578d 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
@@ -13,71 +13,16 @@ import com.odtheking.odin.utils.skyblock.dungeon.DungeonListener
 
 data class Split(val regex: Regex, val name: String, var time: Long = 0L, var ticks: Long = 0L)
 data class SplitsGroup(val splits: List<Split>, val personalBest: PersonalBest?)
+data class SplitRow(val name: String, val time: Long, val tickTime: Long, val isCurrent: Boolean)
 
 object SplitsManager {
 
-    var currentSplits: SplitsGroup = SplitsGroup(emptyList(), null)
+    private var currentSplits: SplitsGroup = SplitsGroup(emptyList(), null)
     private var tickCounter: Long = 0L
 
     init {
         on<ChatPacketEvent> {
-            if (value == "Starting in 1 second.") {
-                tickCounter = 0L
-                currentSplits = when (LocationUtils.currentArea) {
-                    Island.Dungeon -> {
-                        if (Splits.splitLocation == 2) return@on
-                        val floor = DungeonListener.floor ?: return@on
-
-                        with(dungeonSplits[floor.floorNumber].toMutableList()) {
-                            addAll(0, listOf(
-                                Split(MORT_REGEX, "§2Blood Open"),
-                                Split(BLOOD_OPEN_REGEX, "§bBlood Clear"),
-                                Split(Regex("\\[BOSS] The Watcher: You have proven yourself\\. You may pass\\."), "§dPortal Entry")
-                            )
-                            )
-                            add(Split(Regex("^\\s*☠ Defeated (.+) in 0?([\\dhms ]+?)\\s*(\\(NEW RECORD!\\))?$"), "§1Total"))
-                            SplitsGroup(map { it.copy(time = 0L, ticks = 0L) }, Splits.dungeonPBsList[floor.ordinal])
-                        }
-                    }
-
-                    Island.Kuudra -> {
-                        if (Splits.splitLocation == 1) return@on
-                        when (KuudraUtils.kuudraTier) {
-                            5 -> SplitsGroup(kuudraT5SplitsGroup.map { it.copy(time = 0L, ticks = 0L) }, Splits.kuudraT5PBs)
-                            4 -> SplitsGroup(kuudraSplitsGroup.map { it.copy(time = 0L, ticks = 0L) }, Splits.kuudraT4PBs)
-                            3 -> SplitsGroup(kuudraSplitsGroup.map { it.copy(time = 0L, ticks = 0L) }, Splits.kuudraT3PBs)
-                            2 -> SplitsGroup(kuudraSplitsGroup.map { it.copy(time = 0L, ticks = 0L) }, Splits.kuudraT2PBs)
-                            1 -> SplitsGroup(kuudraSplitsGroup.map { it.copy(time = 0L, ticks = 0L) }, Splits.kuudraT1PBs)
-                            else -> SplitsGroup(emptyList(), null)
-                        }
-                    }
-
-                    else -> SplitsGroup(emptyList(), null)
-                }
-            } else {
-                val currentSplit = currentSplits.splits.find { it.regex.matches(value) } ?: return@on
-                if (currentSplit.time != 0L) return@on
-                currentSplit.time = System.currentTimeMillis()
-                currentSplit.ticks = tickCounter
-
-                val index = currentSplits.splits.indexOf(currentSplit).takeIf { it != 0 } ?: return@on
-                val currentSplitTime = (currentSplit.time - currentSplits.splits[index - 1].time) / 1000f
-
-                if (index == currentSplits.splits.size - 1) {
-                    val (times, _, _) = getAndUpdateSplitsTimes(currentSplits)
-                    val capturedSplits = currentSplits.splits.toList()
-                    val capturedPB = currentSplits.personalBest
-                    schedule(10) {
-                        if (capturedSplits.isEmpty()) return@schedule
-                        capturedPB?.time(capturedSplits[index - 1].name, currentSplitTime, "s§7!", "§6${capturedSplits[index - 1].name} §7took §6", Splits.enabled)
-                        capturedPB?.time(capturedSplits[index].name, times.last() / 1000f, "s§7!", "§6Total time §7took §6", Splits.enabled)
-                        times.forEachIndexed { i, it ->
-                            val name = if (i == capturedSplits.size - 1) "Total" else capturedSplits[i].name
-                            if (Splits.sendSplits && Splits.enabled) modMessage("§6$name §7took §6${formatTime((it))}§7.")
-                        }
-                    }
-                } else currentSplits.personalBest?.time(currentSplits.splits[index - 1].name, currentSplitTime, "s§7!", "§6${currentSplits.splits[index - 1].name} §7took §6", Splits.enabled)
-            }
+            if (value == "Starting in 1 second.") startRun() else onSplitMessage(value)
         }
 
         on<TickEvent.Server> {
@@ -85,34 +30,106 @@ object SplitsManager {
         }
 
         on<LevelEvent.Load> {
-            currentSplits = SplitsGroup(mutableListOf(), null)
+            currentSplits = SplitsGroup(emptyList(), null)
             tickCounter = 0L
         }
     }
 
-    fun getAndUpdateSplitsTimes(currentSplits: SplitsGroup): Triple<List<Long>, List<Long>, Int> {
-        if (currentSplits.splits.isEmpty() || currentSplits.splits[0].time == 0L)
-            return Triple(List(currentSplits.splits.size) { 0L }, List(currentSplits.splits.size) { 0L }, -1)
+    private fun startRun() {
+        tickCounter = 0L
+        currentSplits = when (LocationUtils.currentArea) {
+            Island.Dungeon -> buildDungeonSplits() ?: return
+            Island.Kuudra -> buildKuudraSplits() ?: return
+            else -> SplitsGroup(emptyList(), null)
+        }
+    }
+
+    private fun buildDungeonSplits(): SplitsGroup? {
+        if (Splits.splitLocation == 2) return null
+        val floor = DungeonListener.floor ?: return null
+
+        val splits = dungeonSplits[floor.floorNumber].toMutableList().apply {
+            addAll(0, listOf(
+                Split(MORT_REGEX, "§2Blood Open"),
+                Split(BLOOD_OPEN_REGEX, "§bBlood Clear"),
+                Split(PORTAL_ENTRY_REGEX, "§dPortal Entry"),
+            ))
+            add(Split(DUNGEON_CLEARED_REGEX, "§1Total"))
+        }
+        return SplitsGroup(splits.map { it.copy() }, Splits.dungeonPBsList[floor.ordinal])
+    }
+
+    private fun buildKuudraSplits(): SplitsGroup? {
+        if (Splits.splitLocation == 1) return null
+        return when (KuudraUtils.kuudraTier) {
+            5 -> SplitsGroup(kuudraT5SplitsGroup.map { it.copy() }, Splits.kuudraT5PBs)
+            4 -> SplitsGroup(kuudraSplitsGroup.map { it.copy() }, Splits.kuudraT4PBs)
+            3 -> SplitsGroup(kuudraSplitsGroup.map { it.copy() }, Splits.kuudraT3PBs)
+            2 -> SplitsGroup(kuudraSplitsGroup.map { it.copy() }, Splits.kuudraT2PBs)
+            1 -> SplitsGroup(kuudraSplitsGroup.map { it.copy() }, Splits.kuudraT1PBs)
+            else -> SplitsGroup(emptyList(), null)
+        }
+    }
+
+    private fun onSplitMessage(message: String) {
+        val splits = currentSplits.splits
+        val split = splits.find { it.regex.matches(message) } ?: return
+        if (split.time != 0L) return
 
-        val latestTime = if (currentSplits.splits.last().time == 0L) System.currentTimeMillis() else currentSplits.splits.last().time
-        val latestTick = if (currentSplits.splits.last().ticks == 0L) tickCounter else currentSplits.splits.last().ticks
+        split.time = System.currentTimeMillis()
+        split.ticks = tickCounter
 
-        val times = MutableList(currentSplits.splits.size) { 0L }.apply { this[size - 1] = latestTime - currentSplits.splits.first().time }
-        val tickTimes = MutableList(currentSplits.splits.size) { 0L }.apply { this[size - 1] = latestTick - currentSplits.splits.first().ticks }
+        val index = splits.indexOf(split)
+        if (index == 0) return
 
-        var current = currentSplits.splits.size
-        for (i in 0 until currentSplits.splits.size - 1) {
-            if (currentSplits.splits[i + 1].time != 0L) {
-                times[i] = currentSplits.splits[i + 1].time - currentSplits.splits[i].time
-                tickTimes[i] = currentSplits.splits[i + 1].ticks - currentSplits.splits[i].ticks
+        val previous = splits[index - 1]
+        val segmentTime = (split.time - previous.time) / 1000f
+
+        if (index == splits.lastIndex) finishRun(index, segmentTime)
+        else currentSplits.personalBest?.time(previous.name, segmentTime, "s§7!", "§6${previous.name} §7took §6", Splits.enabled)
+    }
+
+    private fun finishRun(totalIndex: Int, lastSegmentTime: Float) {
+        val rows = currentRows()
+        val splits = currentSplits.splits
+        val personalBest = currentSplits.personalBest
+
+        schedule(10) {
+            if (rows.isEmpty()) return@schedule
+            personalBest?.time(splits[totalIndex - 1].name, lastSegmentTime, "s§7!", "§6${splits[totalIndex - 1].name} §7took §6", Splits.enabled)
+            personalBest?.time(splits[totalIndex].name, rows.last().time / 1000f, "s§7!", "§6Total time §7took §6", Splits.enabled)
+            rows.forEachIndexed { i, row ->
+                if (Splits.enabled) modMessage("§6${if (i == rows.lastIndex) "Total" else row.name} §7took §6${formatTime(row.time)}§7.")
+            }
+        }
+    }
+
+    fun currentRows(): List<SplitRow> {
+        val splits = currentSplits.splits
+        if (splits.isEmpty()) return emptyList()
+        if (splits[0].time == 0L) return splits.map { SplitRow(it.name, 0L, 0L, isCurrent = false) }
+
+        val last = splits.last()
+        val latestTime = if (last.time != 0L) last.time else System.currentTimeMillis()
+        val latestTicks = if (last.ticks != 0L) last.ticks else tickCounter
+
+        val times = LongArray(splits.size).apply { this[splits.lastIndex] = latestTime - splits.first().time }
+        val ticks = LongArray(splits.size).apply { this[splits.lastIndex] = latestTicks - splits.first().ticks }
+
+        var currentIndex = -1
+        for (i in 0 until splits.lastIndex) {
+            val next = splits[i + 1]
+            if (next.time != 0L) {
+                times[i] = next.time - splits[i].time
+                ticks[i] = next.ticks - splits[i].ticks
             } else {
-                current = i
-                times[i] = latestTime - currentSplits.splits[i].time
-                tickTimes[i] = latestTick - currentSplits.splits[i].ticks
+                times[i] = latestTime - splits[i].time
+                ticks[i] = latestTicks - splits[i].ticks
+                currentIndex = i
                 break
             }
         }
-        return Triple(times, tickTimes, current)
+        return splits.indices.map { i -> SplitRow(splits[i].name, times[i], ticks[i], isCurrent = i == currentIndex) }
     }
 }
 
@@ -194,6 +211,9 @@ private val dungeonSplits = listOf(
     floor7SplitGroup,
 )
 
+private val PORTAL_ENTRY_REGEX = Regex("\\[BOSS] The Watcher: You have proven yourself\\. You may pass\\.")
+private val DUNGEON_CLEARED_REGEX = Regex("^\\s*☠ Defeated (.+) in 0?([\\dhms ]+?)\\s*(\\(NEW RECORD!\\))?$")
+
 // https://regex101.com/r/BXKhOI/1
 private val BLOOD_OPEN_REGEX = Regex("^\\[BOSS] The Watcher: (Congratulations, you made it through the Entrance\\.|Ah, you've finally arrived\\.|Ah, we meet again\\.\\.\\.|So you made it this far\\.\\.\\. interesting\\.|You've managed to scratch and claw your way here, eh\\?|I'm starting to get tired of seeing you around here\\.\\.\\.|Oh\\.\\. hello\\?|Things feel a little more roomy now, eh\\?)$|^The BLOOD DOOR has been opened!$")
-val MORT_REGEX = Regex("\\[NPC] Mort: Here, I found this map when I first entered the dungeon\\.|\\[NPC] Mort: Right-click the Orb for spells, and Left-click \\(or Drop\\) to use your Ultimate!")
\ No newline at end of file
+val MORT_REGEX = Regex("\\[NPC] Mort: Here, I found this map when I first entered the dungeon\\.|\\[NPC] Mort: Right-click the Orb for spells, and Left-click \\(or Drop\\) to use your Ultimate!")
option to show player name or class on melody progress (#147)
* option to show player name or class on melody progress * Made dungeon class not refetch every frame --------- Co-authored-by: odtheking <odtheking1@gmail.com>
Akar1881 · 2026-08-29 · c21ea6a
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt | 39 +++++++++++++++++++++++++--------------
 1 file changed, 25 insertions(+), 14 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
index 8ad998e..fe8829d 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/MelodyMessage.kt
@@ -2,6 +2,7 @@ package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
+import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
 import com.odtheking.odin.clickgui.settings.impl.StringSetting
 import com.odtheking.odin.events.ChatPacketEvent
 import com.odtheking.odin.events.GuiEvent
@@ -18,6 +19,7 @@ import com.odtheking.odin.utils.render.getStringWidth
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.sendCommand
 import com.odtheking.odin.utils.skyblock.LocationUtils
+import com.odtheking.odin.utils.skyblock.dungeon.DungeonClass
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.skyblock.dungeon.M7Phases
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
@@ -37,25 +39,26 @@ object MelodyMessage : Module(
 
     private val broadcast by BooleanSetting("Broadcast Progress", true, desc = "Broadcasts melody progress to all other odin users in the party.")
     private val melodyGui by HUD("Progress GUI", "Shows a gui with the progress of broadcasting odin users in melody.", true) {
-        if (it) drawMelody(MelodyData(3, 1, 2), 0)
+        if (it) drawMelody(MelodyData(3, 1, 2, DungeonClass.ARCHER), 0, mc.user.name)
 
         if (broadcast && melodyWebSocket.connected) {
             melodies.entries.forEachIndexed { i, (name, data) ->
-                if (!showOwn && name == mc.user.name) return@forEachIndexed
-                drawMelody(data, i)
+                if (showPlayer == 0 && name == mc.user.name) return@forEachIndexed
+                drawMelody(data, i, name)
             }
         }
         40 to 15
     }.withDependency { broadcast }
 
-    private val showOwn: Boolean by BooleanSetting("Show Own", false, desc = "Shows your own progress in the melody GUI.").withDependency { broadcast && melodyGui.enabled }
+    private val showPlayer by SelectorSetting("Show Player", "Name", arrayListOf("None", "Class", "Name", "Class & Name"), desc = "How player details should be rendered in the Melody GUI.").withDependency { broadcast }
 
     val melodyWebSocket = webSocket {
-        onMessage {
-            val (user, type, slot) = try { gson.fromJson(it, UpdateMessage::class.java) } catch (_: Exception) { return@onMessage }
-            val entry = melodies.getOrPut(user) { MelodyData(null, null, null) }
+        onMessage { message ->
+            val (username, type, slot) = try { gson.fromJson(message, UpdateMessage::class.java) } catch (_: Exception) { return@onMessage }
+            val playerClass = DungeonUtils.dungeonTeammates.find { it.name == username }?.clazz ?: return@onMessage
+            val entry = melodies.getOrPut(username) { MelodyData(null, null, null, playerClass) }
             when (type) {
-                0 -> melodies.remove(user)
+                0 -> melodies.remove(username)
                 1 -> entry.clay = slot
                 2 -> entry.purple = slot
                 5 -> entry.pane = slot
@@ -64,7 +67,7 @@ object MelodyMessage : Module(
     }
 
     private val melodies = ConcurrentHashMap<String, MelodyData>()
-    private val lastSent = MelodyData(null, null, null)
+    private val lastSent = MelodyData(null, null, null, DungeonClass.EMPTY)
 
     init {
         on<TerminalEvent.Open> {
@@ -152,16 +155,24 @@ object MelodyMessage : Module(
 
     private val width by lazy { getStringWidth("§d■") }
 
-    private fun GuiGraphicsExtractor.drawMelody(data: MelodyData, index: Int) {
+    private fun GuiGraphicsExtractor.drawMelody(data: MelodyData, index: Int, playerName: String) {
         val y = width * 2 * index
 
         repeat(5) {
             if (data.purple == it) textDim("§d■", width * it, y)
             textDim("${if (data.pane == it) "§a" else "§f"}■", width * it, y + width)
         }
-        data.clay?.let { textDim(it.toString(), width * 5 + 2, y + width / 2) }
+
+        val label = when (showPlayer) {
+            1 -> data.dungeonClass
+            2 -> playerName
+            3 -> "$playerName (${data.dungeonClass})"
+            else -> return
+        }
+
+        data.clay?.let { textDim("$it $label", width * 5 + 2, y + width / 2) }
     }
 
-    private data class UpdateMessage(val user: String, val type: Int, val slot: Int)
-    private data class MelodyData(var purple: Int?, var pane: Int?, var clay: Int?)
-}
\ No newline at end of file
+    private data class UpdateMessage(val username: String, val type: Int, val slot: Int)
+    private data class MelodyData(var purple: Int?, var pane: Int?, var clay: Int?, val dungeonClass: DungeonClass)
+}
Refactored DungeonWaypointsPacks management for more straight forward codeodtheking · 2026-08-28 · 0a76628
查看变更
src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt                                |  14 +++--
 src/main/kotlin/com/odtheking/odin/config/DungeonWaypointConfig.kt                                   |   6 +-
 src/main/kotlin/com/odtheking/odin/config/WaypointPackFileUtils.kt                                   | 151 ++++++++++++++++-----------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt                                 |  19 ++++++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt    |  12 ++--
 .../kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/WaypointPackSelectorScreen.kt   |  14 ++---
 6 files changed, 87 insertions(+), 129 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
index 34175a2..e307bce 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
@@ -1,12 +1,14 @@
 package com.odtheking.odin.commands
 
 import com.github.stivais.commodore.Commodore
+import com.github.stivais.commodore.utils.GreedyString
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.OdinMod.scope
 import com.odtheking.odin.config.DungeonWaypointConfig
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.*
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.modMessage
+import com.odtheking.odin.utils.network.WebUtils
 import com.odtheking.odin.utils.setClipboardContent
 import kotlinx.coroutines.launch
 
@@ -67,11 +69,15 @@ val dungeonWaypointsCommand = Commodore("dwp", "dungeonwaypoints") {
         }
     }
 
-    literal("import").runs { _: String? ->
+    literal("import").runs { importString: GreedyString? ->
         scope.launch {
-            val clipboard = mc.keyboardHandler.clipboard.trim().trim { it == '\n' }
-            val waypoints = DungeonWaypointConfig.decodeWaypoints(clipboard, clipboard.startsWith("{"))
-                ?: return@launch modMessage("§cFailed to decode waypoints from clipboard. §fIs the data valid?")
+            val input = (importString?.let {
+                if (importString.string.startsWith("https://")) WebUtils.fetchString(importString.string).getOrNull()
+                else importString.string
+            } ?: mc.keyboardHandler.clipboard)
+
+            val waypoints = DungeonWaypointConfig.decodeWaypoints(input.trim().trim { it == '\n' }) ?: return@launch modMessage("§cFailed to decode waypoints from clipboard. §fIs the data valid?")
+
             DungeonWaypoints.importEditableWaypoints(waypoints)
             modMessage("Imported waypoints from clipboard!${if (!DungeonWaypoints.enabled) "§7(Make sure to enable the DungeonWayPoints module)" else ""}")
         }
diff --git a/src/main/kotlin/com/odtheking/odin/config/DungeonWaypointConfig.kt b/src/main/kotlin/com/odtheking/odin/config/DungeonWaypointConfig.kt
index 13dd978..63b3a04 100644
--- a/src/main/kotlin/com/odtheking/odin/config/DungeonWaypointConfig.kt
+++ b/src/main/kotlin/com/odtheking/odin/config/DungeonWaypointConfig.kt
@@ -15,7 +15,7 @@ import java.util.zip.GZIPOutputStream
 import kotlin.io.encoding.Base64
 
 object DungeonWaypointConfig {
-    private val gson = GsonBuilder()
+    val gson: Gson = GsonBuilder()
         .registerTypeAdapter(AABB::class.java, AABBSerializer())
         .registerTypeAdapter(DungeonWaypoint::class.java, DungeonWaypointDeserializer())
         .registerTypeAdapter(BlockPos::class.java, BlockPosSerializer())
@@ -39,10 +39,10 @@ object DungeonWaypointConfig {
         }
     }
 
-    fun decodeWaypoints(input: String, isJson: Boolean): MutableMap<String, MutableList<DungeonWaypoint>>? {
+    fun decodeWaypoints(input: String): MutableMap<String, MutableList<DungeonWaypoint>>? {
         return try {
             gson.fromJson(
-                if (isJson) input else decompress(Base64.decode(input)),
+                if (input.startsWith("{")) input else decompress(Base64.decode(input)),
                 object : TypeToken<MutableMap<String, MutableList<DungeonWaypoint>>>() {}.type
             )
         } catch (_: Exception) {
diff --git a/src/main/kotlin/com/odtheking/odin/config/WaypointPackFileUtils.kt b/src/main/kotlin/com/odtheking/odin/config/WaypointPackFileUtils.kt
index 8a9308b..7a1a28f 100644
--- a/src/main/kotlin/com/odtheking/odin/config/WaypointPackFileUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/config/WaypointPackFileUtils.kt
@@ -1,150 +1,91 @@
 package com.odtheking.odin.config
 
-import com.google.gson.GsonBuilder
 import com.google.gson.reflect.TypeToken
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints.DungeonWaypoint
 import com.odtheking.odin.utils.modMessage
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
-import net.minecraft.world.phys.AABB
+import net.minecraft.util.FileUtil
 import java.io.File
 
-object WaypointPackFileUtils {
-
-    data class WaypointPack(
-        val name: String,
-        val file: File,
-        val waypoints: MutableMap<String, MutableList<DungeonWaypoint>>
-    )
-
-    private val gson = GsonBuilder()
-        .registerTypeAdapter(AABB::class.java, DungeonWaypointConfig.AABBSerializer())
-        .registerTypeAdapter(DungeonWaypoint::class.java, DungeonWaypointConfig.DungeonWaypointDeserializer())
-        .setPrettyPrinting()
-        .create()
+private typealias PackMap = MutableMap<String, MutableList<DungeonWaypoint>>
 
-    private val packType =
-        object : TypeToken<MutableMap<String, MutableList<DungeonWaypoint>>>() {}.type
+object WaypointPackFileUtils {
 
     private val legacyConfigFile = File(mc.gameDirectory, "config/odin/dungeon-waypoint-config.json")
     val packsFolder = File(mc.gameDirectory, "config/odin/dungeon-waypoints").apply { mkdirs() }
+    private val packType = object : TypeToken<PackMap>() {}.type
 
     init {
-        migrateLegacyConfigIfNeeded()
-    }
-
-    private fun migrateLegacyConfigIfNeeded() {
-        if (!legacyConfigFile.exists() || packsFolder.listFiles()?.isNotEmpty() == true) return
-        runCatching {
-            val target = File(packsFolder, "default.json")
-            target.writeText(legacyConfigFile.readText())
+        if (listPackNames().isEmpty()) runCatching {
+            val content = legacyConfigFile.takeIf(File::exists)?.readText() ?: DungeonWaypointConfig.gson.toJson(emptyPack())
+            packFile("default").writeText(content)
         }.onFailure { it.printStackTrace() }
     }
 
     private fun packFile(name: String) = File(packsFolder, "$name.json")
-    private fun emptyPack() = mutableMapOf<String, MutableList<DungeonWaypoint>>()
+    private fun emptyPack(): PackMap = mutableMapOf()
 
-    suspend fun loadPack(packName: String) = withContext(Dispatchers.IO) {
+    fun listPackNames(): List<String> =
+        packsFolder.listFiles { f -> f.extension == "json" }?.map { it.nameWithoutExtension }?.sorted() ?: emptyList()
+
+    suspend fun loadPack(packName: String): PackMap = withContext(Dispatchers.IO) {
         runCatching {
-            packFile(packName)
-                .takeIf(File::exists)
-                ?.readText()
-                ?.let { gson.fromJson(it, packType) }
-                ?: emptyPack()
-        }.getOrElse {
-            it.printStackTrace()
-            emptyPack()
-        }
+            packFile(packName).takeIf(File::exists)?.readText()
+                ?.let { DungeonWaypointConfig.gson.fromJson<PackMap>(it, packType) } ?: emptyPack()
+        }.getOrElse { it.printStackTrace(); emptyPack() }
     }
 
-    suspend fun savePack(packName: String, waypoints: MutableMap<String, MutableList<DungeonWaypoint>>) =
-        withContext(Dispatchers.IO) {
-            runCatching {
-                packFile(packName).writeText(gson.toJson(waypoints))
-            }.onFailure { it.printStackTrace() }
-        }
+    suspend fun savePack(packName: String, waypoints: PackMap) = withContext(Dispatchers.IO) {
+        runCatching { packFile(packName).writeText(DungeonWaypointConfig.gson.toJson(waypoints)) }.onFailure { it.printStackTrace() }
+    }
 
-    suspend fun createPack(packName: String) = withContext(Dispatchers.IO) {
+    suspend fun createPack(packName: String): Boolean = withContext(Dispatchers.IO) {
+        val file = packFile(packName)
         when {
-            !isValidPackName(packName) -> {
-                modMessage("§cInvalid pack name! Use only letters, numbers, spaces, hyphens, and underscores.")
-                false
-            }
-            packFile(packName).exists() -> {
-                modMessage("§cPack '$packName' already exists!")
-                false
-            }
-            else -> runCatching {
-                packFile(packName).writeText(gson.toJson(emptyPack()))
-                modMessage("§aCreated new pack: $packName")
-                true
-            }.getOrElse {
-                it.printStackTrace()
-                modMessage("§cFailed to create pack!")
-                false
+            !isValidPackName(packName) -> fail("Invalid pack name!")
+            file.exists() -> fail("Pack '$packName' already exists!")
+            else -> attempt("Created new pack: $packName", "Failed to create pack!") {
+                file.writeText(DungeonWaypointConfig.gson.toJson(emptyPack()))
             }
         }
     }
 
-    suspend fun deletePack(packName: String) = withContext(Dispatchers.IO) {
+    suspend fun deletePack(packName: String): Boolean = withContext(Dispatchers.IO) {
+        val file = packFile(packName)
         when {
-            !packFile(packName).exists() -> {
-                modMessage("§cPack '$packName' does not exist!")
-                false
-            }
-            else -> runCatching {
-                if (!packFile(packName).delete()) error("Failed to delete ${packFile(packName).path}")
-                modMessage("§aDeleted pack: $packName")
-                true
-            }.getOrElse {
-                it.printStackTrace()
-                modMessage("§cFailed to delete pack!")
-                false
+            !file.exists() -> fail("Pack '$packName' does not exist!")
+            else -> attempt("Deleted pack: $packName", "Failed to delete pack!") {
+                check(file.delete()) { "Failed to delete ${file.path}" }
             }
         }
     }
 
-    suspend fun renamePack(oldName: String, newName: String) = withContext(Dispatchers.IO) {
+    suspend fun renamePack(oldName: String, newName: String): Boolean = withContext(Dispatchers.IO) {
+        val oldFile = packFile(oldName)
+        val newFile = packFile(newName)
         when {
-            !isValidPackName(newName) -> {
-                modMessage("§cInvalid pack name! Use only letters, numbers, spaces, hyphens, and underscores.")
-                false
-            }
-            !packFile(oldName).exists() -> {
-                modMessage("§cPack '$oldName' does not exist!")
-                false
-            }
-            packFile(newName).exists() -> {
-                modMessage("§cPack '$newName' already exists!")
-                false
-            }
-            else -> runCatching {
-                if (!packFile(oldName).renameTo(packFile(newName))) error("Failed to rename ${packFile(oldName).path}")
-                modMessage("§aRenamed pack from '$oldName' to '$newName'")
-                true
-            }.getOrElse {
-                it.printStackTrace()
-                modMessage("§cFailed to rename pack!")
-                false
+            !isValidPackName(newName) -> fail("Invalid pack name!")
+            !oldFile.exists() -> fail("Pack '$oldName' does not exist!")
+            newFile.exists() -> fail("Pack '$newName' already exists!")
+            else -> attempt("Renamed pack from '$oldName' to '$newName'", "Failed to rename pack!") {
+                check(oldFile.renameTo(newFile)) { "Failed to rename ${oldFile.path}" }
             }
         }
     }
 
-    suspend fun getAllPacks() = withContext(Dispatchers.IO) {
-        packsFolder.listFiles { f -> f.extension == "json" }
-            ?.mapNotNull { file ->
-                runCatching {
-                    WaypointPack(file.nameWithoutExtension, file, gson.fromJson(file.readText(), packType))
-                }.getOrElse {
-                    println("Failed to load waypoint pack: ${file.name}")
-                    it.printStackTrace()
-                    null
-                }
-            } ?: emptyList()
+    private fun fail(message: String): Boolean {
+        modMessage("§c$message")
+        return false
     }
 
+    private fun attempt(successMessage: String, failureMessage: String, action: () -> Unit): Boolean =
+        runCatching(action).fold(
+            onSuccess = { modMessage("§a$successMessage"); true },
+            onFailure = { it.printStackTrace(); fail(failureMessage) }
+        )
 
     private fun isValidPackName(name: String) =
-        name.isNotBlank() && name.all { it.isLetterOrDigit() || it == '-' || it == '_' || it == ' ' }
+        name.isNotBlank() && FileUtil.sanitizeName(name) == name && FileUtil.isPathPartPortable(name)
 }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
index 3e3b4da..c7c7509 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/LeapMenu.kt
@@ -3,6 +3,8 @@ package com.odtheking.odin.features.impl.dungeon
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
 import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.GuiEvent
+import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.ScreenEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
@@ -18,6 +20,7 @@ import com.odtheking.odin.utils.ui.HoverHandler
 import com.odtheking.odin.utils.ui.widget.CustomGUIImpl
 import net.minecraft.client.gui.components.PlayerFaceExtractor
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.world.item.Items
 import org.lwjgl.glfw.GLFW
 
 object LeapMenu : Module(
@@ -48,7 +51,9 @@ object LeapMenu : Module(
     private val hoverHandler = List(4) { HoverHandler(200L) }
 
     private val EMPTY = DungeonPlayer("Empty", DungeonClass.EMPTY, 0, null)
-    private val leapedRegex = Regex("You have teleported to (\\w{1,16})!")
+    private val leapedRegex = Regex("^You have teleported to (\\w{1,16})!$")
+    private val playerNameRegex = Regex("^(?:\\[.+?] )?(\\w{1,16})$")
+    private val leapIndex = HashMap<String, Int>()
 
     const val BOX_WIDTH = 200
     const val BOX_HEIGHT = 75
@@ -166,6 +171,18 @@ object LeapMenu : Module(
             if (leapAnnounce && DungeonUtils.inDungeons)
                 leapedRegex.find(value)?.groupValues?.get(1)?.let { sendCommand("pc Leaped to ${it}!") }
         }
+
+        on<GuiEvent.SlotUpdate> {
+            currentLeapScreen()?.let {
+                if (packet.item.isEmpty || packet.item.item != Items.PLAYER_HEAD) return@on
+                val (name) = playerNameRegex.find(packet.item.hoverName.string)?.destructured ?: return@on
+                leapIndex[name] = packet.slot
+            }
+        }
+
+        on<LevelEvent.Load> {
+            leapIndex.clear()
+        }
     }
 
     fun AbstractContainerScreen<*>.mouseTrigger(player: DungeonPlayer, quadrant: Int) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
index 9c45673..30d9049 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
@@ -7,6 +7,8 @@ import com.odtheking.odin.features.ModuleManager
 import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
 import net.minecraft.world.phys.AABB
 
 suspend fun DungeonWaypoints.loadWaypoints() {
@@ -50,7 +52,7 @@ internal suspend fun DungeonWaypoints.createPack(packName: String): Boolean {
 }
 
 internal suspend fun DungeonWaypoints.deletePack(packName: String): Boolean {
-    if (WaypointPackFileUtils.getAllPacks().size <= 1) {
+    if (withContext(Dispatchers.IO) { WaypointPackFileUtils.listPackNames().size } <= 1) {
         modMessage("§cCannot delete the only pack!")
         return false
     }
@@ -104,16 +106,12 @@ private suspend fun DungeonWaypoints.ensurePackState(
     requestedSelection: List<String> = selectedPackIds,
     requestedEditPackId: String = editPackId,
 ): WaypointPackState {
-    var availablePacks = WaypointPackFileUtils.getAllPacks()
-    if (availablePacks.isEmpty()) {
-        WaypointPackFileUtils.createPack("default")
-        availablePacks = WaypointPackFileUtils.getAllPacks()
-    }
+    val availablePackIds = withContext(Dispatchers.IO) { WaypointPackFileUtils.listPackNames() }
 
     val normalizedState = WaypointPackState(
         selectedPackIds = requestedSelection.ifEmpty { selectedPackIds }.toMutableList(),
         editPackId = requestedEditPackId.ifBlank { editPackId },
-    ).normalized(availablePacks.map { it.name })
+    ).normalized(availablePackIds)
 
     selectedPackIds = normalizedState.selectedPackIds.toMutableList()
     editPackId = normalizedState.editPackId
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/WaypointPackSelectorScreen.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/WaypointPackSelectorScreen.kt
index d62cbbd..3f903ce 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/WaypointPackSelectorScreen.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/WaypointPackSelectorScreen.kt
@@ -7,7 +7,6 @@ import com.odtheking.odin.config.WaypointPackFileUtils
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.modMessage
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.components.Button
 import net.minecraft.client.gui.components.ScrollableLayout
@@ -25,14 +24,10 @@ class WaypointPackSelectorScreen(private val parent: Screen?) : Screen(Component
     private var deleteConfirmTime = 0L
     private val deleteButtons = mutableMapOf<String, Button>()
 
-    private fun packNames() = WaypointPackFileUtils.packsFolder
-        .listFiles { f -> f.extension == "json" }
-        ?.map { it.nameWithoutExtension }
-        ?.sorted() ?: emptyList()
+    private fun packNames() = WaypointPackFileUtils.listPackNames()
 
-    private fun packWaypointCount(packName: String): Int =
+    private fun packWaypointCount(packName: String): Int? =
         DungeonWaypoints.loadedPacks[packName]?.values?.sumOf { it.size }
-            ?: runCatching { runBlocking { WaypointPackFileUtils.loadPack(packName).values.sumOf { it.size } } }.getOrDefault(0)
 
     override fun init() {
         super.init()
@@ -89,7 +84,8 @@ class WaypointPackSelectorScreen(private val parent: Screen?) : Screen(Component
             .size(280, 24).tooltip(Tooltip.create(Component.literal(if (isEdit) "Currently editing" else "Click to edit"))).build())
 
         val count = packWaypointCount(packName)
-        row.addChild(StringWidget(Component.literal("§a$count §7wp"), font).apply { setWidth(70) })
+        val countText = if (count != null) "§a$count §7wp" else "§7? wp"
+        row.addChild(StringWidget(Component.literal(countText), font).apply { setWidth(70) })
 
         row.addChild(Button.builder(Component.literal("✎")) { showRenameDialog(packName) }
             .size(30, 24).tooltip(Tooltip.create(Component.literal("Rename"))).build())
@@ -204,7 +200,7 @@ class WaypointPackSelectorScreen(private val parent: Screen?) : Screen(Component
             if (name.isNotBlank()) {
                 loading = true
                 OdinMod.scope.launch {
-                    val ok = DungeonWaypointConfig.decodeWaypoints(clipboard, clipboard.startsWith("{"))
+                    val ok = DungeonWaypointConfig.decodeWaypoints(clipboard)
                         ?.takeIf { DungeonWaypoints.importPack(name, it) }
                         ?.let { modMessage("§aImported waypoints as pack '$name'!"); true }
                         ?: false
Fixed bonzo mask cooldown not being adjusted on per catacombs level basisodtheking · 2026-08-27 · 9c99dd9
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt | 26 +++++++++++++++-----------
 1 file changed, 15 insertions(+), 11 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
index f049fe3..80d7769 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
@@ -15,7 +15,6 @@ import com.odtheking.odin.utils.render.ItemStateRenderer.Companion.drawItemStack
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.world.entity.EquipmentSlot
-import net.minecraft.world.item.ItemStack
 
 object InvincibilityTimer : Module(
     name = "Invincibility Timer",
@@ -78,6 +77,7 @@ object InvincibilityTimer : Module(
         width + 20 to visibleTypes.size * 14
     }
     private val showOnlyInBoss by BooleanSetting("Show In Boss", false, desc = "Only shows invincibility timers during dungeon boss fights.")
+    private val cooldownRegex = Regex("^Cooldown: (\\d+)s$")
 
     init {
         on<TickEvent.Server> {
@@ -87,7 +87,9 @@ object InvincibilityTimer : Module(
         on<ChatPacketEvent> {
             if (onlyInDungeons && !DungeonUtils.inDungeons) return@on
             InvincibilityType.entries.firstOrNull { type -> value.matches(type.regex) }?.let { type ->
-                type.proc()
+                val seconds = mc.player?.getItemBySlot(EquipmentSlot.HEAD)?.loreString?.reversed()
+                    ?.firstNotNullOfOrNull { cooldownRegex.matchEntire(it)?.groupValues?.get(1)?.toIntOrNull() }
+                type.proc(seconds)
                 val usedMasks = InvincibilityType.entries.count { it.currentCooldown > 0 }
                 if (invincibilityAnnounce) sendCommand("pc ${type.name.lowercase().capitalizeFirst()} Procced! ($usedMasks/${InvincibilityType.entries.size})")
                 if (invincibilityAlert) alert(type.name.lowercase().capitalizeFirst())
@@ -126,32 +128,34 @@ object InvincibilityTimer : Module(
         val regex: Regex,
         private val maxInvincibilityTime: Int,
         val maxCooldownTime: Int,
-        val itemStack: ItemStack
+        private val textureHash: String
     ) {
         SPIRIT(
             Regex("^Second Wind Activated! Your Spirit Mask saved your life!$"),
-            60, 600,
-            createSkullStack("9bbe721d7ad8ab965f08cbec0b834f779b5197f79da4aea3d13d253ece9dec2")
+            60, 30,
+            "9bbe721d7ad8ab965f08cbec0b834f779b5197f79da4aea3d13d253ece9dec2"
         ),
         BONZO(
             Regex("^Your (?:. )?Bonzo's Mask saved your life!$"),
-            60, 3600,
-            createSkullStack("12716ecbf5b8da00b05f316ec6af61e8bd02805b21eb8e440151468dc656549c")
+            60, 180,
+            "12716ecbf5b8da00b05f316ec6af61e8bd02805b21eb8e440151468dc656549c"
         ),
         PHOENIX(
             Regex("^Your Phoenix Pet saved you from certain death!$"),
-            80, 1200,
-            createSkullStack("66b1b59bc890c9c97527787dde20600c8b86f6b9912d51a6bfcdb0e4c2aa3c97")
+            80, 60,
+            "66b1b59bc890c9c97527787dde20600c8b86f6b9912d51a6bfcdb0e4c2aa3c97"
         );
 
+        val itemStack by lazy { createSkullStack(textureHash) }
+
         var activeTime: Int = 0
             private set
         var currentCooldown: Int = 0
             private set
 
-        fun proc() {
+        fun proc(cooldownTime: Int?) {
             activeTime = maxInvincibilityTime
-            currentCooldown = maxCooldownTime
+            currentCooldown = (cooldownTime ?: maxCooldownTime) * 20
         }
 
         fun tick() {
Fixed DungeonMap while room text settings being set to only secrets rooms without secrets not having checkmark indicatorodtheking · 2026-08-27 · a4f5cde
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index 4cee003..306b443 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -105,7 +105,7 @@ fun GuiGraphicsExtractor.renderIcon(pos: IVec2, identifier: Identifier) {
 fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
     if (room.type.equalsOneOf(RoomType.UNDISCOVERED, RoomType.FAIRY, RoomType.ENTRANCE, RoomType.BLOOD)) return
 
-    if (!room.walkedInto) {
+    if (!room.walkedInto || (DungeonMap.roomText == 2 && (room.data?.maxSecrets ?: 0) == 0)) {
         when (room.checkmark) {
             MapCheckmark.GREEN -> green
             MapCheckmark.WHITE -> white
Made Puzzle timers not count if time is under 5 secondsodtheking · 2026-08-26 · 709583b
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index 757aff4..267c8b1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -201,7 +201,7 @@ object PuzzleSolvers : Module(
 
     fun onPuzzleComplete(puzzleName: String) {
         puzzleTimersMap[puzzleName]?.let {
-            if (it.sentMessage) return
+            if (it.sentMessage || (System.currentTimeMillis() - it.timeEntered) / 1000f < 5) return
             puzzlePBs.time(puzzleName, (System.currentTimeMillis() - it.timeEntered) / 1000f, "s§7!", "§a${puzzleName} §7solved in §6")
             it.sentMessage = true
         }
fix vesuvius not working at all (#143)
* fix vesuvius not working at all * vitality rework compat * Add lava leech and kuudra pet bonus to profit calculation * better detection * should finally be done :sob: * should finally be done :sob:
TurtleOnFire2 · 2026-08-25 · 061351b
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt | 38 +++++++++++++++++++++++++++-----------
 1 file changed, 27 insertions(+), 11 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
index 7e40538..cecd313 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/nether/Vesuvius.kt
@@ -1,6 +1,7 @@
 package com.odtheking.odin.features.impl.nether
 
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
+import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
@@ -13,7 +14,6 @@ import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.network.chat.Component
 import net.minecraft.network.chat.Style
 import net.minecraft.network.chat.TextColor
-import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket
 import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket
 import net.minecraft.world.item.ItemStack
 import net.minecraft.world.item.Items
@@ -26,6 +26,9 @@ object Vesuvius : Module(
     private val hideClaimed by BooleanSetting("Hide Claimed", true, desc = "Hides chests that have already been claimed.")
     private val useSalvagePrices by BooleanSetting("Use Salvaged", false, desc = "Uses the essence you would get by salvaging the piece instead.")
 
+    private val kuudraPetBonus by NumberSetting("Kuudra Pet Bonus", 0.0, 0.0, 20.0, 0.05, "The essence bonus from Kuudra pet.", unit = "%")
+    private val lavaLeechBonus by NumberSetting("Lava Leech Bonus", 0.0, 0.0, 13.0, 0.05, "The essence bonus from the Lava Leech Shard.", unit = "%")
+
     private val vesuviusHud by HUD("Croesus Chest HUD", "Displays all chest contents with prices, sorted by profit.") {
         if (!it) return@HUD 0 to 0
         drawOverlay(true)
@@ -36,7 +39,7 @@ object Vesuvius : Module(
     private val shardRegex = Regex("^([A-Za-z' ]+) Shard(?: x(\\d+))?$")
     private val teethRegex = Regex("^Kuudra Teeth x(\\d+)$")
     private val pearlRegex = Regex("^Heavy Pearl x(\\d+)$")
-    private val chestRegex = Regex("^((Free|Paid) Chest Chest)|(Kuudra - .+)|Vesuvius$")
+    private val chestRegex = Regex("^((Free|Paid) Chest)|(Kuudra - .+)$")
     private val uselessLinesRegex = Regex("^Contents|Cost|Click to open!|FREE|Already opened!|Can't open another chest!|Paid Chest|")
     private val salvageItemsRegex = Regex("^Boots|Chestplate|Helmet|Cloak|Aurora Staff|Hollow Wand")
 
@@ -53,7 +56,7 @@ object Vesuvius : Module(
     init {
         on<GuiEvent.DrawTooltip> {
             val title = screen.title.string
-            if (vesuviusHud.enabled && title.matches(chestRegex) && currentChest != null && title != "Vesuvius") {
+            if (vesuviusHud.enabled && title.matches(chestRegex) && currentChest != null) {
                 guiGraphics.pose().pushMatrix()
                 val sf = mc.window.guiScale
                 guiGraphics.pose().scale(1f / sf, 1f / sf)
@@ -67,16 +70,18 @@ object Vesuvius : Module(
         }
 
         on<GuiEvent.RenderSlot> {
-            if (screen.title.string.equalsOneOf("Vesuvius", "Croesus") && slot.item.hoverName.string == "Kuudra's Hollow") {
+            if (screen.title.string.containsOneOf("Vesuvius", "Croesus") && slot.item.hoverName.string == "Kuudra's Hollow") {
                 if (hideClaimed && slot.item.loreString.any { it == "No more chests to open!"}) cancel()
             }
         }
 
-        onReceive<ClientboundContainerSetSlotPacket> {
-            val title = mc.screen?.title?.string ?:return@onReceive
-            if (!title.matches(chestRegex)) return@onReceive
-            if (slot == 31 && item.item == Items.CHEST) handleKuudraChest(item)
-            if (slot == 14 && item.item == Items.PLAYER_HEAD) handleKuudraChest(item)
+        on<GuiEvent.SlotUpdate> {
+            if (!screen.title.string.matches(chestRegex)) return@on
+
+            val item = packet.item
+
+            if (packet.slot == 31 && item.item == Items.CHEST) handleKuudraChest(item)
+            if (packet.slot.equalsOneOf(13, 14) && item.item == Items.PLAYER_HEAD) handleKuudraChest(item)
         }
 
         onReceive<ClientboundOpenScreenPacket> {
@@ -89,6 +94,8 @@ object Vesuvius : Module(
         var starCount = 0
         val salvage = salvageItemsRegex.containsMatchIn(component.string)
 
+        val essenceBonus = (1 + kuudraPetBonus / 100.0) * (1 + lavaLeechBonus / 100.0)
+
         component.visit({ style, str ->
             val count = str.count { it == '✪' }
             if (count > 0) {
@@ -103,17 +110,19 @@ object Vesuvius : Module(
         val item = component.string.replace("✪", "").trim()
 
         if (item.contains("Molten") && useSalvagePrices) {
-            return (cachedPrices["ESSENCE_CRIMSON"] ?: 0.0) * 600.0
+            return (cachedPrices["ESSENCE_CRIMSON"] ?: 0.0) * 600.0 * essenceBonus
         }
 
         previewEnchantedBookRegex.find(item)?.destructured?.let { (name, level) ->
             val ult = if (name in ultimateEnchants) "ULTIMATE_" else ""
+
+            enchantReplacements[name]?.let { itemId -> return cachedPrices["$itemId-${romanToInt(level)}"] }
             return cachedPrices["ENCHANTED_BOOK-$ult${name.uppercase().replace(" ", "_")}-${romanToInt(level)}"]
         }
 
         previewEssenceRegex.find(item)?.destructured?.let { (name, quantity) ->
             val price = cachedPrices["ESSENCE_${name.uppercase()}"] ?: return null
-            return price * quantity.toDouble()
+            return price * quantity.toDouble() * essenceBonus
         }
 
         shardRegex.find(item)?.let { shard ->
@@ -217,6 +226,13 @@ object Vesuvius : Module(
         "Aurora Staff" to "RUNIC_STAFF",
     )
 
+    private val enchantReplacements = mapOf(
+        "Vivacious Vitality" to "ENCHANTED_BOOK-FEROCIOUS_MANA",
+        "Hardened Vitality" to "ENCHANTED_BOOK-HARDENED_MANA",
+        "Strong Vitality" to "ENCHANTED_BOOK-STRONG_MANA",
+        "Vampiric Vitality" to "ENCHANTED_BOOK-MANA_VAMPIRE",
+    )
+
     private val starCountToEssence = mapOf(
         0 to 120.0,
         1 to 138.0,
Added dev message to debug websocket in DungeonMapodtheking · 2026-08-23 · 97de329
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt | 5 +++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt     | 2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt         | 3 ++-
 3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index a68aaa3..af67ecc 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -13,6 +13,7 @@ import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.IVec2
+import com.odtheking.odin.utils.devMessage
 import com.odtheking.odin.utils.network.WebUtils.gson
 import com.odtheking.odin.utils.network.webSocket
 import com.odtheking.odin.utils.render.hollowFill
@@ -94,13 +95,13 @@ object DungeonMap : Module(
 
     init {
         on<SecretsUpdateEvent> {
-            if (!allowWebsocket || !syncSocket.connected) return@on
+            if (!allowWebsocket) return@on
             room.name?.let { syncSocket.send(gson.toJson(RoomSync(it, foundSecrets, room.topLeft))) }
         }
 
         on<FloorEnterEvent> {
             if (!allowWebsocket) return@on
-            LocationUtils.lobbyId?.let { syncSocket.connect("${ClickGUIModule.webSocketUrl}$it") }
+            LocationUtils.lobbyId?.let { syncSocket.connect("${ClickGUIModule.webSocketUrl}$it") } ?: devMessage("Failed to connect to dungeon websocket, lobbyId is null.")
         }
 
         on<RoomEnterEvent> {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
index e0863e3..05a96c1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
@@ -38,7 +38,7 @@ object SlotBinds : Module(
 
     init {
         on<GuiEvent.SlotClick> {
-            if (mc.hasShiftDown() || screen !is InventoryScreen) return@on
+            if (!mc.hasShiftDown() || screen !is InventoryScreen) return@on
             val clickedSlot = screen.hoveredSlot?.index?.takeIf { it in 5 until 45 } ?: return@on
             val boundSlot = slotBinds[clickedSlot] ?: return@on
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
index 1737c42..7c74b82 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
@@ -3,6 +3,7 @@ package com.odtheking.odin.utils.skyblock
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.LocationChangeEvent
+import com.odtheking.odin.events.core.EventPriority
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.utils.equalsOneOf
@@ -40,7 +41,7 @@ object LocationUtils {
             if (!isInSkyblock) isInSkyblock = objectiveName == "SBScoreboard"
         }
 
-        onReceive<ClientboundSetPlayerTeamPacket> {
+        onReceive<ClientboundSetPlayerTeamPacket> (EventPriority.LOW) {
             if (!isCurrentArea(Island.Unknown)) return@onReceive
             val text = parameters.getOrNull()?.let { it.playerPrefix.string.plus(it.playerSuffix.string).noControlCodes } ?: return@onReceive
 
Fixed croesus not highlighting correctlyodtheking · 2026-08-23 · 1199163
查看变更
src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt                  |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt    | 26 ++++++++++++++++++++------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt |  2 +-
 3 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
index 21978f4..aebbc3b 100644
--- a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
@@ -12,7 +12,7 @@ abstract class GuiEvent(val screen: Screen) : CancellableEvent() {
 
     class Render(screen: Screen, val guiGraphics: GuiGraphicsExtractor, val mouseX: Int, val mouseY: Int) : GuiEvent(screen)
 
-    class SlotClick(screen: Screen, val slot: Slot, val slotId: Int, val button: Int, val containerInput: ContainerInput) : GuiEvent(screen)
+    class SlotClick(screen: Screen, val slot: Slot?, val slotId: Int, val button: Int, val containerInput: ContainerInput) : GuiEvent(screen)
 
     class SlotUpdate(screen: Screen, val packet: ClientboundContainerSetSlotPacket, val menu: AbstractContainerMenu) : GuiEvent(screen)
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
index c63dacb..7585479 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
@@ -14,6 +14,7 @@ import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.*
 import com.odtheking.odin.utils.network.WebUtils.fetchJson
 import com.odtheking.odin.utils.render.getStringWidth
+import com.odtheking.odin.utils.render.roundedOutline
 import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
@@ -72,6 +73,9 @@ object Croesus : Module(
     private val extraStatsRegex = Regex(" {29}> EXTRA STATS <")
     private val chestCostRegex = Regex("^([\\d,]+) Coins$")
     private val shardRegex = Regex("^([A-Za-z ]+) Shard$")
+    private val noMoreChestsRegex = Regex("^No more chests to open!$")
+
+    private const val KISMET_FEATHER_LABEL = "Kismet Feather"
 
     private val ultimateEnchants = setOf(
         "Soul Eater", "Combo", "Legion", "One For All", "Rend",
@@ -110,9 +114,19 @@ object Croesus : Module(
                 val loreString = slot.item.loreString
 
                 if (hideClaimed && loreString.any { it.matches(chestStatusRegex) } && (!includeKey || hasStrikeThrough("Dungeon Chest Key", slot.item.lore ))) cancel()
-                else if (highlightState)
-                    guiGraphics.fill(slot.x, slot.y, slot.x + 16, slot.y + 16,
-                        if (loreString.any { it.matches(chestOpenedRegex) }) Colors.MINECRAFT_GOLD.rgba else Colors.MINECRAFT_GREEN.rgba)
+                else {
+                    if (highlightState) {
+                        val color = when {
+                            loreString.any { it.matches(noMoreChestsRegex) } -> Colors.MINECRAFT_RED.rgba
+                            loreString.any { it.matches(chestOpenedRegex) } -> Colors.MINECRAFT_GOLD.rgba
+                            else -> Colors.MINECRAFT_GREEN.rgba
+                        }
+                        guiGraphics.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, color)
+                    }
+
+                    if (hasStrikeThrough(KISMET_FEATHER_LABEL, slot.item.lore))
+                        guiGraphics.roundedOutline(slot.x, slot.y, slot.x + 16, slot.y + 16, Colors.MINECRAFT_AQUA.rgba, 1f)
+                }
 
             } else if (highlightProfitable && screen.title.string.matches(chestPreviewScreenRegex) && slot.index in mostProfitableSlots) {
                 val color = when (mostProfitableSlots.indexOf(slot.index)) {
@@ -126,11 +140,11 @@ object Croesus : Module(
 
         onReceive<ClientboundContainerSetSlotPacket> {
             val screenTitle = mc.screen?.title?.string ?: return@onReceive
-            val menu = (mc.screen as? AbstractContainerScreen<*>)?.menu ?: return@onReceive
+            val items = (mc.screen as? AbstractContainerScreen<*>)?.menu?.items ?: return@onReceive
 
             when {
-                screenTitle.matches(chestNameRegex) -> handleChestContents(menu.items)
-                screenTitle.matches(chestPreviewScreenRegex) -> handleCroesusScreen(menu.items)
+                screenTitle.matches(chestNameRegex) -> handleChestContents(items)
+                screenTitle.matches(chestPreviewScreenRegex) -> handleCroesusScreen(items)
             }
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
index c27b356..e0863e3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
@@ -38,7 +38,7 @@ object SlotBinds : Module(
 
     init {
         on<GuiEvent.SlotClick> {
-            if (containerInput != ContainerInput.QUICK_MOVE || screen !is InventoryScreen) return@on
+            if (mc.hasShiftDown() || screen !is InventoryScreen) return@on
             val clickedSlot = screen.hoveredSlot?.index?.takeIf { it in 5 until 45 } ?: return@on
             val boundSlot = slotBinds[clickedSlot] ?: return@on
 
Fixed ActionBarListener not registering room secrets when they are 0odtheking · 2026-08-23 · 2e4ae96
查看变更
src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java  |  2 +-
 src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt                       |  3 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt  | 24 ++++++++++++++----------
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt      |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt      |  2 +-
 6 files changed, 20 insertions(+), 15 deletions(-)
diff --git a/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java b/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java
index 016caa2..617e9f3 100644
--- a/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/AbstractContainerScreenMixin.java
@@ -26,7 +26,7 @@ public class AbstractContainerScreenMixin {
 
     @Inject(method = "slotClicked", at = @At("HEAD"), cancellable = true)
     private void onMouseClickedSlot(Slot slot, int slotId, int buttonNum, ContainerInput containerInput, CallbackInfo ci) {
-        if (new GuiEvent.SlotClick((Screen) (Object) this, slotId, buttonNum).postAndCatch()) ci.cancel();
+        if (new GuiEvent.SlotClick((Screen) (Object) this, slot, slotId, buttonNum, containerInput).postAndCatch()) ci.cancel();
     }
 
     @Inject(method = "extractTooltip", at = @At("HEAD"), cancellable = true)
diff --git a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
index 16d97cb..21978f4 100644
--- a/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/GuiEvent.kt
@@ -5,13 +5,14 @@ import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.Screen
 import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket
 import net.minecraft.world.inventory.AbstractContainerMenu
+import net.minecraft.world.inventory.ContainerInput
 import net.minecraft.world.inventory.Slot
 
 abstract class GuiEvent(val screen: Screen) : CancellableEvent() {
 
     class Render(screen: Screen, val guiGraphics: GuiGraphicsExtractor, val mouseX: Int, val mouseY: Int) : GuiEvent(screen)
 
-    class SlotClick(screen: Screen, val slotId: Int, val button: Int) : GuiEvent(screen)
+    class SlotClick(screen: Screen, val slot: Slot, val slotId: Int, val button: Int, val containerInput: ContainerInput) : GuiEvent(screen)
 
     class SlotUpdate(screen: Screen, val packet: ClientboundContainerSetSlotPacket, val menu: AbstractContainerMenu) : GuiEvent(screen)
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
index 9ed996b..0d85254 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
@@ -74,7 +74,7 @@ open class TermSimGUI(
     }
 
     override fun slotClicked(slot: Slot, i: Int, j: Int, clickType: ContainerInput) {
-        if (GuiEvent.SlotClick(this, i, j).postAndCatch()) return
+        if (GuiEvent.SlotClick(this, slot, i, j, clickType).postAndCatch()) return
         slot?.let { delaySlotClick(it, j) }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index 3b7233b..a68aaa3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -12,6 +12,7 @@ import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.IVec2
 import com.odtheking.odin.utils.network.WebUtils.gson
 import com.odtheking.odin.utils.network.webSocket
 import com.odtheking.odin.utils.render.hollowFill
@@ -53,7 +54,7 @@ object DungeonMap : Module(
 
     val disablePred by BooleanSetting("Disable Prediction", false, desc = "Disables special-column room type prediction.")
 
-    val shareSecrets by BooleanSetting("Share Secrets", true, desc = "Shares found secrets in your room with the rest of your dungeon party.")
+    private val allowWebsocket by BooleanSetting("Websocket", true, desc = "Shares information in your room with the rest of your dungeon party.")
 
     private val exampleRooms by lazy { buildExampleRooms() }
     private val exampleDoors by lazy { buildExampleDoors() }
@@ -84,26 +85,29 @@ object DungeonMap : Module(
 
     val syncSocket = webSocket {
         onMessage { message ->
-            val sync = try { gson.fromJson(message, RoomSecretsSync::class.java) } catch (_: Exception) { return@onMessage }
-            val room = DungeonScan.rooms.find { it.name == sync.roomName } ?: return@onMessage
-            if ((room.foundSecrets ?: 0) >= sync.foundSecrets) return@onMessage
-            room.foundSecrets = sync.foundSecrets
+            val (roomName, foundSecrets, position) = try { gson.fromJson(message, RoomSync::class.java) } catch (_: Exception) { return@onMessage }
+            val room = DungeonScan.rooms.find { it.name == roomName && it.topLeft == position } ?: return@onMessage
+            if ((room.foundSecrets ?: -1) < (foundSecrets ?: -1)) room.foundSecrets = foundSecrets
+            room.walkedInto = true
         }
     }
 
     init {
         on<SecretsUpdateEvent> {
-            if (!shareSecrets || !syncSocket.connected) return@on
-            room.name?.let { syncSocket.send(gson.toJson(RoomSecretsSync(it, foundSecrets))) }
+            if (!allowWebsocket || !syncSocket.connected) return@on
+            room.name?.let { syncSocket.send(gson.toJson(RoomSync(it, foundSecrets, room.topLeft))) }
         }
 
         on<FloorEnterEvent> {
-            if (!shareSecrets) return@on
+            if (!allowWebsocket) return@on
             LocationUtils.lobbyId?.let { syncSocket.connect("${ClickGUIModule.webSocketUrl}$it") }
         }
 
-        on<RoomEnterEvent> { if (room == null) syncSocket.shutdown() }
+        on<RoomEnterEvent> {
+            if (room == null) syncSocket.shutdown()
+            else if (allowWebsocket) room.name?.let { syncSocket.send(gson.toJson(RoomSync(it, room.foundSecrets, room.topLeft))) }
+        }
     }
 
-    private data class RoomSecretsSync(val roomName: String, val foundSecrets: Int)
+    private data class RoomSync(val roomName: String, val foundSecrets: Int?, val position: IVec2)
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
index 05a96c1..c27b356 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/SlotBinds.kt
@@ -38,7 +38,7 @@ object SlotBinds : Module(
 
     init {
         on<GuiEvent.SlotClick> {
-            if (!mc.hasShiftDown() || screen !is InventoryScreen) return@on
+            if (containerInput != ContainerInput.QUICK_MOVE || screen !is InventoryScreen) return@on
             val clickedSlot = screen.hoveredSlot?.index?.takeIf { it in 5 until 45 } ?: return@on
             val boundSlot = slotBinds[clickedSlot] ?: return@on
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
index 49df55c..6ac3cc2 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
@@ -55,7 +55,7 @@ object ActionBarListener {
             SECRETS_REGEX.find(msg)?.destructured?.let { (found, max) ->
                 DungeonUtils.currentRoom?.let {
                     val updatedFoundSecrets = found.toIntOrNull() ?: return@let
-                    if (it.data?.maxSecrets != max.toIntOrNull() || (it.foundSecrets ?: 0) >= updatedFoundSecrets) return@let
+                    if (it.data?.maxSecrets != max.toIntOrNull() || (it.foundSecrets ?: -1) >= updatedFoundSecrets) return@let
                     it.foundSecrets = updatedFoundSecrets
                     SecretsUpdateEvent(it, updatedFoundSecrets).postAndCatch()
                 }
Added dungeon room found secrets support to dungeon map with websocket integrationodtheking · 2026-08-22 · 4fd54ad
查看变更
src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt                        |   6 +-
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                        |   1 +
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt              |  17 +----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt       |  44 +++++++++++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt      |  17 -----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt      |  91 ++++++++++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt |  34 +++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/RoomData.kt    |   2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt           |  12 ++++
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt     |   2 +-
 src/main/resources/assets/odin/rooms.json                                        | 244 +++++++++++++++++++++++++++++++++++------------------------------------
 11 files changed, 252 insertions(+), 218 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
index c227c99..5ad900a 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
@@ -10,6 +10,7 @@ import com.odtheking.odin.features.impl.boss.MelodyMessage.melodyWebSocket
 import com.odtheking.odin.features.impl.boss.WitherDragonState
 import com.odtheking.odin.features.impl.boss.WitherDragons
 import com.odtheking.odin.features.impl.boss.WitherDragonsEnum
+import com.odtheking.odin.features.impl.dungeon.map.DungeonMap
 import com.odtheking.odin.features.impl.dungeon.map.DungeonScan
 import com.odtheking.odin.features.impl.dungeon.map.WorldScan
 import com.odtheking.odin.features.impl.nether.NoPre
@@ -35,9 +36,12 @@ import net.minecraft.world.phys.BlockHitResult
 val devCommand = Commodore("oddev") {
 
     literal("ws") {
-        literal("connect").runs { lobby: String ->
+        literal("melody").runs { lobby: String ->
             melodyWebSocket.connect("${webSocketUrl}${lobby}")
         }
+        literal("secrets").runs { lobby: String ->
+            DungeonMap.syncSocket.connect("${webSocketUrl}${lobby}")
+        }
     }
 
     literal("getitem").runs {
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index c8f06f8..5a2404a 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -79,5 +79,6 @@ class FloorEnterEvent(val floor: Floor) : Event
 class ScoreUpdateEvent(val score: Int) : Event
 data object MapUpdateEvent : Event
 class CheckmarkUpdateEvent(val room: DungeonRoom, val checkmark: MapCheckmark) : Event
+class SecretsUpdateEvent(val room: DungeonRoom, val foundSecrets: Int) : Event
 
 object LocationChangeEvent : Event
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index ed83fa2..673fede 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -7,20 +7,17 @@ import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.RoomEnterEvent
 import com.odtheking.odin.events.ScoreUpdateEvent
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.alert
 import com.odtheking.odin.utils.modMessage
-import com.odtheking.odin.utils.noControlCodes
 import com.odtheking.odin.utils.render.drawFilledBox
 import com.odtheking.odin.utils.render.getStringWidth
 import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.core.BlockPos
-import net.minecraft.network.protocol.game.ClientboundSystemChatPacket
 import net.minecraft.world.phys.AABB
 
 object MapInfo : Module(
@@ -178,7 +175,9 @@ object MapInfo : Module(
     private val roomSecrets by HUD("Room Secrets", "Displays the number of secrets in the current room.") {
         if ((!DungeonUtils.inClear) && !it) return@HUD 0 to 0
 
-        val secrets = if (it) 0 to 2 else currentRoomSecrets ?: return@HUD 0 to 0
+        val secrets = if (it) 0 to 2 else DungeonUtils.currentRoom?.foundSecrets?.let { found ->
+            DungeonUtils.currentRoom?.data?.maxSecrets?.let { max -> found to max }
+        } ?: return@HUD 0 to 0
         val color = when {
             secrets.first * 2 < secrets.second -> "§c"
             secrets.first * 4 < secrets.second * 3 -> "§e"
@@ -198,8 +197,6 @@ object MapInfo : Module(
     }
 
     private var portalAABB: AABB? = null
-    private var currentRoomSecrets: Pair<Int, Int>? = null
-    private val secretRegex = Regex("(\\d+)/(\\d+) Secrets")
     private var shownTitle = false
 
     init {
@@ -210,15 +207,7 @@ object MapInfo : Module(
             if (printWhenScore) modMessage("§b${DungeonUtils.score} §ascore reached in §6${DungeonUtils.dungeonTime} §8|| §e${DungeonUtils.floor?.name}.")
         }
 
-        onReceive<ClientboundSystemChatPacket> {
-            if (!overlay) return@onReceive
-            secretRegex.find(content.string.noControlCodes )?.destructured?.let { (found, total) ->
-                currentRoomSecrets = Pair(found.toIntOrNull() ?: 0, total.toIntOrNull() ?: 0)
-            }
-        }
-
         on<RoomEnterEvent> {
-            currentRoomSecrets = null
             if (room?.name == "Blood")
                 portalAABB = AABB.encapsulatingFullBlocks(room.getRealCoords(BlockPos(16, 69, 29)), room.getRealCoords(BlockPos(14, 69, 29))).inflate(0.0, 4.0, 0.0)
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index 483398a..3b7233b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -1,16 +1,21 @@
 package com.odtheking.odin.features.impl.dungeon.map
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
-import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
-import com.odtheking.odin.clickgui.settings.impl.NumberSetting
+import com.odtheking.odin.clickgui.settings.impl.*
+import com.odtheking.odin.events.FloorEnterEvent
+import com.odtheking.odin.events.RoomEnterEvent
+import com.odtheking.odin.events.SecretsUpdateEvent
+import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
+import com.odtheking.odin.features.impl.render.ClickGUIModule
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.network.WebUtils.gson
+import com.odtheking.odin.utils.network.webSocket
 import com.odtheking.odin.utils.render.hollowFill
+import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 
 object DungeonMap : Module(
@@ -21,6 +26,7 @@ object DungeonMap : Module(
 
     val backgroundOutline by ColorSetting("Background Outline", Colors.BLACK, true, desc = "The color of the background border.")
     val backgroundColor by ColorSetting("Background Color", Colors.BLACK.withAlpha(0.1f), true, desc = "Background color of the map.")
+    val roomText by SelectorSetting("Room Text", "Both", listOf("Both", "Room Name", "Room Secrets"), desc = "What to display on the rooms.")
     val textScaling by NumberSetting("Text Scaling", 0.45f, 0.1f, 1f, 0.05f, desc = "Scale of room name text.")
 
     private val playerDropdown by DropdownSetting("Player Settings")
@@ -47,8 +53,11 @@ object DungeonMap : Module(
 
     val disablePred by BooleanSetting("Disable Prediction", false, desc = "Disables special-column room type prediction.")
 
+    val shareSecrets by BooleanSetting("Share Secrets", true, desc = "Shares found secrets in your room with the rest of your dungeon party.")
+
     private val exampleRooms by lazy { buildExampleRooms() }
     private val exampleDoors by lazy { buildExampleDoors() }
+    private const val MAP_PX = 128
 
     private val mapHud by HUD("Dungeon Map", "Displays the dungeon map.", false) { example ->
         if ((!DungeonUtils.inDungeons || (disableBoss && DungeonUtils.inBoss)) && !example) return@HUD 0 to 0
@@ -62,7 +71,7 @@ object DungeonMap : Module(
         }
         else {
             pose().translate(DungeonScan.startX.toFloat(), DungeonScan.startY.toFloat())
-            pose().scale(DungeonScan.roomSize / DungeonScan.MAP_ROOM_SIZE.toFloat())
+            pose().scale(DungeonScan.roomSize / 16f)
 
             renderMap(DungeonScan.rooms, DungeonScan.doors.values, DungeonScan.pathHints)
 
@@ -73,5 +82,28 @@ object DungeonMap : Module(
         MAP_PX to MAP_PX
     }
 
-    private const val MAP_PX = 128
+    val syncSocket = webSocket {
+        onMessage { message ->
+            val sync = try { gson.fromJson(message, RoomSecretsSync::class.java) } catch (_: Exception) { return@onMessage }
+            val room = DungeonScan.rooms.find { it.name == sync.roomName } ?: return@onMessage
+            if ((room.foundSecrets ?: 0) >= sync.foundSecrets) return@onMessage
+            room.foundSecrets = sync.foundSecrets
+        }
+    }
+
+    init {
+        on<SecretsUpdateEvent> {
+            if (!shareSecrets || !syncSocket.connected) return@on
+            room.name?.let { syncSocket.send(gson.toJson(RoomSecretsSync(it, foundSecrets))) }
+        }
+
+        on<FloorEnterEvent> {
+            if (!shareSecrets) return@on
+            LocationUtils.lobbyId?.let { syncSocket.connect("${ClickGUIModule.webSocketUrl}$it") }
+        }
+
+        on<RoomEnterEvent> { if (room == null) syncSocket.shutdown() }
+    }
+
+    private data class RoomSecretsSync(val roomName: String, val foundSecrets: Int)
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
index 97f0b2d..929402e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
@@ -10,18 +10,13 @@ import com.odtheking.odin.features.impl.dungeon.map.tile.*
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.IVec2
-import com.odtheking.odin.utils.renderX
-import com.odtheking.odin.utils.renderZ
 import com.odtheking.odin.utils.skyblock.dungeon.Floor
-import net.minecraft.world.entity.player.Player
 import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.CopyOnWriteArrayList
 
 object DungeonScan {
 
     const val ROOM_SPACING = 4
-    const val MAP_ROOM_SIZE = 16
-    const val MAP_ROOM_GAP = MAP_ROOM_SIZE + ROOM_SPACING
 
     var roomSize = 16
     val roomGap: Int get () = roomSize + ROOM_SPACING
@@ -77,18 +72,6 @@ object DungeonScan {
         }
     }
 
-    fun playerRenderPosition(entity: Player?, mapPos: IVec2): Pair<Float, Float> {
-        entity?.let {
-            val mapX = (it.renderX.toFloat() + 200f) * MAP_ROOM_GAP / 32f
-            val mapZ = (it.renderZ.toFloat() + 200f) * MAP_ROOM_GAP / 32f
-            return mapX to mapZ
-        }
-
-        val pixelX = (mapPos.x + 128) / 2f - startX
-        val pixelY = (mapPos.z + 128) / 2f - startY
-        return pixelX to pixelY
-    }
-
     fun updateViewableDoors() {
         pathHints.clear()
         for ((_, door) in doors) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index ae26983..4cee003 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -1,6 +1,9 @@
 package com.odtheking.odin.features.impl.dungeon.map
 
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.features.impl.dungeon.map.DungeonScan.ROOM_SPACING
+import com.odtheking.odin.features.impl.dungeon.map.DungeonScan.startX
+import com.odtheking.odin.features.impl.dungeon.map.DungeonScan.startY
 import com.odtheking.odin.features.impl.dungeon.map.tile.*
 import com.odtheking.odin.utils.*
 import com.odtheking.odin.utils.Color.Companion.darker
@@ -9,6 +12,7 @@ import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.components.PlayerFaceExtractor
 import net.minecraft.client.renderer.RenderPipelines
 import net.minecraft.resources.Identifier
+import net.minecraft.world.entity.player.Player
 
 private val marker = Identifier.withDefaultNamespace("textures/map/decorations/frame.png")
 private val cross = Identifier.fromNamespaceAndPath("odin", "map/cross.png")
@@ -16,49 +20,49 @@ private val green = Identifier.fromNamespaceAndPath("odin", "map/green_check.png
 private val white = Identifier.fromNamespaceAndPath("odin", "map/white_check.png")
 private val question = Identifier.fromNamespaceAndPath("odin", "map/question.png")
 
+private const val MAP_ROOM_SIZE = 16
+private const val MAP_ROOM_GAP = MAP_ROOM_SIZE + ROOM_SPACING
+
 fun GuiGraphicsExtractor.fillRoom(room: DungeonRoom, color: Int) {
-    val rs = DungeonScan.MAP_ROOM_SIZE
-    val rg = DungeonScan.MAP_ROOM_GAP
-    val ox = room.topLeft.x * rg
-    val oy = room.topLeft.z * rg
+    val ox = room.topLeft.x * MAP_ROOM_GAP
+    val oy = room.topLeft.z * MAP_ROOM_GAP
     when (val shape = room.shape) {
-        RoomShape.OneByOne -> fill(ox, oy, ox + rs, oy + rs, color)
-        RoomShape.TwoByTwo -> fill(ox, oy, ox + rg + rs, oy + rg + rs, color)
+        RoomShape.OneByOne -> fill(ox, oy, ox + MAP_ROOM_SIZE, oy + MAP_ROOM_SIZE, color)
+        RoomShape.TwoByTwo -> fill(ox, oy, ox + MAP_ROOM_GAP + MAP_ROOM_SIZE, oy + MAP_ROOM_GAP + MAP_ROOM_SIZE, color)
 
         RoomShape.L -> when (room.rotation) {
             RoomRotation.WEST -> {
-                fill(ox, oy,      ox + rg + rs, oy + rs,      color)
-                fill(ox, oy + rs, ox + rs,      oy + rg + rs, color)
+                fill(ox, oy,      ox + MAP_ROOM_GAP + MAP_ROOM_SIZE, oy + MAP_ROOM_SIZE, color)
+                fill(ox, oy + MAP_ROOM_SIZE, ox + MAP_ROOM_SIZE, oy + MAP_ROOM_GAP + MAP_ROOM_SIZE, color)
             }
             RoomRotation.NORTH -> {
-                fill(ox,      oy,      ox + rg + rs, oy + rs,      color)
-                fill(ox + rg, oy + rs, ox + rg + rs, oy + rg + rs, color)
+                fill(ox,      oy,      ox + MAP_ROOM_GAP + MAP_ROOM_SIZE, oy + MAP_ROOM_SIZE, color)
+                fill(ox + MAP_ROOM_GAP, oy + MAP_ROOM_SIZE, ox + MAP_ROOM_GAP + MAP_ROOM_SIZE, oy + MAP_ROOM_GAP + MAP_ROOM_SIZE, color)
             }
             else -> {
-                fill(ox,      oy,      ox + rs,      oy + rg + rs, color)
-                fill(ox + rs, oy + rg, ox + rg + rs, oy + rg + rs, color)
+                fill(ox,      oy,      ox + MAP_ROOM_SIZE,      oy + MAP_ROOM_GAP + MAP_ROOM_SIZE, color)
+                fill(ox + MAP_ROOM_SIZE, oy + MAP_ROOM_GAP, ox + MAP_ROOM_GAP + MAP_ROOM_SIZE, oy + MAP_ROOM_GAP + MAP_ROOM_SIZE, color)
             }
         }
 
         else -> {
-            if (room.rotation == RoomRotation.SOUTH) fill(ox, oy, ox + (shape.tileAmount - 1) * rg + rs, oy + rs, color)
-            else fill(ox, oy, ox + rs, oy + (shape.tileAmount - 1) * rg + rs, color)
+            if (room.rotation == RoomRotation.SOUTH) fill(ox, oy, ox + (shape.tileAmount - 1) * MAP_ROOM_GAP + MAP_ROOM_SIZE, oy + MAP_ROOM_SIZE, color)
+            else fill(ox, oy, ox + MAP_ROOM_SIZE, oy + (shape.tileAmount - 1) * MAP_ROOM_GAP + MAP_ROOM_SIZE, color)
         }
     }
 }
 
 fun GuiGraphicsExtractor.renderDoors(doors: Collection<DungeonDoor>) {
-    val rs = DungeonScan.MAP_ROOM_SIZE
-    val rg = DungeonScan.MAP_ROOM_GAP
-    val half = (rs - 8) / 2f
+    val rg = MAP_ROOM_GAP
+    val half = (MAP_ROOM_SIZE - 8) / 2f
 
     for (door in doors) {
         val offset = door.rotation.offset
 
         pose().pushMatrix()
         pose().translate(
-            door.position.x * rg + offset.x * rs + offset.z * half,
-            door.position.z * rg + offset.z * rs + offset.x * half
+            door.position.x * rg + offset.x * MAP_ROOM_SIZE + offset.z * half,
+            door.position.z * rg + offset.z * MAP_ROOM_SIZE + offset.x * half
         )
         fill(0, 0, 4 + 4 * offset.z, 4 + 4 * offset.x, door.color.rgba)
         pose().popMatrix()
@@ -66,30 +70,27 @@ fun GuiGraphicsExtractor.renderDoors(doors: Collection<DungeonDoor>) {
 }
 
 fun GuiGraphicsExtractor.renderPathHints(pathHints: Collection<DungeonTile>) {
-    val rs = DungeonScan.MAP_ROOM_SIZE
-    val rg = DungeonScan.MAP_ROOM_GAP
-
     for ((position, room) in pathHints) {
-        val (x, y) = position.x * rg to position.z * rg
+        val (x, y) = position.x * MAP_ROOM_GAP to position.z * MAP_ROOM_GAP
 
         val colors = if (room?.type == RoomType.BLOOD) arrayOf(DungeonMap.bloodRoomColor.darker(0.5f))
         else if (!DungeonMap.disablePred && room?.isKnown1x1 == true) SpecialColumn.colorGuessForUnknown(position.x)
         else arrayOf(DungeonMap.unknownRoomColor)
 
         when (colors.size) {
-            1 -> fill(x, y, x + rs, y + rs, colors[0].rgba)
+            1 -> fill(x, y, x + MAP_ROOM_SIZE, y + MAP_ROOM_SIZE, colors[0].rgba)
 
             2 -> {
-                val half = rs / 2
-                fill(x, y, x + half, y + rs, colors[0].darker(0.5f).rgba)
-                fill(x + half, y, x + rs, y + rs, colors[1].darker(0.5f).rgba)
+                val half = MAP_ROOM_SIZE / 2
+                fill(x, y, x + half, y + MAP_ROOM_SIZE, colors[0].darker(0.5f).rgba)
+                fill(x + half, y, x + MAP_ROOM_SIZE, y + MAP_ROOM_SIZE, colors[1].darker(0.5f).rgba)
             }
 
             3 -> {
-                val third = rs / 3
-                fill(x, y, x + third, y + rs, colors[0].darker(0.5f).rgba)
-                fill(x + third, y, x + third * 2, y + rs, colors[1].darker(0.5f).rgba)
-                fill(x + third * 2, y, x + rs, y + rs, colors[2].darker(0.5f).rgba)
+                val third = MAP_ROOM_SIZE / 3
+                fill(x, y, x + third, y + MAP_ROOM_SIZE, colors[0].darker(0.5f).rgba)
+                fill(x + third, y, x + third * 2, y + MAP_ROOM_SIZE, colors[1].darker(0.5f).rgba)
+                fill(x + third * 2, y, x + MAP_ROOM_SIZE, y + MAP_ROOM_SIZE, colors[2].darker(0.5f).rgba)
             }
         }
         renderIcon(IVec2(x, y), question)
@@ -97,7 +98,7 @@ fun GuiGraphicsExtractor.renderPathHints(pathHints: Collection<DungeonTile>) {
 }
 
 fun GuiGraphicsExtractor.renderIcon(pos: IVec2, identifier: Identifier) {
-    val size = DungeonScan.MAP_ROOM_SIZE - DungeonScan.ROOM_SPACING
+    val size = MAP_ROOM_SIZE - ROOM_SPACING
     blit(RenderPipelines.GUI_TEXTURED, identifier, pos.x + 2, pos.z + 2, size.toFloat(), size.toFloat(), size, size, size, size)
 }
 
@@ -110,7 +111,7 @@ fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
             MapCheckmark.WHITE -> white
             MapCheckmark.RED -> cross
             else -> null
-        }?.let { texture -> renderIcon(room.topLeft * DungeonScan.MAP_ROOM_GAP, texture) }
+        }?.let { texture -> renderIcon(room.topLeft * MAP_ROOM_GAP, texture) }
         return
     }
 
@@ -124,7 +125,12 @@ fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
         else               -> Color(100, 100, 100)
     }.rgba
 
-    val lines  = room.name?.split(" ") ?: return
+    val secretsLine = if ((room.data?.maxSecrets ?: 0) > 0) " ${room.foundSecrets ?: "?"}/${room.data?.maxSecrets}" else ""
+    val lines = when (DungeonMap.roomText) {
+        0 -> "${room.name}$secretsLine"
+        1 -> room.name
+        else -> secretsLine
+    }?.trim()?.split(" ") ?: return
     val totalH = (lines.size - 1) * fontH * DungeonMap.textScaling
 
     for ((i, line) in lines.withIndex()) {
@@ -136,12 +142,24 @@ fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
     }
 }
 
+private fun playerRenderPosition(entity: Player?, mapPos: IVec2): Pair<Float, Float> {
+    entity?.let {
+        val mapX = (it.renderX.toFloat() + 200f) * MAP_ROOM_GAP / 32f
+        val mapZ = (it.renderZ.toFloat() + 200f) * MAP_ROOM_GAP / 32f
+        return mapX to mapZ
+    }
+
+    val pixelX = (mapPos.x + 128) / 2f - startX
+    val pixelY = (mapPos.z + 128) / 2f - startY
+    return pixelX to pixelY
+}
+
 fun GuiGraphicsExtractor.renderPlayers() {
     val showNames = mc.player?.mainHandItem?.itemId?.equalsOneOf("INFINITE_SPIRIT_LEAP", "SPIRIT_LEAP") == true
 
     for (player in DungeonUtils.dungeonTeammatesNoSelf) {
         if (player.isDead) continue
-        val (px, pz) = DungeonScan.playerRenderPosition(player.entity, player.mapPos)
+        val (px, pz) = playerRenderPosition(player.entity, player.mapPos)
 
         pose().pushMatrix()
         pose().translate(px, pz)
@@ -164,7 +182,7 @@ fun GuiGraphicsExtractor.renderPlayers() {
     }
 
     pose().pushMatrix()
-    val (selfX, selfZ) = DungeonScan.playerRenderPosition(mc.player, IVec2(0, 0))
+    val (selfX, selfZ) = playerRenderPosition(mc.player, IVec2(0, 0))
     pose().translate(selfX, selfZ)
     pose().rotate(Math.toRadians(180.0 + (mc.player?.yRot ?: 0f)).toFloat())
 
@@ -239,7 +257,6 @@ fun buildExampleDoors(): List<DungeonDoor> = listOf(
     DungeonDoor(IVec2(1, 5), DoorRotation.Horizontal, DoorType.Blood, DungeonMap.bloodDoorColor)
 )
 
-
 fun roomTypeColor(type: RoomType): Color = when (type) {
     RoomType.NORMAL   -> DungeonMap.normalRoomColor
     RoomType.PUZZLE   -> DungeonMap.puzzleRoomColor
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
index 1248fc3..af2c698 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
@@ -2,7 +2,6 @@ package com.odtheking.odin.features.impl.dungeon.map.tile
 
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints
-import com.odtheking.odin.features.impl.dungeon.map.DungeonScan
 import com.odtheking.odin.utils.*
 import net.minecraft.core.BlockPos
 import net.minecraft.world.level.block.Blocks
@@ -30,6 +29,8 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
     var center: IVec2? = null
         private set
 
+    var foundSecrets: Int? = null
+
     val isViewable: Boolean get() = walkedInto || checkmark != MapCheckmark.UNDISCOVERED
     val name: String? get() = data?.name
 
@@ -43,26 +44,21 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
     }
 
     private fun recalculateCenter() {
-        val gap = DungeonScan.MAP_ROOM_GAP
-        val half = DungeonScan.MAP_ROOM_SIZE / 2
-        val rot = rotation
-
-        if (rot == null) {
-            val anchor = tiles.minByOrNull { it.sortKey } ?: return
-            center = IVec2(anchor.x * gap + half, anchor.z * gap + half)
-            return
-        }
-
-        val (offX, offZ) = when (shape) {
-            RoomShape.OneByOne -> 0 to 0
-            RoomShape.TwoByTwo -> gap / 2 to gap / 2
-            RoomShape.L -> if (rot == RoomRotation.NORTH || rot == RoomRotation.WEST) gap / 2 to 0 else gap / 2 to gap
-            else -> {
-                val span = (shape.tileAmount - 1) * gap / 2
-                if (rot == RoomRotation.SOUTH) span to 0 else 0 to span
+        rotation?.let { rot ->
+            val (offX, offZ) = when (shape) {
+                RoomShape.OneByOne -> 0 to 0
+                RoomShape.TwoByTwo -> 10 to 10
+                RoomShape.L -> if (rot == RoomRotation.NORTH || rot == RoomRotation.WEST) 10 to 0 else 10 to 20
+                else -> {
+                    val span = (shape.tileAmount - 1) * 10
+                    if (rot == RoomRotation.SOUTH) span to 0 else 0 to span
+                }
             }
+            center = IVec2(topLeft.x * 20 + 8 + offX, topLeft.z * 20 + 8 + offZ)
+        } ?: run {
+            val anchor = tiles.minByOrNull { it.sortKey } ?: return
+            center = IVec2(anchor.x * 20 + 8, anchor.z * 20 + 8)
         }
-        center = IVec2(topLeft.x * gap + half + offX, topLeft.z * gap + half + offZ)
     }
 
     fun inferLayoutFromMap() {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/RoomData.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/RoomData.kt
index 1f9ca75..558e32a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/RoomData.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/RoomData.kt
@@ -11,7 +11,7 @@ data class RoomData private constructor(
     val shape: RoomShape,
     val cores: List<Int>,
     val crypts: Int,
-    val secrets: Int,
+    val maxSecrets: Int,
     val trappedChests: Int,
 ) {
     companion object {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
index 4d6ba1f..49df55c 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
@@ -1,10 +1,12 @@
 package com.odtheking.odin.utils.skyblock
 
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.events.SecretsUpdateEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.utils.noControlCodes
+import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.network.protocol.game.ClientboundSystemChatPacket
 import net.minecraft.world.entity.ai.attributes.Attributes
 import kotlin.math.floor
@@ -15,6 +17,7 @@ object ActionBarListener {
     private val OVERFLOW_MANA_REGEX = Regex("([\\d|,]+)\\uE017")
     private val DEFENSE_REGEX = Regex("([\\d|,]+)\\uE008( Defense)?")
     private val VITALITY_REGEX = Regex("([\\d.,]+)/([\\d.,]+)\\uE028")
+    private val SECRETS_REGEX = Regex("(\\d+)/(\\d+) Secrets")
 
     var currentHealth: Int = 0
         private set
@@ -49,6 +52,15 @@ object ActionBarListener {
             if (!overlay) return@onReceive
             val msg = content.string.noControlCodes
 
+            SECRETS_REGEX.find(msg)?.destructured?.let { (found, max) ->
+                DungeonUtils.currentRoom?.let {
+                    val updatedFoundSecrets = found.toIntOrNull() ?: return@let
+                    if (it.data?.maxSecrets != max.toIntOrNull() || (it.foundSecrets ?: 0) >= updatedFoundSecrets) return@let
+                    it.foundSecrets = updatedFoundSecrets
+                    SecretsUpdateEvent(it, updatedFoundSecrets).postAndCatch()
+                }
+            }
+
             HEALTH_REGEX.find(msg)?.destructured?.let { (_, maxHp) ->
                 maxHealth = maxHp.replace(",", "").toIntOrNull() ?: maxHealth
             }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
index 9892873..74134f6 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
@@ -66,7 +66,7 @@ object DungeonListener {
         }
 
         on<RoomEnterEvent> {
-            dungeonStats.knownSecrets = DungeonScan.rooms.sumOf { if (it.walkedInto) it.data?.secrets ?: 0 else 0 }
+            dungeonStats.knownSecrets = DungeonScan.rooms.sumOf { if (it.walkedInto) it.data?.maxSecrets ?: 0 else 0 }
         }
 
         onReceive<ClientboundPlayerInfoUpdatePacket> {
diff --git a/src/main/resources/assets/odin/rooms.json b/src/main/resources/assets/odin/rooms.json
index 51cb924..92c9cd6 100644
--- a/src/main/resources/assets/odin/rooms.json
+++ b/src/main/resources/assets/odin/rooms.json
@@ -17,7 +17,7 @@
       -1927723170
     ],
     "crypts": 3,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "L"
   },
   {
@@ -26,7 +26,7 @@
     "cores": [
       -1701988142
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -37,7 +37,7 @@
       1440685825
     ],
     "crypts": 4,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x2"
   },
   {
@@ -47,7 +47,7 @@
       313879976
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -60,7 +60,7 @@
       -2080722918
     ],
     "crypts": 5,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "2x2"
   },
   {
@@ -70,7 +70,7 @@
       1765345183,
       2133731337
     ],
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x2"
   },
   {
@@ -80,7 +80,7 @@
       -1092023380
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -89,7 +89,7 @@
     "cores": [
       1491050733
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -99,7 +99,7 @@
       -1764045332
     ],
     "crypts": 2,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -109,7 +109,7 @@
       -807913351
     ],
     "crypts": 1,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -119,7 +119,7 @@
       -1779299819
     ],
     "crypts": 1,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -138,7 +138,7 @@
       -1410316771
     ],
     "crypts": 4,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -160,7 +160,7 @@
       454887921
     ],
     "crypts": 6,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "1x2"
   },
   {
@@ -173,7 +173,7 @@
       112871973
     ],
     "crypts": 21,
-    "secrets": 5,
+    "maxSecrets": 5,
     "trappedChests": 1,
     "shape": "2x2"
   },
@@ -183,7 +183,7 @@
     "cores": [
       -407233803
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -192,7 +192,7 @@
     "cores": [
       -1164795630
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -203,7 +203,7 @@
       -76292695
     ],
     "crypts": 3,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -216,7 +216,7 @@
       -508170344
     ],
     "crypts": 5,
-    "secrets": 8,
+    "maxSecrets": 8,
     "shape": "2x2"
   },
   {
@@ -228,7 +228,7 @@
       1179970013
     ],
     "crypts": 5,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "1x3"
   },
   {
@@ -237,7 +237,7 @@
     "cores": [
       1560740373
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -246,7 +246,7 @@
     "cores": [
       -448181487
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -258,7 +258,7 @@
       -546080393
     ],
     "crypts": 6,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "L"
   },
   {
@@ -268,7 +268,7 @@
       -170648742
     ],
     "crypts": 1,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -288,7 +288,7 @@
       -1879625096
     ],
     "crypts": 6,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -299,7 +299,7 @@
       1376295224
     ],
     "crypts": 2,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "1x2"
   },
   {
@@ -311,7 +311,7 @@
       -414187054
     ],
     "crypts": 4,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "1x3"
   },
   {
@@ -332,7 +332,7 @@
       -959823640
     ],
     "crypts": 3,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x3"
   },
   {
@@ -344,7 +344,7 @@
       1735273877
     ],
     "crypts": 4,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "L"
   },
   {
@@ -354,7 +354,7 @@
       -1373895553
     ],
     "crypts": 3,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -365,7 +365,7 @@
       291108944
     ],
     "crypts": 2,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -376,7 +376,7 @@
       551939185
     ],
     "crypts": 7,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "1x2"
   },
   {
@@ -385,7 +385,7 @@
     "cores": [
       1857926981
     ],
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -403,7 +403,7 @@
       -1779237820
     ],
     "crypts": 6,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -413,7 +413,7 @@
       -697416341
     ],
     "crypts": 6,
-    "secrets": 3,
+    "maxSecrets": 3,
     "trappedChests": 1,
     "shape": "1x1"
   },
@@ -423,7 +423,7 @@
     "cores": [
       -1999038851
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -433,7 +433,7 @@
       -531914593
     ],
     "crypts": 1,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -464,7 +464,7 @@
       908422074
     ],
     "crypts": 8,
-    "secrets": 7,
+    "maxSecrets": 7,
     "shape": "2x2"
   },
   {
@@ -474,7 +474,7 @@
       992885012,
       35550104
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x2"
   },
   {
@@ -484,7 +484,7 @@
       178277726
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -495,7 +495,7 @@
       648931278
     ],
     "crypts": 2,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x2"
   },
   {
@@ -504,7 +504,7 @@
     "cores": [
       -41110951
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -517,7 +517,7 @@
       -1847986179
     ],
     "crypts": 4,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x2"
   },
   {
@@ -530,7 +530,7 @@
       244770880
     ],
     "crypts": 2,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "1x3"
   },
   {
@@ -551,7 +551,7 @@
       1020848351
     ],
     "crypts": 1,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x4"
   },
   {
@@ -561,7 +561,7 @@
       -1752027278,
       -1477567187
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -588,7 +588,7 @@
     "cores": [
       205151025
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -606,7 +606,7 @@
     "cores": [
       -1519890056
     ],
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -615,7 +615,7 @@
     "cores": [
       -695235565
     ],
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -627,7 +627,7 @@
       -854539536
     ],
     "crypts": 4,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "L"
   },
   {
@@ -639,7 +639,7 @@
       161195688,
       -91111782
     ],
-    "secrets": 8,
+    "maxSecrets": 8,
     "shape": "L"
   },
   {
@@ -649,7 +649,7 @@
       643669164
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -659,7 +659,7 @@
       706748439
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -668,7 +668,7 @@
     "cores": [
       1783642819
     ],
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x1"
   },
   {
@@ -678,7 +678,7 @@
       587195362
     ],
     "crypts": 3,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -688,7 +688,7 @@
       -1112550874
     ],
     "crypts": 1,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -698,7 +698,7 @@
       -441650545,
       911747293
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -708,7 +708,7 @@
       1475692888,
       925853313
     ],
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x2"
   },
   {
@@ -720,7 +720,7 @@
       300771790
     ],
     "crypts": 4,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "L"
   },
   {
@@ -732,7 +732,7 @@
       -1740446783
     ],
     "crypts": 4,
-    "secrets": 7,
+    "maxSecrets": 7,
     "shape": "L"
   },
   {
@@ -745,7 +745,7 @@
       -892257050
     ],
     "crypts": 11,
-    "secrets": 10,
+    "maxSecrets": 10,
     "shape": "2x2"
   },
   {
@@ -755,7 +755,7 @@
       302128578
     ],
     "crypts": 3,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x1"
   },
   {
@@ -764,7 +764,7 @@
     "cores": [
       76347246
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -777,7 +777,7 @@
       -408036566
     ],
     "crypts": 2,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x4"
   },
   {
@@ -786,7 +786,7 @@
     "cores": [
       -543960045
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -795,7 +795,7 @@
     "cores": [
       -743021993
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -808,7 +808,7 @@
       -95130049
     ],
     "crypts": 4,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "2x2"
   },
   {
@@ -817,7 +817,7 @@
     "cores": [
       1461632797
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -830,7 +830,7 @@
       1851993785
     ],
     "crypts": 1,
-    "secrets": 3,
+    "maxSecrets": 3,
     "trappedChests": 1,
     "shape": "1x1"
   },
@@ -842,7 +842,7 @@
       -85564220
     ],
     "crypts": 2,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x1"
   },
   {
@@ -851,7 +851,7 @@
     "cores": [
       -726297434
     ],
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -861,7 +861,7 @@
       95969553
     ],
     "crypts": 1,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -870,7 +870,7 @@
     "cores": [
       -725718835
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -881,7 +881,7 @@
       1549240174
     ],
     "crypts": 1,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "1x2"
   },
   {
@@ -891,7 +891,7 @@
       -444976906
     ],
     "crypts": 1,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -904,7 +904,7 @@
       1917735700
     ],
     "crypts": 9,
-    "secrets": 7,
+    "maxSecrets": 7,
     "shape": "1x4"
   },
   {
@@ -916,7 +916,7 @@
       2061161472
     ],
     "crypts": 2,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "L"
   },
   {
@@ -930,7 +930,7 @@
       634229450
     ],
     "crypts": 4,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "1x4"
   },
   {
@@ -942,7 +942,7 @@
       2023286596
     ],
     "crypts": 6,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "1x2"
   },
   {
@@ -952,7 +952,7 @@
       845343581
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -963,7 +963,7 @@
       664490773
     ],
     "crypts": 7,
-    "secrets": 5,
+    "maxSecrets": 5,
     "shape": "1x2"
   },
   {
@@ -972,7 +972,7 @@
     "cores": [
       1865354448
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -985,7 +985,7 @@
       -215394360
     ],
     "crypts": 9,
-    "secrets": 7,
+    "maxSecrets": 7,
     "shape": "1x4"
   },
   {
@@ -1004,7 +1004,7 @@
       -1723880923
     ],
     "crypts": 2,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x1"
   },
   {
@@ -1014,7 +1014,7 @@
       -1362847550
     ],
     "crypts": 3,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1027,7 +1027,7 @@
       -1882412922
     ],
     "crypts": 1,
-    "secrets": 9,
+    "maxSecrets": 9,
     "shape": "2x2"
   },
   {
@@ -1036,7 +1036,7 @@
     "cores": [
       1613728006
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1045,7 +1045,7 @@
     "cores": [
       -1907291752
     ],
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1054,7 +1054,7 @@
     "cores": [
       -609209015
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1067,7 +1067,7 @@
       -608091458
     ],
     "crypts": 1,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x3"
   },
   {
@@ -1077,7 +1077,7 @@
       -1800029469
     ],
     "crypts": 2,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1089,7 +1089,7 @@
       -889320918,
       -1978791555
     ],
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1100,7 +1100,7 @@
       -1986945620
     ],
     "crypts": 4,
-    "secrets": 3,
+    "maxSecrets": 3,
     "trappedChests": 1,
     "shape": "1x1"
   },
@@ -1112,7 +1112,7 @@
       -380862700
     ],
     "crypts": 4,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x2"
   },
   {
@@ -1122,7 +1122,7 @@
       -610787478
     ],
     "crypts": 1,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1132,7 +1132,7 @@
       1781758146
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1143,7 +1143,7 @@
       -771559429
     ],
     "crypts": 1,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1153,7 +1153,7 @@
       -2146618657,
       -2091078305
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -1171,7 +1171,7 @@
     "cores": [
       -1899702429
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1181,7 +1181,7 @@
       -1554372511,
       80464378
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x2"
   },
   {
@@ -1191,7 +1191,7 @@
       568922940
     ],
     "crypts": 2,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -1203,7 +1203,7 @@
       878407749
     ],
     "crypts": 1,
-    "secrets": 5,
+    "maxSecrets": 5,
     "trappedChests": 1,
     "shape": "1x3"
   },
@@ -1214,7 +1214,7 @@
       -566399217
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1224,7 +1224,7 @@
       -998001944
     ],
     "crypts": 1,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -1234,7 +1234,7 @@
       -1188349447
     ],
     "crypts": 5,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -1246,7 +1246,7 @@
       -40178107
     ],
     "crypts": 3,
-    "secrets": 9,
+    "maxSecrets": 9,
     "shape": "L"
   },
   {
@@ -1256,7 +1256,7 @@
       1484939648
     ],
     "crypts": 2,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1266,7 +1266,7 @@
       -151940807
     ],
     "crypts": 2,
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1279,7 +1279,7 @@
       -2033694003
     ],
     "crypts": 1,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "2x2"
   },
   {
@@ -1290,7 +1290,7 @@
       -340486787
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1300,7 +1300,7 @@
       -1740718028
     ],
     "crypts": 1,
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -1313,7 +1313,7 @@
       -1867603567
     ],
     "crypts": 6,
-    "secrets": 6,
+    "maxSecrets": 6,
     "shape": "2x2"
   },
   {
@@ -1330,7 +1330,7 @@
     "cores": [
       1575914755
     ],
-    "secrets": 3,
+    "maxSecrets": 3,
     "shape": "1x1"
   },
   {
@@ -1339,7 +1339,7 @@
     "cores": [
       -1444508608
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1357,7 +1357,7 @@
       -738945309,
       29612250
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1366,7 +1366,7 @@
     "cores": [
       535698739
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -1383,7 +1383,7 @@
     "cores": [
       872815555
     ],
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   },
   {
@@ -1392,7 +1392,7 @@
     "cores": [
       -333637832
     ],
-    "secrets": 2,
+    "maxSecrets": 2,
     "shape": "1x1"
   },
   {
@@ -1415,7 +1415,7 @@
       520419865
     ],
     "crypts": 3,
-    "secrets": 8,
+    "maxSecrets": 8,
     "shape": "1x4"
   },
   {
@@ -1427,7 +1427,7 @@
       1276013917
     ],
     "crypts": 5,
-    "secrets": 7,
+    "maxSecrets": 7,
     "shape": "L"
   },
   {
@@ -1439,7 +1439,7 @@
       1140917779
     ],
     "crypts": 6,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "L"
   },
   {
@@ -1451,7 +1451,7 @@
       926568393
     ],
     "crypts": 8,
-    "secrets": 4,
+    "maxSecrets": 4,
     "shape": "1x3"
   },
   {
@@ -1461,7 +1461,7 @@
       -1403940419
     ],
     "crypts": 1,
-    "secrets": 1,
+    "maxSecrets": 1,
     "shape": "1x1"
   }
 ]
\ No newline at end of file
Simplified DungeonRoom center mathodtheking · 2026-08-15 · 6d3df81
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt       |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt      | 58 +++++++++++++++++++++-------------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt          |  3 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt |  3 +--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt | 42 ++++++++++++++----------------------------
 5 files changed, 39 insertions(+), 69 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index d78c24a..483398a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -48,7 +48,7 @@ object DungeonMap : Module(
     val disablePred by BooleanSetting("Disable Prediction", false, desc = "Disables special-column room type prediction.")
 
     private val exampleRooms by lazy { buildExampleRooms() }
-    private val exampleDoors by lazy { buildExampleDoors(exampleRooms) }
+    private val exampleDoors by lazy { buildExampleDoors() }
 
     private val mapHud by HUD("Dungeon Map", "Displays the dungeon map.", false) { example ->
         if ((!DungeonUtils.inDungeons || (disableBoss && DungeonUtils.inBoss)) && !example) return@HUD 0 to 0
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index 799807c..ae26983 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -217,44 +217,28 @@ fun buildExampleRooms(): List<DungeonRoom> {
     )
 }
 
-fun buildExampleDoors(rooms: List<DungeonRoom>): List<DungeonDoor> {
-    val roomByTile = rooms.flatMap { room -> room.occupiedTiles().map { it to room } }.toMap()
-
-    fun door(position: IVec2, rotation: DoorRotation, type: DoorType): DungeonDoor {
-        val door = DungeonDoor(position, rotation, type)
-        door.color = when (type) {
-            DoorType.Wither -> DungeonMap.witherDoorColor
-            DoorType.Blood  -> DungeonMap.bloodDoorColor
-            DoorType.Fairy  -> DungeonMap.fairyDoorColor
-            DoorType.Normal -> listOfNotNull(roomByTile[position], roomByTile[position + rotation.offset])
-                .firstOrNull { it.type != RoomType.NORMAL && it.type != RoomType.FAIRY }
-                ?.type?.let(::roomTypeColor) ?: DungeonMap.normalDoorColor
-        }
-        return door
-    }
+fun buildExampleDoors(): List<DungeonDoor> = listOf(
+    DungeonDoor(IVec2(2, 0), DoorRotation.Vertical, DoorType.Normal, DungeonMap.entranceRoomColor),
+    DungeonDoor(IVec2(1, 0), DoorRotation.Vertical, DoorType.Normal, DungeonMap.puzzleRoomColor),
+    DungeonDoor(IVec2(0, 1), DoorRotation.Horizontal, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(2, 2), DoorRotation.Horizontal, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(1, 2), DoorRotation.Vertical, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(4, 0), DoorRotation.Vertical, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(3, 1), DoorRotation.Horizontal, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(3, 3), DoorRotation.Vertical, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(1, 3), DoorRotation.Horizontal, DoorType.Fairy, DungeonMap.fairyDoorColor),
+    DungeonDoor(IVec2(2, 3), DoorRotation.Vertical, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(4, 3), DoorRotation.Vertical, DoorType.Normal, DungeonMap.championRoomColor),
+    DungeonDoor(IVec2(5, 3), DoorRotation.Vertical, DoorType.Normal, DungeonMap.trapRoomColor),
+    DungeonDoor(IVec2(0, 4), DoorRotation.Horizontal, DoorType.Normal, DungeonMap.puzzleRoomColor),
+    DungeonDoor(IVec2(1, 4), DoorRotation.Horizontal, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(1, 4), DoorRotation.Vertical, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(3, 4), DoorRotation.Horizontal, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(3, 4), DoorRotation.Vertical, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(5, 4), DoorRotation.Vertical, DoorType.Normal, DungeonMap.normalDoorColor),
+    DungeonDoor(IVec2(1, 5), DoorRotation.Horizontal, DoorType.Blood, DungeonMap.bloodDoorColor)
+)
 
-    return listOf(
-        door(IVec2(2, 0), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(1, 0), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(0, 1), DoorRotation.Horizontal, DoorType.Normal),
-        door(IVec2(2, 2), DoorRotation.Horizontal, DoorType.Normal),
-        door(IVec2(1, 2), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(4, 0), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(3, 1), DoorRotation.Horizontal, DoorType.Normal),
-        door(IVec2(3, 3), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(1, 3), DoorRotation.Horizontal, DoorType.Fairy),
-        door(IVec2(2, 3), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(4, 3), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(5, 3), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(0, 4), DoorRotation.Horizontal, DoorType.Normal),
-        door(IVec2(1, 4), DoorRotation.Horizontal, DoorType.Normal),
-        door(IVec2(1, 4), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(3, 4), DoorRotation.Horizontal, DoorType.Normal),
-        door(IVec2(3, 4), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(5, 4), DoorRotation.Vertical, DoorType.Normal),
-        door(IVec2(1, 5), DoorRotation.Horizontal, DoorType.Blood),
-    )
-}
 
 fun roomTypeColor(type: RoomType): Color = when (type) {
     RoomType.NORMAL   -> DungeonMap.normalRoomColor
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
index 5cea508..d6a9c3d 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
@@ -6,6 +6,7 @@ import com.odtheking.odin.events.MapUpdateEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.impl.dungeon.map.tile.*
+import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.IVec2
 import com.odtheking.odin.utils.equalsOneOf
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
@@ -214,7 +215,7 @@ object MapScan {
 
     private fun addOrFixDoor(position: IVec2, rotation: DoorRotation, doorType: DoorType) {
         DungeonScan.doors.getOrPut(IVec2(-12 + 2 * position.x + rotation.offset.x, -12 + 2 * position.z + rotation.offset.z))
-        { DungeonDoor(position, rotation, doorType) }.type = doorType
+        { DungeonDoor(position, rotation, doorType, Colors.WHITE) }.type = doorType
     }
 
     private fun getPx(colors: ByteArray, x: Int, z: Int): Byte =
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
index 3920671..a5fd672 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
@@ -1,14 +1,13 @@
 package com.odtheking.odin.features.impl.dungeon.map.tile
 
 import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.IVec2
 
 class DungeonDoor(
     val position: IVec2,
     val rotation: DoorRotation,
     var type: DoorType,
-    var color: Color = Colors.WHITE,
+    var color: Color,
 ) {
     val originTileIndex = position.x + position.z * 6
     val destinationTileIndex = position.x + rotation.offset.x + (position.z + rotation.offset.z) * 6
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
index 81ebca3..1248fc3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
@@ -42,41 +42,27 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
         }
     }
 
-    fun occupiedTiles(): List<IVec2> {
-        val rot = rotation ?: return emptyList()
-        return tileOffsets(rot).map { topLeft + it }
-    }
-
-    private fun tileOffsets(rot: RoomRotation): List<IVec2> = when (shape) {
-        RoomShape.OneByOne -> listOf(IVec2(0, 0))
-        RoomShape.TwoByTwo -> listOf(IVec2(0, 0), IVec2(1, 0), IVec2(0, 1), IVec2(1, 1))
-        RoomShape.L -> when (rot) {
-            RoomRotation.WEST  -> listOf(IVec2(0, 0), IVec2(1, 0), IVec2(0, 1))
-            RoomRotation.NORTH -> listOf(IVec2(0, 0), IVec2(1, 0), IVec2(1, 1))
-            else               -> listOf(IVec2(0, 0), IVec2(0, 1), IVec2(1, 1))
-        }
-        else -> if (rot == RoomRotation.SOUTH) (0 until shape.tileAmount).map { IVec2(it, 0) }
-                else (0 until shape.tileAmount).map { IVec2(0, it) }
-    }
-
     private fun recalculateCenter() {
-        fun tileCenter(tile: IVec2) = IVec2(tile.x * DungeonScan.MAP_ROOM_GAP + DungeonScan.MAP_ROOM_SIZE / 2, tile.z * DungeonScan.MAP_ROOM_GAP + DungeonScan.MAP_ROOM_SIZE / 2)
-
+        val gap = DungeonScan.MAP_ROOM_GAP
+        val half = DungeonScan.MAP_ROOM_SIZE / 2
         val rot = rotation
+
         if (rot == null) {
-            if (tiles.isEmpty()) return
-            center = tileCenter(tiles.minBy { it.sortKey })
+            val anchor = tiles.minByOrNull { it.sortKey } ?: return
+            center = IVec2(anchor.x * gap + half, anchor.z * gap + half)
             return
         }
 
-        val corners = occupiedTiles().map(::tileCenter)
-        val z = when (shape) {
-            RoomShape.L ->
-                if (rot == RoomRotation.NORTH || rot == RoomRotation.WEST) corners.minOf { it.z }
-                else corners.maxOf { it.z }
-            else -> (corners.minOf { it.z } + corners.maxOf { it.z }) / 2
+        val (offX, offZ) = when (shape) {
+            RoomShape.OneByOne -> 0 to 0
+            RoomShape.TwoByTwo -> gap / 2 to gap / 2
+            RoomShape.L -> if (rot == RoomRotation.NORTH || rot == RoomRotation.WEST) gap / 2 to 0 else gap / 2 to gap
+            else -> {
+                val span = (shape.tileAmount - 1) * gap / 2
+                if (rot == RoomRotation.SOUTH) span to 0 else 0 to span
+            }
         }
-        center = IVec2((corners.minOf { it.x } + corners.maxOf { it.x }) / 2, z)
+        center = IVec2(topLeft.x * gap + half + offX, topLeft.z * gap + half + offZ)
     }
 
     fun inferLayoutFromMap() {
Readded Dragon titleodtheking · 2026-08-15 · 5959d23
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt     |  1 +
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt | 26 ++++++++++----------------
 2 files changed, 11 insertions(+), 16 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
index 8113388..f29c765 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
@@ -44,6 +44,7 @@ object WitherDragons : Module(
 
     private val dragonAlerts by DropdownSetting("Dragon Alerts Dropdown")
     private val sendNotification by BooleanSetting("Send Dragon Confirmation", true, desc = "Sends a confirmation message when a dragon dies.").withDependency { dragonAlerts }
+    val dragonTitle by BooleanSetting("Dragon Title", true, desc = "Displays a title for spawning dragons.").withDependency { dragonAlerts }
     val sendTime by BooleanSetting("Send Dragon Time Alive", true, desc = "Sends a message when a dragon dies with the time it was alive.").withDependency { dragonAlerts }
     val sendSpawned by BooleanSetting("Send Dragon Spawned", true, desc = "Sends a message when a dragon has spawned.").withDependency { dragonAlerts }
     val sendSpray by BooleanSetting("Send Ice Sprayed", true, desc = "Sends a message when a dragon has been ice sprayed.").withDependency { dragonAlerts }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
index 062fd05..b78dcc2 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
@@ -9,6 +9,7 @@ import com.odtheking.odin.features.impl.boss.WitherDragons.sendSpawned
 import com.odtheking.odin.features.impl.boss.WitherDragons.sendTime
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.alert
 import com.odtheking.odin.utils.modMessage
 import net.minecraft.core.BlockPos
 import net.minecraft.core.particles.ParticleTypes
@@ -29,14 +30,13 @@ enum class WitherDragonsEnum(
     var timesSpawned: Int = 0,
     var entityUUID: UUID? = null,
     var isSprayed: Boolean = false,
-    var spawnedTime: Long = 0,
-    val skipKillTime: Int = 0
+    var spawnedTime: Long = 0
 ) {
-    Red(BlockPos(27, 14, 59), BlockPos(32, 22, 59), AABB(14.5, 13.0, 45.5, 39.5, 28.0, 70.5), 'c', Colors.MINECRAFT_RED, 24.0..30.0, 56.0..62.0, skipKillTime = 50),
-    Orange(BlockPos(85, 14, 56), BlockPos(80, 23, 56), AABB(72.0, 8.0, 47.0, 102.0, 28.0, 77.0), '6', Colors.MINECRAFT_GOLD, 82.0..88.0, 53.0..59.0, skipKillTime = 62),
-    Green(BlockPos(27, 14, 94), BlockPos(32, 23, 94), AABB(7.0, 8.0, 80.0, 37.0, 28.0, 110.0), 'a', Colors.MINECRAFT_GREEN, 23.0..29.0, 91.0..97.0, skipKillTime = 52),
-    Blue(BlockPos(84, 14, 94), BlockPos(79, 23, 94), AABB(71.5, 13.0, 82.5, 96.5, 26.0, 107.5), 'b', Colors.MINECRAFT_AQUA, 82.0..88.0, 91.0..97.0, skipKillTime = 47),
-    Purple(BlockPos(56, 14, 125), BlockPos(56, 22, 120), AABB(45.5, 13.0, 113.5, 68.5, 23.0, 136.5), '5', Colors.MINECRAFT_DARK_PURPLE, 53.0..59.0, 122.0..128.0, skipKillTime = 38);
+    Red(BlockPos(27, 14, 59), BlockPos(32, 22, 59), AABB(14.5, 13.0, 45.5, 39.5, 28.0, 70.5), 'c', Colors.MINECRAFT_RED, 24.0..30.0, 56.0..62.0),
+    Orange(BlockPos(85, 14, 56), BlockPos(80, 23, 56), AABB(72.0, 8.0, 47.0, 102.0, 28.0, 77.0), '6', Colors.MINECRAFT_GOLD, 82.0..88.0, 53.0..59.0),
+    Green(BlockPos(27, 14, 94), BlockPos(32, 23, 94), AABB(7.0, 8.0, 80.0, 37.0, 28.0, 110.0), 'a', Colors.MINECRAFT_GREEN, 23.0..29.0, 91.0..97.0),
+    Blue(BlockPos(84, 14, 94), BlockPos(79, 23, 94), AABB(71.5, 13.0, 82.5, 96.5, 26.0, 107.5), 'b', Colors.MINECRAFT_AQUA, 82.0..88.0, 91.0..97.0),
+    Purple(BlockPos(56, 14, 125), BlockPos(56, 22, 120), AABB(45.5, 13.0, 113.5, 68.5, 23.0, 136.5), '5', Colors.MINECRAFT_DARK_PURPLE, 53.0..59.0, 122.0..128.0);
 
     fun setAlive(entityId: UUID?) {
         if (entityId != null) this.entityUUID = entityId
@@ -48,15 +48,8 @@ enum class WitherDragonsEnum(
         spawnedTime = currentTick
         isSprayed = false
 
-        if (sendSpawned && WitherDragons.enabled) {
-            val numberSuffix = when (timesSpawned) {
-                1 -> "st"
-                2 -> "nd"
-                3 -> "rd"
-                else -> "th"
-            }
-            modMessage("§${colorCode}${name} §fdragon spawned. This is the §${colorCode}${timesSpawned}${numberSuffix}§f time it has spawned.")
-        }
+        if (sendSpawned && WitherDragons.enabled)
+            modMessage("§${colorCode}${name} §fdragon spawned §8(§7${timesSpawned}§8)")
     }
 
     fun setDead(realTime: Boolean) {
@@ -131,6 +124,7 @@ fun handleSpawnPacket(particle: ClientboundLevelParticlesPacket) {
 
     if (dragons.isNotEmpty() && (dragons.size == 2 || spawned >= 2) && priorityDragon == null)
         priorityDragon = findPriority(dragons).also { dragon ->
+            if (WitherDragons.dragonTitle && WitherDragons.enabled) alert("§${dragon.colorCode}${dragon.name} is spawning!", true)
             if (dragonPriorityToggle && WitherDragons.enabled && dragons.size > 1) modMessage("${dragons.joinToString(", ") { "§${it.colorCode}${it.name}" }}§r -> §${dragon.colorCode}${dragon.name} §7is your priority dragon!")
         }
 }
Bump version Optimize importsodtheking · 2026-08-15 · 811c8a7
查看变更
gradle.properties                                                              | 2 +-
 src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt          | 1 -
 src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt                     | 1 -
 src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt                  | 1 -
 src/main/kotlin/com/odtheking/odin/features/Category.kt                        | 2 --
 src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt                   | 1 -
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt          | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt     | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt            | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/SpiritBear.kt            | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt        | 2 --
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt         | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt            | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt     | 1 -
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt | 1 -
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt       | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Highlight.kt          | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MageBeam.kt           | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt            | 2 --
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt | 4 +---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt      | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt      | 9 +++++++--
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt      | 1 -
 src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt           | 1 -
 src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt            | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt          | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/RenderTest.kt          | 6 +-----
 src/main/kotlin/com/odtheking/odin/features/impl/render/Shenanigans.kt         | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt           | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt   | 3 ---
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt     | 2 +-
 src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt                        | 1 -
 src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt             | 2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt             | 2 +-
 35 files changed, 28 insertions(+), 47 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index 7485bec..944c612 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ loom_version=1.16-SNAPSHOT
 mod_id=odin
 mod_name=Odin
 mod_description=A Hypixel Skyblock QoL mod
-mod_version=0.3.0
+mod_version=0.3.1
 maven_group=com.odtheking
 archives_base_name=odin-fabric
 # Mod Dependencies
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
index e9dc622..34175a2 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
@@ -6,7 +6,6 @@ import com.odtheking.odin.OdinMod.scope
 import com.odtheking.odin.config.DungeonWaypointConfig
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.*
 import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.handlers.schedule
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.setClipboardContent
 import kotlinx.coroutines.launch
diff --git a/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
index f735cf5..1b8dc47 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
@@ -10,7 +10,6 @@ import com.odtheking.odin.clickgui.HudManager
 import com.odtheking.odin.features.ModuleManager
 import com.odtheking.odin.features.impl.render.ClickGUIModule
 import com.odtheking.odin.utils.*
-import com.odtheking.odin.utils.handlers.schedule
 import com.odtheking.odin.utils.skyblock.PartyUtils
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 
diff --git a/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt
index ce6b9b1..edcc2c9 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt
@@ -3,7 +3,6 @@ package com.odtheking.odin.commands
 import com.github.stivais.commodore.Commodore
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.features.impl.boss.termsim.StartGUI
-import com.odtheking.odin.utils.handlers.schedule
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 
 val termSimCommand = Commodore("termsim") {
diff --git a/src/main/kotlin/com/odtheking/odin/features/Category.kt b/src/main/kotlin/com/odtheking/odin/features/Category.kt
index f0b34fb..4842109 100644
--- a/src/main/kotlin/com/odtheking/odin/features/Category.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/Category.kt
@@ -1,7 +1,5 @@
 package com.odtheking.odin.features
 
-import com.odtheking.odin.features.Category.Companion.categories
-
 @ConsistentCopyVisibility
 data class Category private constructor(val name: String) {
     companion object {
diff --git a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
index dd1220a..ac7baac 100644
--- a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
@@ -10,7 +10,6 @@ import com.odtheking.odin.clickgui.settings.impl.KeybindSetting
 import com.odtheking.odin.config.ModuleConfig
 import com.odtheking.odin.events.InputEvent
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.features.ModuleManager.configs
 import com.odtheking.odin.features.impl.boss.*
 import com.odtheking.odin.features.impl.dungeon.*
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
index f42480a..b3bf60e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ArrowsDevice.kt
@@ -6,8 +6,8 @@ import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.events.BlockUpdateEvent
 import com.odtheking.odin.events.ChatPacketEvent
-import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
index e893b53..5b06ed4 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/InactiveWaypoints.kt
@@ -3,8 +3,8 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.events.ChatPacketEvent
-import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color.Companion.withAlpha
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
index 81ede31..9bbac68 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/KingRelics.kt
@@ -3,9 +3,9 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.TickEvent
-import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.events.core.onSend
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SpiritBear.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SpiritBear.kt
index b68397d..b2ded7e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/SpiritBear.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/SpiritBear.kt
@@ -1,8 +1,8 @@
 package com.odtheking.odin.features.impl.boss
 
 import com.odtheking.odin.events.BlockUpdateEvent
-import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index 7924d2e..11d443a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -9,13 +9,11 @@ import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
 import net.minecraft.client.Options
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
-import net.minecraft.client.gui.screens.options.VideoSettingsScreen
 import net.minecraft.network.chat.Component
 import org.lwjgl.glfw.GLFW
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
index 29d05fd..ec50781 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalTimes.kt
@@ -3,9 +3,9 @@ package com.odtheking.odin.features.impl.boss
 import com.odtheking.odin.clickgui.settings.impl.ActionSetting
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.TerminalEvent
 import com.odtheking.odin.events.TickEvent
-import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.features.impl.boss.termsim.TermSimGUI
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 2ff7cf4..63f22fe 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -4,8 +4,8 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.HudElement
 import com.odtheking.odin.events.ChatPacketEvent
-import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
index 9b3a374..062fd05 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
@@ -9,7 +9,6 @@ import com.odtheking.odin.features.impl.boss.WitherDragons.sendSpawned
 import com.odtheking.odin.features.impl.boss.WitherDragons.sendTime
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.alert
 import com.odtheking.odin.utils.modMessage
 import net.minecraft.core.BlockPos
 import net.minecraft.core.particles.ParticleTypes
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
index 4be8782..9a2c4f4 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
@@ -8,7 +8,6 @@ import com.odtheking.odin.features.impl.boss.TerminalSolver.hideClicked
 import com.odtheking.odin.features.impl.boss.TerminalSolver.renderDebug
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.render.roundedFill
 import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
index e789928..7ec5cad 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonQueue.kt
@@ -4,8 +4,8 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.ChatPacketEvent
-import com.odtheking.odin.events.PartyEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.PartyEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Highlight.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Highlight.kt
index 215e315..9403509 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Highlight.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Highlight.kt
@@ -3,9 +3,9 @@ package com.odtheking.odin.features.impl.dungeon
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
+import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.TickEvent
-import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
index 7012f25..f049fe3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/InvincibilityTimer.kt
@@ -6,8 +6,8 @@ import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
 import com.odtheking.odin.events.ChatPacketEvent
 import com.odtheking.odin.events.GuiEvent
-import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.*
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MageBeam.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MageBeam.kt
index ef58887..b722909 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MageBeam.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MageBeam.kt
@@ -3,9 +3,9 @@ package com.odtheking.odin.features.impl.dungeon
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
+import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.TickEvent
-import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index 4fa9b55..ed83fa2 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -9,11 +9,9 @@ import com.odtheking.odin.events.ScoreUpdateEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
-import com.odtheking.odin.features.impl.dungeon.map.tile.RoomType
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.alert
-import com.odtheking.odin.utils.handlers.TickTask
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.noControlCodes
 import com.odtheking.odin.utils.render.drawFilledBox
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
index d242e01..86bd0df 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
@@ -4,8 +4,8 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ListSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onSend
 import com.odtheking.odin.features.Module
@@ -19,8 +19,6 @@ import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket
 import net.minecraft.world.phys.AABB
 import net.minecraft.world.phys.Vec3
-import java.util.*
-import kotlin.concurrent.schedule
 
 object PositionalMessages : Module(
     name = "Positional Messages",
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt
index 2a9145e..b69969b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/SecretClicked.kt
@@ -3,9 +3,9 @@ package com.odtheking.odin.features.impl.dungeon
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
 import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.SecretPickupEvent
-import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color.Companion.withAlpha
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
index 2f1caab..1d63a86 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
@@ -6,11 +6,16 @@ import com.odtheking.odin.events.LocationChangeEvent
 import com.odtheking.odin.events.RoomEnterEvent
 import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
-import com.odtheking.odin.features.impl.dungeon.map.tile.*
-import com.odtheking.odin.utils.*
+import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
+import com.odtheking.odin.features.impl.dungeon.map.tile.RoomData
+import com.odtheking.odin.features.impl.dungeon.map.tile.RoomShape
+import com.odtheking.odin.utils.IVec2
+import com.odtheking.odin.utils.devMessage
+import com.odtheking.odin.utils.equalsOneOf
 import com.odtheking.odin.utils.skyblock.Island
 import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
+import com.odtheking.odin.utils.toIVec2
 import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents
 import net.minecraft.world.level.block.Blocks
 import net.minecraft.world.level.block.state.BlockState
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
index 1f82da1..eca3571 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
@@ -17,7 +17,6 @@ import com.odtheking.odin.utils.getChatBreak
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.network.WebUtils.fetchJson
 import com.odtheking.odin.utils.network.WebUtils.postData
-import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.ui.rendering.NVGRenderer
 import kotlinx.coroutines.launch
 import net.minecraft.network.chat.ClickEvent
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt
index 88559dd..0b8f3cd 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/Etherwarp.kt
@@ -31,7 +31,6 @@ import net.minecraft.world.level.block.piston.PistonHeadBlock
 import net.minecraft.world.level.block.state.BlockState
 import net.minecraft.world.phys.AABB
 import net.minecraft.world.phys.Vec3
-import java.util.*
 import kotlin.math.abs
 import kotlin.math.ceil
 import kotlin.math.max
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt
index 886d0f7..e2593cb 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/GyroWand.kt
@@ -3,8 +3,8 @@ package com.odtheking.odin.features.impl.render
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
index e6492d7..f770088 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
@@ -14,7 +14,7 @@ import com.odtheking.odin.utils.network.WebUtils.postData
 import kotlinx.coroutines.launch
 import net.fabricmc.fabric.api.client.rendering.v1.RenderStateDataKey
 import net.minecraft.client.renderer.entity.state.AvatarRenderState
-import java.util.UUID
+import java.util.*
 
 object PlayerSize : Module(
     name = "Player Size",
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/RenderTest.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/RenderTest.kt
index 8174b6a..b432d10 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/RenderTest.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/RenderTest.kt
@@ -8,11 +8,7 @@ import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Category
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.render.drawCylinder
-import com.odtheking.odin.utils.render.drawFilledBox
-import com.odtheking.odin.utils.render.drawLine
-import com.odtheking.odin.utils.render.drawStyledBox
-import com.odtheking.odin.utils.render.drawWireFrameBox
+import com.odtheking.odin.utils.render.*
 import net.minecraft.world.phys.AABB
 import kotlin.math.ceil
 import kotlin.math.sqrt
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/Shenanigans.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/Shenanigans.kt
index 0d7d578..bcdbfa9 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/Shenanigans.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/Shenanigans.kt
@@ -1,7 +1,7 @@
 package com.odtheking.odin.features.impl.render
 
-import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.utils.render.drawTexturedQuad
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt
index 21d249e..ff44c6a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/Waypoints.kt
@@ -6,8 +6,8 @@ import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
 import com.odtheking.odin.clickgui.settings.impl.KeybindSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.events.ChatPacketEvent
-import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
index 0d85cb4..82ca16e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
@@ -1,17 +1,14 @@
 package com.odtheking.odin.features.impl.skyblock
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
-import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
 import com.odtheking.odin.clickgui.settings.impl.KeybindSetting
 import com.odtheking.odin.events.ScreenEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.clickSlot
-import com.odtheking.odin.utils.modMessage
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
 import org.lwjgl.glfw.GLFW
-import java.security.Key
 
 object LoadoutKeybinds : Module(
     name = "Loadout Keybinds",
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
index c1aefad..4187589 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
@@ -7,8 +7,8 @@ import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.textDim
-import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.ActionBarListener
+import com.odtheking.odin.utils.skyblock.LocationUtils
 import net.minecraft.network.chat.Component
 import kotlin.math.abs
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt
index 133160d..48dec3b 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt
@@ -5,7 +5,6 @@ import com.odtheking.odin.events.core.onReceive
 import net.minecraft.network.protocol.game.ClientboundSetTimePacket
 import net.minecraft.network.protocol.ping.ClientboundPongResponsePacket
 import net.minecraft.util.Util
-import java.util.ArrayDeque
 import kotlin.math.min
 
 object ServerUtils {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
index fe855b4..1737c42 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
@@ -1,8 +1,8 @@
 package com.odtheking.odin.utils.skyblock
 
 import com.odtheking.odin.OdinMod.mc
-import com.odtheking.odin.events.LocationChangeEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.LocationChangeEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.utils.equalsOneOf
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
index 653f43f..1c07559 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
@@ -1,8 +1,8 @@
 package com.odtheking.odin.utils.skyblock
 
 import com.odtheking.odin.events.ChatPacketEvent
-import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.impl.skyblock.Splits
 import com.odtheking.odin.utils.PersonalBest
Cleaned up DungeonMap implementation and added example previewodtheking · 2026-08-15 · a8f7aa9
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt       |  41 ++++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt      |  10 +++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt      | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt          |   1 -
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt |  51 +++++++++++++++------------
 5 files changed, 157 insertions(+), 83 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index b81c4f7..d78c24a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -12,7 +12,6 @@ import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.hollowFill
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import net.minecraft.client.gui.GuiGraphicsExtractor
 
 object DungeonMap : Module(
     name = "Dungeon Map",
@@ -21,7 +20,7 @@ object DungeonMap : Module(
     private val disableBoss by BooleanSetting("Disable in Boss", true, desc = "Disables the map during boss fights.")
 
     val backgroundOutline by ColorSetting("Background Outline", Colors.BLACK, true, desc = "The color of the background border.")
-    val backgroundColor by ColorSetting("Background Color", Colors.BLACK.withAlpha(0.2f), true, desc = "Background color of the map.")
+    val backgroundColor by ColorSetting("Background Color", Colors.BLACK.withAlpha(0.1f), true, desc = "Background color of the map.")
     val textScaling by NumberSetting("Text Scaling", 0.45f, 0.1f, 1f, 0.05f, desc = "Scale of room name text.")
 
     private val playerDropdown by DropdownSetting("Player Settings")
@@ -48,29 +47,31 @@ object DungeonMap : Module(
 
     val disablePred by BooleanSetting("Disable Prediction", false, desc = "Disables special-column room type prediction.")
 
-    private val mapHud by HUD("Dungeon Map", "Displays the dungeon map.", false) { example ->
-        when {
-            (!DungeonUtils.inDungeons || (disableBoss && DungeonUtils.inBoss)) && !example -> 0 to 0
-            example -> renderExampleMap()
-            else    -> renderDungeonMap()
-        }
-    }
+    private val exampleRooms by lazy { buildExampleRooms() }
+    private val exampleDoors by lazy { buildExampleDoors(exampleRooms) }
 
-    private const val MAP_PX = 128
-
-    private fun GuiGraphicsExtractor.renderExampleMap(): Pair<Int, Int> {
+    private val mapHud by HUD("Dungeon Map", "Displays the dungeon map.", false) { example ->
+        if ((!DungeonUtils.inDungeons || (disableBoss && DungeonUtils.inBoss)) && !example) return@HUD 0 to 0
         fill(0, 0, MAP_PX, MAP_PX, backgroundColor.rgba)
         hollowFill(0, 0, MAP_PX, MAP_PX, 1, backgroundOutline)
-        centeredText(mc.font, "MAP", MAP_PX / 2, MAP_PX / 2 - mc.font.lineHeight / 2, Colors.WHITE.rgba)
-        return MAP_PX to MAP_PX
-    }
+        pose().pushMatrix()
 
-    private fun GuiGraphicsExtractor.renderDungeonMap(): Pair<Int, Int> {
-        fill(0, 0, MAP_PX, MAP_PX, backgroundColor.rgba)
-        hollowFill(0, 0, MAP_PX, MAP_PX, 1, Colors.gray26)
+        if (example) {
+            pose().translate(5f, 5f)
+            renderMap(exampleRooms, exampleDoors, emptyList())
+        }
+        else {
+            pose().translate(DungeonScan.startX.toFloat(), DungeonScan.startY.toFloat())
+            pose().scale(DungeonScan.roomSize / DungeonScan.MAP_ROOM_SIZE.toFloat())
+
+            renderMap(DungeonScan.rooms, DungeonScan.doors.values, DungeonScan.pathHints)
 
-        renderMap()
+            if (!DungeonUtils.inBoss) renderPlayers()
+        }
+        pose().popMatrix()
 
-        return MAP_PX to MAP_PX
+        MAP_PX to MAP_PX
     }
+
+    private const val MAP_PX = 128
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
index 01f3884..97f0b2d 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
@@ -20,9 +20,11 @@ import java.util.concurrent.CopyOnWriteArrayList
 object DungeonScan {
 
     const val ROOM_SPACING = 4
+    const val MAP_ROOM_SIZE = 16
+    const val MAP_ROOM_GAP = MAP_ROOM_SIZE + ROOM_SPACING
 
     var roomSize = 16
-    var roomGap = 20
+    val roomGap: Int get () = roomSize + ROOM_SPACING
 
     val connectionGap: Int get() = roomSize + ROOM_SPACING / 2
     var startX = 5
@@ -54,14 +56,12 @@ object DungeonScan {
         viewableDoors.clear()
         pathHints.clear()
         roomSize = 16
-        roomGap = 20
         startX = 5
         startY = 5
     }
 
     fun initClient(floor: Floor) {
         roomSize = if (floor.floorNumber <= 3) 18 else 16
-        roomGap = roomSize + ROOM_SPACING
 
         startX = when {
             floor.floorNumber <= 1 -> 22
@@ -79,8 +79,8 @@ object DungeonScan {
 
     fun playerRenderPosition(entity: Player?, mapPos: IVec2): Pair<Float, Float> {
         entity?.let {
-            val mapX = (it.renderX.toFloat() + 200f) * roomGap / 32f
-            val mapZ = (it.renderZ.toFloat() + 200f) * roomGap / 32f
+            val mapX = (it.renderX.toFloat() + 200f) * MAP_ROOM_GAP / 32f
+            val mapZ = (it.renderZ.toFloat() + 200f) * MAP_ROOM_GAP / 32f
             return mapX to mapZ
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index c5aa78d..799807c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -16,24 +16,9 @@ private val green = Identifier.fromNamespaceAndPath("odin", "map/green_check.png
 private val white = Identifier.fromNamespaceAndPath("odin", "map/white_check.png")
 private val question = Identifier.fromNamespaceAndPath("odin", "map/question.png")
 
-internal fun GuiGraphicsExtractor.renderMap() {
-    pose().pushMatrix()
-    pose().translate(DungeonScan.startX.toFloat(), DungeonScan.startY.toFloat())
-
-    renderDoors()
-
-    for (room in DungeonScan.rooms) {
-        if (room.isViewable) fillRoom(room, roomTypeColor(room.type).rgba, DungeonScan.roomSize, DungeonScan.roomGap)
-    }
-
-    for (room in DungeonScan.rooms) renderRoomText(room)
-
-    if (!DungeonUtils.inBoss) renderPlayers()
-
-    pose().popMatrix()
-}
-
-private fun GuiGraphicsExtractor.fillRoom(room: DungeonRoom, color: Int, rs: Int, rg: Int) {
+fun GuiGraphicsExtractor.fillRoom(room: DungeonRoom, color: Int) {
+    val rs = DungeonScan.MAP_ROOM_SIZE
+    val rg = DungeonScan.MAP_ROOM_GAP
     val ox = room.topLeft.x * rg
     val oy = room.topLeft.z * rg
     when (val shape = room.shape) {
@@ -62,12 +47,12 @@ private fun GuiGraphicsExtractor.fillRoom(room: DungeonRoom, color: Int, rs: Int
     }
 }
 
-private fun GuiGraphicsExtractor.renderDoors() {
-    val rs = DungeonScan.roomSize
-    val rg = DungeonScan.roomGap
+fun GuiGraphicsExtractor.renderDoors(doors: Collection<DungeonDoor>) {
+    val rs = DungeonScan.MAP_ROOM_SIZE
+    val rg = DungeonScan.MAP_ROOM_GAP
     val half = (rs - 8) / 2f
 
-    for ((_, door) in DungeonScan.doors) {
+    for (door in doors) {
         val offset = door.rotation.offset
 
         pose().pushMatrix()
@@ -78,13 +63,17 @@ private fun GuiGraphicsExtractor.renderDoors() {
         fill(0, 0, 4 + 4 * offset.z, 4 + 4 * offset.x, door.color.rgba)
         pose().popMatrix()
     }
+}
+
+fun GuiGraphicsExtractor.renderPathHints(pathHints: Collection<DungeonTile>) {
+    val rs = DungeonScan.MAP_ROOM_SIZE
+    val rg = DungeonScan.MAP_ROOM_GAP
 
-    for (unknownTile in DungeonScan.pathHints) {
-        val (x, y) = unknownTile.position.x * rg to unknownTile.position.z * rg
-        val rs = DungeonScan.roomSize
+    for ((position, room) in pathHints) {
+        val (x, y) = position.x * rg to position.z * rg
 
-        val colors = if (unknownTile.room?.type == RoomType.BLOOD) arrayOf(DungeonMap.bloodRoomColor.darker(0.5f))
-        else if (!DungeonMap.disablePred && unknownTile.room?.isKnown1x1 == true) SpecialColumn.colorGuessForUnknown(unknownTile.position.x)
+        val colors = if (room?.type == RoomType.BLOOD) arrayOf(DungeonMap.bloodRoomColor.darker(0.5f))
+        else if (!DungeonMap.disablePred && room?.isKnown1x1 == true) SpecialColumn.colorGuessForUnknown(position.x)
         else arrayOf(DungeonMap.unknownRoomColor)
 
         when (colors.size) {
@@ -108,21 +97,20 @@ private fun GuiGraphicsExtractor.renderDoors() {
 }
 
 fun GuiGraphicsExtractor.renderIcon(pos: IVec2, identifier: Identifier) {
-    val rs = DungeonScan.roomSize - 4
-    blit(RenderPipelines.GUI_TEXTURED, identifier, pos.x + 2, pos.z + 2, rs.toFloat(), rs.toFloat(), rs, rs, rs, rs)
+    val size = DungeonScan.MAP_ROOM_SIZE - DungeonScan.ROOM_SPACING
+    blit(RenderPipelines.GUI_TEXTURED, identifier, pos.x + 2, pos.z + 2, size.toFloat(), size.toFloat(), size, size, size, size)
 }
 
-private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
+fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
     if (room.type.equalsOneOf(RoomType.UNDISCOVERED, RoomType.FAIRY, RoomType.ENTRANCE, RoomType.BLOOD)) return
-    val scannedRoom = DungeonScan.tiles.getOrNull(room.topLeft.x + room.topLeft.z * 6)?.room
 
-    if (scannedRoom?.walkedInto != true) {
+    if (!room.walkedInto) {
         when (room.checkmark) {
             MapCheckmark.GREEN -> green
             MapCheckmark.WHITE -> white
             MapCheckmark.RED -> cross
             else -> null
-        }?.let { texture -> renderIcon(room.topLeft * DungeonScan.roomGap, texture) }
+        }?.let { texture -> renderIcon(room.topLeft * DungeonScan.MAP_ROOM_GAP, texture) }
         return
     }
 
@@ -136,7 +124,7 @@ private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
         else               -> Color(100, 100, 100)
     }.rgba
 
-    val lines  = scannedRoom.name?.split(" ") ?: return
+    val lines  = room.name?.split(" ") ?: return
     val totalH = (lines.size - 1) * fontH * DungeonMap.textScaling
 
     for ((i, line) in lines.withIndex()) {
@@ -148,7 +136,7 @@ private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
     }
 }
 
-private fun GuiGraphicsExtractor.renderPlayers() {
+fun GuiGraphicsExtractor.renderPlayers() {
     val showNames = mc.player?.mainHandItem?.itemId?.equalsOneOf("INFINITE_SPIRIT_LEAP", "SPIRIT_LEAP") == true
 
     for (player in DungeonUtils.dungeonTeammatesNoSelf) {
@@ -189,6 +177,85 @@ private fun GuiGraphicsExtractor.renderPlayers() {
     pose().popMatrix()
 }
 
+fun GuiGraphicsExtractor.renderMap(rooms: Collection<DungeonRoom>, doors: Collection<DungeonDoor>, pathHints: Collection<DungeonTile>) {
+    renderDoors(doors)
+    for (room in rooms) if (room.isViewable) fillRoom(room, roomTypeColor(room.type).rgba)
+    for (room in rooms) renderRoomText(room)
+    renderPathHints(pathHints)
+}
+
+fun buildExampleRooms(): List<DungeonRoom> {
+    fun room(type: RoomType, topLeft: IVec2, rotation: RoomRotation?, checkmark: MapCheckmark, walkedInto: Boolean, data: RoomData? = null) =
+        DungeonRoom(type, topLeft, data).apply {
+            data?.let { shape = it.shape }
+            this.rotation = rotation
+            this.checkmark = checkmark
+            this.walkedInto = walkedInto
+        }
+
+    return listOf(
+        room(RoomType.ENTRANCE, IVec2(2, 0), RoomRotation.NORTH, MapCheckmark.GREEN, true),
+        room(RoomType.NORMAL,   IVec2(0, 0), RoomRotation.WEST,  MapCheckmark.NONE,  true, RoomData.getRoomData(1051405699)), // hallway
+        room(RoomType.PUZZLE,   IVec2(1, 0), RoomRotation.NORTH, MapCheckmark.NONE,  true, RoomData.getRoomData(379499781)), // water board
+        room(RoomType.NORMAL,   IVec2(3, 0), RoomRotation.WEST,  MapCheckmark.WHITE, true, RoomData.getRoomData(90256084)), // waterfall
+        room(RoomType.NORMAL,   IVec2(4, 0), RoomRotation.SOUTH, MapCheckmark.NONE,  true, RoomData.getRoomData(1024359556)), // bridges
+        room(RoomType.NORMAL,   IVec2(1, 1), RoomRotation.SOUTH, MapCheckmark.WHITE, true, RoomData.getRoomData(1250712883)), // museum
+        room(RoomType.NORMAL,   IVec2(4, 1), RoomRotation.SOUTH, MapCheckmark.NONE,  true, RoomData.getRoomData(950592972)), // cathedral
+        room(RoomType.NORMAL,   IVec2(1, 3), RoomRotation.NORTH, MapCheckmark.WHITE, true, RoomData.getRoomData(-333637832)), // water
+        room(RoomType.FAIRY,    IVec2(2, 3), RoomRotation.SOUTH, MapCheckmark.GREEN, true),
+        room(RoomType.CHAMPION, IVec2(4, 3), RoomRotation.SOUTH, MapCheckmark.GREEN,  false, RoomData.getRoomData(-1334473473)), // dragon
+        room(RoomType.TRAP,     IVec2(5, 3), RoomRotation.NORTH, MapCheckmark.GREEN, false, RoomData.getRoomData(1590699551)), // old trap
+        room(RoomType.PUZZLE,   IVec2(0, 4), RoomRotation.WEST,  MapCheckmark.NONE,  true, RoomData.getRoomData(799715466)), // tp maze
+        room(RoomType.NORMAL,   IVec2(1, 4), RoomRotation.EAST,  MapCheckmark.WHITE, true, RoomData.getRoomData(1484939648)), // spikes
+        room(RoomType.NORMAL,   IVec2(2, 4), RoomRotation.WEST,  MapCheckmark.WHITE, true, RoomData.getRoomData(-1764045332)), // beams
+        room(RoomType.NORMAL,   IVec2(3, 4), RoomRotation.NORTH, MapCheckmark.NONE,  false, RoomData.getRoomData(76347246)), // mirror
+        room(RoomType.NORMAL,   IVec2(4, 4), RoomRotation.WEST,  MapCheckmark.NONE,  false, RoomData.getRoomData(-1899702429)), // silver sword
+        room(RoomType.NORMAL,   IVec2(5, 4), RoomRotation.WEST,  MapCheckmark.WHITE,  false, RoomData.getRoomData(-151940807)), // staircase
+        room(RoomType.NORMAL,   IVec2(0, 5), RoomRotation.SOUTH, MapCheckmark.WHITE, true, RoomData.getRoomData(259768244)), // archway
+        room(RoomType.BLOOD,    IVec2(2, 5), RoomRotation.EAST,  MapCheckmark.NONE,  true),
+        room(RoomType.NORMAL,   IVec2(3, 5), RoomRotation.SOUTH, MapCheckmark.NONE,  false, RoomData.getRoomData(222124420)), // wizard
+    )
+}
+
+fun buildExampleDoors(rooms: List<DungeonRoom>): List<DungeonDoor> {
+    val roomByTile = rooms.flatMap { room -> room.occupiedTiles().map { it to room } }.toMap()
+
+    fun door(position: IVec2, rotation: DoorRotation, type: DoorType): DungeonDoor {
+        val door = DungeonDoor(position, rotation, type)
+        door.color = when (type) {
+            DoorType.Wither -> DungeonMap.witherDoorColor
+            DoorType.Blood  -> DungeonMap.bloodDoorColor
+            DoorType.Fairy  -> DungeonMap.fairyDoorColor
+            DoorType.Normal -> listOfNotNull(roomByTile[position], roomByTile[position + rotation.offset])
+                .firstOrNull { it.type != RoomType.NORMAL && it.type != RoomType.FAIRY }
+                ?.type?.let(::roomTypeColor) ?: DungeonMap.normalDoorColor
+        }
+        return door
+    }
+
+    return listOf(
+        door(IVec2(2, 0), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(1, 0), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(0, 1), DoorRotation.Horizontal, DoorType.Normal),
+        door(IVec2(2, 2), DoorRotation.Horizontal, DoorType.Normal),
+        door(IVec2(1, 2), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(4, 0), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(3, 1), DoorRotation.Horizontal, DoorType.Normal),
+        door(IVec2(3, 3), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(1, 3), DoorRotation.Horizontal, DoorType.Fairy),
+        door(IVec2(2, 3), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(4, 3), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(5, 3), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(0, 4), DoorRotation.Horizontal, DoorType.Normal),
+        door(IVec2(1, 4), DoorRotation.Horizontal, DoorType.Normal),
+        door(IVec2(1, 4), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(3, 4), DoorRotation.Horizontal, DoorType.Normal),
+        door(IVec2(3, 4), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(5, 4), DoorRotation.Vertical, DoorType.Normal),
+        door(IVec2(1, 5), DoorRotation.Horizontal, DoorType.Blood),
+    )
+}
+
 fun roomTypeColor(type: RoomType): Color = when (type) {
     RoomType.NORMAL   -> DungeonMap.normalRoomColor
     RoomType.PUZZLE   -> DungeonMap.puzzleRoomColor
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
index 1d075bc..5cea508 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
@@ -47,7 +47,6 @@ object MapScan {
             val length = end - index
             if (length == 16 || length == 18) {
                 DungeonScan.roomSize = length
-                DungeonScan.roomGap = length + DungeonScan.ROOM_SPACING
                 DungeonScan.startX = (index % MAP_SIZE) % DungeonScan.roomGap
                 DungeonScan.startY = (index / MAP_SIZE) % DungeonScan.roomGap
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
index 13c435c..81ebca3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
@@ -3,19 +3,18 @@ package com.odtheking.odin.features.impl.dungeon.map.tile
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints
 import com.odtheking.odin.features.impl.dungeon.map.DungeonScan
-import com.odtheking.odin.utils.IVec2
-import com.odtheking.odin.utils.modMessage
-import com.odtheking.odin.utils.rotateAroundNorth
-import com.odtheking.odin.utils.rotateToNorth
-import com.odtheking.odin.utils.sortKey
+import com.odtheking.odin.utils.*
 import net.minecraft.core.BlockPos
 import net.minecraft.world.level.block.Blocks
-import kotlin.math.roundToInt
 
 class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData? = null) {
     val tiles: ArrayList<IVec2> = ArrayList(4)
     var topLeft: IVec2 = initialPosition
     var rotation: RoomRotation? = null
+        set(value) {
+            field = value
+            recalculateCenter()
+        }
 
     var shape: RoomShape = RoomShape.OneByOne
 
@@ -43,33 +42,41 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
         }
     }
 
-    private fun recalculateCenter() {
-        if (tiles.isEmpty()) return
-        val rs = DungeonScan.roomSize.takeIf { it != -1 } ?: return
+    fun occupiedTiles(): List<IVec2> {
+        val rot = rotation ?: return emptyList()
+        return tileOffsets(rot).map { topLeft + it }
+    }
 
-        val rg = DungeonScan.roomGap
-        val half = rs / 2
+    private fun tileOffsets(rot: RoomRotation): List<IVec2> = when (shape) {
+        RoomShape.OneByOne -> listOf(IVec2(0, 0))
+        RoomShape.TwoByTwo -> listOf(IVec2(0, 0), IVec2(1, 0), IVec2(0, 1), IVec2(1, 1))
+        RoomShape.L -> when (rot) {
+            RoomRotation.WEST  -> listOf(IVec2(0, 0), IVec2(1, 0), IVec2(0, 1))
+            RoomRotation.NORTH -> listOf(IVec2(0, 0), IVec2(1, 0), IVec2(1, 1))
+            else               -> listOf(IVec2(0, 0), IVec2(0, 1), IVec2(1, 1))
+        }
+        else -> if (rot == RoomRotation.SOUTH) (0 until shape.tileAmount).map { IVec2(it, 0) }
+                else (0 until shape.tileAmount).map { IVec2(0, it) }
+    }
 
-        fun tileCenter(tile: IVec2) = tile.x * rg + half to tile.z * rg + half
+    private fun recalculateCenter() {
+        fun tileCenter(tile: IVec2) = IVec2(tile.x * DungeonScan.MAP_ROOM_GAP + DungeonScan.MAP_ROOM_SIZE / 2, tile.z * DungeonScan.MAP_ROOM_GAP + DungeonScan.MAP_ROOM_SIZE / 2)
 
         val rot = rotation
         if (rot == null) {
-            val tile = tiles.minBy { it.sortKey }
-            val (x, z) = tileCenter(tile)
-            center = IVec2(x, z)
+            if (tiles.isEmpty()) return
+            center = tileCenter(tiles.minBy { it.sortKey })
             return
         }
 
-        val centers = tiles.map { tileCenter(it) }
-        val x = (centers.minOf { it.first } + centers.maxOf { it.first }) / 2f
+        val corners = occupiedTiles().map(::tileCenter)
         val z = when (shape) {
             RoomShape.L ->
-                if (rot == RoomRotation.NORTH || rot == RoomRotation.WEST) centers.minOf { it.second }.toFloat()
-                else centers.maxOf { it.second }.toFloat()
-            else ->
-                (centers.minOf { it.second } + centers.maxOf { it.second }) / 2f
+                if (rot == RoomRotation.NORTH || rot == RoomRotation.WEST) corners.minOf { it.z }
+                else corners.maxOf { it.z }
+            else -> (corners.minOf { it.z } + corners.maxOf { it.z }) / 2
         }
-        center = IVec2(x.roundToInt(), z.roundToInt())
+        center = IVec2((corners.minOf { it.x } + corners.maxOf { it.x }) / 2, z)
     }
 
     fun inferLayoutFromMap() {
Fixed Splits fixed width not working Made Splits example show actual splits Fixed QuizTimer setting not having dependancies Fixed PuzzleHUD not rendering in example Fixed MapInfo sending 300 score more than onceodtheking · 2026-08-15 · abe388f
查看变更
src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt                   |  2 +-
 src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt                              |  4 ++--
 src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt                           |  5 +++--
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt                  |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt                     |  5 ++++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PuzzleHud.kt                   |  7 +++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt                     | 45 ++++++++++++++++++++++++++++-----------------
 src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt                         | 11 ++---------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt                      |  2 +-
 10 files changed, 46 insertions(+), 39 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
index e2958c2..e9dc622 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DungeonWaypointCommand.kt
@@ -13,7 +13,7 @@ import kotlinx.coroutines.launch
 
 val dungeonWaypointsCommand = Commodore("dwp", "dungeonwaypoints") {
     runs {
-        schedule(0) { mc.setScreen(WaypointPackSelectorScreen(mc.screen)) }
+        mc.schedule { mc.setScreen(WaypointPackSelectorScreen(mc.screen)) }
     }
 
     literal("fill").runs {
diff --git a/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
index 1a60d46..f735cf5 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
@@ -16,11 +16,11 @@ import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 
 val mainCommand = Commodore("odin", "od") {
     runs {
-        schedule(0) { mc.setScreen(ClickGUI) }
+        mc.schedule { mc.setScreen(ClickGUI) }
     }
 
     literal("edithud").runs {
-        schedule(0) { mc.setScreen(HudManager) }
+        mc.schedule { mc.setScreen(HudManager) }
     }
 
     literal("tps").runs {
diff --git a/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt
index 6c3d332..ce6b9b1 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/TermsimCommand.kt
@@ -1,16 +1,17 @@
 package com.odtheking.odin.commands
 
 import com.github.stivais.commodore.Commodore
+import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.features.impl.boss.termsim.StartGUI
 import com.odtheking.odin.utils.handlers.schedule
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 
 val termSimCommand = Commodore("termsim") {
     runs { ping: Long? ->
-        schedule(0) { StartGUI.open(ping ?: 0) }
+        mc.schedule { StartGUI.open(ping ?: 0) }
     }
 
     runs { terminal: TerminalTypes, ping: Long? ->
-        schedule(0) { terminal.getSimulator().open(ping ?: 0L) }
+        mc.schedule { terminal.getSimulator().open(ping ?: 0L) }
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
index 90c4178..8113388 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
@@ -40,7 +40,7 @@ object WitherDragons : Module(
     }.withDependency { dragonTimerDropDown }
 
     private val dragonBoxes by BooleanSetting("Dragon Boxes", true, desc = "Displays boxes for where M7 dragons spawn.")
-    private val dragonTracers by BooleanSetting("Dragon Tracer", true, desc = "Draws a line to spawning dragons.")
+    private val dragonTracers by BooleanSetting("Target Tracer", true, desc = "Draws a line to priority spawning dragon.")
 
     private val dragonAlerts by DropdownSetting("Dragon Alerts Dropdown")
     private val sendNotification by BooleanSetting("Send Dragon Confirmation", true, desc = "Sends a confirmation message when a dragon dies.").withDependency { dragonAlerts }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index 0b38ca7..4fa9b55 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -202,10 +202,12 @@ object MapInfo : Module(
     private var portalAABB: AABB? = null
     private var currentRoomSecrets: Pair<Int, Int>? = null
     private val secretRegex = Regex("(\\d+)/(\\d+) Secrets")
+    private var shownTitle = false
 
     init {
         on<ScoreUpdateEvent> {
-            if (!DungeonUtils.inDungeons || (!scoreTitle && !printWhenScore) || score < 300) return@on
+            if (shownTitle || !DungeonUtils.inDungeons || (!scoreTitle && !printWhenScore) || score < 300) return@on
+            shownTitle = true
             if (scoreTitle) alert("§c300 Score!")
             if (printWhenScore) modMessage("§b${DungeonUtils.score} §ascore reached in §6${DungeonUtils.dungeonTime} §8|| §e${DungeonUtils.floor?.name}.")
         }
@@ -231,6 +233,7 @@ object MapInfo : Module(
         }
 
         on<LevelEvent.Load> {
+            shownTitle = false
             portalAABB = null
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PuzzleHud.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PuzzleHud.kt
index 702f876..f9576fe 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PuzzleHud.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PuzzleHud.kt
@@ -23,9 +23,8 @@ object PuzzleHud : Module(
         PuzzleEntry(Puzzle.UNKNOWN.displayName, "✦", "§6", null)
     )
 
-    private val hud by HUD("Puzzle HUD Position", "Displays dungeon puzzle statuses on the HUD.") { example ->
-        if (!DungeonUtils.inDungeons || DungeonUtils.inBoss && !example) return@HUD 0 to 0
-
+    private val hud by HUD("Puzzle HUD", "Displays dungeon puzzle statuses on the HUD.", false) { example ->
+        if (!DungeonUtils.inClear && !example) return@HUD 0 to 0
         if (!example && showOnlyPre && DungeonUtils.openRoomCount != 0) return@HUD 0 to 0
 
         val puzzleCount = if (example) 4 else DungeonUtils.puzzleCount
@@ -54,7 +53,7 @@ object PuzzleHud : Module(
         width = textDim(headerText, 0, 0, Colors.WHITE).first
 
         entries.forEachIndexed { index, entry ->
-            val playerText = if (showPlayerNames && entry.player != null) " §7(${entry.player})" else ""
+            val playerText = if (showPlayerNames && entry.player != null) " §7(§6${entry.player}§7)" else ""
             val newWidth = textDim("§e${entry.name}§8: [${entry.statusColor}${entry.statusIcon}§8]$playerText", 0, (index + 1) * lineHeight, Colors.WHITE).first
             if (newWidth > width) width = newWidth
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index c181831..757aff4 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -86,7 +86,7 @@ object PuzzleSolvers : Module(
     private val quizTimerHud by HUD("Quiz Timer Hud", "Displays a timer counting down until you must answer the quiz question.") { example ->
         val timer = if (example) Triple(220, 220, 1) else QuizSolver.timer
         if (quizSolver && timer.first > 0) textDim(formatQuizTimer(timer), 0, 0, Colors.WHITE) else 0 to 0
-    }
+    }.withDependency { quizDropdown && quizSolver }
     private val quizReset by ActionSetting("Reset Quiz", desc = "Resets the solver.") { QuizSolver.reset() }.withDependency { quizDropdown && quizSolver }
 
     private val boulderDropDown by DropdownSetting("Boulder")
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
index 6049324..de6e4b8 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/Splits.kt
@@ -10,6 +10,7 @@ import com.odtheking.odin.utils.render.getStringWidth
 import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.skyblock.SplitsManager.currentSplits
 import com.odtheking.odin.utils.skyblock.SplitsManager.getAndUpdateSplitsTimes
+import com.odtheking.odin.utils.skyblock.floor7SplitGroup
 import com.odtheking.odin.utils.toFixed
 
 object Splits : Module(
@@ -17,44 +18,54 @@ object Splits : Module(
     description = "Provides visual timers for Kuudra and Dungeons."
 ) {
     private val hud by HUD("Splits Display HUD", "Shows timers for each split.") { example ->
-        val totalWidth = getStringWidth("Split 0: 0h 00m 00s" + if (showTickTime) " (0h 00m 00s)" else "") + 2
+        val timeWidth = 75 + if (showTickTime) 20 else -20
 
         if (example) {
-            repeat(5) { i ->
-                val exampleTime = "0h 00m 00s" + if (showTickTime) " §8(§70s§8)" else ""
+            val labelWidth = floor7SplitGroup.maxOf { getStringWidth("${it.name}:") }
+            val timeExample = "59m 59s" + if (showTickTime) " §8(§759.9§8)" else ""
+            val totalWidth = labelWidth + 4 + timeWidth + 2
+
+            floor7SplitGroup.forEachIndexed { i, split ->
                 if (fixedWidth) {
-                    text("Split $i:", 0, i * 9, Colors.WHITE)
-                    text(exampleTime, totalWidth - getStringWidth("0h 00m 00s" + if (showTickTime) " (0s)" else ""), i * 9, Colors.WHITE)
-                } else {
-                    text("Split $i: $exampleTime", 0, i * 9, Colors.WHITE)
-                }
+                    text("${split.name}:", 0, i * 9, Colors.WHITE)
+                    text(timeExample, totalWidth - getStringWidth(timeExample) - 2, i * 9, Colors.WHITE)
+                } else text("${split.name}: $timeExample", 0, i * 9, Colors.WHITE)
             }
-            return@HUD totalWidth to 9 * 5
+            return@HUD totalWidth to 9 * floor7SplitGroup.size
         }
 
         val (times, tickTimes, current) = getAndUpdateSplitsTimes(currentSplits)
         if (currentSplits.splits.isEmpty()) return@HUD 0 to 0
+        val splits = currentSplits.splits.dropLast(1)
 
-        val maxWidth = currentSplits.splits.dropLast(1).maxOf { getStringWidth(it.name) }
+        val labelWidth = splits.maxOfOrNull { getStringWidth(it.name) } ?: 0
+        val totalWidth = labelWidth + 4 + timeWidth + 2
 
-        currentSplits.splits.dropLast(1).forEachIndexed { index, split ->
+        splits.forEachIndexed { index, split ->
             val time = formatTime(if (index >= times.size) 0 else times[index])
             text(split.name, 0, index * 9, Colors.WHITE)
 
-            val displayText = if (showTickTime && index < tickTimes.size) "$time §8(§7${(tickTimes[index] / 20f).toFixed()}§8)" else time
-            val timeX = if (fixedWidth) totalWidth - getStringWidth(displayText) else maxWidth + 4
+            val displayText = if (showTickTime && index < tickTimes.size) "$time §8(§7${(tickTimes[index] / 20f).toFixed()}§8)"
+            else time
+
+            val timeX = if (fixedWidth) labelWidth + 4 + timeWidth - getStringWidth(displayText)
+            else labelWidth + 4
 
             text(displayText, timeX, index * 9, Colors.WHITE)
         }
 
         if (bossEntrySplit && currentSplits.splits.size > 3) {
-            text("§9Boss Entry", 0, (currentSplits.splits.size - 1) * 9, Colors.WHITE)
+            val y = splits.size * 9
+            text("§9Boss Entry", 0, y, Colors.WHITE)
 
             val totalTime = formatTime(times.take(3).sum())
-            val displayText = if (showTickTime) "$totalTime §8(§7${(tickTimes.take(3).sum() / 20f).toFixed()}§8)" else totalTime
-            val timeX = if (fixedWidth) totalWidth - getStringWidth(displayText) else maxWidth + 4
+            val displayText = if (showTickTime) "$totalTime §8(§7${(tickTimes.take(3).sum() / 20f).toFixed()}§8)"
+            else totalTime
+
+            val timeX = if (fixedWidth) labelWidth + 4 + timeWidth - getStringWidth(displayText)
+            else labelWidth + 4
 
-            text(displayText, timeX, (currentSplits.splits.size - 1) * 9, Colors.WHITE)
+            text(displayText, timeX, y, Colors.WHITE)
         }
 
         totalWidth to 9 * (currentSplits.splits.size + (if (bossEntrySplit) 1 else 0))
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt b/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
index 963ad11..347dc6e 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
@@ -83,18 +83,11 @@ class ItemStateRenderer(vertexConsumers: MultiBufferSource.BufferSource)
     }
 
     companion object {
-        private val trackingCache = object : LinkedHashMap<ItemStack, TrackingItemStackRenderState>(16, 0.75f, true) {
-            override fun removeEldestEntry(eldest: MutableMap.MutableEntry<ItemStack, TrackingItemStackRenderState>) = size > 10
-        }
-
         fun GuiGraphicsExtractor.drawItemStack(item: ItemStack, x: Int, y: Int) {
             if (item.isEmpty) return
 
-            val tracking = trackingCache.getOrPut(item) {
-                TrackingItemStackRenderState().also {
-                    mc.itemModelResolver.updateForTopItem(it, item, ItemDisplayContext.GUI, mc.level, mc.player, 0)
-                }
-            }
+            val tracking = TrackingItemStackRenderState()
+            mc.itemModelResolver.updateForTopItem(tracking, item, ItemDisplayContext.GUI, mc.level, mc.player, 0)
 
             val state = State(
                 GuiItemRenderState(
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
index 151b243..653f43f 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
@@ -174,7 +174,7 @@ private val floor6SplitGroup = mutableListOf(
     Split(Regex("^\\[BOSS] Sadan: You did it\\. I understand now, you have earned my respect\\.$"), "§4Cleared"),
 )
 
-private val floor7SplitGroup = mutableListOf(
+val floor7SplitGroup = mutableListOf(
     Split(entryRegexes[6], "§5Maxor"),
     Split(Regex("\\[BOSS] Storm: Pathetic Maxor, just like expected\\."), "§3Storm"),
     Split(Regex("\\[BOSS] Goldor: Who dares trespass into my domain\\?"), "§6Terminals"),
Added Quiz timerodtheking · 2026-08-14 · 91d6c63
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt | 16 ++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt    | 19 +++++++++++++++++++
 2 files changed, 35 insertions(+)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index 4fef198..c181831 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -11,6 +11,7 @@ import com.odtheking.odin.features.impl.dungeon.map.tile.RoomType
 import com.odtheking.odin.utils.*
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.handlers.TickTask
+import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.core.BlockPos
 import net.minecraft.network.chat.ClickEvent
@@ -82,6 +83,10 @@ object PuzzleSolvers : Module(
     private val quizSolver by BooleanSetting("Quiz Solver", true, desc = "Solver for the trivia puzzle.").withDependency { quizDropdown }
     private val quizColor by ColorSetting("Quiz Color", Colors.MINECRAFT_GREEN.withAlpha(.75f), true, desc = "Color for the quiz solver.").withDependency { quizDropdown && quizSolver }
     private val quizDepth by BooleanSetting("Quiz Depth", false, desc = "Depth check for the trivia puzzle.").withDependency { quizDropdown && quizSolver }
+    private val quizTimerHud by HUD("Quiz Timer Hud", "Displays a timer counting down until you must answer the quiz question.") { example ->
+        val timer = if (example) Triple(220, 220, 1) else QuizSolver.timer
+        if (quizSolver && timer.first > 0) textDim(formatQuizTimer(timer), 0, 0, Colors.WHITE) else 0 to 0
+    }
     private val quizReset by ActionSetting("Reset Quiz", desc = "Resets the solver.") { QuizSolver.reset() }.withDependency { quizDropdown && quizSolver }
 
     private val boulderDropDown by DropdownSetting("Boulder")
@@ -109,6 +114,7 @@ object PuzzleSolvers : Module(
         on<TickEvent.Server> {
             if (!DungeonUtils.inClear) return@on
             if (waterSolver) WaterSolver.onServerTick()
+            QuizSolver.onServerTick()
         }
 
         on<LevelEvent.Load> {
@@ -200,4 +206,14 @@ object PuzzleSolvers : Module(
             it.sentMessage = true
         }
     }
+
+    private fun formatQuizTimer(timer: Triple<Int, Int, Int>): String {
+        val (tick, maxTick, stage) = timer
+        val color = when {
+            tick.toFloat() >= maxTick * 0.66 -> "§a"
+            tick.toFloat() >= maxTick * 0.33 -> "§6"
+            else -> "§c"
+        }
+        return "§5Quiz §8(§f$stage§8/§f3§8): $color${(tick / 20f).toFixed()}s"
+    }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt
index 5466c9b..04ff661 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt
@@ -12,6 +12,9 @@ import net.minecraft.core.BlockPos
 import net.minecraft.world.phys.AABB
 
 object QuizSolver {
+    private const val START_DURATION = 220
+    private const val QUESTION_DURATION = 100
+
     private var answers: MutableMap<String, List<String>> = JsonResourceLoader.loadJson(
         "/assets/odin/puzzles/quizAnswers.json", mutableMapOf()
     )
@@ -20,7 +23,22 @@ object QuizSolver {
     private var triviaOptions: MutableList<TriviaAnswer> = MutableList(3) { TriviaAnswer(null, false) }
     private data class TriviaAnswer(var blockPos: BlockPos?, var isCorrect: Boolean)
 
+    var timer: Triple<Int, Int, Int> = Triple(0, 0, 0)
+        private set
+
+    fun onServerTick() {
+        val (tick, maxTick, stage) = timer
+        if (tick <= 0) return
+        timer = Triple(tick - 1, maxTick, stage)
+    }
+
     fun onMessage(msg: String) {
+        when (msg) {
+            "[STATUE] Oruo the Omniscient: I am Oruo the Omniscient. I have lived many lives. I have learned all there is to know." -> timer = Triple(START_DURATION, START_DURATION, 1)
+            "[STATUE] Oruo the Omniscient: 2 questions left... Then you will have proven your worth to me!" -> timer = Triple(QUESTION_DURATION, QUESTION_DURATION, 2)
+            "[STATUE] Oruo the Omniscient: One more question!" -> timer = Triple(QUESTION_DURATION, QUESTION_DURATION, 3)
+        }
+
         if (msg.startsWith("[STATUE] Oruo the Omniscient: ") && msg.endsWith("correctly!")) {
             if (msg.contains("answered the final question")) {
                 onPuzzleComplete("Quiz")
@@ -66,5 +84,6 @@ object QuizSolver {
     fun reset() {
         triviaOptions = MutableList(3) { TriviaAnswer(null, false) }
         triviaAnswers = null
+        timer = Triple(0, 0, 0)
     }
 }
\ No newline at end of file
cleaned up WitherDragons settingsodtheking · 2026-08-14 · d6485c4
查看变更
src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                               |  1 +
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt                  |  6 +----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt              |  6 +----
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt             |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt                     | 88 ++++++++++++++++++-----------------------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt             |  6 +++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt                         | 11 +++++++--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt            | 12 ++++++++-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt               | 43 ++++++++++++++++++--------------
 10 files changed, 78 insertions(+), 99 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index 1ba4f4f..c8f06f8 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -76,6 +76,7 @@ abstract class PacketEvent(val packet: Packet<*>) : CancellableEvent() { // idea
 
 class RoomEnterEvent(val room: DungeonRoom?) : Event
 class FloorEnterEvent(val floor: Floor) : Event
+class ScoreUpdateEvent(val score: Int) : Event
 data object MapUpdateEvent : Event
 class CheckmarkUpdateEvent(val room: DungeonRoom, val checkmark: MapCheckmark) : Event
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
index ffe4336..90c4178 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragons.kt
@@ -40,15 +40,11 @@ object WitherDragons : Module(
     }.withDependency { dragonTimerDropDown }
 
     private val dragonBoxes by BooleanSetting("Dragon Boxes", true, desc = "Displays boxes for where M7 dragons spawn.")
-
-    private val dragonTitleDropDown by DropdownSetting("Dragon Spawn Dropdown")
-    val dragonTitle by BooleanSetting("Dragon Title", true, desc = "Displays a title for spawning dragons.").withDependency { dragonTitleDropDown }
-    private val dragonTracers by BooleanSetting("Dragon Tracer", false, desc = "Draws a line to spawning dragons.").withDependency { dragonTitleDropDown }
+    private val dragonTracers by BooleanSetting("Dragon Tracer", true, desc = "Draws a line to spawning dragons.")
 
     private val dragonAlerts by DropdownSetting("Dragon Alerts Dropdown")
     private val sendNotification by BooleanSetting("Send Dragon Confirmation", true, desc = "Sends a confirmation message when a dragon dies.").withDependency { dragonAlerts }
     val sendTime by BooleanSetting("Send Dragon Time Alive", true, desc = "Sends a message when a dragon dies with the time it was alive.").withDependency { dragonAlerts }
-    val sendSpawning by BooleanSetting("Send Dragon Spawning", true, desc = "Sends a message when a dragon is spawning.").withDependency { dragonAlerts }
     val sendSpawned by BooleanSetting("Send Dragon Spawned", true, desc = "Sends a message when a dragon has spawned.").withDependency { dragonAlerts }
     val sendSpray by BooleanSetting("Send Ice Sprayed", true, desc = "Sends a message when a dragon has been ice sprayed.").withDependency { dragonAlerts }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
index 769872c..9b3a374 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/WitherDragonsEnum.kt
@@ -6,7 +6,6 @@ import com.odtheking.odin.features.impl.boss.WitherDragons.currentTick
 import com.odtheking.odin.features.impl.boss.WitherDragons.dragonPriorityToggle
 import com.odtheking.odin.features.impl.boss.WitherDragons.priorityDragon
 import com.odtheking.odin.features.impl.boss.WitherDragons.sendSpawned
-import com.odtheking.odin.features.impl.boss.WitherDragons.sendSpawning
 import com.odtheking.odin.features.impl.boss.WitherDragons.sendTime
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Colors
@@ -125,8 +124,6 @@ fun handleSpawnPacket(particle: ClientboundLevelParticlesPacket) {
 
         if (particle.x !in dragon.xRange || particle.z !in dragon.zRange) return@fold newSpawned to dragons
 
-        if (sendSpawning && WitherDragons.enabled) modMessage("§${dragon.colorCode}$dragon §fdragon is spawning.")
-
         dragon.state = WitherDragonState.SPAWNING
         dragon.timeToSpawn = 100
         dragons.add(dragon)
@@ -135,8 +132,7 @@ fun handleSpawnPacket(particle: ClientboundLevelParticlesPacket) {
 
     if (dragons.isNotEmpty() && (dragons.size == 2 || spawned >= 2) && priorityDragon == null)
         priorityDragon = findPriority(dragons).also { dragon ->
-            if (WitherDragons.dragonTitle && WitherDragons.enabled) alert("§${dragon.colorCode}${dragon.name} is spawning!", true)
-            if (dragonPriorityToggle && WitherDragons.enabled) modMessage("${dragons.joinToString(", ") { "§${it.colorCode}${it.name}" }}§r -> §${dragon.colorCode}${dragon.name} §7is your priority dragon!")
+            if (dragonPriorityToggle && WitherDragons.enabled && dragons.size > 1) modMessage("${dragons.joinToString(", ") { "§${it.colorCode}${it.name}" }}§r -> §${dragon.colorCode}${dragon.name} §7is your priority dragon!")
         }
 }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
index afa74e2..9ed996b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termsim/TermSimGUI.kt
@@ -75,7 +75,7 @@ open class TermSimGUI(
 
     override fun slotClicked(slot: Slot, i: Int, j: Int, clickType: ContainerInput) {
         if (GuiEvent.SlotClick(this, i, j).postAndCatch()) return
-        slot.let { delaySlotClick(it, i) }
+        slot?.let { delaySlotClick(it, j) }
     }
 
     fun clickIndex(index: Int, button: Int) {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index 492252e..0b38ca7 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -5,6 +5,7 @@ import com.odtheking.odin.clickgui.settings.impl.*
 import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.RoomEnterEvent
+import com.odtheking.odin.events.ScoreUpdateEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
@@ -34,71 +35,51 @@ object MapInfo : Module(
     private val printWhenScore by BooleanSetting("Print Score Time", true, desc = "Sends elapsed time in chat when 300 score is reached.")
     val togglePaul by SelectorSetting("Paul Settings", "Automatic", arrayListOf("Automatic", "Force Disable", "Force Enable"), desc = "Toggle Paul's settings.")
 
-    private var cachedScore = 0
-    private var cachedSecretCount = 0
-    private var cachedNeededSecrets = 0
-    private var cachedTotalSecrets = 0
-    private var cachedKnownSecrets = 0
-    private var cachedMimicKilled = false
-    private var cachedPrinceKilled = false
-    private var cachedBatKilled = false
-    private var cachedCryptCount = 0
-    private var cachedDeathCount = 0
-
     private val fullHud: HudElement by HUD("Full Hud", "Displays a full hud with score, secrets, crypts, and mimic info.") {
         if ((!DungeonUtils.inDungeons || (disableInBoss && DungeonUtils.inBoss)) && !it) return@HUD 0 to 0
 
-        val score = cachedScore
-        val secretCount = cachedSecretCount
-        val neededSecrets = cachedNeededSecrets
-        val totalSecrets = cachedTotalSecrets
-        val mimicKilled = cachedMimicKilled
-        val princeKilled = cachedPrinceKilled
-        val batKilled = cachedBatKilled
-        val cryptCount = cachedCryptCount
-
         val showRemaining = fullAddRemaining && alternate
         val useNeededSecrets = fullRemaining != 0 || showRemaining
 
         val scoreText = buildString {
             append("§7Score: ")
-            append(colorizeScore(score))
+            append(colorizeScore(DungeonUtils.score))
         }
 
         val secretText = buildString {
             append("§7Secrets: §b")
-            append(secretCount)
+            append(DungeonUtils.secretCount)
             if (showRemaining) {
                 append("§7-§d")
-                append((neededSecrets - secretCount).coerceAtLeast(0))
+                append((DungeonUtils.neededSecretsAmount - DungeonUtils.secretCount).coerceAtLeast(0))
             }
             append("§7-§e")
-            append(if (useNeededSecrets) neededSecrets else (neededSecrets - secretCount).coerceAtLeast(0))
+            append(if (useNeededSecrets) DungeonUtils.neededSecretsAmount else (DungeonUtils.neededSecretsAmount - DungeonUtils.secretCount).coerceAtLeast(0))
             append("§7-§c")
-            append(totalSecrets)
+            append(DungeonUtils.totalSecrets)
         }
 
         val unknownSecretsText = if (unknown == 0) {
             buildString {
                 append("§7Deaths: §c")
-                append(colorizeDeaths(cachedDeathCount))
+                append(colorizeDeaths(DungeonUtils.deathCount))
             }
         } else {
             buildString {
                 append("§7Unfound: §e")
-                append((totalSecrets - cachedKnownSecrets).coerceAtLeast(0))
+                append((DungeonUtils.totalSecrets - DungeonUtils.knownSecrets).coerceAtLeast(0))
             }
         }
 
         val mimicText = buildString {
-            append("${if (mimicKilled) "§a" else "§c"}\uD83D\uDCE6")
-            append(" §8| ${if (princeKilled) "§a" else "§c"}\uD83E\uDD34")
-            append(" §8| ${if (batKilled) "§a" else "§c"}\uD83E\uDD87")
+            append("${if (DungeonUtils.mimicKilled) "§a" else "§c"}\uD83D\uDCE6")
+            append(" §8| ${if (DungeonUtils.princeKilled) "§a" else "§c"}\uD83E\uDD34")
+            append(" §8| ${if (DungeonUtils.batKilled) "§a" else "§c"}\uD83E\uDD87")
         }
 
         val cryptText = buildString {
             append("§7Crypts: ")
-            append(colorizeCrypts(cryptCount))
+            append(colorizeCrypts(DungeonUtils.cryptCount.coerceAtMost(5)))
         }
 
         val trText = if (alternate) cryptText else scoreText
@@ -135,21 +116,17 @@ object MapInfo : Module(
     private val compactSecrets: HudElement by HUD("Compact Secrets", "Displays a compact secrets hud with score and secrets.") {
         if ((!DungeonUtils.inDungeons || (disableInBoss && DungeonUtils.inBoss)) && !it) return@HUD 0 to 0
 
-        val secretCount = cachedSecretCount
-        val neededSecrets = cachedNeededSecrets
-        val totalSecrets = cachedTotalSecrets
-
         val secretText = buildString {
             append("§7Secrets: §b")
-            append(secretCount)
+            append(DungeonUtils.secretCount)
             if (compactAddRemaining) {
                 append("§7-§d")
-                append((neededSecrets - secretCount).coerceAtLeast(0))
+                append((DungeonUtils.neededSecretsAmount - DungeonUtils.secretCount).coerceAtLeast(0))
             }
             append("§7-§e")
-            append(if (compactRemaining == 0 || fullAddRemaining) neededSecrets else (neededSecrets - secretCount).coerceAtLeast(0))
+            append(if (compactRemaining == 0 || fullAddRemaining) DungeonUtils.neededSecretsAmount else (DungeonUtils.neededSecretsAmount - DungeonUtils.secretCount).coerceAtLeast(0))
             append("§7-§c")
-            append(totalSecrets)
+            append(DungeonUtils.totalSecrets)
         }
 
         val width = getStringWidth(secretText)
@@ -170,10 +147,10 @@ object MapInfo : Module(
 
     private val compactScore: HudElement by HUD("Compact Score", "Displays a compact score hud with score info.") {
         if ((!DungeonUtils.inDungeons || (disableInBoss && DungeonUtils.inBoss)) && !it) return@HUD 0 to 0
-        val score = cachedScore
-        val mimicKilled = cachedMimicKilled
-        val princeKilled = cachedPrinceKilled
-        val batKilled = cachedBatKilled
+        val score = DungeonUtils.score
+        val mimicKilled = DungeonUtils.mimicKilled
+        val princeKilled = DungeonUtils.princeKilled
+        val batKilled = DungeonUtils.batKilled
 
         val missing = (if (mimicKilled) 0 else 2) + (if (princeKilled) 0 else 1) + (if (batKilled) 0 else 1)
 
@@ -225,28 +202,12 @@ object MapInfo : Module(
     private var portalAABB: AABB? = null
     private var currentRoomSecrets: Pair<Int, Int>? = null
     private val secretRegex = Regex("(\\d+)/(\\d+) Secrets")
-    var shownTitle = false
 
     init {
-        TickTask(1) {
-            if (!enabled || !DungeonUtils.inDungeons) return@TickTask
-            cachedScore = DungeonUtils.score
-            cachedSecretCount = DungeonUtils.secretCount
-            cachedNeededSecrets = DungeonUtils.neededSecretsAmount
-            cachedTotalSecrets = DungeonUtils.totalSecrets
-            cachedKnownSecrets = DungeonUtils.knownSecrets
-            cachedMimicKilled = DungeonUtils.mimicKilled
-            cachedPrinceKilled = DungeonUtils.princeKilled
-            cachedBatKilled = DungeonUtils.batKilled
-            cachedCryptCount = DungeonUtils.cryptCount.coerceAtMost(5)
-            cachedDeathCount = DungeonUtils.deathCount
-        }
-
-        TickTask(10) {
-            if (!enabled || !DungeonUtils.inDungeons || shownTitle || (!scoreTitle && !printWhenScore) || DungeonUtils.score < 300) return@TickTask
+        on<ScoreUpdateEvent> {
+            if (!DungeonUtils.inDungeons || (!scoreTitle && !printWhenScore) || score < 300) return@on
             if (scoreTitle) alert("§c300 Score!")
-            if (printWhenScore) modMessage("§b${DungeonUtils.score} §ascore reached in §6${DungeonUtils.dungeonTime} || ${DungeonUtils.floor?.name}.")
-            shownTitle = true
+            if (printWhenScore) modMessage("§b${DungeonUtils.score} §ascore reached in §6${DungeonUtils.dungeonTime} §8|| §e${DungeonUtils.floor?.name}.")
         }
 
         onReceive<ClientboundSystemChatPacket> {
@@ -263,14 +224,13 @@ object MapInfo : Module(
         }
 
         on<RenderEvent.Extract> {
-            if (!highlightPortal || !DungeonUtils.inClear || cachedScore < 300) return@on
+            if (!highlightPortal || !DungeonUtils.inClear || DungeonUtils.score < 300) return@on
             portalAABB?.let { pos ->
                 drawFilledBox(pos, Colors.MINECRAFT_GREEN.withAlpha(0.5f), depth = true)
             }
         }
 
         on<LevelEvent.Load> {
-            shownTitle = false
             portalAABB = null
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
index 838c710..01f3884 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
@@ -10,6 +10,8 @@ import com.odtheking.odin.features.impl.dungeon.map.tile.*
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.IVec2
+import com.odtheking.odin.utils.renderX
+import com.odtheking.odin.utils.renderZ
 import com.odtheking.odin.utils.skyblock.dungeon.Floor
 import net.minecraft.world.entity.player.Player
 import java.util.concurrent.ConcurrentHashMap
@@ -77,8 +79,8 @@ object DungeonScan {
 
     fun playerRenderPosition(entity: Player?, mapPos: IVec2): Pair<Float, Float> {
         entity?.let {
-            val mapX = (it.x.toFloat() + 200f) * roomGap / 32f
-            val mapZ = (it.z.toFloat() + 200f) * roomGap / 32f
+            val mapX = (it.renderX.toFloat() + 200f) * roomGap / 32f
+            val mapZ = (it.renderZ.toFloat() + 200f) * roomGap / 32f
             return mapX to mapZ
         }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index 08c7080..4fef198 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -56,7 +56,7 @@ object PuzzleSolvers : Module(
     private val blazeLineNext by BooleanSetting("Blaze Solver Next Line", true, desc = "Shows the next line to click.").withDependency { blazeSolver && blazeDropDown }
     private val blazeLineAmount by NumberSetting("Blaze Solver Lines", 1, 1, 10, 1, desc = "Amount of lines to show.").withDependency { blazeSolver && blazeDropDown }
     private val blazeLineWidth by NumberSetting("Blaze Solver Lines Width", 2f, 0.5, 5, 0.1, desc = "Width for blaze lines.").withDependency { blazeSolver && blazeDropDown }
-    private val blazeStyle by SelectorSetting("Blaze Style", "Outline", arrayListOf("Filled", "Outline", "Filled Outline"), desc = "Whether or not the box should be filled.").withDependency { blazeSolver && blazeDropDown }
+    private val blazeStyle by SelectorSetting("Blaze Style", "Filled Outline", arrayListOf("Filled", "Outline", "Filled Outline"), desc = "Whether or not the box should be filled.").withDependency { blazeSolver && blazeDropDown }
     private val blazeFirstColor by ColorSetting("First Color", Colors.MINECRAFT_GREEN.withAlpha(.75f), true, desc = "Color for the first blaze.").withDependency { blazeSolver && blazeDropDown }
     private val blazeSecondColor by ColorSetting("Second Color", Colors.MINECRAFT_GOLD.withAlpha(.75f), true, desc = "Color for the second blaze.").withDependency { blazeSolver && blazeDropDown }
     private val blazeThirdColor by ColorSetting("Third Color", Colors.MINECRAFT_RED.withAlpha(.75f), true, desc = "Color for the third blaze.").withDependency { blazeSolver && blazeDropDown }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt b/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
index 347dc6e..963ad11 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/render/ItemRenderer.kt
@@ -83,11 +83,18 @@ class ItemStateRenderer(vertexConsumers: MultiBufferSource.BufferSource)
     }
 
     companion object {
+        private val trackingCache = object : LinkedHashMap<ItemStack, TrackingItemStackRenderState>(16, 0.75f, true) {
+            override fun removeEldestEntry(eldest: MutableMap.MutableEntry<ItemStack, TrackingItemStackRenderState>) = size > 10
+        }
+
         fun GuiGraphicsExtractor.drawItemStack(item: ItemStack, x: Int, y: Int) {
             if (item.isEmpty) return
 
-            val tracking = TrackingItemStackRenderState()
-            mc.itemModelResolver.updateForTopItem(tracking, item, ItemDisplayContext.GUI, mc.level, mc.player, 0)
+            val tracking = trackingCache.getOrPut(item) {
+                TrackingItemStackRenderState().also {
+                    mc.itemModelResolver.updateForTopItem(it, item, ItemDisplayContext.GUI, mc.level, mc.player, 0)
+                }
+            }
 
             val state = State(
                 GuiItemRenderState(
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
index a40d2e9..9892873 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
@@ -44,7 +44,11 @@ object DungeonListener {
 
     init {
         on<TickEvent.End> {
-            if (DungeonUtils.inDungeons) inBoss = getBoss()
+            if (!DungeonUtils.inDungeons) return@on
+            val newInBoss = getBoss()
+            if (newInBoss == inBoss) return@on
+            inBoss = newInBoss
+            DungeonUtils.updateScore()
         }
 
         on<LevelEvent.Load> {
@@ -58,6 +62,7 @@ object DungeonListener {
             inBoss = false
             floor = null
             paul = false
+            DungeonUtils.updateScore()
         }
 
         on<RoomEnterEvent> {
@@ -69,6 +74,7 @@ object DungeonListener {
             updateDungeonTeammates(tabListEntries)
             updateDungeonStats(tabListEntries)
             getDungeonPuzzles(tabListEntries)
+            DungeonUtils.updateScore()
         }
 
         onReceive<ClientboundSetPlayerTeamPacket> {
@@ -86,6 +92,7 @@ object DungeonListener {
             clearedRegex.find(text)?.groupValues?.get(1)?.toIntOrNull()?.let {
                 if (dungeonStats.percentCleared != it && expectingBloodUpdate) dungeonStats.bloodDone = true
                 dungeonStats.percentCleared = it
+                DungeonUtils.updateScore()
             }
         }
 
@@ -116,7 +123,10 @@ object DungeonListener {
 
                 "blaze done!", "blaze done", "blaze puzzle solved!" ->
                     puzzles.find { it == Puzzle.BLAZE }.let { it?.status = PuzzleStatus.Completed }
+
+                else -> return@on
             }
+            DungeonUtils.updateScore()
         }
 
         onReceive<ClientboundRemoveEntitiesPacket> {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
index 3367a09..7b498c0 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
@@ -1,6 +1,7 @@
 package com.odtheking.odin.utils.skyblock.dungeon
 
 import com.odtheking.odin.OdinMod.mc
+import com.odtheking.odin.events.ScoreUpdateEvent
 import com.odtheking.odin.features.impl.dungeon.MapInfo.togglePaul
 import com.odtheking.odin.features.impl.dungeon.map.WorldScan
 import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
@@ -117,28 +118,34 @@ object DungeonUtils {
     inline val bloodDone: Boolean
         get() = DungeonListener.dungeonStats.bloodDone
 
-    inline val score: Int
-        get() {
-            val completed = completedRoomCount + (if (!bloodDone) 1 else 0) + (if (!inBoss) 1 else 0)
-            val total = if (totalRooms != 0) totalRooms else 36
+    var score: Int = 0
+        private set
 
-            val exploration = floor?.let {
-                val secretScore = if (totalSecrets > 0) {
-                    floor(secretCount.toDouble() / (totalSecrets.toDouble() * it.requiredPercentage) * 40.0)
-                        .toInt().coerceIn(0, 40)
-                } else 0
+    fun updateScore() {
+        val completed = completedRoomCount + (if (!bloodDone) 1 else 0) + (if (!inBoss) 1 else 0)
+        val total = if (totalRooms != 0) totalRooms else 36
 
-                secretScore + floor(completed.toFloat() / total * 60f).coerceIn(0f, 60f).toInt()
-            } ?: 0
+        val exploration = floor?.let {
+            val secretScore = if (totalSecrets > 0) {
+                floor(secretCount.toDouble() / (totalSecrets.toDouble() * it.requiredPercentage) * 40.0)
+                    .toInt().coerceIn(0, 40)
+            } else 0
 
-            val skillRooms = floor(completed.toFloat() / total * 80f).coerceIn(0f, 80f).toInt()
-            val puzzlePenalty = (puzzleCount - puzzles.count { it.status == PuzzleStatus.Completed }) * 10
+            secretScore + floor(completed.toFloat() / total * 60f).coerceIn(0f, 60f).toInt()
+        } ?: 0
 
-            return exploration + (20 + skillRooms - puzzlePenalty - (deathCount * 2 - 1).coerceAtLeast(0)).coerceIn(
-                20,
-                100
-            ) + getBonusScore + 100
-        }
+        val skillRooms = floor(completed.toFloat() / total * 80f).coerceIn(0f, 80f).toInt()
+        val puzzlePenalty = (puzzleCount - puzzles.count { it.status == PuzzleStatus.Completed }) * 10
+
+        val newScore = exploration + (20 + skillRooms - puzzlePenalty - (deathCount * 2 - 1).coerceAtLeast(0)).coerceIn(
+            20,
+            100
+        ) + getBonusScore + 100
+
+        if (newScore == score) return
+        score = newScore
+        ScoreUpdateEvent(newScore).postAndCatch()
+    }
 
     inline val neededSecretsAmount: Int
         get() =
Add new islands Torrhus Canyon and Safari, rename Galatea to Moonglade Marsh (#137)天赐manna · 2026-08-14 · 546d5f1
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt
index 7d02d5b..c47b539 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt
@@ -22,7 +22,9 @@ enum class Island(val displayName: String) {
     Mineshaft("Mineshaft"),
     Rift("The Rift"),
     BackwaterBayou("Backwater Bayou"),
-    Galatea("Galatea"),
+    MoongladeMarsh("Moonglade Marsh"),
     LotusAtoll("Lotus Atoll"),
+    TorrhusCanyon("Torrhus Canyon"),
+    Safari("Safari"),
     Unknown("(Unknown)");
-}
\ No newline at end of file
+}
Update JDK version from openjdk21 to openjdk25 (#136)Xmarmalade · 2026-08-10 · 6132ac9
查看变更
jitpack.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/jitpack.yml b/jitpack.yml
index a202792..aaa5890 100644
--- a/jitpack.yml
+++ b/jitpack.yml
@@ -1,2 +1,2 @@
 jdk:
-  - openjdk21
\ No newline at end of file
+  - openjdk25
Fixed golden apple not being handled correctly in StartsWithHandlerodtheking · 2026-08-09 · 991354e
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
index 2f97a23..e8f9893 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
@@ -9,6 +9,7 @@ import net.minecraft.client.gui.screens.inventory.ContainerScreen
 import net.minecraft.core.component.DataComponents
 import net.minecraft.core.registries.BuiltInRegistries
 import net.minecraft.world.item.ItemStack
+import net.minecraft.world.item.Items
 
 class StartsWithHandler(private val letter: String): TerminalHandler(TerminalTypes.STARTS_WITH) {
 
@@ -44,6 +45,6 @@ class StartsWithHandler(private val letter: String): TerminalHandler(TerminalTyp
     override fun renderSlot(slotIndex: Int): Pair<Color, String?> = TerminalSolver.startsWithColor to null
 
     companion object {
-        private val enchantOverrides = BuiltInRegistries.ITEM.filter { it.components().has(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) }
+        private val enchantOverrides = BuiltInRegistries.ITEM.filter { it.components().has(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) } + Items.GOLDEN_APPLE
     }
 }
Add torrhus canyon fairy souls to quiz answers (#135)
* Update fairy soul amount * Add intermediate color to secret spawn timer
Aneby · 2026-08-08 · a05c28d
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt | 6 +++++-
 src/main/resources/assets/odin/puzzles/quizAnswers.json             | 2 +-
 2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index e6c3967..2ff7cf4 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -88,7 +88,11 @@ object TickTimers : Module(
         else if (DungeonUtils.openRoomCount != 0 && !DungeonUtils.inBoss) {
             // Color change at < 10 because that's when items are picked up.
             val time = 20 - secretsCounter % 20
-            val color = if (time < 10) "§a" else "§c"
+            val color = when {
+                time < 5 -> "§a"
+                time < 10 -> "§6"
+                else -> "§c"
+            }
             textDim(formatTimer(time, 20, "§7Secret:", overrideColor = color), 0, 0, Colors.MINECRAFT_DARK_RED)
         }
         else 0 to 0
diff --git a/src/main/resources/assets/odin/puzzles/quizAnswers.json b/src/main/resources/assets/odin/puzzles/quizAnswers.json
index 403d76d..d51f974 100644
--- a/src/main/resources/assets/odin/puzzles/quizAnswers.json
+++ b/src/main/resources/assets/odin/puzzles/quizAnswers.json
@@ -10,7 +10,7 @@
   "What is the status of Scarf?": ["Apprentice Necromancer"],
   "What is the name of the lady of the Nether?": ["Elle"],
   "What is the status of Bonzo?": ["New Necromancer"],
-  "How many total Fairy Souls are there?": ["273 Fairy Souls"],
+  "How many total Fairy Souls are there?": ["289 Fairy Souls"],
   "How many Fairy Souls are there in Backwater Bayou?": ["5 Fairy Souls"],
   "What is the status of Thorn?": ["Shaman Necromancer"],
   "How many Fairy Souls are there in Spider's Den?": ["19 Fairy Souls"],
mixin cleanupodtheking · 2026-08-03 · 5736841
查看变更
src/main/java/com/odtheking/mixin/mixins/AvatarRendererMixin.java       |  8 ++++----
 src/main/java/com/odtheking/mixin/mixins/ChatScreenMixin.java           |  4 ++--
 src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java |  4 ++--
 src/main/java/com/odtheking/mixin/mixins/ConnectionMixin.java           |  4 ++--
 src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java       |  2 +-
 src/main/java/com/odtheking/mixin/mixins/FontMixin.java                 | 16 ++++++++--------
 src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java  |  4 ++--
 src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt  |  2 +-
 9 files changed, 23 insertions(+), 23 deletions(-)
diff --git a/src/main/java/com/odtheking/mixin/mixins/AvatarRendererMixin.java b/src/main/java/com/odtheking/mixin/mixins/AvatarRendererMixin.java
index 02f6cfc..558e5f4 100644
--- a/src/main/java/com/odtheking/mixin/mixins/AvatarRendererMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/AvatarRendererMixin.java
@@ -15,16 +15,16 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 public class AvatarRendererMixin {
 
     @Inject(method = "scale(Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;Lcom/mojang/blaze3d/vertex/PoseStack;)V", at = @At("HEAD"))
-    private void scale(AvatarRenderState avatarRenderState, PoseStack poseStack, CallbackInfo ci) {
-        PlayerSize.preRenderCallbackScaleHook(avatarRenderState, poseStack);
+    private void scale(AvatarRenderState state, PoseStack poseStack, CallbackInfo ci) {
+        PlayerSize.preRenderCallbackScaleHook(state, poseStack);
     }
 
     @Inject(
             method = "extractRenderState(Lnet/minecraft/world/entity/Avatar;Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;F)V",
             at = @At("HEAD")
     )
-    private void extractRenderState(Avatar avatar, AvatarRenderState avatarRenderState, float f, CallbackInfo ci) {
-        if (!(avatar instanceof AbstractClientPlayer clientAvatarEntity)) return;
+    private void extractRenderState(Avatar entity, AvatarRenderState avatarRenderState, float f, CallbackInfo ci) {
+        if (!(entity instanceof AbstractClientPlayer clientAvatarEntity)) return;
         avatarRenderState.setData(PlayerSize.getGAME_PROFILE_KEY(), clientAvatarEntity.getGameProfile());
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/ChatScreenMixin.java b/src/main/java/com/odtheking/mixin/mixins/ChatScreenMixin.java
index 61e9a5f..8f476e6 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ChatScreenMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ChatScreenMixin.java
@@ -11,7 +11,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 public class ChatScreenMixin {
 
     @Inject(method = "handleChatInput", at = @At("HEAD"), cancellable = true)
-    private void onSendMessage(String message, boolean addToHistory, CallbackInfo ci) {
-        if (new MessageSentEvent(message).postAndCatch()) ci.cancel();
+    private void onSendMessage(String msg, boolean addToRecent, CallbackInfo ci) {
+        if (new MessageSentEvent(msg).postAndCatch()) ci.cancel();
     }
 }
diff --git a/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java b/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
index ba2b5f4..1acb34a 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ClientPacketListenerMixin.java
@@ -26,9 +26,9 @@ public class ClientPacketListenerMixin {
     }
 
     @Inject(method = "handleContainerSetSlot", at = @At("TAIL"))
-    private void onSetSlot(ClientboundContainerSetSlotPacket clientboundContainerSetSlotPacket, CallbackInfo ci) {
+    private void onSetSlot(ClientboundContainerSetSlotPacket packet, CallbackInfo ci) {
         if (getMc().screen instanceof AbstractContainerScreen<?> container)
-            new GuiEvent.SlotUpdate(getMc().screen, clientboundContainerSetSlotPacket, container.getMenu()).postAndCatch();
+            new GuiEvent.SlotUpdate(getMc().screen, packet, container.getMenu()).postAndCatch();
     }
 
     @WrapOperation(method = "handleBundlePacket", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/protocol/Packet;handle(Lnet/minecraft/network/PacketListener;)V"))
diff --git a/src/main/java/com/odtheking/mixin/mixins/ConnectionMixin.java b/src/main/java/com/odtheking/mixin/mixins/ConnectionMixin.java
index 6879ba5..6384626 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ConnectionMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ConnectionMixin.java
@@ -16,13 +16,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 public abstract class ConnectionMixin {
 
     @Inject(method = "channelRead0(Lio/netty/channel/ChannelHandlerContext;Lnet/minecraft/network/protocol/Packet;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;genericsFtw(Lnet/minecraft/network/protocol/Packet;Lnet/minecraft/network/PacketListener;)V"), cancellable = true)
-    private void channelRead0(ChannelHandlerContext channelHandlerContext, Packet<?> packet, CallbackInfo ci) {
+    private void channelRead0(ChannelHandlerContext ctx, Packet<?> packet, CallbackInfo ci) {
         if (packet instanceof ClientboundPingPacket pingPacket && pingPacket.getId() != 0) TickEvent.Server.INSTANCE.postAndCatch();
         if (new PacketEvent.Receive(packet).postAndCatch()) ci.cancel();
     }
 
     @Inject(method = "sendPacket", at = @At("HEAD"), cancellable = true)
-    private void sendImmediately(Packet<?> packet, ChannelFutureListener channelFutureListener, boolean flush, CallbackInfo ci) {
+    private void sendImmediately(Packet<?> packet, ChannelFutureListener listener, boolean flush, CallbackInfo ci) {
         if (new PacketEvent.Send(packet).postAndCatch()) ci.cancel();
     }
 }
\ No newline at end of file
diff --git a/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java b/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java
index 07a7167..74ca475 100644
--- a/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/EntityRendererMixin.java
@@ -16,7 +16,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 public abstract class EntityRendererMixin<T extends Entity> {
 
     @Inject(method = "shouldRender", at = @At("HEAD"), cancellable = true)
-    private void onRender(T entity, Frustum frustum, double d, double e, double f, CallbackInfoReturnable<Boolean> cir) {
+    private void onRender(T entity, Frustum culler, double camX, double camY, double camZ, CallbackInfoReturnable<Boolean> cir) {
         if (!HidePlayers.shouldRenderPlayer(entity)) cir.setReturnValue(false);
 
         if (RenderOptimizer.hideEntityDeathAnimation() && entity instanceof LivingEntity livingEntity && livingEntity.isDeadOrDying())
diff --git a/src/main/java/com/odtheking/mixin/mixins/FontMixin.java b/src/main/java/com/odtheking/mixin/mixins/FontMixin.java
index bc7ff29..bbbcc0e 100644
--- a/src/main/java/com/odtheking/mixin/mixins/FontMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/FontMixin.java
@@ -12,25 +12,25 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable;
 @Mixin(Font.class)
 public class FontMixin {
 
-    @ModifyVariable(method = "prepareText(Ljava/lang/String;FFIZI)Lnet/minecraft/client/gui/Font$PreparedText;", at = @At("HEAD"), argsOnly = true)
+    @ModifyVariable(method = "prepareText(Ljava/lang/String;FFIZI)Lnet/minecraft/client/gui/Font$PreparedText;", at = @At("HEAD"), argsOnly = true, name = "text")
     private String onPrepareTextString(String text) {
         if (!CustomNameReplacer.isEnabled()) return text;
         return CustomNameReplacer.replaceStringIfNeeded(text);
     }
 
-    @ModifyVariable(method = "prepareText(Lnet/minecraft/util/FormattedCharSequence;FFIZZI)Lnet/minecraft/client/gui/Font$PreparedText;", at = @At("HEAD"), argsOnly = true)
+    @ModifyVariable(method = "prepareText(Lnet/minecraft/util/FormattedCharSequence;FFIZZI)Lnet/minecraft/client/gui/Font$PreparedText;", at = @At("HEAD"), argsOnly = true, name = "text")
     private FormattedCharSequence onPrepareTextSequence(FormattedCharSequence text) {
         if (!CustomNameReplacer.isEnabled()) return text;
         return CustomNameReplacer.replaceSequenceIfNeeded(text);
     }
 
-    @ModifyVariable(method = "width(Ljava/lang/String;)I", at = @At("HEAD"), argsOnly = true)
-    private String onWidthString(String text) {
-        if (!CustomNameReplacer.isEnabled()) return text;
-        return CustomNameReplacer.replaceStringIfNeeded(text);
+    @ModifyVariable(method = "width(Ljava/lang/String;)I", at = @At("HEAD"), argsOnly = true, name = "str")
+    private String onWidthString(String str) {
+        if (!CustomNameReplacer.isEnabled()) return str;
+        return CustomNameReplacer.replaceStringIfNeeded(str);
     }
 
-    @ModifyVariable(method = "width(Lnet/minecraft/network/chat/FormattedText;)I", at = @At("HEAD"), argsOnly = true)
+    @ModifyVariable(method = "width(Lnet/minecraft/network/chat/FormattedText;)I", at = @At("HEAD"), argsOnly = true, name = "text")
     private FormattedText onWidthFormattedText(FormattedText text) {
         if (!CustomNameReplacer.isEnabled()) return text;
         if (text instanceof Component component) {
@@ -40,7 +40,7 @@ public class FontMixin {
         return text;
     }
 
-    @ModifyVariable(method = "width(Lnet/minecraft/util/FormattedCharSequence;)I", at = @At("HEAD"), argsOnly = true)
+    @ModifyVariable(method = "width(Lnet/minecraft/util/FormattedCharSequence;)I", at = @At("HEAD"), argsOnly = true, name = "text")
     private FormattedCharSequence onWidthSequence(FormattedCharSequence text) {
         if (!CustomNameReplacer.isEnabled()) return text;
         return CustomNameReplacer.replaceSequenceIfNeeded(text);
diff --git a/src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java b/src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java
index 2dc9c89..6a7515a 100644
--- a/src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java
@@ -13,9 +13,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
 @Mixin(MultiPlayerGameMode.class)
 public class MultiPlayerGameModeMixin {
+
     @Inject(method = "useItemOn(Lnet/minecraft/client/player/LocalPlayer;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;", at = @At("RETURN"))
     void useItemOnReturn(LocalPlayer player, InteractionHand hand, BlockHitResult blockHit, CallbackInfoReturnable<InteractionResult> cir) {
-        var returnValue = cir.getReturnValue();
-        new UseItemOnPostEvent(hand, blockHit, returnValue).postAndCatch();
+        new UseItemOnPostEvent(hand, blockHit, cir.getReturnValue()).postAndCatch();
     }
 }
diff --git a/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java b/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java
index 517fd64..d5be233 100644
--- a/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java
+++ b/src/main/java/com/odtheking/mixin/mixins/ScreenEffectRendererMixin.java
@@ -14,7 +14,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
 public class ScreenEffectRendererMixin {
 
     @Inject(method = "renderFire", at = @At("HEAD"), cancellable = true)
-    private static void onRenderFireOverlay(PoseStack poseStack, MultiBufferSource multiBufferSource, TextureAtlasSprite textureAtlasSprite, CallbackInfo ci) {
+    private static void onRenderFireOverlay(PoseStack poseStack, MultiBufferSource bufferSource, TextureAtlasSprite sprite, CallbackInfo ci) {
         if (RenderOptimizer.shouldDisableFireOverlay()) ci.cancel();
     }
 }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
index d5147f0..4d6ba1f 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
@@ -68,7 +68,7 @@ object ActionBarListener {
 
             val vitalityMatch = VITALITY_REGEX.find(msg)
             isVitalityShown = vitalityMatch != null
-            vitalityMatch?.destructured?.let { (cVitality, mVitality, _) ->
+            vitalityMatch?.destructured?.let { (cVitality, mVitality) ->
                 currentVitality = cVitality.replace(",", "").toDoubleOrNull()?.toInt() ?: currentVitality
                 maxVitality = mVitality.replace(",", "").toDoubleOrNull()?.toInt() ?: maxVitality
             }
Fixed vitality action bar schitzoodtheking · 2026-08-02 · 200c1a2
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt | 5 ++++-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt     | 4 ++--
 2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
index 869d86e..c1aefad 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
@@ -33,6 +33,7 @@ object PlayerDisplay : Module(
     private val hideMana by BooleanSetting("Hide Mana", true, desc = "Hides the mana bar.").withDependency { hideActionBar }
     private val hideOverflow by BooleanSetting("Hide Overflow Mana", true, desc = "Hides the overflow mana bar.").withDependency { hideActionBar }
     private val hideDefense by BooleanSetting("Hide Defense", true, desc = "Hides the defense bar.").withDependency { hideActionBar }
+    private val hideVitality by BooleanSetting("Hide Vitality", true, desc = "Hides the vitality bar.").withDependency { hideActionBar }
     private val overflow by DropdownSetting("Overflow Mana")
     private val separateOverflow by BooleanSetting("Separate Overflow Mana", true, desc = "Separates the overflow mana from the mana bar.").withDependency { overflow }
     private val hideZeroSF by BooleanSetting("Hide 0 Overflow", true, desc = "Hides the overflow mana when it's 0.").withDependency { overflow && separateOverflow }
@@ -125,7 +126,8 @@ object PlayerDisplay : Module(
     private val HEALTH_REGEX = Regex("[\\d|,]+/[\\d|,]+\\uE010")
     private val MANA_REGEX = Regex("[\\d|,]+/[\\d|,]+\\uE003( Mana)?")
     private val OVERFLOW_MANA_REGEX = Regex("§?[\\d|,]+\\uE017")
-    private val DEFENSE_REGEX = Regex("[\\d|,]+§a\\uE008 Defense")
+    private val DEFENSE_REGEX = Regex("[\\d|,]+(§.)?\\uE008( Defense)?")
+    private val VITALITY_REGEX = Regex("[\\d.,]+/[\\d.,]+(§.)?\\uE028")
 
     @JvmStatic
     fun modifyText(text: Component): Component {
@@ -135,6 +137,7 @@ object PlayerDisplay : Module(
         toReturn = if (hideMana) toReturn.replace(MANA_REGEX, "") else toReturn
         toReturn = if (hideOverflow) toReturn.replace(OVERFLOW_MANA_REGEX, "") else toReturn
         toReturn = if (hideDefense) toReturn.replace(DEFENSE_REGEX, "") else toReturn
+        toReturn = if (hideVitality) toReturn.replace(VITALITY_REGEX, "") else toReturn
         return Component.literal(toReturn.trim())
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
index 73b4415..d5147f0 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
@@ -13,8 +13,8 @@ object ActionBarListener {
     private val HEALTH_REGEX = Regex("([\\d|,]+)/([\\d|,]+)\\uE010")
     private val MANA_REGEX = Regex("([\\d|,]+)/([\\d|,]+)\\uE003")
     private val OVERFLOW_MANA_REGEX = Regex("([\\d|,]+)\\uE017")
-    private val DEFENSE_REGEX = Regex("([\\d|,]+)\\uE008 Defense")
-    private val VITALITY_REGEX = Regex("([\\d.,]+)/([\\d.,]+)♨( Vitality)?")
+    private val DEFENSE_REGEX = Regex("([\\d|,]+)\\uE008( Defense)?")
+    private val VITALITY_REGEX = Regex("([\\d.,]+)/([\\d.,]+)\\uE028")
 
     var currentHealth: Int = 0
         private set
Bump versionodtheking · 2026-08-02 · 015a581
查看变更
gradle.properties                                                           | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/gradle.properties b/gradle.properties
index 19ce37b..7485bec 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -10,7 +10,7 @@ loom_version=1.16-SNAPSHOT
 mod_id=odin
 mod_name=Odin
 mod_description=A Hypixel Skyblock QoL mod
-mod_version=0.2.3
+mod_version=0.3.0
 maven_group=com.odtheking
 archives_base_name=odin-fabric
 # Mod Dependencies
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index 3f0113c..c5aa78d 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -179,7 +179,7 @@ private fun GuiGraphicsExtractor.renderPlayers() {
     val (selfX, selfZ) = DungeonScan.playerRenderPosition(mc.player, IVec2(0, 0))
     pose().translate(selfX, selfZ)
     pose().rotate(Math.toRadians(180.0 + (mc.player?.yRot ?: 0f)).toFloat())
-    DungeonUtils.currentDungeonPlayer
+
     if (DungeonMap.playerHead) {
         mc.player?.skin?.let { skin ->
             fill(-5, -5, 5, 5, DungeonUtils.currentDungeonPlayer.clazz.color.rgba)
fix clickgui chevron orientation (#134)Cezqr · 2026-08-01 · f85db3b
查看变更
src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/DropdownSetting.kt | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/DropdownSetting.kt b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/DropdownSetting.kt
index f107c62..84899f4 100644
--- a/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/DropdownSetting.kt
+++ b/src/main/kotlin/com/odtheking/odin/clickgui/settings/impl/DropdownSetting.kt
@@ -38,8 +38,8 @@ class DropdownSetting(
         val offset = (imageSize - 24f) / 2f
 
         NVGRenderer.push()
-        NVGRenderer.translate(x + width - 18f, y + height / 2f - 4f)
-        NVGRenderer.rotate(toggleAnimation.get(0f, Math.PI.toFloat() / 2f, enabled))
+        NVGRenderer.translate(x + width - 22f, y + height / 2f)
+        NVGRenderer.rotate(toggleAnimation.get(Math.PI.toFloat() / 2f, 0f, enabled))
         NVGRenderer.translate(-(12f + offset), -(12f + offset))
         NVGRenderer.image(ClickGUI.chevronImage, 0f, 0f, imageSize, imageSize)
         NVGRenderer.pop()
Added Account for Server Lag and Ignore Melody to First Click Protection (now all in a dropdown together) (#112)
* Added Account for Server Lag and Ignore Melody to First Click Protection (now all in a dropdown together) * shorten name + slight fix old one was too long * slight cleanup * maybe git will actually respect me for once i hate git * slight cleanup part 2 (im gonna go insane at some point) * default to off * rebase onto main I hope + add toggle for notifying when firstclickprot blocks a click + fix notify not working on custom term gui * disable servertickprot in singleplayer * small fix to servertickprot * option to disable fc for termsim + add ticks opened to term debug * Removed pointless settings --------- Co-authored-by: Snowpls1 <snowpls1@github.com> Co-authored-by: odtheking <odtheking1@gmail.com>
SnowPls · 2026-07-30 · 70f7edb
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt                             |  2 ++
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt                                | 20 ++++++++++++--------
 src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt                         |  8 ++++----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt | 11 ++++++++++-
 4 files changed, 28 insertions(+), 13 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt
index be06f5e..cf3a28e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSimulator.kt
@@ -1,5 +1,6 @@
 package com.odtheking.odin.features.impl.boss
 
+import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.features.impl.boss.termsim.*
@@ -12,6 +13,7 @@ object TerminalSimulator : Module(
     description = "Simulates a floor 7 terminal from phase 3."
 ) {
     private val ping by NumberSetting("Ping", 0, 0, 500, 50, desc = "Ping of the terminal.")
+    val disableFirstClickProtection by BooleanSetting("First Click Protection", false, desc = "")
 
     val termSimPBs = PersonalBest(this, "TermsimPBs")
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index 8cb6dfe..7924d2e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -4,6 +4,7 @@ import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.TerminalEvent
+import com.odtheking.odin.events.TickEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color.Companion.darker
@@ -27,7 +28,6 @@ object TerminalSolver : Module(
     val customTermSize by NumberSetting("Term Size", 2f, 1f, 3f, 0.1f, desc = "The size of the custom terminal GUI.").withDependency { renderType == 2 }
     val roundness by NumberSetting("Roundness", 5, 0f, 15f, 1f, desc = "The roundness of the custom terminal gui.").withDependency { renderType == 2 }
     val gap by NumberSetting("Slot gap", 2, 0, 8, 1, desc = "The gap between the slots in the custom terminal gui.").withDependency { renderType == 2 }
-
     private val solverSettings by DropdownSetting("Solver Functionality")
     private val cancelToolTip by BooleanSetting("Stop Tooltips", true, desc = "Stops rendering tooltips in terminals.").withDependency { (renderType == 0 || renderType == 1) && solverSettings }
     private val middleClickGUI by BooleanSetting("Middle Click GUI", true, desc = "Replaces right click with middle click in terminals.").withDependency { (renderType == 0 || renderType == 1) && solverSettings }
@@ -35,11 +35,13 @@ object TerminalSolver : Module(
     private val cancelMelodySolver by BooleanSetting("Stop Melody Solver", false, desc = "Stops rendering the melody solver.").withDependency { solverSettings }
     val melodyTermSize by NumberSetting("Melody Size", 1.5f, 1f, 3f, 0.1f, desc = "The size of the melody terminal GUI.").withDependency { !cancelMelodySolver && solverSettings && renderType == 2 }
     val showNumbers by BooleanSetting("Show Numbers", true, desc = "Shows numbers in the order terminal.").withDependency { solverSettings }
-    val firstClickProt by NumberSetting("First Click Protection", 500, 350, 800, 10, unit = "ms", desc = "The amount of time after opening a terminal where clicks are blocked to prevent bans (recommended value is 500 minus your ping).").withDependency { solverSettings }
+    private val firstClickProtSettings by DropdownSetting("First Click Protect Dropdown")
+    val firstClickProt by NumberSetting("First Click Protection", 500, 350, 800, 10, unit = "ms", desc = "The amount of time after opening a terminal where clicks are blocked to prevent bans (recommended value is 500 minus your ping).").withDependency { firstClickProtSettings }
+    val shouldFirstClickProtWithTicks by BooleanSetting("Account For Server Lag",  false, desc = "Prevents bans from clicking when the server lags after opening the terminal (disabled in singleplayer").withDependency { firstClickProtSettings }
+    val firstClickProtTicks by NumberSetting("Lag Protection Ticks", 8, 7, 16, unit = "ticks", desc = "Each tick = 50ms (recommended value is 8)").withDependency { shouldFirstClickProtWithTicks && firstClickProtSettings }
     val hideClicked by BooleanSetting("Hide Clicked", false, desc = "Visually hides your first click before a gui updates instantly to improve perceived response time. Does not affect actual click time.").withDependency { solverSettings }
     val terminalReloadThreshold by NumberSetting("Resolve timeout", 600, 300, 1000, 10, unit = "ms", desc = "The amount of time before the terminal reloads after a click wasn't registered while using hide clicked.").withDependency { hideClicked && solverSettings }
     private val debug by BooleanSetting("Debug", false, desc = "Shows debug terminals.").withDependency { solverSettings }
-
     private val showColors by DropdownSetting("Color Settings")
     val backgroundColor by ColorSetting("Background", Colors.gray26, true, desc = "Background color of the terminal solver.").withDependency { showColors }
 
@@ -67,15 +69,16 @@ object TerminalSolver : Module(
     private val renderMelody get() = !(cancelMelodySolver && TerminalUtils.currentTerm?.type == TerminalTypes.MELODY)
 
     init {
+        on<TickEvent.Server> {
+            TerminalUtils.currentTerm?.ticksOpened++
+        }
+
         on<GuiEvent.SlotClick> {
             val term = TerminalUtils.currentTerm ?: return@on
 
             if (blockIncorrectClicks && !term.canClick(slotId, button)) return@on cancel()
 
-            if (term.shouldProtect()) {
-                modMessage("§cBlocked first click ${System.currentTimeMillis() - term.timeOpened}ms after opening.")
-                return@on cancel()
-            }
+            if (term.shouldProtect()) return@on cancel()
 
             if (middleClickGUI) {
                 term.click(slotId, if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button, hideClicked && !term.isClicked)
@@ -128,6 +131,7 @@ object TerminalSolver : Module(
                 "§7Window Name: §f${mc.screen?.title?.string}",
                 "§7Container ID: §f${menu.containerId}",
                 "§7Time Open: §f${System.currentTimeMillis() - term.timeOpened}ms",
+                "§7Ticks Open: §f${term.ticksOpened}",
                 "§7Is Clicked: §f${term.isClicked}",
                 "§7Window Count: §f${term.windowCount}",
                 "§7Solution: §f${term.solution.joinToString(", ")}",
@@ -151,4 +155,4 @@ object TerminalSolver : Module(
             pose().popMatrix()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
index f2ba388..4be8782 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/termGUI/CustomTermGui.kt
@@ -8,6 +8,7 @@ import com.odtheking.odin.features.impl.boss.TerminalSolver.hideClicked
 import com.odtheking.odin.features.impl.boss.TerminalSolver.renderDebug
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.render.roundedFill
 import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
@@ -108,10 +109,9 @@ abstract class TermGui {
         TerminalUtils.currentTerm?.let { term ->
             val screen = mc.screen ?: return@let
             val btn = if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button
-            if (System.currentTimeMillis() - term.timeOpened >= TerminalSolver.firstClickProt &&
-                !GuiEvent.CustomTermGuiClick(screen, slotIndex, btn).postAndCatch() &&
-                term.canClick(slotIndex, btn)
-            ) term.click(slotIndex, btn, hideClicked && !term.isClicked)
+            if (term.shouldProtect()) return@let
+            if (!GuiEvent.CustomTermGuiClick(screen, slotIndex, btn).postAndCatch() && term.canClick(slotIndex, btn))
+                term.click(slotIndex, btn, hideClicked && !term.isClicked)
         }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
index bd378ea..cbd16ac 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
@@ -5,10 +5,15 @@ import com.google.common.primitives.SignedBytes
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.PacketEvent
+import com.odtheking.odin.features.impl.boss.TerminalSimulator
 import com.odtheking.odin.features.impl.boss.TerminalSolver.firstClickProt
+import com.odtheking.odin.features.impl.boss.TerminalSolver.firstClickProtTicks
+import com.odtheking.odin.features.impl.boss.TerminalSolver.shouldFirstClickProtWithTicks
 import com.odtheking.odin.features.impl.boss.termsim.TermSimGUI
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.clickSlot
+import com.odtheking.odin.utils.skyblock.Island
+import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap
 import net.minecraft.client.gui.screens.inventory.ContainerScreen
@@ -25,6 +30,7 @@ abstract class TerminalHandler(val type: TerminalTypes) {
     val timeOpened = System.currentTimeMillis()
     var isClicked = false
     var windowCount = 0
+    var ticksOpened = -1
 
     open fun updateSlot(event: GuiEvent.SlotUpdate) {
         if (event.packet.slot !in 0 until type.windowSize) return
@@ -77,5 +83,8 @@ abstract class TerminalHandler(val type: TerminalTypes) {
 
     open fun canClick(slotIndex: Int, button: Int): Boolean = slotIndex in solution
 
-    fun shouldProtect(): Boolean = System.currentTimeMillis() - timeOpened < firstClickProt
+    fun shouldProtect(): Boolean =
+            !(TerminalSimulator.disableFirstClickProtection && mc.screen is TermSimGUI)
+            && (System.currentTimeMillis() - timeOpened < firstClickProt ||
+            (!LocationUtils.isCurrentArea(Island.SinglePlayer) && shouldFirstClickProtWithTicks && ticksOpened < firstClickProtTicks))
 }
\ No newline at end of file
Detailed TPS statistics (#124)
* Detailed TPS statistics * Slightly cleaner --------- Co-authored-by: odtheking <odtheking1@gmail.com>
Paweł Nowak · 2026-07-30 · 243f98d
查看变更
src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt                |  4 ++--
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt |  4 ++--
 src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt                   | 25 +++++++++++++++++++++++--
 3 files changed, 27 insertions(+), 6 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
index 2066799..1a60d46 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/MainCommand.kt
@@ -24,7 +24,7 @@ val mainCommand = Commodore("odin", "od") {
     }
 
     literal("tps").runs {
-        modMessage("§aTPS: §f${ServerUtils.averageTps}")
+        modMessage(ServerUtils.getTpsString())
     }
 
     literal("ping").runs {
@@ -119,4 +119,4 @@ private enum class KuudraTier(private val test: String) {
     T1("normal"), T2("hot"), T3("burning"), T4("fiery"), T5("infernal");
 
     fun instance() = "kuudra_${test}"
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
index 66dfed8..acef565 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
@@ -135,7 +135,7 @@ object ChatCommands : Module(
             "dice" -> if (dice) channelMessage((1..6).random(), name, channel)
             "racism" -> if (racism) channelMessage("$name is ${Random.nextInt(1, 101)}% racist. Racism is not allowed!", name, channel)
             "ping" -> if (ping) channelMessage("Current Ping: ${ServerUtils.currentPing}ms", name, channel)
-            "tps" -> if (tps) channelMessage("Current TPS: ${ServerUtils.averageTps.toFixed(1)}", name, channel)
+            "tps" -> if (tps) channelMessage(ServerUtils.getTpsString(), name, channel)
             "fps" -> if (fps) channelMessage("Current FPS: ${mc.fps}", name, channel)
             "time" -> if (time) channelMessage("Current Time: ${ZonedDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z"))}", name, channel)
             "location" -> if (location) channelMessage("Current Location: ${LocationUtils.currentArea.displayName}", name, channel)
@@ -265,4 +265,4 @@ object ChatCommands : Module(
     private enum class ChatChannel {
         PARTY, GUILD, PRIVATE
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt
index 0753021..133160d 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/ServerUtils.kt
@@ -5,6 +5,7 @@ import com.odtheking.odin.events.core.onReceive
 import net.minecraft.network.protocol.game.ClientboundSetTimePacket
 import net.minecraft.network.protocol.ping.ClientboundPongResponsePacket
 import net.minecraft.util.Util
+import java.util.ArrayDeque
 import kotlin.math.min
 
 object ServerUtils {
@@ -18,10 +19,30 @@ object ServerUtils {
     var averagePing: Int = 0
         private set
 
+    private const val TPS_HISTORY = 20
+
+    private val tpsLog = FloatArray(TPS_HISTORY)
+    private var tpsIndex = 0
+    private var tpsCount = 0
+
+    private fun addTpsSample(tps: Float) {
+        tpsLog[tpsIndex] = tps
+        tpsIndex = (tpsIndex + 1) % TPS_HISTORY
+        if (tpsCount < TPS_HISTORY) tpsCount++
+    }
+
+    fun getTpsString(): String {
+        if (tpsLog.isEmpty()) return "Current: ${averageTps.toFixed(1)}"
+
+        return "Current: ${averageTps.toFixed(1)} (max/min/avg) ${tpsLog.max().toFixed(1)}/${tpsLog.min().toFixed(1)}/${tpsLog.average().toFloat().toFixed(1)}"
+    }
+
     init {
         onReceive<ClientboundSetTimePacket> {
-            if (prevTime != 0L)
+            if (prevTime != 0L) {
                 averageTps = (20000f / (System.currentTimeMillis() - prevTime + 1)).coerceIn(0f, 20f)
+                addTpsSample(averageTps)
+            }
 
             prevTime = System.currentTimeMillis()
         }
@@ -46,4 +67,4 @@ object ServerUtils {
             averagePing = (total / sampleSize).toInt()
         }
     }
-}
\ No newline at end of file
+}
fixed waterboard solver breaking when shift clicking a lever (#131)
* fixed waterboard solver breaking when shift clicking a lever * fixed secret spawn timer showing in boss & changed color to signify pickup --------- Co-authored-by: CheaterV <kurdokoleno@proton.me>
cheater · 2026-07-30 · fa868b3
查看变更
src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java                  | 21 +++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                               |  4 ++++
 src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt                     | 14 +++++++++-----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt |  6 +++++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt   | 10 +++++++---
 src/main/resources/odin.mixins.json                                                     |  1 +
 6 files changed, 47 insertions(+), 9 deletions(-)
diff --git a/src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java b/src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java
new file mode 100644
index 0000000..2dc9c89
--- /dev/null
+++ b/src/main/java/com/odtheking/mixin/mixins/MultiPlayerGameModeMixin.java
@@ -0,0 +1,21 @@
+package com.odtheking.mixin.mixins;
+
+import com.odtheking.odin.events.UseItemOnPostEvent;
+import net.minecraft.client.multiplayer.MultiPlayerGameMode;
+import net.minecraft.client.player.LocalPlayer;
+import net.minecraft.world.InteractionHand;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.phys.BlockHitResult;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(MultiPlayerGameMode.class)
+public class MultiPlayerGameModeMixin {
+    @Inject(method = "useItemOn(Lnet/minecraft/client/player/LocalPlayer;Lnet/minecraft/world/InteractionHand;Lnet/minecraft/world/phys/BlockHitResult;)Lnet/minecraft/world/InteractionResult;", at = @At("RETURN"))
+    void useItemOnReturn(LocalPlayer player, InteractionHand hand, BlockHitResult blockHit, CallbackInfoReturnable<InteractionResult> cir) {
+        var returnValue = cir.getReturnValue();
+        new UseItemOnPostEvent(hand, blockHit, returnValue).postAndCatch();
+    }
+}
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index 6568ae7..1ba4f4f 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -16,9 +16,12 @@ import net.minecraft.network.chat.Component
 import net.minecraft.network.protocol.Packet
 import net.minecraft.network.protocol.game.ClientboundSoundPacket
 import net.minecraft.world.BossEvent
+import net.minecraft.world.InteractionHand
+import net.minecraft.world.InteractionResult
 import net.minecraft.world.entity.Entity
 import net.minecraft.world.entity.item.ItemEntity
 import net.minecraft.world.level.block.state.BlockState
+import net.minecraft.world.phys.BlockHitResult
 import net.minecraft.world.phys.Vec3
 
 class InputEvent(val key: InputConstants.Key) : CancellableEvent() // better mixin is prob ideal no need for cancellable
@@ -27,6 +30,7 @@ class BlockUpdateEvent(val pos: BlockPos, val old: BlockState, val updated: Bloc
 
 class BlockInteractEvent(val pos: BlockPos) : CancellableEvent()
 class EntityInteractEvent(val pos: Vec3, val entity: Entity) : CancellableEvent()
+class UseItemOnPostEvent(val hand: InteractionHand, val hitResult: BlockHitResult, val interactionResult: InteractionResult) : Event
 
 class ChatPacketEvent(val value: String, val component: Component) : Event // mixin instead of packet (still needs to run before vanilla processing for hideMessage()
 class MessageSentEvent(val message: String) : CancellableEvent()
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 962d9e5..e6c3967 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -12,7 +12,6 @@ import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.MORT_REGEX
-import com.odtheking.odin.utils.skyblock.SplitsManager
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.toFixed
 import net.minecraft.network.protocol.game.ClientboundSetTimePacket
@@ -85,8 +84,13 @@ object TickTimers : Module(
     private var secretsCounter = 0
 
     private val secretsHud by HUD("Secrets Hud", "Displays a timer for secret spawn ticks.") {
-        if (it) textDim(formatTimer(15, 20, "§7Secret:"), 0, 0, Colors.MINECRAFT_DARK_RED)
-        else if (DungeonUtils.openRoomCount != 0) textDim(formatTimer(20 - secretsCounter % 20, 20, "§7Secret:"), 0, 0, Colors.MINECRAFT_DARK_RED)
+        if (it) textDim(formatTimer(15, 20, "§7Secret:", overrideColor = "§c"), 0, 0, Colors.MINECRAFT_DARK_RED)
+        else if (DungeonUtils.openRoomCount != 0 && !DungeonUtils.inBoss) {
+            // Color change at < 10 because that's when items are picked up.
+            val time = 20 - secretsCounter % 20
+            val color = if (time < 10) "§a" else "§c"
+            textDim(formatTimer(time, 20, "§7Secret:", overrideColor = color), 0, 0, Colors.MINECRAFT_DARK_RED)
+        }
         else 0 to 0
     }
 
@@ -151,8 +155,8 @@ object TickTimers : Module(
         }
     }
 
-    private fun formatTimer(time: Int, max: Int, prefix: String): String {
-        val color = when {
+    private fun formatTimer(time: Int, max: Int, prefix: String, overrideColor: String? = null): String {
+        val color = overrideColor ?: when {
             time.toFloat() >= max * 0.66 -> "§a"
             time.toFloat() >= max * 0.33 -> "§6"
             else -> "§c"
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index 95e7669..08c7080 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -170,10 +170,14 @@ object PuzzleSolvers : Module(
 
         onSend<ServerboundUseItemOnPacket> {
             if (!DungeonUtils.inClear || this.hand == InteractionHand.OFF_HAND) return@onSend
-            if (waterSolver) WaterSolver.waterInteract(this)
             if (boulderSolver) BoulderSolver.playerInteract(this)
        }
 
+        on<UseItemOnPostEvent> {
+            if (!DungeonUtils.inClear || this.hand == InteractionHand.OFF_HAND) return@on
+            if (waterSolver) WaterSolver.waterInteract(this)
+        }
+
         on<RenderEvent.Extract> {
             if (!DungeonUtils.inClear) return@on
             if (iceFillSolver) IceFillSolver.onRenderWorld(this, iceFillColor)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
index 5b58e91..0710338 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
@@ -2,6 +2,7 @@ package com.odtheking.odin.features.impl.dungeon.puzzlesolvers
 
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.RenderEvent
+import com.odtheking.odin.events.UseItemOnPostEvent
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.JsonResourceLoader
 import com.odtheking.odin.utils.modMessage
@@ -11,7 +12,7 @@ import com.odtheking.odin.utils.render.drawTracer
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.toFixed
 import net.minecraft.core.BlockPos
-import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket
+import net.minecraft.world.InteractionResult
 import net.minecraft.world.level.block.Blocks
 import net.minecraft.world.phys.Vec3
 
@@ -97,8 +98,11 @@ object WaterSolver {
         }
     }
 
-    fun waterInteract(event: ServerboundUseItemOnPacket) {
-        if (solutions.isEmpty()) return
+    fun waterInteract(event: UseItemOnPostEvent) {
+        // Interaction result different from SUCCESS either doesn't send the click packet to the server or
+        // doesn't consume the action. Not consuming the action leads to UseItemOn being called with the OFF_HAND
+        // which Hypixel treats as a regular click, causing a double lever flick i.e. the gate doesn't open.
+        if (solutions.isEmpty() || event.interactionResult != InteractionResult.SUCCESS) return
         LeverBlock.entries.find { it.leverPos == event.hitResult.blockPos }?.let {
             if (it == LeverBlock.WATER && openedWaterTicks == -1) openedWaterTicks = tickCounter
             it.i++
diff --git a/src/main/resources/odin.mixins.json b/src/main/resources/odin.mixins.json
index 655e191..61537ba 100644
--- a/src/main/resources/odin.mixins.json
+++ b/src/main/resources/odin.mixins.json
@@ -22,6 +22,7 @@
     "mixins.LocalPlayerMixin",
     "mixins.MinecraftMixin",
     "mixins.MouseHandlerMixin",
+    "mixins.MultiPlayerGameModeMixin",
     "mixins.ScreenEffectRendererMixin",
     "mixins.ScreenMixin"
   ],
Added vitality support to PlayerDisplayodtheking · 2026-07-30 · fc63980
查看变更
src/main/kotlin/com/odtheking/odin/OdinMod.kt                                                 |  4 +---
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt                    | 33 ++++++++++++++++++++++-----------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/{SkyblockPlayer.kt => ActionBarListener.kt} | 35 ++++++++++++++++++++++-------------
 3 files changed, 45 insertions(+), 27 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/OdinMod.kt b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
index 3d2424d..36d41a4 100644
--- a/src/main/kotlin/com/odtheking/odin/OdinMod.kt
+++ b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
@@ -9,7 +9,6 @@ import com.odtheking.odin.features.impl.render.Shenanigans
 import com.odtheking.odin.utils.IrisCompatability
 import com.odtheking.odin.utils.ServerUtils
 import com.odtheking.odin.utils.handlers.TickTasks
-import com.odtheking.odin.utils.network.WebUtils.postData
 import com.odtheking.odin.utils.render.ItemStateRenderer
 import com.odtheking.odin.utils.render.RenderBatchManager
 import com.odtheking.odin.utils.render.RoundRectPIPRenderer
@@ -21,7 +20,6 @@ import com.odtheking.odin.utils.ui.rendering.NVGPIPRenderer
 import com.odtheking.odin.utils.ui.widget.CustomGUIImpl
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
 import net.fabricmc.api.ClientModInitializer
 import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
 import net.fabricmc.fabric.api.client.rendering.v1.PictureInPictureRendererRegistry
@@ -71,7 +69,7 @@ object OdinMod : ClientModInitializer {
 
         listOf(
             this, LocationUtils, TickTasks, KuudraUtils,
-            SkyblockPlayer, ServerUtils, EventDispatcher,
+            ActionBarListener, ServerUtils, EventDispatcher,
             DungeonListener, PartyUtils, TerminalUtils,
             DungeonUtils, SplitsManager, DungeonScan,
             IrisCompatability, RenderBatchManager,
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
index 386e377..869d86e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
@@ -8,7 +8,7 @@ import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.LocationUtils
-import com.odtheking.odin.utils.skyblock.SkyblockPlayer
+import com.odtheking.odin.utils.skyblock.ActionBarListener
 import net.minecraft.network.chat.Component
 import kotlin.math.abs
 
@@ -43,7 +43,7 @@ object PlayerDisplay : Module(
         val text = when {
             example -> 3000 to 4000
             !LocationUtils.isInSkyblock -> return@HUD 0 to 0
-            SkyblockPlayer.currentHealth != 0 && SkyblockPlayer.maxHealth != 0 -> SkyblockPlayer.currentHealth to SkyblockPlayer.maxHealth
+            ActionBarListener.currentHealth != 0 && ActionBarListener.maxHealth != 0 -> ActionBarListener.currentHealth to ActionBarListener.maxHealth
             else -> return@HUD 0 to 0
         }
         return@HUD textDim(generateText(text.first, text.second, "❤"), 0, 0, healthColor)
@@ -52,13 +52,13 @@ object PlayerDisplay : Module(
 
     private val manaHud by HUD("Mana HUD", "Displays the player's mana.") { example ->
         val text = when {
-            example -> generateText(2000, 20000, "✎") + (if (!separateOverflow) " ${generateText(SkyblockPlayer.overflowMana, "ʬ", hideZeroSF)}" else "")
+            example -> generateText(2000, 20000, "✎") + (if (!separateOverflow) " ${generateText(ActionBarListener.overflowMana, "ʬ", hideZeroSF)}" else "")
 
             !LocationUtils.isInSkyblock -> return@HUD 0 to 0
-            SkyblockPlayer.maxMana != 0 -> when {
-                SkyblockPlayer.currentMana == 0 && separateOverflow -> return@HUD 0 to 0
-                else -> generateText(SkyblockPlayer.currentMana, SkyblockPlayer.maxMana, "✎") +
-                        (if (!separateOverflow && overflowManaHud.enabled) " ${generateText(SkyblockPlayer.overflowMana, "ʬ", hideZeroSF)}" else "")
+            ActionBarListener.maxMana != 0 -> when {
+                ActionBarListener.currentMana == 0 && separateOverflow -> return@HUD 0 to 0
+                else -> generateText(ActionBarListener.currentMana, ActionBarListener.maxMana, "✎") +
+                        (if (!separateOverflow && overflowManaHud.enabled) " ${generateText(ActionBarListener.overflowMana, "ʬ", hideZeroSF)}" else "")
             }
 
             else -> return@HUD 0 to 0
@@ -71,7 +71,7 @@ object PlayerDisplay : Module(
         val text = when {
             example -> 333
             !LocationUtils.isInSkyblock -> return@HUD 0 to 0
-            separateOverflow -> SkyblockPlayer.overflowMana
+            separateOverflow -> ActionBarListener.overflowMana
             else -> return@HUD 0 to 0
         }
         return@HUD textDim(generateText(text, "ʬ", hideZeroSF), 0, 0, overflowManaColor)
@@ -82,18 +82,29 @@ object PlayerDisplay : Module(
         val text = when {
             example -> 1000
             !LocationUtils.isInSkyblock -> return@HUD 0 to 0
-            SkyblockPlayer.currentDefense != 0 -> SkyblockPlayer.currentDefense
+            ActionBarListener.currentDefense != 0 -> ActionBarListener.currentDefense
             else -> return@HUD 0 to 0
         }
         return@HUD textDim(generateText(text, "❈", true), 0, 0, defenseColor)
     }
     private val defenseColor by ColorSetting("Defense Color", Colors.MINECRAFT_GREEN, true, desc = "The color of the defense text.")
 
+    private val vitalityHud by HUD("Vitality HUD", "Displays the player's vitality.") { example ->
+        val text = when {
+            example -> 100 to 100
+            !LocationUtils.isInSkyblock -> return@HUD 0 to 0
+            ActionBarListener.isVitalityShown && ActionBarListener.maxVitality != 0 -> ActionBarListener.currentVitality to ActionBarListener.maxVitality
+            else -> return@HUD 0 to 0
+        }
+        return@HUD textDim(generateText(text.first, text.second, "♨"), 0, 0, vitalityColor)
+    }
+    private val vitalityColor by ColorSetting("Vitality Color", Colors.MINECRAFT_DARK_RED, true, "The color of the vitality text.")
+
     private val ehpHud by HUD("EHP HUD", "Displays the player's effective health (EHP).") { example ->
         val text = when {
             example -> 1000000
             !LocationUtils.isInSkyblock -> return@HUD 0 to 0
-            SkyblockPlayer.effectiveHP != 0 -> SkyblockPlayer.effectiveHP
+            ActionBarListener.effectiveHP != 0 -> ActionBarListener.effectiveHP
             else -> return@HUD 0 to 0
         }
         return@HUD textDim(generateText(text, "", true), 0, 0, ehpColor)
@@ -104,7 +115,7 @@ object PlayerDisplay : Module(
         val text = when {
             example -> 100
             !LocationUtils.isInSkyblock -> return@HUD 0 to 0
-            SkyblockPlayer.currentSpeed != 0 -> SkyblockPlayer.currentSpeed
+            ActionBarListener.currentSpeed != 0 -> ActionBarListener.currentSpeed
             else -> return@HUD 0 to 0
         }
         return@HUD textDim(generateText(text, "✦", true), 0, 0, speedColor)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
similarity index 73%
rename from src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt
rename to src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
index a90ad3d..73b4415 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/ActionBarListener.kt
@@ -9,33 +9,35 @@ import net.minecraft.network.protocol.game.ClientboundSystemChatPacket
 import net.minecraft.world.entity.ai.attributes.Attributes
 import kotlin.math.floor
 
-object SkyblockPlayer {
-    /*
-    in module there should be:
-    health display current/Max
-    health bar
-    defense display
-    mana display current/Max
-    mana bar
-    current speed
-    current ehp
-    current overflow mana
-     */
-
+object ActionBarListener {
     private val HEALTH_REGEX = Regex("([\\d|,]+)/([\\d|,]+)\\uE010")
     private val MANA_REGEX = Regex("([\\d|,]+)/([\\d|,]+)\\uE003")
     private val OVERFLOW_MANA_REGEX = Regex("([\\d|,]+)\\uE017")
     private val DEFENSE_REGEX = Regex("([\\d|,]+)\\uE008 Defense")
+    private val VITALITY_REGEX = Regex("([\\d.,]+)/([\\d.,]+)♨( Vitality)?")
 
     var currentHealth: Int = 0
         private set
     var maxHealth: Int = 0
+        private set
     var currentMana: Int = 0
+        private set
     var maxMana: Int = 0
+        private set
     var currentSpeed: Int = 0
+        private set
     var currentDefense: Int = 0
+        private set
     var overflowMana: Int = 0
+        private set
     var effectiveHP: Int = 0
+        private set
+    var currentVitality: Int = 0
+        private set
+    var maxVitality: Int = 0
+        private set
+    var isVitalityShown: Boolean = false
+        private set
 
     init {
         on<TickEvent.End> {
@@ -64,6 +66,13 @@ object SkyblockPlayer {
                 currentDefense = it.replace(",", "").toIntOrNull() ?: currentDefense
             }
 
+            val vitalityMatch = VITALITY_REGEX.find(msg)
+            isVitalityShown = vitalityMatch != null
+            vitalityMatch?.destructured?.let { (cVitality, mVitality, _) ->
+                currentVitality = cVitality.replace(",", "").toDoubleOrNull()?.toInt() ?: currentVitality
+                maxVitality = mVitality.replace(",", "").toDoubleOrNull()?.toInt() ?: maxVitality
+            }
+
             effectiveHP = (currentHealth * (1 + currentDefense / 100))
         }
     }
Cleaned up PositionalMessagesodtheking · 2026-07-30 · 53f4945
查看变更
src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt                            | 48 +++++++++++++++---------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt          | 44 +++++++++++++++++++++-----------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt |  2 +-
 3 files changed, 37 insertions(+), 57 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
index 3d5f284..f5b5fb4 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/PosMsgCommand.kt
@@ -32,11 +32,11 @@ val posMsgCommand = Commodore("posmsg") {
                 }
             }
 
-            runs { x: Double, y: Double, z: Double, delay: Long, distance: Double, color: String, message: GreedyString ->
+            runs { x: Double, y: Double, z: Double, delay: Int, distance: Double, color: String, message: GreedyString? ->
+                if (posMessageStrings.any { it.message == message?.string }) return@runs
                 val color = getColorFromString(color) ?: return@runs modMessage("Unknown color $color")
-                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, null, null, null, delay, distance, color, message.string
-                ).takeUnless { it in posMessageStrings } ?: return@runs modMessage("This message already exists!"))
-                modMessage("Message \"${message}\" added at $x, $y, $z, with ${delay}ms delay, triggered up to $distance blocks away.")
+                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, null, null, null, delay, distance, color, message?.string))
+                modMessage("Message \"${message}\" added at $x, $y, $z, with ${delay}t delay, triggered up to $distance blocks away.")
                 ModuleManager.saveConfigurations()
             }
         }
@@ -50,11 +50,11 @@ val posMsgCommand = Commodore("posmsg") {
                 }
             }
 
-            runs { x: Double, y: Double, z: Double, x2: Double, y2: Double, z2: Double, delay: Long, color: String, message: GreedyString ->
+            runs { x: Double, y: Double, z: Double, x2: Double, y2: Double, z2: Double, delay: Int, color: String, message: GreedyString? ->
+                if (posMessageStrings.any { it.message == message?.string }) return@runs
                 val color = getColorFromString(color) ?: return@runs modMessage("Unknown color $color")
-                posMessageStrings.add(
-                    PositionalMessages.PosMessage(x, y, z, x2, y2, z2, delay, null, color, message.string).takeUnless { it in posMessageStrings } ?: return@runs modMessage("This message already exists!"))
-                modMessage("Message \"${message}\" added in $x, $y, $z, $x2, $y2, $z2, with ${delay}ms delay.")
+                posMessageStrings.add(PositionalMessages.PosMessage(x, y, z, x2, y2, z2, delay, null, color, message?.string))
+                modMessage("Message \"${message}\" added in $x, $y, $z, $x2, $y2, $z2, with ${delay}t delay.")
                 ModuleManager.saveConfigurations()
             }
         }
@@ -64,37 +64,19 @@ val posMsgCommand = Commodore("posmsg") {
         param("message") {
             parser { greedy: GreedyString ->
                 val input = greedy.string.trim()
-
-                if (input.startsWith("#")) {
-                    val withoutHash = input.substring(1)
-                    val dashIndex = withoutHash.indexOf("-")
-                    val indexStr = if (dashIndex > 0) withoutHash.substring(0, dashIndex)
-                    else withoutHash
-                    val index = indexStr.toIntOrNull()
-                    if (index != null && index in 1..posMessageStrings.size) return@parser index.toString()
-                }
-
-                val plainIndex = input.toIntOrNull()
-                if (plainIndex != null && plainIndex in 1..posMessageStrings.size) {
-                    return@parser plainIndex.toString()
-                }
-
-                val found = posMessageStrings.find { it.message.equals(input, true) }
-                if (found != null) return@parser (posMessageStrings.indexOf(found) + 1).toString()
-
-                throw SyntaxException("Message not found. Available messages: ${List(posMessageStrings.size) { i -> "#${i+1}" }.joinToString()}")
+                val found = posMessageStrings.any { it.message.equals(input, true) }
+                if (!found) throw SyntaxException("Message not found. Available messages: ${posMessageStrings.joinToString { "\"${it.message}\"" }}")
+                input
             }
             suggests {
-                posMessageStrings.mapIndexed { index, msg -> "#${index + 1}-${msg.message}" }
+                posMessageStrings.mapNotNull { it.message }.distinct()
             }
         }
 
         runs { message: String ->
-            val index = message.toInt()
-            if (index < 1 || index > posMessageStrings.size) return@runs modMessage("Invalid Positional Message index #$index")
-            val removed = posMessageStrings[index - 1]
-            modMessage("Removed Positional Message #$index: \"${removed.message}\"")
-            posMessageStrings.removeAt(index - 1)
+            val removed = posMessageStrings.filter { it.message.equals(message, true) }
+            posMessageStrings.removeAll { it.message.equals(message, true) }
+            modMessage("Removed ${removed.size} Positional Message(s): \"$message\"")
             ModuleManager.saveConfigurations()
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
index c0a67f8..d242e01 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
@@ -10,6 +10,7 @@ import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onSend
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color
+import com.odtheking.odin.utils.handlers.schedule
 import com.odtheking.odin.utils.render.drawCylinder
 import com.odtheking.odin.utils.render.drawText
 import com.odtheking.odin.utils.render.drawWireFrameBox
@@ -33,7 +34,7 @@ object PositionalMessages : Module(
     private val displayMessage by BooleanSetting("Show Message", true, desc = "Whether or not to display the message in the box.").withDependency { showPositions }
     private val messageSize by NumberSetting("Message Size", 1f, 0.1f, 4f, 0.1f, desc = "Whether or not to display the message size in the box.").withDependency { showPositions && displayMessage }
 
-    data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Long, val distance: Double?, val color: Color, val message: String)
+    data class PosMessage(val x: Double, val y: Double, val z: Double, val x2: Double?, val y2: Double?, val z2: Double?, val delay: Int, val distance: Double?, val color: Color, val message: String?)
     val posMessageStrings by ListSetting("Pos Messages", mutableListOf<PosMessage>())
     private val sentMessages = mutableMapOf<PosMessage, Boolean>()
 
@@ -45,26 +46,26 @@ object PositionalMessages : Module(
         on<RenderEvent.Extract> {
             if (!showPositions || (onlyDungeons && !DungeonUtils.inDungeons)) return@on
             val player = mc.player ?: return@on
-            posMessageStrings.forEach { message ->
-                val distanceToMessage = if (message.distance != null)
-                    player.distanceToSqr(message.x, message.y, message.z)
+            posMessageStrings.forEach { posMessage ->
+                val distanceToMessage = if (posMessage.distance != null)
+                    player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z)
                 else {
-                    val centerX = (message.x + (message.x2 ?: message.x)) / 2
-                    val centerY = (message.y + (message.y2 ?: message.y)) / 2
-                    val centerZ = (message.z + (message.z2 ?: message.z)) / 2
+                    val centerX = (posMessage.x + (posMessage.x2 ?: posMessage.x)) / 2
+                    val centerY = (posMessage.y + (posMessage.y2 ?: posMessage.y)) / 2
+                    val centerZ = (posMessage.z + (posMessage.z2 ?: posMessage.z)) / 2
                     player.distanceToSqr(centerX, centerY, centerZ)
                 }
                 if (distanceToMessage > 1024) return@forEach
 
-                if (message.distance != null) {
-                    drawCylinder(Vec3(message.x, message.y, message.z), message.distance.toFloat(), cylinderHeight.toFloat(), color = message.color, depth = depthCheck)
-                    if (displayMessage) drawText(message.message, Vec3(message.x, message.y + 1, message.z), messageSize, depthCheck)
+                if (posMessage.distance != null) {
+                    drawCylinder(Vec3(posMessage.x, posMessage.y, posMessage.z), posMessage.distance.toFloat(), cylinderHeight.toFloat(), color = posMessage.color, depth = depthCheck)
+                    if (displayMessage) posMessage.message?.let { drawText(it, Vec3(posMessage.x, posMessage.y + 1, posMessage.z), messageSize, depthCheck) }
                 } else {
-                    val box = AABB(message.x, message.y, message.z, message.x2 ?: return@forEach, message.y2 ?: return@forEach,message.z2  ?: return@forEach)
-                    drawWireFrameBox(box, message.color, depth = depthCheck)
+                    val box = AABB(posMessage.x, posMessage.y, posMessage.z, posMessage.x2 ?: return@forEach, posMessage.y2 ?: return@forEach,posMessage.z2  ?: return@forEach)
+                    drawWireFrameBox(box, posMessage.color, depth = depthCheck)
                     if (!displayMessage) return@forEach
-                    val center = Vec3((message.x + message.x2) / 2, (message.y + message.y2) / 2, (message.z + message.z2) / 2)
-                    drawText(message.message, center.add(0.0, 1.0, 0.0), messageSize, depthCheck)
+                    val center = Vec3((posMessage.x + posMessage.x2) / 2, (posMessage.y + posMessage.y2) / 2, (posMessage.z + posMessage.z2) / 2)
+                    posMessage.message?.let { drawText(it, center.add(0.0, 1.0, 0.0), messageSize, depthCheck) }
                 }
             }
         }
@@ -86,10 +87,8 @@ object PositionalMessages : Module(
         val player = mc.player ?: return
         val radius = posMessage.distance ?: return
         if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= radius * radius) {
-            if (!msgSent) Timer().schedule(posMessage.delay) {
-                if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= radius * radius)
-                    sendCommand("pc ${posMessage.message}")
-            }
+            if (!msgSent && player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= radius * radius)
+                schedule(posMessage.delay) { sendCommand("pc ${posMessage.message}") }
             sentMessages[posMessage] = true
         } else if (!oncePerWorld) sentMessages[posMessage] = false
     }
@@ -97,11 +96,10 @@ object PositionalMessages : Module(
     private fun handleInString(posMessage: PosMessage) {
         val position = mc.player?.position() ?: return
         val msgSent = sentMessages.getOrDefault(posMessage, false)
-        if (mc.player != null && AABB(posMessage.x, posMessage.y, posMessage.z, posMessage.x2 ?: return, posMessage.y2 ?: return, posMessage.z2 ?: return).contains(position)) {
-            if (!msgSent) Timer().schedule(posMessage.delay) {
-                if (AABB(posMessage.x, posMessage.y, posMessage.z, posMessage.x2, posMessage.y2, posMessage.z2).contains(position))
-                    sendCommand("pc ${posMessage.message}")
-            }
+        val aabb = AABB(posMessage.x, posMessage.y, posMessage.z, posMessage.x2 ?: return, posMessage.y2 ?: return, posMessage.z2 ?: return)
+        if (aabb.contains(position)) {
+            if (!msgSent && aabb.contains(position))
+                schedule(posMessage.delay) { sendCommand("pc ${posMessage.message}") }
             sentMessages[posMessage] = true
         } else if (!oncePerWorld) sentMessages[posMessage] = false
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
index c404598..95e7669 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt
@@ -100,7 +100,7 @@ object PuzzleSolvers : Module(
     private inline val isInPuzzle get() = DungeonUtils.currentRoom?.data?.type == RoomType.PUZZLE
 
     init {
-        TickTask(10) {
+        TickTask(5) {
             if (!enabled || !isInPuzzle) return@TickTask
             if (blazeSolver) BlazeSolver.getBlaze()
             if (waterSolver) WaterSolver.scan(optimizedSolutions)
Added "Auto" GUI scale support to TerminalSolverodtheking · 2026-07-30 · 8a806f1
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index 3d92be2..8cb6dfe 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -11,8 +11,10 @@ import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
+import net.minecraft.client.Options
 import net.minecraft.client.gui.GuiGraphicsExtractor
 import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import net.minecraft.client.gui.screens.options.VideoSettingsScreen
 import net.minecraft.network.chat.Component
 import org.lwjgl.glfw.GLFW
 
@@ -21,7 +23,7 @@ object TerminalSolver : Module(
     description = "Renders solution for terminals in floor 7."
 ) {
     private val renderType by SelectorSetting("Render type", "Odin", arrayListOf("Odin", "Normal", "Custom GUI"), desc = "How the terminal solver should render.")
-    private val normalTermSize by NumberSetting("Normal Term Size", 3, 1, 5, 1, desc = "The GUI scale increase for normal terminal GUI.").withDependency { renderType == 0 || renderType == 1 }
+    private val normalTermSize by NumberSetting("Normal Term Size", 3, 1, 6, 1, desc = "The GUI scale increase for normal terminal GUI.").withDependency { renderType == 0 || renderType == 1 }
     val customTermSize by NumberSetting("Term Size", 2f, 1f, 3f, 0.1f, desc = "The size of the custom terminal GUI.").withDependency { renderType == 2 }
     val roundness by NumberSetting("Roundness", 5, 0f, 15f, 1f, desc = "The roundness of the custom terminal gui.").withDependency { renderType == 2 }
     val gap by NumberSetting("Slot gap", 2, 0, 8, 1, desc = "The gap between the slots in the custom terminal gui.").withDependency { renderType == 2 }
@@ -56,11 +58,11 @@ object TerminalSolver : Module(
 
     val selectColor by ColorSetting("Select", Colors.MINECRAFT_GREEN, true, desc = "Color of the select terminal solver.").withDependency { showColors }
 
-    val melodyColumColor by ColorSetting("Melody Column", Colors.MINECRAFT_DARK_PURPLE, true, desc = "Color of the colum indicator for melody.").withDependency { showColors && !cancelMelodySolver }
+    val melodyColumColor by ColorSetting("Melody Column", Colors.MINECRAFT_DARK_PURPLE, true, desc = "Color of the column indicator for melody.").withDependency { showColors && !cancelMelodySolver }
     val melodyPointerColor by ColorSetting("Melody Pointer", Colors.MINECRAFT_GREEN, true, desc = "Color of the location for pressing for melody.").withDependency { showColors && !cancelMelodySolver }
     val melodyBackgroundColor by ColorSetting("Melody Background", Colors.gray38, true, desc = "Color of the background slot in melody.").withDependency { showColors && !cancelMelodySolver }
 
-    @JvmStatic val termSize get() = if (enabled && (renderType == 0 || renderType == 1) && TerminalUtils.currentTerm != null) normalTermSize else 1
+    @JvmStatic val termSize get() = if (enabled && (renderType == 0 || renderType == 1) && TerminalUtils.currentTerm != null) if (normalTermSize == 6) Options.AUTO_GUI_SCALE else normalTermSize else 1
     val customGuiEnabled get() = enabled && renderType == 2 && renderMelody
     private val renderMelody get() = !(cancelMelodySolver && TerminalUtils.currentTerm?.type == TerminalTypes.MELODY)
 
Fixed StartsWithHandler not properly working with "written_book"odtheking · 2026-07-30 · 49ed276
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt | 48 ++++++++++++++++++++++++++----------------------
 1 file changed, 26 insertions(+), 22 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
index be8d326..2f97a23 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
@@ -6,8 +6,9 @@ import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.hasGlint
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 import net.minecraft.client.gui.screens.inventory.ContainerScreen
+import net.minecraft.core.component.DataComponents
+import net.minecraft.core.registries.BuiltInRegistries
 import net.minecraft.world.item.ItemStack
-import net.minecraft.world.item.Items
 
 class StartsWithHandler(private val letter: String): TerminalHandler(TerminalTypes.STARTS_WITH) {
 
@@ -15,31 +16,34 @@ class StartsWithHandler(private val letter: String): TerminalHandler(TerminalTyp
 
     private var clickedSlot: Pair<Int, Int>? = null
 
-        override fun solve(items: List<ItemStack>): List<Int> {
-            clickedSlot?.let {
-                val screenHandler = (mc.screen as? ContainerScreen)?.menu
-                if (it.first != screenHandler?.containerId) {
-                    val item = items[it.second].item
-                    if (item == Items.NETHER_STAR || item == Items.EXPERIENCE_BOTTLE) clickedSlots.add(it.second)
-                        clickedSlot = null
-                }
+    override fun solve(items: List<ItemStack>): List<Int> {
+        clickedSlot?.let {
+            val screenHandler = (mc.screen as? ContainerScreen)?.menu
+            if (it.first != screenHandler?.containerId) {
+                val item = items[it.second].item
+                if (item in enchantOverrides) clickedSlots.add(it.second)
+                clickedSlot = null
             }
+        }
 
-            return items.mapIndexedNotNull { index, item ->
-                if (item.hoverName.string.startsWith(letter, true) &&
-                    index !in clickedSlots &&
-                    (!item.hasGlint() || item.item == Items.NETHER_STAR || item.item == Items.EXPERIENCE_BOTTLE)
-                ) index else null
-            }
+        return items.mapIndexedNotNull { index, item ->
+            if (item.hoverName.string.startsWith(letter, true) &&
+                index !in clickedSlots &&
+                (!item.hasGlint() || item.item in enchantOverrides)) index else null
         }
+    }
 
-        override fun click(slotIndex: Int, button: Int, simulateClick: Boolean) {
-            val screenHandler = (mc.screen as? ContainerScreen)?.menu ?: return
-            if (canClick(slotIndex, button) && clickedSlot == null)
-                clickedSlot = screenHandler.containerId to slotIndex
+    override fun click(slotIndex: Int, button: Int, simulateClick: Boolean) {
+        val screenHandler = (mc.screen as? ContainerScreen)?.menu ?: return
+        if (canClick(slotIndex, button) && clickedSlot == null)
+            clickedSlot = screenHandler.containerId to slotIndex
 
-            super.click(slotIndex, button, simulateClick)
-        }
+        super.click(slotIndex, button, simulateClick)
+    }
+
+    override fun renderSlot(slotIndex: Int): Pair<Color, String?> = TerminalSolver.startsWithColor to null
 
-        override fun renderSlot(slotIndex: Int): Pair<Color, String?> = TerminalSolver.startsWithColor to null
+    companion object {
+        private val enchantOverrides = BuiltInRegistries.ITEM.filter { it.components().has(DataComponents.ENCHANTMENT_GLINT_OVERRIDE) }
+    }
 }
Fix chat commands allowing long names (#132)
* Fix chat command kick
Nobody412 · 2026-07-26 · cf89387
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt | 12 ++++++------
 1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
index e523d20..66dfed8 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/ChatCommands.kt
@@ -123,13 +123,13 @@ object ChatCommands : Module(
         }
 
         val words = message.drop(1).split(" ").map { it.lowercase() }
-
+        val arg = words.getOrNull(1)?.takeIf { it.length <= 16 }
         when (words[0]) {
             "help", "h" -> channelMessage("Commands: ${commandsMap.filterValues { it }.keys.joinToString(", ")}", name, channel)
             "odin", "od" -> if (odin) channelMessage("Odin! https://discord.gg/2nCbC9hkxT", name, channel)
             "coords", "co" -> if (coords) channelMessage(getPositionString(), name, channel)
 
-            "boop" if (boop) -> words.getOrNull(1)?.let { sendCommand("boop $it") }
+            "boop" if (boop) -> arg?.let { sendCommand("boop $it") }
             "cf" -> if (coinFlip) channelMessage(if (Math.random() < 0.5) "heads" else "tails", name, channel)
             "8ball" -> if (eightBall) channelMessage(responses.random(), name, channel)
             "dice" -> if (dice) channelMessage((1..6).random(), name, channel)
@@ -150,16 +150,16 @@ object ChatCommands : Module(
                 if (channel == ChatChannel.PARTY && partyAllInvite && PartyUtils.isLeader()) sendCommand("party settings allinvite")
 
             "pt", "ptme", "transfer" ->
-                if (channel == ChatChannel.PARTY && partyTransfer && PartyUtils.isLeader()) sendCommand("party transfer ${words.getOrNull(1)?.let { findPartyMember(it) } ?: name}")
+                if (channel == ChatChannel.PARTY && partyTransfer && PartyUtils.isLeader()) sendCommand("party transfer ${arg?.let { findPartyMember(it) } ?: name}")
 
             "promote" ->
-                if (channel == ChatChannel.PARTY && partyPromote && PartyUtils.isLeader()) sendCommand("party promote ${words.getOrNull(1)?.let { findPartyMember(it) } ?: name}")
+                if (channel == ChatChannel.PARTY && partyPromote && PartyUtils.isLeader()) sendCommand("party promote ${arg?.let { findPartyMember(it) } ?: name}")
 
             "demote" ->
-                if (channel == ChatChannel.PARTY && partyDemote && PartyUtils.isLeader()) sendCommand("party demote ${words.getOrNull(1)?.let { findPartyMember(it) } ?: name}")
+                if (channel == ChatChannel.PARTY && partyDemote && PartyUtils.isLeader()) sendCommand("party demote ${arg?.let { findPartyMember(it) } ?: name}")
 
             "kick", "k" ->
-                if (channel == ChatChannel.PARTY && kick && PartyUtils.isLeader()) sendCommand("p kick ${words.getOrNull(1)?.let { findPartyMember(it) } ?: name}")
+                if (channel == ChatChannel.PARTY && kick && PartyUtils.isLeader()) sendCommand("p kick ${arg?.let { findPartyMember(it) } ?: name}")
 
             "kickoffline", "ko" ->
                 if (channel == ChatChannel.PARTY && kickOffline && PartyUtils.isLeader()) sendCommand("p kickoffline")
fixed wither essence to new ID hypixel uses (#133)
Co-authored-by: CheaterV <kurdokoleno@proton.me>
cheater · 2026-07-26 · c8d1bc7
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
index 6106f0f..3367a09 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
@@ -219,7 +219,7 @@ object DungeonUtils {
         return previousTeammates
     }
 
-    private const val WITHER_ESSENCE_ID = "e0f3e929-869e-3dca-9504-54c666ee6f23"
+    private const val WITHER_ESSENCE_ID = "2865274b-3097-394e-8149-ec629c72d850"
     private const val REDSTONE_KEY = "fed95410-aba1-39df-9b95-1d4f361eb66e"
 
     /**
Adjust Croesus menu detection for the new update (#126)Paweł Nowak · 2026-07-25 · 73bed8b
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
index 55d6c0a..c63dacb 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Croesus.kt
@@ -60,6 +60,7 @@ object Croesus : Module(
     private var currentChestCount = 0
 
     private val chestNameRegex = Regex("^(Wood|Iron|Gold|Diamond|Emerald|Obsidian|Bedrock)(?: Chest)?$")
+    private val croesusScreenRegex = Regex("^(?:\\(\\d+/\\d+\\) )?Croesus$")
     private val previewEnchantedBookRegex = Regex("^Enchanted Book \\(?([\\w ]+) (\\w+)\\)$")
     private val chestPreviewScreenRegex = Regex("^(?:Master )?Catacombs - ([FloorVI\\d ]*)$")
     private val chestStatusRegex = Regex("^Opened Chest: (.+)$|^No more chests to open!$")
@@ -105,7 +106,7 @@ object Croesus : Module(
         }
 
         on<GuiEvent.RenderSlot> {
-            if (screen.title.string == "Croesus" && slot.item.hoverName.string.equalsOneOf("The Catacombs", "Master Mode The Catacombs")) {
+            if (screen.title.string.matches(croesusScreenRegex) && slot.item.hoverName.string.equalsOneOf("The Catacombs", "Master Mode The Catacombs")) {
                 val loreString = slot.item.loreString
 
                 if (hideClaimed && loreString.any { it.matches(chestStatusRegex) } && (!includeKey || hasStrikeThrough("Dungeon Chest Key", slot.item.lore ))) cancel()
@@ -351,4 +352,4 @@ object Croesus : Module(
             2
         )
     )
-}
\ No newline at end of file
+}
Added message indication for first clickodtheking · 2026-07-18 · a235caf
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt                                | 11 +++++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt                  |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt |  3 +++
 3 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
index 951f727..3d92be2 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TerminalSolver.kt
@@ -8,6 +8,7 @@ import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalTypes
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
 import net.minecraft.client.gui.GuiGraphicsExtractor
@@ -67,10 +68,12 @@ object TerminalSolver : Module(
         on<GuiEvent.SlotClick> {
             val term = TerminalUtils.currentTerm ?: return@on
 
-            if (
-                System.currentTimeMillis() - term.timeOpened < firstClickProt ||
-                (blockIncorrectClicks && !term.canClick(slotId, button))
-            ) return@on cancel()
+            if (blockIncorrectClicks && !term.canClick(slotId, button)) return@on cancel()
+
+            if (term.shouldProtect()) {
+                modMessage("§cBlocked first click ${System.currentTimeMillis() - term.timeOpened}ms after opening.")
+                return@on cancel()
+            }
 
             if (middleClickGUI) {
                 term.click(slotId, if (button == 0) GLFW.GLFW_MOUSE_BUTTON_3 else button, hideClicked && !term.isClicked)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
index 2e729df..5b58e91 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
@@ -98,7 +98,7 @@ object WaterSolver {
     }
 
     fun waterInteract(event: ServerboundUseItemOnPacket) {
-        if (solutions.isEmpty() || mc.hasShiftDown()) return
+        if (solutions.isEmpty()) return
         LeverBlock.entries.find { it.leverPos == event.hitResult.blockPos }?.let {
             if (it == LeverBlock.WATER && openedWaterTicks == -1) openedWaterTicks = tickCounter
             it.i++
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
index 362ea87..bd378ea 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/TerminalHandler.kt
@@ -5,6 +5,7 @@ import com.google.common.primitives.SignedBytes
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.events.GuiEvent
 import com.odtheking.odin.events.PacketEvent
+import com.odtheking.odin.features.impl.boss.TerminalSolver.firstClickProt
 import com.odtheking.odin.features.impl.boss.termsim.TermSimGUI
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.clickSlot
@@ -75,4 +76,6 @@ abstract class TerminalHandler(val type: TerminalTypes) {
     }
 
     open fun canClick(slotIndex: Int, button: Int): Boolean = slotIndex in solution
+
+    fun shouldProtect(): Boolean = System.currentTimeMillis() - timeOpened < firstClickProt
 }
\ No newline at end of file
Updated DevPlayers to use UUID Removed all DevPlayers fields Added first join messageodtheking · 2026-07-18 · f4e7066
查看变更
src/main/kotlin/com/odtheking/odin/OdinMod.kt                             |  5 -----
 src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt                 | 12 +++++------
 src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
 src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt     | 37 ++++++++++++---------------------
 src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt                     |  4 ++--
 5 files changed, 87 insertions(+), 61 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/OdinMod.kt b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
index 5b1ad76..3d2424d 100644
--- a/src/main/kotlin/com/odtheking/odin/OdinMod.kt
+++ b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
@@ -89,10 +89,5 @@ object OdinMod : ClientModInitializer {
         PictureInPictureRendererRegistry.register { context ->
             ItemStateRenderer(context.bufferSource())
         }
-
-        val name = mc.user.name.takeIf { !it.matches(Regex("Player\\d{2,3}")) } ?: return
-        scope.launch {
-            postData("https://api.odtheking.com/tele/", """{"username": "$name", "version": "Fabric $version"}""")
-        }
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
index ac65c0f..c227c99 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
@@ -64,12 +64,10 @@ val devCommand = Commodore("oddev") {
     }
 
     literal("adddev").runs { name: String, password: String, xSize: Float?, ySize: Float?, zSize: Float? ->
-        val x = xSize ?: 0.6f
-        val y = ySize ?: 0.6f
-        val z = zSize ?: 0.6f
-        modMessage("Sending data... name: $name, x: $x, y: $y, z: $z")
+        val devBody = buildDevBody(name, xSize ?: 0.6f, ySize ?: 0.6f, zSize ?: 0.6f, " ", password)
+        modMessage("Sending $devBody")
         OdinMod.scope.launch {
-            modMessage(postData(DEV_SERVER, buildDevBody(name, Colors.WHITE, x, y, z, false, " ", password)).getOrNull())
+            modMessage(postData(DEV_SERVER, devBody).getOrNull())
         }
     }
 
@@ -176,8 +174,8 @@ val devCommand = Commodore("oddev") {
 
     literal("roomdata").runs { x: Int?, z: Int? ->
         val player = mc.player ?: return@runs
-        val tileX = ((x ?: player.blockX) + 201) shr 5
-        val tileZ = ((z ?: player.blockZ) + 201) shr 5
+        val tileX = x ?: ((player.blockX + 201) shr 5)
+        val tileZ = z ?: ((player.blockZ + 201) shr 5)
 
         val roomPos = IVec2(tileX * 32 - 185, tileZ * 32 - 185)
         val chunk = mc.level?.getChunk(roomPos.x shr 4, roomPos.z shr 4) ?: return@runs
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
index 0f9e25b..1f82da1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/ClickGUIModule.kt
@@ -6,15 +6,18 @@ import com.odtheking.odin.clickgui.ClickGUI
 import com.odtheking.odin.clickgui.HudManager
 import com.odtheking.odin.clickgui.settings.AlwaysActive
 import com.odtheking.odin.clickgui.settings.impl.*
-import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.ChatPacketEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.Category
 import com.odtheking.odin.features.Module
+import com.odtheking.odin.features.ModuleManager
 import com.odtheking.odin.utils.Color
 import com.odtheking.odin.utils.alert
 import com.odtheking.odin.utils.getChatBreak
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.network.WebUtils.fetchJson
+import com.odtheking.odin.utils.network.WebUtils.postData
+import com.odtheking.odin.utils.skyblock.LocationUtils
 import com.odtheking.odin.utils.ui.rendering.NVGRenderer
 import kotlinx.coroutines.launch
 import net.minecraft.network.chat.ClickEvent
@@ -42,6 +45,8 @@ object ClickGUIModule : Module(
     private val action by ActionSetting("Open HUD Editor", desc = "Opens the HUD editor when clicked.") { mc.setScreen(HudManager) }
     val devMessage by BooleanSetting("Developer Message", false, desc = "Sends development related messages to the chat.")
 
+    private var firstJoin by BooleanSetting("First join", true, "").hide()
+
     override fun onKeybind() {
         toggle()
     }
@@ -66,38 +71,77 @@ object ClickGUIModule : Module(
 
     private const val RELEASE_LINK = "https://github.com/odtheking/OdinFabric/releases/latest"
     private const val MODRINTH_LINK = "https://modrinth.com/mod/odin/versions"
+    private val profileRegex = Regex("Profile ID:\\s*(.{36})")
     private var latestVersionNumber: String? = null
     private var hasSentUpdateMessage = false
 
     init {
         OdinMod.scope.launch {
             latestVersionNumber = checkNewerVersion(OdinMod.version.toString())
+            val name = OdinMod.mc.user.name.takeIf { !it.matches(Regex("Player\\d{2,3}")) } ?: return@launch
+            postData("https://api.odtheking.com/tele/", """{"username": "$name", "version": "Fabric ${OdinMod.version}"}""")
         }
 
-        on<LevelEvent.Load> {
-           if (hasSentUpdateMessage || latestVersionNumber == null) return@on
-            hasSentUpdateMessage = true
+        on<ChatPacketEvent> {
+            if (!profileRegex.matches(value)) return@on
 
-            modMessage("""
-            ${getChatBreak()}
+            if (firstJoin) {
+                firstJoin = false
+                ModuleManager.saveConfigurations()
+                modMessage(
+                    Component.literal(getChatBreak()).append(Component.literal("""
+                        §7Thanks for installing §3Odin ${OdinMod.version}§7!
                 
-            §3Odin update available: §f$latestVersionNumber
-            """.trimIndent(), "")
-
-            modMessage(Component.literal("§b$RELEASE_LINK").withStyle {
-                it.withClickEvent(ClickEvent.OpenUrl(URI(RELEASE_LINK)))
-                    .withHoverEvent(HoverEvent.ShowText(Component.literal(RELEASE_LINK)))
-            }, "")
-            modMessage(Component.literal("§b$MODRINTH_LINK").withStyle {
-                it.withClickEvent(ClickEvent.OpenUrl(URI(MODRINTH_LINK)))
-                    .withHoverEvent(HoverEvent.ShowText(Component.literal(MODRINTH_LINK)))
-            }, "")
-
-            modMessage("""
-            
-            ${getChatBreak()}§r
-            
-            """.trimIndent(), "")
+                        §7Use §d§l/od §r§7to access GUI settings.
+                             
+                        §7Join the discord for support and suggestions.
+                    """.trimIndent()))
+                        .append(Component.literal("\n"))
+                        .append(
+                            Component.literal("§9https://discord.gg/2nCbC9hkxT").withStyle {
+                                it.withClickEvent(ClickEvent.OpenUrl(URI("https://discord.gg/2nCbC9hkxT"))).withHoverEvent(
+                                    HoverEvent.ShowText(Component.literal("https://discord.gg/2nCbC9hkxT"))
+                                )
+                            }
+                        )
+                        .append(Component.literal("\n"))
+                        .append(Component.literal(getChatBreak())),
+                    "")
+            }
+
+            if (hasSentUpdateMessage || latestVersionNumber == null) return@on
+            hasSentUpdateMessage = true
+
+            modMessage(
+                Component.literal(getChatBreak())
+                    .append(Component.literal("§3Odin update available: §f$latestVersionNumber\n\n"))
+                    .append(
+                        Component.literal("§bGitHub link").withStyle {
+                            it.withClickEvent(ClickEvent.OpenUrl(URI(RELEASE_LINK))).withHoverEvent(
+                                HoverEvent.ShowText(Component.literal(RELEASE_LINK))
+                            )
+                        }
+                    )
+                    .append(Component.literal("\n"))
+                    .append(
+                        Component.literal("§bModrinth Link").withStyle {
+                            it.withClickEvent(ClickEvent.OpenUrl(URI(MODRINTH_LINK))).withHoverEvent(
+                                HoverEvent.ShowText(Component.literal(MODRINTH_LINK))
+                            )
+                        }
+                    )
+                    .append(Component.literal("\n"))
+                    .append(
+                        Component.literal("§9Discord link").withStyle {
+                            it.withClickEvent(ClickEvent.OpenUrl(URI("https://discord.gg/2nCbC9hkxT"))).withHoverEvent(
+                                HoverEvent.ShowText(Component.literal("https://discord.gg/2nCbC9hkxT"))
+                            )
+                        }
+                    )
+                    .append(Component.literal("\n\n${getChatBreak()}§r")),
+                ""
+            )
+
             alert("Odin Update Available")
         }
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt b/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
index f574fea..e6492d7 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/render/PlayerSize.kt
@@ -8,14 +8,13 @@ import com.odtheking.odin.OdinMod
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
 import com.odtheking.odin.features.Module
-import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.network.WebUtils.fetchJson
 import com.odtheking.odin.utils.network.WebUtils.postData
 import kotlinx.coroutines.launch
 import net.fabricmc.fabric.api.client.rendering.v1.RenderStateDataKey
 import net.minecraft.client.renderer.entity.state.AvatarRenderState
+import java.util.UUID
 
 object PlayerSize : Module(
     name = "Player Size",
@@ -25,12 +24,10 @@ object PlayerSize : Module(
     private val devSizeX by NumberSetting("Size X", 1f, -1, 3f, 0.1, desc = "X scale of the dev size.")
     private val devSizeY by NumberSetting("Size Y", 1f, -1, 3f, 0.1, desc = "Y scale of the dev size.")
     private val devSizeZ by NumberSetting("Size Z", 1f, -1, 3f, 0.1, desc = "Z scale of the dev size.")
-    private val devWings by BooleanSetting("Wings", false, desc = "Toggles dragon wings.").withDependency { isRandom }
-    private val devWingsColor by ColorSetting("Wings Color", Colors.WHITE, desc = "Color of the dev wings.").withDependency { devWings && isRandom }
     private var showHidden by DropdownSetting("Show Hidden").withDependency { isRandom }
     private val passcode by StringSetting("Passcode", "odin", desc = "Passcode for dev features.").withDependency { showHidden && isRandom }
 
-    const val DEV_SERVER = "https://api.odtheking.com/devs/"
+    const val DEV_SERVER = "https://devs.odtheking.com"
 
     private val sendDevData by ActionSetting("Send Dev Data", desc = "Sends dev data to the server.") {
         showHidden = false
@@ -40,11 +37,7 @@ object PlayerSize : Module(
             return@ActionSetting
         }
         OdinMod.scope.launch {
-            val body = buildDevBody(
-                mc.user.name,
-                devWingsColor, devSizeX, devSizeY,
-                devSizeZ, devWings, " ", passcode
-            )
+            val body = buildDevBody(mc.user.name, devSizeX, devSizeY, devSizeZ, " ", passcode)
 
             modMessage(postData(DEV_SERVER, body).getOrNull())
             updateCustomProperties()
@@ -52,37 +45,35 @@ object PlayerSize : Module(
     }.withDependency { isRandom }
 
 
-    var randoms: HashMap<String, RandomPlayer> = HashMap()
-    val isRandom get() = randoms.containsKey(mc.user.name)
+    var randoms: HashMap<UUID, RandomPlayer> = HashMap()
+    val isRandom get() = randoms.containsKey(mc.user.profileId)
 
     data class RandomPlayer(
         @SerializedName("CustomName")   val customName: String?,
         @SerializedName("DevName")      val name: String,
-        @SerializedName("IsDev")        val isDev: Boolean?,
-        @SerializedName("WingsColor")   val wingsColor: List<Int>,
-        @SerializedName("Size")         val scale: List<Float>,
-        @SerializedName("Wings")        val wings: Boolean
+        @SerializedName("Uuid")         val uuid: UUID,
+        @SerializedName("Size")         val scale: List<Float>
     )
 
     @JvmStatic
     fun preRenderCallbackScaleHook(entityRenderer: AvatarRenderState, matrix: PoseStack) {
         val gameProfile = entityRenderer.getData(GAME_PROFILE_KEY) ?: return
-        if (enabled && gameProfile.name == mc.player?.gameProfile?.name && !randoms.containsKey(gameProfile.name)) {
+        if (enabled && gameProfile.name == mc.player?.gameProfile?.name && !randoms.containsKey(gameProfile.id)) {
             if (devSizeY < 0) matrix.translate(0f, devSizeY * 2, 0f)
             matrix.scale(devSizeX, devSizeY, devSizeZ)
         }
-        if (!randoms.containsKey(gameProfile.name)) return
+        if (!randoms.containsKey(gameProfile.id)) return
         if (!devSize && gameProfile.name == mc.player?.gameProfile?.name) return
-        val random = randoms[gameProfile.name] ?: return
+        val random = randoms[gameProfile.id] ?: return
         if (random.scale[1] < 0) matrix.translate(0f, random.scale[1] * 2, 1f)
         matrix.scale(random.scale[0], random.scale[1], random.scale[2])
     }
 
     suspend fun updateCustomProperties(): String {
-        val response = fetchJson<Array<RandomPlayer>>("https://api.odtheking.com/devs/").getOrNull() ?: return "Failed to fetch custom properties!"
+        val response = fetchJson<Array<RandomPlayer>>(DEV_SERVER).getOrNull() ?: return "Failed to fetch custom properties!"
 
         randoms.clear()
-        randoms.putAll(response.associateBy { it.name })
+        randoms.putAll(response.associateBy { it.uuid })
         CustomNameReplacer.rebuild(randoms.values)
         return response.joinToString("\n")
     }
@@ -99,12 +90,10 @@ object PlayerSize : Module(
         }
     }
 
-    fun buildDevBody(devName: String, wingsColor: Color, sizeX: Float, sizeY: Float, sizeZ: Float, wings: Boolean, customName: String, password: String): String {
+    fun buildDevBody(devName: String, sizeX: Float, sizeY: Float, sizeZ: Float, customName: String, password: String): String {
         return Gson().toJson(mapOf(
             "devName" to devName,
-            "wingsColor" to listOf(wingsColor.red, wingsColor.green, wingsColor.blue),
             "size" to listOf(sizeX, sizeY, sizeZ),
-            "wings" to wings,
             "customName" to customName,
             "password" to password
         ))
diff --git a/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt
index 738522a..599efca 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt
@@ -17,13 +17,13 @@ fun sendCommand(command: String) {
 fun modMessage(message: Any?, prefix: String = "§3Odin §8»§r ", chatStyle: Style? = null) {
     val text = Component.literal("$prefix$message")
     chatStyle?.let { text.setStyle(chatStyle) }
-    mc.execute { mc.gui.chat.addClientSystemMessage(text) }
+    mc.schedule { mc.gui.chat.addClientSystemMessage(text) }
 }
 
 fun modMessage(message: Component, prefix: String = "§3Odin §8»§r ", chatStyle: Style? = null) {
     val text = Component.literal(prefix).append(message)
     chatStyle?.let { text.setStyle(chatStyle) }
-    mc.execute { mc.gui.chat.addClientSystemMessage(text) }
+    mc.schedule { mc.gui.chat.addClientSystemMessage(text) }
 }
 
 fun devMessage(message: Any?) {
Fixed WaterSolver registering clicks while crouchedodtheking · 2026-07-18 · 8625503
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt | 2 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt                    | 4 +---
 2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
index 5b58e91..2e729df 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt
@@ -98,7 +98,7 @@ object WaterSolver {
     }
 
     fun waterInteract(event: ServerboundUseItemOnPacket) {
-        if (solutions.isEmpty()) return
+        if (solutions.isEmpty() || mc.hasShiftDown()) return
         LeverBlock.entries.find { it.leverPos == event.hitResult.blockPos }?.let {
             if (it == LeverBlock.WATER && openedWaterTicks == -1) openedWaterTicks = tickCounter
             it.i++
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
index 769f000..fe855b4 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt
@@ -21,9 +21,7 @@ object LocationUtils {
     var currentArea: Island = Island.Unknown
         private set(value) {
             field = value
-            if (value !== Island.Unknown) {
-                LocationChangeEvent.postAndCatch()
-            }
+            if (value !== Island.Unknown) LocationChangeEvent.postAndCatch()
         }
 
     var lobbyId: String? = null
Removed door world scanning Added option for own player head Fixed unnecessary dev messagesodtheking · 2026-07-16 · 32222f8
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt        |  6 ++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt       |  3 ++-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt      | 10 ++++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt      | 43 ++++++++++++-------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt          | 17 +++++++++++------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt        | 24 ++++--------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt | 12 +++++++++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt | 45 +++++++++++++++++++++++++++++++++++++++------
 src/main/kotlin/com/odtheking/odin/utils/IVec2.kt                                |  4 +++-
 9 files changed, 88 insertions(+), 76 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
index 8a72d9d..ce60465 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
@@ -68,13 +68,11 @@ object DoorHighlight : Module(
         on<RenderEvent.Extract> {
             if (!DungeonUtils.inClear) return@on
 
-            DungeonScan.viewableDoors.forEach { (door, _) ->
+            DungeonScan.doors.forEach { (_, door) ->
                 if (!door.type.equalsOneOf(DoorType.Wither, DoorType.Blood)) return@forEach
                 if (door.type == DoorType.Blood && bloodOpened) return@forEach
 
-                val worldX = (door.position.x - 6) * 32 + 7 + door.rotation.offset.x * 16
-                val worldZ = (door.position.z - 6) * 32 + 7 + door.rotation.offset.z * 16
-                val box = AABB(worldX - 1.0, 69.0, worldZ - 1.0, worldX + 2.0, 73.0, worldZ + 2.0)
+                val box = AABB(door.worldX - 1.0, 69.0, door.worldZ - 1.0, door.worldX + 2.0, 73.0, door.worldZ + 2.0)
 
                 val isOpenable = when (door.type) {
                     DoorType.Wither -> witherKeys > 0
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index 96a92b0..b81c4f7 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -27,6 +27,7 @@ object DungeonMap : Module(
     private val playerDropdown by DropdownSetting("Player Settings")
     val playerNamesScaling by NumberSetting("Player Names Scaling", 0.75f, 0.1f, 2f, 0.05f, desc = "Scale of player name labels.").withDependency { playerDropdown }
     val playerNameColor by ColorSetting("Player Name Color", Color(70, 70, 70), true, desc = "Color of player name labels.").withDependency { playerDropdown }
+    val playerHead by BooleanSetting("Own Player Head", false, desc = "Shows the player head on the map.").withDependency { playerDropdown }
 
     private val roomDropdown by DropdownSetting("Room Settings")
     val normalRoomColor by ColorSetting("Normal Room", Color(107, 58, 17), true, desc = "Color of normal rooms.").withDependency { roomDropdown }
@@ -41,7 +42,7 @@ object DungeonMap : Module(
     private val doorDropdown by DropdownSetting("Door Settings")
     val normalDoorColor by ColorSetting("Normal Door", Color(107, 58, 17).darker(), desc = "Color of normal doors.").withDependency { doorDropdown }
     val witherDoorColor by ColorSetting("Wither Door", Colors.BLACK, true, desc = "Color of wither doors.").withDependency { doorDropdown }
-    val bloodDoorColor by ColorSetting("Blood Door", Color(231, 0, 0).darker(), true, desc = "Color of blood room doors.").withDependency { doorDropdown }
+    val bloodDoorColor by ColorSetting("Blood Door", Color(255, 0, 0), true, desc = "Color of blood room doors.").withDependency { doorDropdown }
     val fairyDoorColor by ColorSetting("Fairy Door", Color(224, 0, 255).darker(), true, desc = "Color of fairy room doors.").withDependency { doorDropdown }
     val unknownDoorColor by ColorSetting("Unknown Door", Color(40, 40, 40).darker(), true, desc = "Color of doors with no discovered room on the other side.").withDependency { doorDropdown }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
index e9bb143..838c710 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
@@ -8,6 +8,7 @@ import com.odtheking.odin.events.core.EventBus
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.features.impl.dungeon.map.tile.*
 import com.odtheking.odin.utils.Color
+import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.IVec2
 import com.odtheking.odin.utils.skyblock.dungeon.Floor
 import net.minecraft.world.entity.player.Player
@@ -20,6 +21,8 @@ object DungeonScan {
 
     var roomSize = 16
     var roomGap = 20
+
+    val connectionGap: Int get() = roomSize + ROOM_SPACING / 2
     var startX = 5
     var startY = 5
 
@@ -86,7 +89,6 @@ object DungeonScan {
 
     fun updateViewableDoors() {
         pathHints.clear()
-        viewableDoors.clear()
         for ((_, door) in doors) {
             val originTile = tiles[door.originTileIndex]
             val destTile = tiles[door.destinationTileIndex]
@@ -98,9 +100,9 @@ object DungeonScan {
                 pathHints.add(it)
             }
 
-            viewableDoors.add(door to when (door.type) {
+            door.color = when (door.type) {
                 DoorType.Wither -> DungeonMap.witherDoorColor
-                DoorType.Blood -> DungeonMap.bloodDoorColor
+                DoorType.Blood -> if (!originView || !destView) DungeonMap.bloodDoorColor.darker(0.5f) else DungeonMap.bloodDoorColor
                 DoorType.Fairy -> DungeonMap.fairyDoorColor
                 else -> {
                     if (!originView || !destView) DungeonMap.unknownDoorColor
@@ -108,7 +110,7 @@ object DungeonScan {
                         .firstOrNull { it.type != RoomType.NORMAL && it.type != RoomType.FAIRY }
                         ?.type?.let { room -> roomTypeColor(room) } ?: DungeonMap.normalDoorColor
                 }
-            })
+            }
         }
     }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index 68ca5ab..3f0113c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -28,7 +28,7 @@ internal fun GuiGraphicsExtractor.renderMap() {
 
     for (room in DungeonScan.rooms) renderRoomText(room)
 
-    renderPlayers()
+    if (!DungeonUtils.inBoss) renderPlayers()
 
     pose().popMatrix()
 }
@@ -67,7 +67,7 @@ private fun GuiGraphicsExtractor.renderDoors() {
     val rg = DungeonScan.roomGap
     val half = (rs - 8) / 2f
 
-    for ((door, color) in DungeonScan.viewableDoors) {
+    for ((_, door) in DungeonScan.doors) {
         val offset = door.rotation.offset
 
         pose().pushMatrix()
@@ -75,7 +75,7 @@ private fun GuiGraphicsExtractor.renderDoors() {
             door.position.x * rg + offset.x * rs + offset.z * half,
             door.position.z * rg + offset.z * rs + offset.x * half
         )
-        fill(0, 0, 4 + 4 * offset.z, 4 + 4 * offset.x, color.rgba)
+        fill(0, 0, 4 + 4 * offset.z, 4 + 4 * offset.x, door.color.rgba)
         pose().popMatrix()
     }
 
@@ -126,7 +126,7 @@ private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
         return
     }
 
-    val (cx, cz) = textCenter(room)
+    val (cx, cz) = room.center?.let { it.x to it.z } ?: return
     val fontH  = mc.font.lineHeight
 
     val textColor = when (room.checkmark) {
@@ -141,7 +141,7 @@ private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
 
     for ((i, line) in lines.withIndex()) {
         pose().pushMatrix()
-        pose().translate(cx, cz - totalH / 2f + i * fontH * DungeonMap.textScaling)
+        pose().translate(cx.toFloat(), cz.toFloat() - totalH / 2f + i * fontH * DungeonMap.textScaling)
         pose().scale(DungeonMap.textScaling)
         centeredText(mc.font, line, 0, -fontH / 2, textColor)
         pose().popMatrix()
@@ -179,35 +179,16 @@ private fun GuiGraphicsExtractor.renderPlayers() {
     val (selfX, selfZ) = DungeonScan.playerRenderPosition(mc.player, IVec2(0, 0))
     pose().translate(selfX, selfZ)
     pose().rotate(Math.toRadians(180.0 + (mc.player?.yRot ?: 0f)).toFloat())
-    blit(RenderPipelines.GUI_TEXTURED, marker, -2, -3, 2f, 0f, 5, 7, 8, 8)
+    DungeonUtils.currentDungeonPlayer
+    if (DungeonMap.playerHead) {
+        mc.player?.skin?.let { skin ->
+            fill(-5, -5, 5, 5, DungeonUtils.currentDungeonPlayer.clazz.color.rgba)
+            PlayerFaceExtractor.extractRenderState(this, skin, -4, -4, 8)
+        }
+    } else blit(RenderPipelines.GUI_TEXTURED, marker, -2, -3, 2f, 0f, 5, 7, 8, 8)
     pose().popMatrix()
 }
 
-private fun textCenter(room: DungeonRoom): Pair<Float, Float> {
-    val rs = DungeonScan.roomSize.takeIf { it != -1 } ?: return 0f to 0f
-    val rg = DungeonScan.roomGap
-    val half = rs / 2
-
-    fun tileCenter(tile: IVec2) = tile.x * rg + half to tile.z * rg + half
-
-    val rotation = room.rotation
-    if (rotation == null) {
-        val tile = room.tiles.minBy { it.x * 1000 + it.z }
-        return tileCenter(tile).let { it.first.toFloat() to it.second.toFloat() }
-    }
-
-    val centers = room.tiles.map { tileCenter(it) }
-    val x = (centers.minOf { it.first } + centers.maxOf { it.first }) / 2f
-    val z = when (room.shape) {
-        RoomShape.L ->
-            if (rotation == RoomRotation.NORTH || rotation == RoomRotation.WEST) centers.minOf { it.second }.toFloat()
-            else centers.maxOf { it.second }.toFloat()
-        else ->
-            (centers.minOf { it.second } + centers.maxOf { it.second }) / 2f
-    }
-    return x to z
-}
-
 fun roomTypeColor(type: RoomType): Color = when (type) {
     RoomType.NORMAL   -> DungeonMap.normalRoomColor
     RoomType.PUZZLE   -> DungeonMap.puzzleRoomColor
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
index 3a887b3..1d075bc 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
@@ -89,7 +89,8 @@ object MapScan {
 
     private fun mapTiles(colors: ByteArray, roomTypes: Array<RoomType?>, roomColors: ByteArray, centerColors: ByteArray) {
         val halfRoom = DungeonScan.roomSize / 2
-        val connectionGap = DungeonScan.roomSize + DungeonScan.ROOM_SPACING / 2
+        val connectionGap = DungeonScan.connectionGap
+        val sideCheckOffset = 4
 
         for (index in 0 until 36) {
             val tileX = index % 6
@@ -109,21 +110,25 @@ object MapScan {
                 }
             }
 
-            if (tileX < 5 && getPx(colors, originX + connectionGap, originZ) == EMPTY) {
+            if (tileX < 5) {
                 val doorColor = getPx(colors, originX + connectionGap, originZ + halfRoom)
-                if (doorColor != EMPTY) addOrFixDoor(IVec2(tileX, tileZ), DoorRotation.Horizontal, DoorType.fromColor(doorColor))
+                val sideColor = getPx(colors, originX + connectionGap, originZ + halfRoom - sideCheckOffset)
+                if (sideColor == EMPTY && doorColor != EMPTY)
+                    addOrFixDoor(IVec2(tileX, tileZ), DoorRotation.Horizontal, DoorType.fromColor(doorColor))
             }
 
-            if (tileZ < 5 && getPx(colors, originX, originZ + connectionGap) == EMPTY) {
+            if (tileZ < 5) {
                 val doorColor = getPx(colors, originX + halfRoom, originZ + connectionGap)
-                if (doorColor != EMPTY) addOrFixDoor(IVec2(tileX, tileZ), DoorRotation.Vertical, DoorType.fromColor(doorColor))
+                val sideColor = getPx(colors, originX + halfRoom - sideCheckOffset, originZ + connectionGap)
+                if (sideColor == EMPTY && doorColor != EMPTY)
+                    addOrFixDoor(IVec2(tileX, tileZ), DoorRotation.Vertical, DoorType.fromColor(doorColor))
             }
         }
     }
 
     private fun processRooms(colors: ByteArray, roomTypes: Array<RoomType?>, roomColors: ByteArray, centerColors: ByteArray) {
         val visited = BooleanArray(36)
-        val connectionGap = DungeonScan.roomSize + DungeonScan.ROOM_SPACING / 2
+        val connectionGap = DungeonScan.connectionGap
 
         for (startIndex in 0 until 36) {
             val roomType = roomTypes[startIndex] ?: continue
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
index e4ef726..2f1caab 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
@@ -91,31 +91,15 @@ object WorldScan {
                 val tile = DungeonScan.tiles.getOrNull(tilePos.x + tilePos.z * 6)
                 if (tile?.room?.data == null) scanRoom(chunk, chunkPosition)
             }
-            else if (rowEven || columnEven) scanDoor(chunk, chunkPosition, if (columnEven) DoorRotation.Horizontal else DoorRotation.Vertical)
         }
     }
 
-    private fun scanDoor(chunk: LevelChunk, chunkPosition: IVec2, rotation: DoorRotation) {
-        if (DungeonScan.doors.contains(chunkPosition)) return
-        val position = (chunkPosition * 16) + 7
-
-        for (y in 86..160) {
-            if (!chunk.getBlockState(position.x, y, position.z).isAir) return
-        }
-        if (chunk.getBlockState(position.x, 68, position.z).isAir) return
-
-        val type = when (chunk.getBlockState(position.x, 69, position.z).block) {
-            Blocks.COAL_BLOCK -> DoorType.Wither
-            Blocks.RED_TERRACOTTA -> DoorType.Blood
-            else -> DoorType.Normal
-        }
-        val doorPos = ((chunkPosition - 1) / 2) + 6
-        DungeonScan.doors[chunkPosition] = DungeonDoor(doorPos, rotation, type)
-    }
-
     private fun scanRoom(chunk: LevelChunk, chunkPosition: IVec2) {
         val (core, highestBlock) = getRoomCore(chunk, (chunkPosition * 16) + 7)
-        val data = RoomData.getRoomData(core) ?: return devMessage("Unknown room data for core: $core $chunkPosition")
+        val data = RoomData.getRoomData(core) ?: run {
+            if (core == -318865360) return
+            else return devMessage("Unknown room data for core: $core $chunkPosition")
+        }
 
         val tilePosition = (chunkPosition / 2) + 6
         val tile = DungeonScan.tiles.getOrNull(tilePosition.x + (tilePosition.z * 6)) ?: return
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
index 58770ca..3920671 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
@@ -1,14 +1,20 @@
 package com.odtheking.odin.features.impl.dungeon.map.tile
 
+import com.odtheking.odin.utils.Color
+import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.IVec2
 
 class DungeonDoor(
     val position: IVec2,
     val rotation: DoorRotation,
-    var type: DoorType
+    var type: DoorType,
+    var color: Color = Colors.WHITE,
 ) {
-    val originTileIndex by lazy { position.x + position.z * 6 }
-    val destinationTileIndex by lazy { position.x + rotation.offset.x + (position.z + rotation.offset.z) * 6 }
+    val originTileIndex = position.x + position.z * 6
+    val destinationTileIndex = position.x + rotation.offset.x + (position.z + rotation.offset.z) * 6
+
+    val worldX = (position.x - 6) * 32 + 7 + rotation.offset.x * 16
+    val worldZ = (position.z - 6) * 32 + 7 + rotation.offset.z * 16
 }
 
 enum class DoorType {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
index bd0b929..13c435c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
@@ -2,12 +2,15 @@ package com.odtheking.odin.features.impl.dungeon.map.tile
 
 import com.odtheking.odin.OdinMod.mc
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints
+import com.odtheking.odin.features.impl.dungeon.map.DungeonScan
 import com.odtheking.odin.utils.IVec2
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.rotateAroundNorth
 import com.odtheking.odin.utils.rotateToNorth
+import com.odtheking.odin.utils.sortKey
 import net.minecraft.core.BlockPos
 import net.minecraft.world.level.block.Blocks
+import kotlin.math.roundToInt
 
 class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData? = null) {
     val tiles: ArrayList<IVec2> = ArrayList(4)
@@ -25,6 +28,9 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
 
     var isKnown1x1: Boolean = false
 
+    var center: IVec2? = null
+        private set
+
     val isViewable: Boolean get() = walkedInto || checkmark != MapCheckmark.UNDISCOVERED
     val name: String? get() = data?.name
 
@@ -33,7 +39,37 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
             tiles.add(segment.position)
             topLeft = IVec2(tiles.minOf { it.x }, tiles.minOf { it.z })
             highestBlock?.let { if (tiles.size == data?.shape?.tileAmount) inferLayout(it) }
+            recalculateCenter()
+        }
+    }
+
+    private fun recalculateCenter() {
+        if (tiles.isEmpty()) return
+        val rs = DungeonScan.roomSize.takeIf { it != -1 } ?: return
+
+        val rg = DungeonScan.roomGap
+        val half = rs / 2
+
+        fun tileCenter(tile: IVec2) = tile.x * rg + half to tile.z * rg + half
+
+        val rot = rotation
+        if (rot == null) {
+            val tile = tiles.minBy { it.sortKey }
+            val (x, z) = tileCenter(tile)
+            center = IVec2(x, z)
+            return
+        }
+
+        val centers = tiles.map { tileCenter(it) }
+        val x = (centers.minOf { it.first } + centers.maxOf { it.first }) / 2f
+        val z = when (shape) {
+            RoomShape.L ->
+                if (rot == RoomRotation.NORTH || rot == RoomRotation.WEST) centers.minOf { it.second }.toFloat()
+                else centers.maxOf { it.second }.toFloat()
+            else ->
+                (centers.minOf { it.second } + centers.maxOf { it.second }) / 2f
         }
+        center = IVec2(x.roundToInt(), z.roundToInt())
     }
 
     fun inferLayoutFromMap() {
@@ -78,8 +114,7 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
     }
 
     private fun resolveGeometryRotation(): RoomRotation? {
-        val bottomRight = tiles.maxByOrNull { it.x * 1000 + it.z } ?: return null
-        val topLeft = tiles.minByOrNull { it.x * 1000 + it.z } ?: return null
+        val bottomRight = tiles.maxByOrNull { it.sortKey } ?: return null
 
         return if (shape == RoomShape.L) {
             val other = tiles.find { it != topLeft && it != bottomRight } ?: return null
@@ -95,8 +130,7 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
     }
 
     private fun resolveClayPos(rotation: RoomRotation, height: Int): BlockPos? {
-        val bottomRight = tiles.maxByOrNull { it.x * 1000 + it.z } ?: return null
-        val topLeft = tiles.minByOrNull { it.x * 1000 + it.z } ?: return null
+        val bottomRight = tiles.maxByOrNull { it.sortKey } ?: return null
 
         val (tlX, tlZ) = getRealPosition(topLeft.x, topLeft.z)
         val (brX, brZ) = getRealPosition(bottomRight.x, bottomRight.z)
@@ -145,8 +179,7 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
         return BlockPos(x + rotation.dx, y, z + rotation.dz)
     }
 
-    fun getRealPosition() = topLeft.x * 32 - 185 to topLeft.z * 32 - 185
-    fun getRealPosition(x: Int, z: Int) = x * 32 - 185 to z * 32 - 185
+    fun getRealPosition(x: Int = topLeft.x, z: Int = topLeft.z) = x * 32 - 185 to z * 32 - 185
 
     fun getRelativeCoords(pos: BlockPos): BlockPos {
         val clay = clayPos ?: return BlockPos.ZERO
diff --git a/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt b/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt
index 1e8239d..07ee1c0 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt
@@ -57,4 +57,6 @@ value class IVec2 private constructor(val data: Long) {
 
 fun ChunkPos.toIVec2(): IVec2 {
     return IVec2(x, z)
-}
\ No newline at end of file
+}
+
+val IVec2.sortKey: Int get() = x * 1000 + z
\ No newline at end of file
Fixed bat slain not being detected Fixed MapInfo text space issueodtheking · 2026-07-16 · b0bb9a1
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt | 19 ++++++++-----------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt   |  3 ++-
 2 files changed, 10 insertions(+), 12 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index 158e989..492252e 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -80,7 +80,7 @@ object MapInfo : Module(
 
         val unknownSecretsText = if (unknown == 0) {
             buildString {
-                append("§7D: §c")
+                append("§7Deaths: §c")
                 append(colorizeDeaths(cachedDeathCount))
             }
         } else {
@@ -91,12 +91,9 @@ object MapInfo : Module(
         }
 
         val mimicText = buildString {
-            append("§7M: ")
-            append(if (mimicKilled) "§a✔" else "§c✘")
-            append(" §8| §7P: ")
-            append(if (princeKilled) "§a✔" else "§c✘")
-            append(" §8| §7B: ")
-            append(if (batKilled) "§a✔" else "§c✘")
+            append("${if (mimicKilled) "§a" else "§c"}\uD83D\uDCE6")
+            append(" §8| ${if (princeKilled) "§a" else "§c"}\uD83E\uDD34")
+            append(" §8| ${if (batKilled) "§a" else "§c"}\uD83E\uDD87")
         }
 
         val cryptText = buildString {
@@ -261,20 +258,20 @@ object MapInfo : Module(
 
         on<RoomEnterEvent> {
             currentRoomSecrets = null
-            if (room?.data?.type == RoomType.BLOOD) {
+            if (room?.name == "Blood")
                 portalAABB = AABB.encapsulatingFullBlocks(room.getRealCoords(BlockPos(16, 69, 29)), room.getRealCoords(BlockPos(14, 69, 29))).inflate(0.0, 4.0, 0.0)
-            }
         }
 
         on<RenderEvent.Extract> {
-            if (!highlightPortal || !DungeonUtils.inClear || DungeonUtils.score < 300) return@on
-            portalAABB?.let{ pos ->
+            if (!highlightPortal || !DungeonUtils.inClear || cachedScore < 300) return@on
+            portalAABB?.let { pos ->
                 drawFilledBox(pos, Colors.MINECRAFT_GREEN.withAlpha(0.5f), depth = true)
             }
         }
 
         on<LevelEvent.Load> {
             shownTitle = false
+            portalAABB = null
         }
     }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
index fee6bb2..1c02e1c 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
@@ -42,11 +42,12 @@ object Mimic : Module(
 
         on<ChatPacketEvent> {
             if (value.matches(princeRegex)) princeKilled()
+            if (value.matches(batRegex)) batKilled()
         }
     }
 
     private fun mimicKilled() {
-        if (DungeonUtils.mimicKilled || DungeonUtils.inBoss) return
+        if (DungeonUtils.mimicKilled || !DungeonUtils.inClear) return
         if (mimicMessageToggle) sendCommand("pc Mimic Killed!")
         DungeonListener.dungeonStats.mimicKilled = true
     }
Fixed PositionalMessages radius being incorrectodtheking · 2026-07-16 · be32e42
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
index 588d318..c0a67f8 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/PositionalMessages.kt
@@ -84,9 +84,10 @@ object PositionalMessages : Module(
     private fun handleAtString(posMessage: PosMessage) {
         val msgSent = sentMessages.getOrDefault(posMessage, false)
         val player = mc.player ?: return
-        if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= (posMessage.distance ?: return)) {
+        val radius = posMessage.distance ?: return
+        if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= radius * radius) {
             if (!msgSent) Timer().schedule(posMessage.delay) {
-                if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= posMessage.distance)
+                if (player.distanceToSqr(posMessage.x, posMessage.y, posMessage.z) <= radius * radius)
                     sendCommand("pc ${posMessage.message}")
             }
             sentMessages[posMessage] = true
Fixed PlayerDisplay element hiderodtheking · 2026-07-16 · d91b520
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt |  8 ++++----
 src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt               | 16 +++-------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt         |  2 +-
 3 files changed, 8 insertions(+), 18 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
index 0ba5494..386e377 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PlayerDisplay.kt
@@ -111,10 +111,10 @@ object PlayerDisplay : Module(
     }
     private val speedColor by ColorSetting("Speed color", Colors.WHITE, true, "The color of the speed text.")
 
-    private val HEALTH_REGEX = Regex("[\\d|,]+/[\\d|,]+❤")
-    private val MANA_REGEX = Regex("[\\d|,]+/[\\d|,]+✎( Mana)?")
-    private val OVERFLOW_MANA_REGEX = Regex("§?[\\d|,]+ʬ")
-    private val DEFENSE_REGEX = Regex("[\\d|,]+§a❈ Defense")
+    private val HEALTH_REGEX = Regex("[\\d|,]+/[\\d|,]+\\uE010")
+    private val MANA_REGEX = Regex("[\\d|,]+/[\\d|,]+\\uE003( Mana)?")
+    private val OVERFLOW_MANA_REGEX = Regex("§?[\\d|,]+\\uE017")
+    private val DEFENSE_REGEX = Regex("[\\d|,]+§a\\uE008 Defense")
 
     @JvmStatic
     fun modifyText(text: Component): Component {
diff --git a/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
index 9b46988..a422bd5 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
@@ -79,7 +79,7 @@ object WebUtils {
                 if (!cont.isActive) return@whenComplete
 
                 if (response.statusCode() in 200..299) cont.resume(Result.success(response))
-                else cont.resume(Result.failure(InputStreamException(response.statusCode(), response.body())))
+                else cont.resume(Result.failure(InputStreamException(response.body(), request.uri().toString())))
             }
         }
     }
@@ -90,15 +90,5 @@ object WebUtils {
         return mayor.get("name")?.asString == "Paul" && mayor.getAsJsonArray("perks")?.any { it.asJsonObject.get("name")?.asString == "EZPZ" } == true
     }
 
-    class InputStreamException(
-        code: Int,
-        body: String
-    ) : Exception(
-        buildString {
-            append(code)
-            if (body.isNotBlank()) {
-                append(": ")
-                append(body)
-            }
-        }
-    )}
\ No newline at end of file
+    class InputStreamException(body: String, link: String) : Exception("$body : $link")
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
index f5997b6..151b243 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SplitsManager.kt
@@ -196,4 +196,4 @@ private val dungeonSplits = listOf(
 
 // https://regex101.com/r/BXKhOI/1
 private val BLOOD_OPEN_REGEX = Regex("^\\[BOSS] The Watcher: (Congratulations, you made it through the Entrance\\.|Ah, you've finally arrived\\.|Ah, we meet again\\.\\.\\.|So you made it this far\\.\\.\\. interesting\\.|You've managed to scratch and claw your way here, eh\\?|I'm starting to get tired of seeing you around here\\.\\.\\.|Oh\\.\\. hello\\?|Things feel a little more roomy now, eh\\?)$|^The BLOOD DOOR has been opened!$")
-private val MORT_REGEX = Regex("\\[NPC] Mort: Here, I found this map when I first entered the dungeon\\.|\\[NPC] Mort: Right-click the Orb for spells, and Left-click \\(or Drop\\) to use your Ultimate!")
\ No newline at end of file
+val MORT_REGEX = Regex("\\[NPC] Mort: Here, I found this map when I first entered the dungeon\\.|\\[NPC] Mort: Right-click the Orb for spells, and Left-click \\(or Drop\\) to use your Ultimate!")
\ No newline at end of file
Fixed secrets spawn timerodtheking · 2026-07-16 · 4e5cede
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt | 52 +++++++++++++++++++++++++---------------------------
 1 file changed, 25 insertions(+), 27 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 54a48ae..962d9e5 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -11,6 +11,8 @@ import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.textDim
+import com.odtheking.odin.utils.skyblock.MORT_REGEX
+import com.odtheking.odin.utils.skyblock.SplitsManager
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import com.odtheking.odin.utils.toFixed
 import net.minecraft.network.protocol.game.ClientboundSetTimePacket
@@ -80,21 +82,21 @@ object TickTimers : Module(
     }
 
     private var outboundsTime = -1
+    private var secretsCounter = 0
 
     private val secretsHud by HUD("Secrets Hud", "Displays a timer for secret spawn ticks.") {
-        if (it)                    textDim(formatTimer(15, 20, "§7Secret:"), 0, 0, Colors.MINECRAFT_DARK_RED)
-        else if (secretsTime >= 0) textDim(formatTimer(secretsTime, 20, "§7Secret:"), 0, 0, Colors.MINECRAFT_DARK_RED)
+        if (it) textDim(formatTimer(15, 20, "§7Secret:"), 0, 0, Colors.MINECRAFT_DARK_RED)
+        else if (DungeonUtils.openRoomCount != 0) textDim(formatTimer(20 - secretsCounter % 20, 20, "§7Secret:"), 0, 0, Colors.MINECRAFT_DARK_RED)
         else 0 to 0
     }
 
-    private var secretsTime = -1
-
     init {
         on<ChatPacketEvent> {
             when {
-                necronHud.enabled && value.matches(necronRegex) -> necronTime = 60
-                goldorHud.enabled && value.matches(goldorRegex) -> goldorTickTime = 60
-                goldorHud.enabled && value.matches(coreOpeningRegex) -> {
+                value.matches(MORT_REGEX) -> secretsCounter = 0
+                value.matches(necronRegex) -> necronTime = 60
+                value.matches(goldorRegex) -> goldorTickTime = 60
+                value.matches(coreOpeningRegex) -> {
                     goldorStartTime = -1
                     goldorTickTime = -1
                 }
@@ -102,11 +104,11 @@ object TickTimers : Module(
                     if (goldorHud.enabled) goldorStartTime = 104
                     if (stormHud.enabled) padTickTime = -1
                 }
-                (stormHud.enabled || lightningHud.enabled) && value.matches(stormPadRegex) -> {
-                    if (stormHud.enabled) padTickTime = 20
-                    if (lightningHud.enabled) lightningTickTime = 560
+                value.matches(stormPadRegex) -> {
+                    padTickTime = 20
+                    lightningTickTime = 560
                 }
-                pyHud.enabled && !pyTriggered && value.matches(stormPyRegex) -> {
+                !pyTriggered && value.matches(stormPyRegex) -> {
                     pyTriggered = true
                     pyTickTime = 95
                 }
@@ -115,29 +117,25 @@ object TickTimers : Module(
 
         on<TickEvent.Server> {
             if (!DungeonUtils.inDungeons) return@on
-            if (outboundsTime == 0 && outboundsHud.enabled) outboundsTime = 40
-            if (outboundsTime >= 0 && outboundsHud.enabled) outboundsTime--
-            if (secretsTime == 0 && secretsHud.enabled && !DungeonUtils.inBoss) secretsTime = 20
-            if (secretsTime >= 0 && secretsHud.enabled) secretsTime--
+            if (outboundsTime == 0) outboundsTime = 40
+            if (outboundsTime >= 0) outboundsTime--
+            secretsCounter++
             if (!DungeonUtils.inBoss) return@on
             if (goldorTickTime == 0 && goldorStartTime <= 0 && goldorHud.enabled) goldorTickTime = 60
-            if (goldorStartTime >= 0 && goldorHud.enabled) goldorStartTime--
-            if (goldorTickTime >= 0 && goldorHud.enabled) goldorTickTime--
+            if (goldorStartTime >= 0) goldorStartTime--
+            if (goldorTickTime >= 0) goldorTickTime--
             if (padTickTime == 0 && stormHud.enabled) padTickTime = 20
-            if (padTickTime >= 0 && stormHud.enabled) padTickTime--
-            if (lightningTickTime >= 0 && lightningHud.enabled) lightningTickTime--
-            if (pyTickTime >= 0 && pyHud.enabled) pyTickTime--
-            if (necronTime >= 0 && necronHud.enabled) necronTime--
+            if (padTickTime >= 0) padTickTime--
+            if (lightningTickTime >= 0) lightningTickTime--
+            if (pyTickTime >= 0) pyTickTime--
+            if (necronTime >= 0) necronTime--
         }
 
         onReceive<ClientboundSetTimePacket> {
             if (!DungeonUtils.inClear) return@onReceive
             if (DungeonUtils.openRoomCount == 0) {
                 if (outboundsHud.enabled) outboundsTime = 40 - (gameTime % 40).toInt()
-            } else {
-                if (secretsHud.enabled) secretsTime = 30 - (gameTime % 20).toInt()
-                outboundsTime = -1
-            }
+            } else outboundsTime = -1
         }
 
         on<LevelEvent.Load> {
@@ -148,8 +146,8 @@ object TickTimers : Module(
             pyTickTime = -1
             pyTriggered = false
             necronTime = -1
-            secretsTime = -1
             outboundsTime = -1
+            secretsCounter = 0
         }
     }
 
@@ -162,4 +160,4 @@ object TickTimers : Module(
         val timeDisplay = if (displayInTicks) "$time${if (symbolDisplay) "t" else ""}" else "${(time / 20f).toFixed()}${if (symbolDisplay) "s" else ""}"
         return "${if (showPrefix) "$prefix " else ""}$color$timeDisplay"
     }
-}
+}
\ No newline at end of file
Cleaned up map implodtheking · 2026-07-14 · ea08835
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt       |  17 +++++++++--------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt      |   2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt      |  42 +++++++++++++++++++++---------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt          |   7 ++-----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt        |  11 +++++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt |   9 +++++----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt |   3 +--
 src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt                            |   2 +-
 src/main/kotlin/com/odtheking/odin/utils/IVec2.kt                                |  12 ++++++++----
 src/main/resources/assets/odin/map/{x.png => cross.png}                          | Bin
 src/main/resources/assets/odin/map/{green.png => green_check.png}                | Bin
 src/main/resources/assets/odin/map/marker.png                                    | Bin 236 -> 0 bytes
 src/main/resources/assets/odin/map/{white.png => white_check.png}                | Bin
 src/main/resources/assets/odin/textures/map/cross.png                            | Bin 205 -> 0 bytes
 src/main/resources/assets/odin/textures/map/green_check.png                      | Bin 188 -> 0 bytes
 src/main/resources/assets/odin/textures/map/question.png                         | Bin 162 -> 0 bytes
 src/main/resources/assets/odin/textures/map/white_check.png                      | Bin 186 -> 0 bytes
 17 files changed, 55 insertions(+), 50 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index f2cf26b..96a92b0 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -7,6 +7,7 @@ import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
 import com.odtheking.odin.clickgui.settings.impl.NumberSetting
 import com.odtheking.odin.features.Module
 import com.odtheking.odin.utils.Color
+import com.odtheking.odin.utils.Color.Companion.darker
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.render.hollowFill
@@ -14,10 +15,9 @@ import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.client.gui.GuiGraphicsExtractor
 
 object DungeonMap : Module(
-    "Dungeon Map",
+    name = "Dungeon Map",
     description = "Displays the dungeon map."
 ) {
-
     private val disableBoss by BooleanSetting("Disable in Boss", true, desc = "Disables the map during boss fights.")
 
     val backgroundOutline by ColorSetting("Background Outline", Colors.BLACK, true, desc = "The color of the background border.")
@@ -28,12 +28,6 @@ object DungeonMap : Module(
     val playerNamesScaling by NumberSetting("Player Names Scaling", 0.75f, 0.1f, 2f, 0.05f, desc = "Scale of player name labels.").withDependency { playerDropdown }
     val playerNameColor by ColorSetting("Player Name Color", Color(70, 70, 70), true, desc = "Color of player name labels.").withDependency { playerDropdown }
 
-    private val doorDropdown by DropdownSetting("Door Settings")
-    val normalDoorColor by ColorSetting("Normal Door", Color(107, 58, 17), desc = "Color of normal doors.").withDependency { doorDropdown }
-    val witherDoorColor by ColorSetting("Wither Door", Colors.BLACK, true, desc = "Color of wither doors.").withDependency { doorDropdown }
-    val bloodDoorColor by ColorSetting("Blood Door", Color(231, 0, 0), true, desc = "Color of blood room doors.").withDependency { doorDropdown }
-    val fairyDoorColor by ColorSetting("Fairy Door", Color(224, 0, 255), true, desc = "Color of fairy room doors.").withDependency { doorDropdown }
-
     private val roomDropdown by DropdownSetting("Room Settings")
     val normalRoomColor by ColorSetting("Normal Room", Color(107, 58, 17), true, desc = "Color of normal rooms.").withDependency { roomDropdown }
     val puzzleRoomColor by ColorSetting("Puzzle Room", Color(117, 0, 133), true, desc = "Color of puzzle rooms.").withDependency { roomDropdown }
@@ -44,6 +38,13 @@ object DungeonMap : Module(
     val championRoomColor by ColorSetting("Champion Room", Color(254, 223, 0), true, desc = "Color of champion rooms.").withDependency { roomDropdown }
     val unknownRoomColor by ColorSetting("Unknown Room", Color(40, 40, 40), true, desc = "Color of unknown rooms hinted by a door with no discovered room on the other side.").withDependency { roomDropdown }
 
+    private val doorDropdown by DropdownSetting("Door Settings")
+    val normalDoorColor by ColorSetting("Normal Door", Color(107, 58, 17).darker(), desc = "Color of normal doors.").withDependency { doorDropdown }
+    val witherDoorColor by ColorSetting("Wither Door", Colors.BLACK, true, desc = "Color of wither doors.").withDependency { doorDropdown }
+    val bloodDoorColor by ColorSetting("Blood Door", Color(231, 0, 0).darker(), true, desc = "Color of blood room doors.").withDependency { doorDropdown }
+    val fairyDoorColor by ColorSetting("Fairy Door", Color(224, 0, 255).darker(), true, desc = "Color of fairy room doors.").withDependency { doorDropdown }
+    val unknownDoorColor by ColorSetting("Unknown Door", Color(40, 40, 40).darker(), true, desc = "Color of doors with no discovered room on the other side.").withDependency { doorDropdown }
+
     val disablePred by BooleanSetting("Disable Prediction", false, desc = "Disables special-column room type prediction.")
 
     private val mapHud by HUD("Dungeon Map", "Displays the dungeon map.", false) { example ->
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
index fb76cb2..e9bb143 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt
@@ -103,7 +103,7 @@ object DungeonScan {
                 DoorType.Blood -> DungeonMap.bloodDoorColor
                 DoorType.Fairy -> DungeonMap.fairyDoorColor
                 else -> {
-                    if (!originView || !destView) DungeonMap.unknownRoomColor
+                    if (!originView || !destView) DungeonMap.unknownDoorColor
                     else listOfNotNull(originTile.room, destTile.room)
                         .firstOrNull { it.type != RoomType.NORMAL && it.type != RoomType.FAIRY }
                         ?.type?.let { room -> roomTypeColor(room) } ?: DungeonMap.normalDoorColor
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
index 7eb4f5d..68ca5ab 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt
@@ -11,21 +11,21 @@ import net.minecraft.client.renderer.RenderPipelines
 import net.minecraft.resources.Identifier
 
 private val marker = Identifier.withDefaultNamespace("textures/map/decorations/frame.png")
-private val cross = Identifier.fromNamespaceAndPath("odin", "textures/map/cross.png")
-private val green = Identifier.fromNamespaceAndPath("odin", "textures/map/green_check.png")
-private val white = Identifier.fromNamespaceAndPath("odin", "textures/map/white_check.png")
-private val question = Identifier.fromNamespaceAndPath("odin", "textures/map/question.png")
+private val cross = Identifier.fromNamespaceAndPath("odin", "map/cross.png")
+private val green = Identifier.fromNamespaceAndPath("odin", "map/green_check.png")
+private val white = Identifier.fromNamespaceAndPath("odin", "map/white_check.png")
+private val question = Identifier.fromNamespaceAndPath("odin", "map/question.png")
 
 internal fun GuiGraphicsExtractor.renderMap() {
     pose().pushMatrix()
     pose().translate(DungeonScan.startX.toFloat(), DungeonScan.startY.toFloat())
 
+    renderDoors()
+
     for (room in DungeonScan.rooms) {
         if (room.isViewable) fillRoom(room, roomTypeColor(room.type).rgba, DungeonScan.roomSize, DungeonScan.roomGap)
     }
 
-    renderDoors()
-
     for (room in DungeonScan.rooms) renderRoomText(room)
 
     renderPlayers()
@@ -92,15 +92,15 @@ private fun GuiGraphicsExtractor.renderDoors() {
 
             2 -> {
                 val half = rs / 2
-                fill(x, y, x + half, y + rs, colors[0].rgba)
-                fill(x + half, y, x + rs, y + rs, colors[1].rgba)
+                fill(x, y, x + half, y + rs, colors[0].darker(0.5f).rgba)
+                fill(x + half, y, x + rs, y + rs, colors[1].darker(0.5f).rgba)
             }
 
             3 -> {
                 val third = rs / 3
-                fill(x, y, x + third, y + rs, colors[0].rgba)
-                fill(x + third, y, x + third * 2, y + rs, colors[1].rgba)
-                fill(x + third * 2, y, x + rs, y + rs, colors[2].rgba)
+                fill(x, y, x + third, y + rs, colors[0].darker(0.5f).rgba)
+                fill(x + third, y, x + third * 2, y + rs, colors[1].darker(0.5f).rgba)
+                fill(x + third * 2, y, x + rs, y + rs, colors[2].darker(0.5f).rgba)
             }
         }
         renderIcon(IVec2(x, y), question)
@@ -108,8 +108,8 @@ private fun GuiGraphicsExtractor.renderDoors() {
 }
 
 fun GuiGraphicsExtractor.renderIcon(pos: IVec2, identifier: Identifier) {
-    val rs = DungeonScan.roomSize
-    blit(RenderPipelines.GUI_TEXTURED, identifier, pos.x, pos.z, rs.toFloat(), rs.toFloat(), rs, rs, rs, rs)
+    val rs = DungeonScan.roomSize - 4
+    blit(RenderPipelines.GUI_TEXTURED, identifier, pos.x + 2, pos.z + 2, rs.toFloat(), rs.toFloat(), rs, rs, rs, rs)
 }
 
 private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
@@ -136,7 +136,7 @@ private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
         else               -> Color(100, 100, 100)
     }.rgba
 
-    val lines  = scannedRoom?.name?.split(" ") ?: return
+    val lines  = scannedRoom.name?.split(" ") ?: return
     val totalH = (lines.size - 1) * fontH * DungeonMap.textScaling
 
     for ((i, line) in lines.withIndex()) {
@@ -151,13 +151,6 @@ private fun GuiGraphicsExtractor.renderRoomText(room: DungeonRoom) {
 private fun GuiGraphicsExtractor.renderPlayers() {
     val showNames = mc.player?.mainHandItem?.itemId?.equalsOneOf("INFINITE_SPIRIT_LEAP", "SPIRIT_LEAP") == true
 
-    pose().pushMatrix()
-    val (selfX, selfZ) = DungeonScan.playerRenderPosition(mc.player, IVec2(0, 0))
-    pose().translate(selfX, selfZ)
-    pose().rotate(Math.toRadians(180.0 + (mc.player?.yRot ?: 0f)).toFloat())
-    blit(RenderPipelines.GUI_TEXTURED, marker, -2, -3, 2f, 0f, 5, 7, 8, 8)
-    pose().popMatrix()
-
     for (player in DungeonUtils.dungeonTeammatesNoSelf) {
         if (player.isDead) continue
         val (px, pz) = DungeonScan.playerRenderPosition(player.entity, player.mapPos)
@@ -181,6 +174,13 @@ private fun GuiGraphicsExtractor.renderPlayers() {
 
         pose().popMatrix()
     }
+
+    pose().pushMatrix()
+    val (selfX, selfZ) = DungeonScan.playerRenderPosition(mc.player, IVec2(0, 0))
+    pose().translate(selfX, selfZ)
+    pose().rotate(Math.toRadians(180.0 + (mc.player?.yRot ?: 0f)).toFloat())
+    blit(RenderPipelines.GUI_TEXTURED, marker, -2, -3, 2f, 0f, 5, 7, 8, 8)
+    pose().popMatrix()
 }
 
 private fun textCenter(room: DungeonRoom): Pair<Float, Float> {
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
index 6cd0740..3a887b3 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt
@@ -209,11 +209,8 @@ object MapScan {
     }
 
     private fun addOrFixDoor(position: IVec2, rotation: DoorRotation, doorType: DoorType) {
-        val chunkPos = IVec2(-12 + 2 * position.x + rotation.offset.x, -12 + 2 * position.z + rotation.offset.z)
-        val originIndex = position.x + position.z * 6
-        val destPos = IVec2(position.x + rotation.offset.x, position.z + rotation.offset.z)
-        val destIndex = destPos.x + destPos.z * 6
-        DungeonScan.doors.getOrPut(chunkPos) { DungeonDoor(position, rotation, doorType, originIndex, destIndex) }.type = doorType
+        DungeonScan.doors.getOrPut(IVec2(-12 + 2 * position.x + rotation.offset.x, -12 + 2 * position.z + rotation.offset.z))
+        { DungeonDoor(position, rotation, doorType) }.type = doorType
     }
 
     private fun getPx(colors: ByteArray, x: Int, z: Int): Byte =
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
index f7c9bc4..e4ef726 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
@@ -86,7 +86,11 @@ object WorldScan {
         val rowEven = chunkPosition.x % 2 == 0
 
         if (chunkPosition.x in -12..-2 && chunkPosition.z in -12..-2) {
-            if (rowEven && columnEven) scanRoom(chunk, chunkPosition)
+            if (rowEven && columnEven) {
+                val tilePos = (chunkPosition / 2) + 6
+                val tile = DungeonScan.tiles.getOrNull(tilePos.x + tilePos.z * 6)
+                if (tile?.room?.data == null) scanRoom(chunk, chunkPosition)
+            }
             else if (rowEven || columnEven) scanDoor(chunk, chunkPosition, if (columnEven) DoorRotation.Horizontal else DoorRotation.Vertical)
         }
     }
@@ -106,7 +110,7 @@ object WorldScan {
             else -> DoorType.Normal
         }
         val doorPos = ((chunkPosition - 1) / 2) + 6
-        DungeonScan.doors[chunkPosition] = DungeonDoor(doorPos, rotation, type, doorPos.x + doorPos.z * 6, doorPos.x + rotation.offset.x + (doorPos.z + rotation.offset.z) * 6)
+        DungeonScan.doors[chunkPosition] = DungeonDoor(doorPos, rotation, type)
     }
 
     private fun scanRoom(chunk: LevelChunk, chunkPosition: IVec2) {
@@ -126,6 +130,7 @@ object WorldScan {
         }
 
         if (tile.room !== room) {
+            room.highestBlock = highestBlock
             tile.room = room
             room.addSegment(tile)
         }
@@ -134,8 +139,6 @@ object WorldScan {
             room.data = data
             room.type = data.type
         }
-
-        if (room.tiles.size == data.shape.tileAmount) room.inferLayout(highestBlock)
     }
 
     private val stringBuilder = StringBuilder(1024)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
index fe232c8..58770ca 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt
@@ -5,10 +5,11 @@ import com.odtheking.odin.utils.IVec2
 class DungeonDoor(
     val position: IVec2,
     val rotation: DoorRotation,
-    var type: DoorType,
-    val originTileIndex: Int,
-    val destinationTileIndex: Int,
-)
+    var type: DoorType
+) {
+    val originTileIndex by lazy { position.x + position.z * 6 }
+    val destinationTileIndex by lazy { position.x + rotation.offset.x + (position.z + rotation.offset.z) * 6 }
+}
 
 enum class DoorType {
     Normal,
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
index ef6d1a0..bd0b929 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt
@@ -32,6 +32,7 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
         if (!tiles.contains(segment.position)) {
             tiles.add(segment.position)
             topLeft = IVec2(tiles.minOf { it.x }, tiles.minOf { it.z })
+            highestBlock?.let { if (tiles.size == data?.shape?.tileAmount) inferLayout(it) }
         }
     }
 
@@ -53,8 +54,6 @@ class DungeonRoom(var type: RoomType, initialPosition: IVec2, var data: RoomData
     }
 
     fun inferLayout(highestBlock: Int) {
-        this.highestBlock = highestBlock
-
         val roomData = data ?: return
 
         if (applyFairyFallback(highestBlock)) return
diff --git a/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt
index c790022..738522a 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/ChatUtils.kt
@@ -27,7 +27,7 @@ fun modMessage(message: Component, prefix: String = "§3Odin §8»§r ", chatSty
 }
 
 fun devMessage(message: Any?) {
-    if (!ClickGUIModule.devMessage) return
+    if (!ClickGUIModule.devMessage) return println(message)
     modMessage(message, "§3Odin§bDev §8»§r ")
 }
 
diff --git a/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt b/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt
index 93b251e..1e8239d 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/IVec2.kt
@@ -30,10 +30,18 @@ value class IVec2 private constructor(val data: Long) {
         return IVec2(x + rhs, z + rhs)
     }
 
+    operator fun plus(rhs: IVec2): IVec2 {
+        return IVec2(x + rhs.x, z + rhs.z)
+    }
+
     operator fun minus(rhs: Int): IVec2 {
         return IVec2(x - rhs, z - rhs)
     }
 
+    operator fun minus(rhs: IVec2): IVec2 {
+        return IVec2(x - rhs.x, z - rhs.z)
+    }
+
     operator fun times(rhs: Int): IVec2 {
         return IVec2(x * rhs, z * rhs)
     }
@@ -45,10 +53,6 @@ value class IVec2 private constructor(val data: Long) {
     override fun toString(): String {
         return "IVec2(x=$x, z=$z)"
     }
-
-    fun flip(): IVec2 {
-        return IVec2(z, x)
-    }
 }
 
 fun ChunkPos.toIVec2(): IVec2 {
diff --git a/src/main/resources/assets/odin/map/x.png b/src/main/resources/assets/odin/map/cross.png
similarity index 100%
rename from src/main/resources/assets/odin/map/x.png
rename to src/main/resources/assets/odin/map/cross.png
diff --git a/src/main/resources/assets/odin/map/green.png b/src/main/resources/assets/odin/map/green_check.png
similarity index 100%
rename from src/main/resources/assets/odin/map/green.png
rename to src/main/resources/assets/odin/map/green_check.png
diff --git a/src/main/resources/assets/odin/map/marker.png b/src/main/resources/assets/odin/map/marker.png
deleted file mode 100644
index 00edd7c..0000000
Binary files a/src/main/resources/assets/odin/map/marker.png and /dev/null differ
diff --git a/src/main/resources/assets/odin/map/white.png b/src/main/resources/assets/odin/map/white_check.png
similarity index 100%
rename from src/main/resources/assets/odin/map/white.png
rename to src/main/resources/assets/odin/map/white_check.png
diff --git a/src/main/resources/assets/odin/textures/map/cross.png b/src/main/resources/assets/odin/textures/map/cross.png
deleted file mode 100644
index 5099d32..0000000
Binary files a/src/main/resources/assets/odin/textures/map/cross.png and /dev/null differ
diff --git a/src/main/resources/assets/odin/textures/map/green_check.png b/src/main/resources/assets/odin/textures/map/green_check.png
deleted file mode 100644
index a17ca5a..0000000
Binary files a/src/main/resources/assets/odin/textures/map/green_check.png and /dev/null differ
diff --git a/src/main/resources/assets/odin/textures/map/question.png b/src/main/resources/assets/odin/textures/map/question.png
deleted file mode 100644
index f9278b0..0000000
Binary files a/src/main/resources/assets/odin/textures/map/question.png and /dev/null differ
diff --git a/src/main/resources/assets/odin/textures/map/white_check.png b/src/main/resources/assets/odin/textures/map/white_check.png
deleted file mode 100644
index 8aa2f15..0000000
Binary files a/src/main/resources/assets/odin/textures/map/white_check.png and /dev/null differ
Added bat slain support to Mimic moduleodtheking · 2026-07-14 · 188adea
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt          | 12 +++++++++---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt            | 12 +++++++++++-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt |  4 ++++
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt    |  4 ++++
 4 files changed, 28 insertions(+), 4 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index 94d8c80..158e989 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -41,6 +41,7 @@ object MapInfo : Module(
     private var cachedKnownSecrets = 0
     private var cachedMimicKilled = false
     private var cachedPrinceKilled = false
+    private var cachedBatKilled = false
     private var cachedCryptCount = 0
     private var cachedDeathCount = 0
 
@@ -53,6 +54,7 @@ object MapInfo : Module(
         val totalSecrets = cachedTotalSecrets
         val mimicKilled = cachedMimicKilled
         val princeKilled = cachedPrinceKilled
+        val batKilled = cachedBatKilled
         val cryptCount = cachedCryptCount
 
         val showRemaining = fullAddRemaining && alternate
@@ -78,7 +80,7 @@ object MapInfo : Module(
 
         val unknownSecretsText = if (unknown == 0) {
             buildString {
-                append("§7Deaths: §c")
+                append("§7D: §c")
                 append(colorizeDeaths(cachedDeathCount))
             }
         } else {
@@ -93,6 +95,8 @@ object MapInfo : Module(
             append(if (mimicKilled) "§a✔" else "§c✘")
             append(" §8| §7P: ")
             append(if (princeKilled) "§a✔" else "§c✘")
+            append(" §8| §7B: ")
+            append(if (batKilled) "§a✔" else "§c✘")
         }
 
         val cryptText = buildString {
@@ -169,12 +173,13 @@ object MapInfo : Module(
 
     private val compactScore: HudElement by HUD("Compact Score", "Displays a compact score hud with score info.") {
         if ((!DungeonUtils.inDungeons || (disableInBoss && DungeonUtils.inBoss)) && !it) return@HUD 0 to 0
-
         val score = cachedScore
         val mimicKilled = cachedMimicKilled
         val princeKilled = cachedPrinceKilled
+        val batKilled = cachedBatKilled
+
+        val missing = (if (mimicKilled) 0 else 2) + (if (princeKilled) 0 else 1) + (if (batKilled) 0 else 1)
 
-        val missing = (if (mimicKilled) 0 else 2) + (if (princeKilled) 0 else 1)
         val scoreText = buildString {
             append("§7Score: ")
             append(colorizeScore(score))
@@ -235,6 +240,7 @@ object MapInfo : Module(
             cachedKnownSecrets = DungeonUtils.knownSecrets
             cachedMimicKilled = DungeonUtils.mimicKilled
             cachedPrinceKilled = DungeonUtils.princeKilled
+            cachedBatKilled = DungeonUtils.batKilled
             cachedCryptCount = DungeonUtils.cryptCount.coerceAtMost(5)
             cachedDeathCount = DungeonUtils.deathCount
         }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
index 407e9c7..fee6bb2 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/Mimic.kt
@@ -15,7 +15,7 @@ import net.minecraft.world.level.Level
 
 object Mimic : Module(
     name = "Mimic",
-    description = "Highlights and announces mimic kills in dungeons."
+    description = "Announces Mimic, Prince and Bat kills in dungeons."
 ) {
     private val mimicMessageToggle by BooleanSetting("Send Mimic Message", true, desc = "Toggles the mimic killed message.")
     private val reset by ActionSetting("Mimic Killed", desc = "Sends Mimic killed message in party chat.") { mimicKilled() }
@@ -23,7 +23,11 @@ object Mimic : Module(
     private val princeMessageToggle by BooleanSetting("Send Prince Message", true, desc = "Toggles the prince killed message.")
     private val princeReset by ActionSetting("Prince Killed", desc = "Sends Prince killed message in party chat.") { princeKilled() }
 
+    private val batMessageToggle by BooleanSetting("Send Bat Message", true, desc = "Toggles the bat killed message.")
+    private val batReset by ActionSetting("Bat Killed", desc = "Sends Bat killed message in party chat.") { batKilled() }
+
     private val princeRegex = Regex("^A Prince falls\\. \\+1 Bonus Score$")
+    private val batRegex = Regex("^A Bat has been slain\\. \\+1 Bonus Score$")
 
     init {
         onReceive<ClientboundEntityEventPacket> {
@@ -52,4 +56,10 @@ object Mimic : Module(
         if (princeMessageToggle) sendCommand("pc Prince Killed!")
         DungeonListener.dungeonStats.princeKilled = true
     }
+
+    private fun batKilled() {
+        if (DungeonUtils.batKilled || !DungeonUtils.inClear) return
+        if (batMessageToggle) sendCommand("pc Bat Killed!")
+        DungeonListener.dungeonStats.batKilled = true
+    }
 }
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
index 99f51e4..a40d2e9 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt
@@ -111,6 +111,9 @@ object DungeonListener {
                 "prince killed", "prince slain", "prince killed!", "prince dead", "prince dead!", ->
                     dungeonStats.princeKilled = true
 
+                "bat killed", "bat slain", "bat killed!", "bat dead", "bat dead!" ->
+                    dungeonStats.batKilled = true
+
                 "blaze done!", "blaze done", "blaze puzzle solved!" ->
                     puzzles.find { it == Puzzle.BLAZE }.let { it?.status = PuzzleStatus.Completed }
             }
@@ -203,6 +206,7 @@ object DungeonListener {
         var elapsedTime: String = "0s",
         private var _mimicKilled: Boolean = false,
         var princeKilled: Boolean = false,
+        var batKilled: Boolean = false,
         var doorOpener: String = "Unknown",
         var bloodDone: Boolean = false,
         var puzzleCount: Int = 0,
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
index ffa63c7..6106f0f 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt
@@ -95,6 +95,9 @@ object DungeonUtils {
     inline val princeKilled: Boolean
         get() = DungeonListener.dungeonStats.princeKilled
 
+    inline val batKilled: Boolean
+        get() = DungeonListener.dungeonStats.batKilled
+
     inline val currentRoom: DungeonRoom?
         get() = WorldScan.currentRoom
 
@@ -106,6 +109,7 @@ object DungeonUtils {
             var score = cryptCount.coerceAtMost(5)
             if (mimicKilled) score += 2
             if (princeKilled) score += 1
+            if (batKilled) score += 1
             if ((isPaul && togglePaul == 0) || togglePaul == 2) score += 10
             return score
         }
Made network error messages contain bodyodtheking · 2026-07-14 · 1d3a53e
查看变更
src/main/kotlin/com/odtheking/odin/commands/CataCommand.kt                   |  4 +---
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt |  2 +-
 src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt                 | 20 +++++++++++++++-----
 3 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/commands/CataCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/CataCommand.kt
index bcd3c9c..94f4de9 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/CataCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/CataCommand.kt
@@ -28,9 +28,7 @@ fun fetchAndDisplayCataStats(result: Result<HypixelData.PlayerInfo>) {
             playerInfo.memberData?.let { displayCataStats(playerInfo.name, it) }
                 ?: modMessage("§cNo profile found for §6${playerInfo.name}§c!")
         },
-        onFailure = { error ->
-            modMessage("§cFailed to fetch dungeon stats: ${error.message}")
-        }
+        onFailure = { modMessage("§c${it.message}") }
     )
 }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt
index 02ba201..bada326 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt
@@ -23,7 +23,7 @@ object CommandKeybinds : Module(
         if (!enabled || !LocationUtils.isInSkyblock) return@onPress
         sendCommand("armor")
     }
-    private val equipmentSets by KeybindSetting("Equipment Wardrobe", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the equipment wardrobe menu.").onPress {
+    private val equipmentSets by KeybindSetting("Equip Wardrobe", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the equipment wardrobe menu.").onPress {
         if (!enabled || !LocationUtils.isInSkyblock) return@onPress
         sendCommand("equipment")
     }
diff --git a/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt b/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
index 45477c0..9b46988 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/network/WebUtils.kt
@@ -29,12 +29,12 @@ object WebUtils {
     suspend fun fetchString(url: String): Result<String> =
         executeRequest(createGetRequest(url))
             .mapCatching { response -> response.body() }
-            .onFailure { logger.warn("Failed to fetch from $url: ${it.message}") }
+            .onFailure { logger.warn("Failed to fetch from $url: ${it.message} ${it.cause}") }
 
     suspend fun getInputStream(url: String): Result<InputStream> =
         executeRequest(createGetRequest(url))
             .map { response -> response.body().byteInputStream() }
-            .onFailure { logger.warn("Failed to get input stream from $url: ${it.message}") }
+            .onFailure { logger.warn("Failed to get input stream from $url: ${it.message} ${it.cause}") }
 
     suspend fun postData(url: String, body: String): Result<String> =
         executeRequest(
@@ -79,7 +79,7 @@ object WebUtils {
                 if (!cont.isActive) return@whenComplete
 
                 if (response.statusCode() in 200..299) cont.resume(Result.success(response))
-                else cont.resume(Result.failure(InputStreamException(response.statusCode(), request.uri().toString())))
+                else cont.resume(Result.failure(InputStreamException(response.statusCode(), response.body())))
             }
         }
     }
@@ -90,5 +90,15 @@ object WebUtils {
         return mayor.get("name")?.asString == "Paul" && mayor.getAsJsonArray("perks")?.any { it.asJsonObject.get("name")?.asString == "EZPZ" } == true
     }
 
-    class InputStreamException(code: Int, url: String) : Exception("Failed to get input stream from $url: HTTP $code")
-}
\ No newline at end of file
+    class InputStreamException(
+        code: Int,
+        body: String
+    ) : Exception(
+        buildString {
+            append(code)
+            if (body.isNotBlank()) {
+                append(": ")
+                append(body)
+            }
+        }
+    )}
\ No newline at end of file
Fixed secret spawn timer being incorrectly offsetodtheking · 2026-07-14 · 98546c2
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
index 09c2c22..54a48ae 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/TickTimers.kt
@@ -135,7 +135,7 @@ object TickTimers : Module(
             if (DungeonUtils.openRoomCount == 0) {
                 if (outboundsHud.enabled) outboundsTime = 40 - (gameTime % 40).toInt()
             } else {
-                if (secretsHud.enabled) secretsTime = 20 - (gameTime % 20).toInt()
+                if (secretsHud.enabled) secretsTime = 30 - (gameTime % 20).toInt()
                 outboundsTime = -1
             }
         }
Fixed PetKeybinds pages regexodtheking · 2026-07-14 · 563970d
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
index 6b9d07b..c00401f 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
@@ -35,7 +35,7 @@ object PetKeybinds : Module(
     private val pet9 by KeybindSetting("Pet 9", GLFW.GLFW_KEY_9, "Pet 9 on the list.").withDependency { advanced }
 
     private val petSlots = intArrayOf(10, 11, 12, 13, 14, 15, 16, 19, 20)
-    private val petsRegex = Regex("Pets(?: \\((\\d)/(\\d)\\))?")
+    private val petsRegex = Regex("(?:\\((\\d)/(\\d)\\)\\s*)?Pets")
 
     init {
         on<ScreenEvent.MouseClick> {
@@ -47,7 +47,6 @@ object PetKeybinds : Module(
         }
     }
 
-
     private fun onClick(screen: AbstractContainerScreen<*>, keyCode: Int): Boolean {
         val (current, total) = petsRegex.find(screen.title.string)?.destructured?.let {
             (it.component1().toIntOrNull() ?: 1) to (it.component2().toIntOrNull() ?: 1)
Fixed SkyblockPlayer stat trackingodtheking · 2026-07-14 · 4f5ed60
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt
index d98b0b2..a90ad3d 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/SkyblockPlayer.kt
@@ -22,10 +22,10 @@ object SkyblockPlayer {
     current overflow mana
      */
 
-    private val HEALTH_REGEX = Regex("([\\d|,]+)/([\\d|,]+)❤")
-    private val MANA_REGEX = Regex("([\\d|,]+)/([\\d|,]+)✎")
-    private val OVERFLOW_MANA_REGEX = Regex("([\\d|,]+)ʬ")
-    private val DEFENSE_REGEX = Regex("([\\d|,]+)❈ Defense")
+    private val HEALTH_REGEX = Regex("([\\d|,]+)/([\\d|,]+)\\uE010")
+    private val MANA_REGEX = Regex("([\\d|,]+)/([\\d|,]+)\\uE003")
+    private val OVERFLOW_MANA_REGEX = Regex("([\\d|,]+)\\uE017")
+    private val DEFENSE_REGEX = Regex("([\\d|,]+)\\uE008 Defense")
 
     var currentHealth: Int = 0
         private set
Cleaned up StartsWithHandlerodtheking · 2026-07-14 · 3a7c12a
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
index 05c35ca..be8d326 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
@@ -35,15 +35,9 @@ class StartsWithHandler(private val letter: String): TerminalHandler(TerminalTyp
 
         override fun click(slotIndex: Int, button: Int, simulateClick: Boolean) {
             val screenHandler = (mc.screen as? ContainerScreen)?.menu ?: return
-            if (canClick(slotIndex, button) && clickedSlot == null) {
+            if (canClick(slotIndex, button) && clickedSlot == null)
                 clickedSlot = screenHandler.containerId to slotIndex
 
-                val clickedItem = screenHandler.slots.getOrNull(slotIndex)?.item
-                if (clickedItem == Items.NETHER_STAR || clickedItem == Items.EXPERIENCE_BOTTLE) {
-                    clickedSlots.add(slotIndex)
-                }
-            }
-
             super.click(slotIndex, button, simulateClick)
         }
 
Fix Starts With X solver (#117)
fix starts with x solver not highlighting netherstar and xp bottle
Jqnxc1 · 2026-07-14 · 2513b96
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt | 47 ++++++++++++++++++++++++++++-------------------
 1 file changed, 28 insertions(+), 19 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
index c229200..05c35ca 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/terminals/terminalhandler/StartsWithHandler.kt
@@ -15,28 +15,37 @@ class StartsWithHandler(private val letter: String): TerminalHandler(TerminalTyp
 
     private var clickedSlot: Pair<Int, Int>? = null
 
-    override fun solve(items: List<ItemStack>): List<Int> {
-        clickedSlot?.let {
-            val screenHandler = (mc.screen as? ContainerScreen)?.menu
-            if (it.first != screenHandler?.containerId) {
-                val item = items[it.second].item
-                if (item == Items.NETHER_STAR || item == Items.EXPERIENCE_BOTTLE) clickedSlots.add(it.second)
-                clickedSlot = null
+        override fun solve(items: List<ItemStack>): List<Int> {
+            clickedSlot?.let {
+                val screenHandler = (mc.screen as? ContainerScreen)?.menu
+                if (it.first != screenHandler?.containerId) {
+                    val item = items[it.second].item
+                    if (item == Items.NETHER_STAR || item == Items.EXPERIENCE_BOTTLE) clickedSlots.add(it.second)
+                        clickedSlot = null
+                }
             }
-        }
 
-        return items.mapIndexedNotNull { index, item ->
-            if (item.hoverName.string.startsWith(letter, true) && !item.hasGlint() && index !in clickedSlots) index else null
+            return items.mapIndexedNotNull { index, item ->
+                if (item.hoverName.string.startsWith(letter, true) &&
+                    index !in clickedSlots &&
+                    (!item.hasGlint() || item.item == Items.NETHER_STAR || item.item == Items.EXPERIENCE_BOTTLE)
+                ) index else null
+            }
         }
-    }
 
-    override fun click(slotIndex: Int, button: Int, simulateClick: Boolean) {
-        val screenHandler = (mc.screen as? ContainerScreen)?.menu ?: return
-        if (canClick(slotIndex, button) && clickedSlot == null)
-            clickedSlot = screenHandler.containerId to slotIndex
+        override fun click(slotIndex: Int, button: Int, simulateClick: Boolean) {
+            val screenHandler = (mc.screen as? ContainerScreen)?.menu ?: return
+            if (canClick(slotIndex, button) && clickedSlot == null) {
+                clickedSlot = screenHandler.containerId to slotIndex
 
-        super.click(slotIndex, button, simulateClick)
-    }
+                val clickedItem = screenHandler.slots.getOrNull(slotIndex)?.item
+                if (clickedItem == Items.NETHER_STAR || clickedItem == Items.EXPERIENCE_BOTTLE) {
+                    clickedSlots.add(slotIndex)
+                }
+            }
+
+            super.click(slotIndex, button, simulateClick)
+        }
 
-    override fun renderSlot(slotIndex: Int): Pair<Color, String?> = TerminalSolver.startsWithColor to null
-}
\ No newline at end of file
+        override fun renderSlot(slotIndex: Int): Pair<Color, String?> = TerminalSolver.startsWithColor to null
+}
Keybind enhancements for the Loadouts update (#115)
* fix: wardrobe keybinds not working after 0.26 * fix: change equipment keybind to /stats * fix: clarify command keybinds after 0.26 * feat: add support for equipment in wardrobe keybinds
AlphaNecron · 2026-07-14 · 8b45ba5
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt  | 16 ++++++++++++----
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt |  8 ++++----
 2 files changed, 16 insertions(+), 8 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt
index 05c0b7f..02ba201 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/CommandKeybinds.kt
@@ -19,14 +19,22 @@ object CommandKeybinds : Module(
         if (!enabled || !LocationUtils.isInSkyblock) return@onPress
         sendCommand("storage")
     }
-    private val wardrobe by KeybindSetting("Wardrobe", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the wardrobe menu.").onPress {
+    private val armorSets by KeybindSetting("Armor Wardrobe", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the armor wardrobe menu.").onPress {
         if (!enabled || !LocationUtils.isInSkyblock) return@onPress
-        sendCommand("wardrobe")
+        sendCommand("armor")
     }
-    private val equipment by KeybindSetting("Equipment", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the equipment menu.").onPress {
+    private val equipmentSets by KeybindSetting("Equipment Wardrobe", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the equipment wardrobe menu.").onPress {
         if (!enabled || !LocationUtils.isInSkyblock) return@onPress
         sendCommand("equipment")
     }
+    private val loadouts by KeybindSetting("Loadouts", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the loadouts menu.").onPress {
+        if (!enabled || !LocationUtils.isInSkyblock) return@onPress
+        sendCommand("loadout")
+    }
+    private val stats by KeybindSetting("Stats", GLFW.GLFW_KEY_UNKNOWN, desc = "Opens the stats menu.").onPress {
+        if (!enabled || !LocationUtils.isInSkyblock) return@onPress
+        sendCommand("stats")
+    }
     private val dhub by KeybindSetting("Dungeon Hub", GLFW.GLFW_KEY_UNKNOWN, desc = "Warps to the dungeon hub.").onPress {
         if (!enabled || !LocationUtils.isInSkyblock) return@onPress
         sendCommand("warp dungeon_hub")
@@ -35,4 +43,4 @@ object CommandKeybinds : Module(
         if (!enabled || !LocationUtils.isInSkyblock) return@onPress
         sendCommand("potionbag")
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt
index 76a6ff5..c87bffc 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/WardrobeKeybinds.kt
@@ -20,7 +20,7 @@ object WardrobeKeybinds : Module(
     private val nextPageKeybind by KeybindSetting("Next Page", GLFW.GLFW_KEY_RIGHT, desc = "Keybind to go to the next page in the wardrobe.")
     private val previousPageKeybind by KeybindSetting("Previous Page", GLFW.GLFW_KEY_LEFT, desc = "Keybind to go to the previous page in the wardrobe.")
     private val unequipKeybind by KeybindSetting("Unequip", GLFW.GLFW_KEY_UNKNOWN, desc = "Keybind to unequip the currently equipped item in the wardrobe.")
-    private val disallowUnequippingEquipped by BooleanSetting("Disable Unequip", desc = "Prevents unequipping equipped armor.")
+    private val disallowUnequippingEquipped by BooleanSetting("Disable Unequip", desc = "Prevents unequipping equipped set.")
 
     private val advanced by DropdownSetting("Show Settings")
     private val wardrobe1 by KeybindSetting("Wardrobe 1", GLFW.GLFW_KEY_1, desc = "Keybind to equip the first wardrobe slot.").withDependency { advanced }
@@ -33,7 +33,7 @@ object WardrobeKeybinds : Module(
     private val wardrobe8 by KeybindSetting("Wardrobe 8", GLFW.GLFW_KEY_8, desc = "Keybind to equip the eighth wardrobe slot.").withDependency { advanced }
     private val wardrobe9 by KeybindSetting("Wardrobe 9", GLFW.GLFW_KEY_9, desc = "Keybind to equip the ninth wardrobe slot.").withDependency { advanced }
 
-    private val wardrobeRegex = Regex("Wardrobe \\((\\d)/(\\d)\\)")
+    private val wardrobeRegex = Regex("\\((\\d)/(\\d)\\) (Armor|Equipment) Sets")
     private val equippedRegex = Regex("Slot (\\d): Equipped")
 
     init {
@@ -62,7 +62,7 @@ object WardrobeKeybinds : Module(
                 val keyIndex = arrayOf(wardrobe1, wardrobe2, wardrobe3, wardrobe4, wardrobe5, wardrobe6, wardrobe7, wardrobe8, wardrobe9)
                     .indexOfFirst { it.value == keyCode }.takeIf { it != -1 } ?: return false
 
-                if (equippedIndex == keyIndex + 36 && disallowUnequippingEquipped) return modMessage("§cArmor already equipped.").let { false }
+                if (equippedIndex == keyIndex + 36 && disallowUnequippingEquipped) return modMessage("§cSet already equipped.").let { false }
                 keyIndex + 36
             }
         }
@@ -70,4 +70,4 @@ object WardrobeKeybinds : Module(
         mc.player?.clickSlot(screen.menu.containerId, index)
         return true
     }
-}
\ No newline at end of file
+}
Worldscan scanRoom message kept spamming -> changed to devMessage (#116)
* fix change Worldscan modMessage to devMessage * Updated the blaze regex
Juliansss · 2026-07-12 · affec5b
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt             | 2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
index 63b2666..f7c9bc4 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt
@@ -111,7 +111,7 @@ object WorldScan {
 
     private fun scanRoom(chunk: LevelChunk, chunkPosition: IVec2) {
         val (core, highestBlock) = getRoomCore(chunk, (chunkPosition * 16) + 7)
-        val data = RoomData.getRoomData(core) ?: return modMessage("Unknown room data for core: $core $chunkPosition")
+        val data = RoomData.getRoomData(core) ?: return devMessage("Unknown room data for core: $core $chunkPosition")
 
         val tilePosition = (chunkPosition / 2) + 6
         val tile = DungeonScan.tiles.getOrNull(tilePosition.x + (tilePosition.z * 6)) ?: return
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
index be7a045..6e973ed 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BlazeSolver.kt
@@ -19,7 +19,7 @@ object BlazeSolver {
     private var blazes = mutableListOf<ArmorStand>()
     private var roomType = 0
     private var lastBlazeCount = 10
-    private val blazeHealthRegex = Regex("^\\[Lv15] ♨ Blaze [\\d,]+/([\\d,]+)❤$")
+    private val blazeHealthRegex = Regex("^\\[Lv15]  Blaze [\\d,]+/([\\d,]+)❤$")
 
     fun getBlaze() {
         if (!DungeonUtils.inDungeons || DungeonUtils.currentRoom?.data?.name?.equalsOneOf("Lower Blaze", "Higher Blaze") == false) return
Added LoadoutKeybindsodtheking · 2026-07-04 · cb333da
查看变更
src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt                 |  2 +-
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt     |  2 +-
 3 files changed, 79 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
index a6e616b..dd1220a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
@@ -74,7 +74,7 @@ object ModuleManager {
 
             //skyblock
             ChatCommands, NoCursorReset, Ragnarock, SpringBoots, WardrobeKeybinds, PetKeybinds, AutoSprint,
-            CommandKeybinds, SlotBinds, Splits,
+            CommandKeybinds, SlotBinds, Splits, LoadoutKeybinds,
 
             // nether
             SupplyHelper, BuildHelper, RemovePerks, NoPre, PearlWaypoints, FreshTools, KuudraInfo, Misc, Vesuvius,
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
new file mode 100644
index 0000000..0d85cb4
--- /dev/null
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/LoadoutKeybinds.kt
@@ -0,0 +1,77 @@
+package com.odtheking.odin.features.impl.skyblock
+
+import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
+import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
+import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
+import com.odtheking.odin.clickgui.settings.impl.KeybindSetting
+import com.odtheking.odin.events.ScreenEvent
+import com.odtheking.odin.events.core.on
+import com.odtheking.odin.features.Module
+import com.odtheking.odin.utils.clickSlot
+import com.odtheking.odin.utils.modMessage
+import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
+import org.lwjgl.glfw.GLFW
+import java.security.Key
+
+object LoadoutKeybinds : Module(
+    name = "Loadout Keybinds",
+    description = "Allows you to use keybinds to navigate the loadout.",
+    key = null
+) {
+    private val nextPageKeybind by KeybindSetting("Next Page", GLFW.GLFW_KEY_RIGHT, desc = "Keybind to go to the next page in the loadout.")
+    private val previousPageKeybind by KeybindSetting("Previous Page", GLFW.GLFW_KEY_LEFT, desc = "Keybind to go to the previous page in the loadout.")
+
+    private val advanced by DropdownSetting("Show Settings")
+    private val loadout1 by KeybindSetting("Loadout 1", GLFW.GLFW_KEY_1, desc = "Keybind to equip the first loadout slot.").withDependency { advanced }
+    private val loadout2 by KeybindSetting("Loadout 2", GLFW.GLFW_KEY_2, desc = "Keybind to equip the second loadout slot.").withDependency { advanced }
+    private val loadout3 by KeybindSetting("Loadout 3", GLFW.GLFW_KEY_3, desc = "Keybind to equip the third loadout slot.").withDependency { advanced }
+    private val loadout4 by KeybindSetting("Loadout 4", GLFW.GLFW_KEY_4, desc = "Keybind to equip the fourth loadout slot.").withDependency { advanced }
+    private val loadout5 by KeybindSetting("Loadout 5", GLFW.GLFW_KEY_5, desc = "Keybind to equip the fifth loadout slot.").withDependency { advanced }
+    private val loadout6 by KeybindSetting("Loadout 6", GLFW.GLFW_KEY_6, desc = "Keybind to equip the sixth loadout slot.").withDependency { advanced }
+    private val loadout7 by KeybindSetting("Loadout 7", GLFW.GLFW_KEY_7, desc = "Keybind to equip the seventh loadout slot.").withDependency { advanced }
+    private val loadout8 by KeybindSetting("Loadout 8", GLFW.GLFW_KEY_8, desc = "Keybind to equip the eighth loadout slot.").withDependency { advanced }
+    private val loadout9 by KeybindSetting("Loadout 9", GLFW.GLFW_KEY_9, desc = "Keybind to equip the ninth loadout slot.").withDependency { advanced }
+    private val loadout10 by KeybindSetting("Loadout 10", GLFW.GLFW_KEY_0, desc = "Keybind to equip the tenth loadout slot.").withDependency { advanced }
+    private val loadout11 by KeybindSetting("Loadout 11", GLFW.GLFW_KEY_MINUS, desc = "Keybind to equip the eleventh loadout slot.").withDependency { advanced }
+    private val loadout12 by KeybindSetting("Loadout 12", GLFW.GLFW_KEY_EQUAL, desc = "Keybind to equip the twelfth loadout slot.").withDependency { advanced }
+
+    private val loadoutRegex = Regex("\\((\\d)/(\\d)\\) Loadout")
+    private val loadoutSlots = intArrayOf(
+        14, 15, 16,
+        23, 24, 25,
+        32, 33, 34,
+        41, 42, 43
+    )
+
+    init {
+        on<ScreenEvent.MouseClick> {
+            if (screen is AbstractContainerScreen<*> && onClick(screen, click.button())) cancel()
+        }
+
+        on<ScreenEvent.KeyPress> {
+            if (screen is AbstractContainerScreen<*> && onClick(screen, input.key)) cancel()
+        }
+    }
+
+    private fun onClick(screen: AbstractContainerScreen<*>, keyCode: Int): Boolean {
+        val (current, total) = loadoutRegex.find(screen.title.string)?.destructured?.let {
+            it.component1().toIntOrNull() to it.component2().toIntOrNull()
+        } ?: return false
+        if (current == null || total == null) return false
+
+        val index = when (keyCode) {
+            nextPageKeybind.value -> if (current < total) 44 else return false
+            previousPageKeybind.value -> if (current > 1) 17 else return false
+            else -> {
+                val keyIndex = arrayOf(loadout1, loadout2, loadout3, loadout4, loadout5, loadout6, loadout7, loadout8, loadout9, loadout10, loadout11, loadout12)
+                    .indexOfFirst { it.value == keyCode }
+                if (keyIndex == -1) return false
+
+                loadoutSlots.getOrNull(keyIndex) ?: return false
+            }
+        }
+
+        mc.player?.clickSlot(screen.menu.containerId, index)
+        return true
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
index 976d285..6b9d07b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
@@ -34,6 +34,7 @@ object PetKeybinds : Module(
     private val pet8 by KeybindSetting("Pet 8", GLFW.GLFW_KEY_8, "Pet 8 on the list.").withDependency { advanced }
     private val pet9 by KeybindSetting("Pet 9", GLFW.GLFW_KEY_9, "Pet 9 on the list.").withDependency { advanced }
 
+    private val petSlots = intArrayOf(10, 11, 12, 13, 14, 15, 16, 19, 20)
     private val petsRegex = Regex("Pets(?: \\((\\d)/(\\d)\\))?")
 
     init {
@@ -46,7 +47,6 @@ object PetKeybinds : Module(
         }
     }
 
-    private val petSlots = intArrayOf(10, 11, 12, 13, 14, 15, 16, 19, 20)
 
     private fun onClick(screen: AbstractContainerScreen<*>, keyCode: Int): Boolean {
         val (current, total) = petsRegex.find(screen.title.string)?.destructured?.let {
Update DungeonMap module nameodtheking · 2026-07-04 · f45fa42
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
index 4fa87f3..f2cf26b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt
@@ -13,7 +13,10 @@ import com.odtheking.odin.utils.render.hollowFill
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
 import net.minecraft.client.gui.GuiGraphicsExtractor
 
-object DungeonMap : Module("Bad map", description = "Displays the dungeon map.") {
+object DungeonMap : Module(
+    "Dungeon Map",
+    description = "Displays the dungeon map."
+) {
 
     private val disableBoss by BooleanSetting("Disable in Boss", true, desc = "Disables the map during boss fights.")
 
dungeon map (#78)
* begin on dungeon scanning refactor and dungeon map * Added room state and player location tracking Fixed rotations * Fixed merge * remove unnecessary null saftey * Added doors and rooms support to DungeonMapScan * Fixed segments not being added to rooms properly * fix merge * cleaned up some stuff I guess * Basic map works * Improve map rendering * Removed old scanning and migrated to new scanning * Update module name * Started on client side map * Made Map consistently stable * Port map to 26.1 * Possibly fix out of sync map item scanning * Cleanup map impl Added DoorHighlight * Added better singleplayer support * Added Room prediction Added RoomClear module Fixed Layers and Redstone Crypt not getting scanned properly * Removed debug message * Fixed MapUpdateEvent syntax --------- Co-authored-by: odtheking <odtheking1@gmail.com>
Stivais · 2026-07-04 · 6d4797b
查看变更
src/main/kotlin/com/odtheking/odin/OdinMod.kt                                                      |   6 +-
 src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt                                          |  58 +++++-----
 src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt                                          |  15 ++-
 src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt                                       |   7 +-
 src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt                                |  15 +--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt                          | 112 +++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonMap.kt                             | 189 -------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/KeyHighlight.kt                           |  66 -----------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt                                |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/RoomClear.kt                              |  28 +++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt  |  25 +++--
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/SecretWaypoints.kt       |   1 -
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/Door.kt                               |  93 ----------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungMap.kt                            | 322 -----------------------------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonMap.kt                         |  71 ++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungeonScan.kt                        | 114 +++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRenderer.kt                        | 221 ++++++++++++++++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapRoom.kt                            | 244 ----------------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScan.kt                            | 221 ++++++++++++++++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/MapScanner.kt                         | 229 -------------------------------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/SpecialColumn.kt                      | 187 ++++++++++++++-----------------
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/Tile.kt                               |  24 ----
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/Vec2i.kt                              |  15 ---
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/WorldScan.kt                          | 185 ++++++++++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonDoor.kt                   |  34 ++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/DungeonRoom.kt                   | 163 +++++++++++++++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/tile/RoomData.kt                      |  90 +++++++++++++++
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BeamsSolver.kt              |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/BoulderSolver.kt            |   1 -
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/IceFillSolver.kt            |   5 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/PuzzleSolvers.kt            |  12 +-
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/QuizSolver.kt               |   1 -
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/TPMazeSolver.kt             |   1 -
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WaterSolver.kt              |   1 -
 src/main/kotlin/com/odtheking/odin/features/impl/dungeon/puzzlesolvers/WeirdosSolver.kt            |   2 -
 src/main/kotlin/com/odtheking/odin/utils/IVec2.kt                                                  |  56 ++++++++++
 src/main/kotlin/com/odtheking/odin/utils/VecUtils.kt                                               |  26 ++---
 src/main/kotlin/com/odtheking/odin/utils/skyblock/LocationUtils.kt                                 |   8 +-
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonEnums.kt                          |  17 +--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonListener.kt                       |  23 ++--
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/DungeonUtils.kt                          |  19 +---
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/ScanUtils.kt                             | 180 ------------------------------
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/tiles/Room.kt                            |  23 ----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/tiles/RoomShape.kt                       |  20 ----
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/tiles/RoomState.kt                       |   5 -
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/tiles/RoomType.kt                        |   5 -
 src/main/kotlin/com/odtheking/odin/utils/skyblock/dungeon/tiles/Rotations.kt                       |  12 --
 src/main/resources/assets/odin/rooms.json                                                          |   7 +-
 src/main/resources/assets/odin/textures/map/cross.png                                              | Bin 0 -> 205 bytes
 src/main/resources/assets/odin/textures/map/green_check.png                                        | Bin 0 -> 188 bytes
 src/main/resources/assets/odin/textures/map/question.png                                           | Bin 0 -> 162 bytes
 src/main/resources/assets/odin/textures/map/white_check.png                                        | Bin 0 -> 186 bytes
 53 files changed, 1497 insertions(+), 1677 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/OdinMod.kt b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
index 700c650..5b1ad76 100644
--- a/src/main/kotlin/com/odtheking/odin/OdinMod.kt
+++ b/src/main/kotlin/com/odtheking/odin/OdinMod.kt
@@ -4,6 +4,7 @@ import com.odtheking.odin.commands.*
 import com.odtheking.odin.events.EventDispatcher
 import com.odtheking.odin.events.core.EventBus
 import com.odtheking.odin.features.ModuleManager
+import com.odtheking.odin.features.impl.dungeon.map.DungeonScan
 import com.odtheking.odin.features.impl.render.Shenanigans
 import com.odtheking.odin.utils.IrisCompatability
 import com.odtheking.odin.utils.ServerUtils
@@ -15,7 +16,6 @@ import com.odtheking.odin.utils.render.RoundRectPIPRenderer
 import com.odtheking.odin.utils.skyblock.*
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonListener
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.ScanUtils
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.TerminalUtils
 import com.odtheking.odin.utils.ui.rendering.NVGPIPRenderer
 import com.odtheking.odin.utils.ui.widget.CustomGUIImpl
@@ -73,9 +73,9 @@ object OdinMod : ClientModInitializer {
             this, LocationUtils, TickTasks, KuudraUtils,
             SkyblockPlayer, ServerUtils, EventDispatcher,
             DungeonListener, PartyUtils, TerminalUtils,
-            ScanUtils, DungeonUtils, SplitsManager,
+            DungeonUtils, SplitsManager, DungeonScan,
             IrisCompatability, RenderBatchManager,
-            ModuleManager, CustomGUIImpl, Shenanigans
+            ModuleManager, CustomGUIImpl, Shenanigans,
         ).forEach { EventBus.subscribe(it) }
 
         PictureInPictureRendererRegistry.register { context ->
diff --git a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
index b936a8f..ac65c0f 100644
--- a/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
+++ b/src/main/kotlin/com/odtheking/odin/commands/DevCommand.kt
@@ -10,6 +10,8 @@ import com.odtheking.odin.features.impl.boss.MelodyMessage.melodyWebSocket
 import com.odtheking.odin.features.impl.boss.WitherDragonState
 import com.odtheking.odin.features.impl.boss.WitherDragons
 import com.odtheking.odin.features.impl.boss.WitherDragonsEnum
+import com.odtheking.odin.features.impl.dungeon.map.DungeonScan
+import com.odtheking.odin.features.impl.dungeon.map.WorldScan
 import com.odtheking.odin.features.impl.nether.NoPre
 import com.odtheking.odin.features.impl.render.ClickGUIModule.webSocketUrl
 import com.odtheking.odin.features.impl.render.PlayerSize
@@ -23,9 +25,7 @@ import com.odtheking.odin.utils.skyblock.PartyUtils
 import com.odtheking.odin.utils.skyblock.Supply
 import com.odtheking.odin.utils.skyblock.dungeon.Blessing
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils.getRelativeCoords
-import com.odtheking.odin.utils.skyblock.dungeon.ScanUtils
-import com.odtheking.odin.utils.skyblock.dungeon.ScanUtils.getRoomCenter
+import com.odtheking.odin.utils.skyblock.dungeon.Floor
 import com.odtheking.odin.utils.ui.rendering.NVGRenderer
 import kotlinx.coroutines.launch
 import net.minecraft.core.registries.BuiltInRegistries
@@ -127,7 +127,7 @@ val devCommand = Commodore("oddev") {
                             |DungeonTime: ${DungeonUtils.dungeonTime}
                             |currentDungeonPlayer: ${DungeonUtils.currentDungeonPlayer.name}, ${DungeonUtils.currentDungeonPlayer.clazz}, ${DungeonUtils.currentDungeonPlayer.isDead}
                             |doorOpener: ${DungeonUtils.doorOpener}
-                            |currentRoom: ${DungeonUtils.currentRoom?.data?.name}, roomsPassed: ${DungeonUtils.passedRooms.map { it.data.name }}
+                            |currentRoom: ${WorldScan.currentRoom?.data?.name}, roomsPassed: ${DungeonScan.rooms.filter { it.walkedInto }.map { it.data?.name }}
                             |Teammates: ${DungeonUtils.dungeonTeammates.joinToString { "§${it.clazz.colorCode}${it.name} (${it.clazz} [${it.clazzLvl}])§r" }}
                             |TeammatesNoSelf: ${DungeonUtils.dungeonTeammatesNoSelf.map { it.name }}
                             |LeapTeammates: ${DungeonUtils.leapTeammates.map { it.name }}
@@ -157,13 +157,8 @@ val devCommand = Commodore("oddev") {
                 append(" §8│ §eIn §f${(dragon.timeToSpawn / 20f).toFixed()}§es")
             }
 
-            val spawnInfo = buildString {
-                append(" §8│ §7Spawned: §fx${dragon.timesSpawned}")
-            }
-
-            val uuid = buildString {
-                append(" §8│ §7UUID: §f${dragon.entityUUID}")
-            }
+            val spawnInfo = buildString { append(" §8│ §7Spawned: §fx${dragon.timesSpawned}") }
+            val uuid = buildString { append(" §8│ §7UUID: §f${dragon.entityUUID}") }
 
             val flags = buildString {
                 val flagList = mutableListOf<String>()
@@ -179,30 +174,39 @@ val devCommand = Commodore("oddev") {
         }
     }
 
-    literal("roomdata").runs {
+    literal("roomdata").runs { x: Int?, z: Int? ->
         val player = mc.player ?: return@runs
-        val vec2 = Vec2(player.x.toInt(), player.z.toInt())
-        val chunk = mc.level?.getChunk(vec2.x shr 4, vec2.z shr 4) ?: return@runs
-        val roomCenter = getRoomCenter(vec2.x, vec2.z)
-        val room = ScanUtils.scanRoom(roomCenter)
-        val core = ScanUtils.getCore(roomCenter)
+        val tileX = ((x ?: player.blockX) + 201) shr 5
+        val tileZ = ((z ?: player.blockZ) + 201) shr 5
+
+        val roomPos = IVec2(tileX * 32 - 185, tileZ * 32 - 185)
+        val chunk = mc.level?.getChunk(roomPos.x shr 4, roomPos.z shr 4) ?: return@runs
+        val core = WorldScan.getRoomCore(chunk, roomPos)
+        val room = DungeonScan.tiles[tileX + tileZ * 6].room ?: return@runs modMessage("§cNo room data found for the current tile.")
+
         modMessage(
             """
-            Middle: ${roomCenter.x}, ${roomCenter.z}
-            Room: ${ScanUtils.coreToRoomData[core]?.name}
-            Core: $core
-            Rotation: ${room?.rotation ?: "NONE"}
-            Positions: ${room?.roomComponents?.joinToString { "(${it.x}, ${it.z})" } ?: "None"}
-            Height: ${ScanUtils.getTopLayerOfRoom(vec2, chunk)}
-            """.trimIndent(), "")
-        setClipboardContent(core.toString())
-        modMessage("§aCopied $core to clipboard!")
+            Middle: ${roomPos.x} ${roomPos.z}
+            Room: ${room.name} ${room.shape}
+            Core: ${core.first} height: ${core.second}
+            Rotation: ${room.rotation ?: "NONE"} ${room.clayPos}
+            Positions: ${room.tiles.joinToString { tile -> "${room.getRealPosition(tile.x, tile.z)}" }}
+            """.trimIndent(), ""
+        )
+        setClipboardContent(core.first.toString())
+        modMessage("§aCopied §f${core.first} §ato clipboard!")
+    }
+
+    literal("setfloor").runs { floorNumber: Int ->
+        Floor.entries.find { it.floorNumber == floorNumber }?.let { floor ->
+            DungeonScan.initClient(floor)
+        } ?: modMessage("§cInvalid floor number: $floorNumber. Valid floors are 1-7.")
     }
 
     literal("relative").runs {
         mc.hitResult?.let {
             if (it !is BlockHitResult) return@runs
-            DungeonUtils.currentRoom?.getRelativeCoords(it.blockPos)?.let { vec2 ->
+            WorldScan.currentRoom?.getRelativeCoords(it.blockPos)?.let { vec2 ->
                 modMessage("Relative coords: ${vec2.x}, ${vec2.z}")
             }
         }
diff --git a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
index f85600b..6568ae7 100644
--- a/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
+++ b/src/main/kotlin/com/odtheking/odin/events/CustomEvents.kt
@@ -3,9 +3,11 @@ package com.odtheking.odin.events
 import com.mojang.blaze3d.platform.InputConstants
 import com.odtheking.odin.events.core.CancellableEvent
 import com.odtheking.odin.events.core.Event
+import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
+import com.odtheking.odin.features.impl.dungeon.map.tile.MapCheckmark
 import com.odtheking.odin.utils.render.RenderConsumer
+import com.odtheking.odin.utils.skyblock.dungeon.Floor
 import com.odtheking.odin.utils.skyblock.dungeon.terminals.terminalhandler.TerminalHandler
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.Room
 import net.fabricmc.fabric.api.client.rendering.v1.level.AbstractLevelRenderContext
 import net.fabricmc.fabric.api.client.rendering.v1.level.LevelRenderContext
 import net.minecraft.client.multiplayer.ClientLevel
@@ -31,8 +33,6 @@ class MessageSentEvent(val message: String) : CancellableEvent()
 
 class RenderBossBarEvent(val bossBar: BossEvent) : CancellableEvent()
 
-class RoomEnterEvent(val room: Room?) : CancellableEvent()
-
 interface SecretPickupEvent : Event { // all are currently packet based but can probably use mixins
     class Interact(val blockPos: BlockPos, val blockState: BlockState) : SecretPickupEvent
     class Item(val entity: ItemEntity) : SecretPickupEvent
@@ -68,4 +68,11 @@ abstract class PartyEvent(val members: List<String>) : Event {
 abstract class PacketEvent(val packet: Packet<*>) : CancellableEvent() { // ideally used less
     class Receive(packet: Packet<*>) : PacketEvent(packet)
     class Send(packet: Packet<*>) : PacketEvent(packet)
-}
\ No newline at end of file
+}
+
+class RoomEnterEvent(val room: DungeonRoom?) : Event
+class FloorEnterEvent(val floor: Floor) : Event
+data object MapUpdateEvent : Event
+class CheckmarkUpdateEvent(val room: DungeonRoom, val checkmark: MapCheckmark) : Event
+
+object LocationChangeEvent : Event
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
index cf55fdb..a6e616b 100644
--- a/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/ModuleManager.kt
@@ -14,6 +14,7 @@ import com.odtheking.odin.features.ModuleManager.configs
 import com.odtheking.odin.features.impl.boss.*
 import com.odtheking.odin.features.impl.dungeon.*
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints
+import com.odtheking.odin.features.impl.dungeon.map.DungeonMap
 import com.odtheking.odin.features.impl.dungeon.puzzlesolvers.PuzzleSolvers
 import com.odtheking.odin.features.impl.nether.*
 import com.odtheking.odin.features.impl.render.*
@@ -59,9 +60,9 @@ object ModuleManager {
         registerModules(config = ModuleConfig(file = File(OdinMod.configFile, "odin-config.json")),
             // dungeon
             PuzzleSolvers, BlessingDisplay, LeapMenu, SecretClicked, MapInfo, Mimic, DungeonQueue,
-            KeyHighlight, BloodCamp, PositionalMessages, TerracottaTimer, BreakerDisplay, LividSolver,
-            InvincibilityTimer, SpiritBear, DungeonWaypoints, ExtraStats, BetterPartyFinder, Croesus, MageBeam, DungeonMap,
-            SecretsCounter, PuzzleHud,
+            DoorHighlight, BloodCamp, PositionalMessages, TerracottaTimer, BreakerDisplay, LividSolver,
+            InvincibilityTimer, SpiritBear, DungeonWaypoints, ExtraStats, BetterPartyFinder, Croesus, MageBeam,
+            SecretsCounter, DungeonMap, PuzzleHud, RoomClear,
 
             // boss
             TerminalSimulator, TerminalSolver, TerminalTimes, TerminalSounds, TickTimers, ArrowAlign,
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
index f32193c..5fc2aa0 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/boss/ExtraStats.kt
@@ -136,11 +136,8 @@ object ExtraStats : Module(
         val defeatedText = if (extraStats.bossKilled == null) "§c§lFAILED §a- §e${DungeonUtils.dungeonTime}"
         else "§aDefeated §c${extraStats.bossKilled} §ain §e${DungeonUtils.dungeonTime}${if (extraStats.timePB) " §d§l(NEW RECORD!)" else ""}"
 
-        val passedRoomsText = "Passed rooms: \n${DungeonUtils.passedRooms.joinToString("\n") { room -> "§a${room.data.name}" }}"
-
-        val message = Component.literal(getChatBreak()).withStyle {
-            it.withHoverEvent(HoverEvent.ShowText(Component.literal(passedRoomsText)))
-        }   .append("\n\n")
+        val message = Component.literal(getChatBreak())
+            .append("\n\n")
             .append(getCenteredText((if (DungeonUtils.floor?.isMM == true) "§cMaster Mode" else "§cThe Catacombs") + " §r- §e${DungeonUtils.floor?.name}"))
             .append("\n\n")
             .append(getCenteredText(defeatedText))
@@ -190,13 +187,7 @@ object ExtraStats : Module(
             ).append("\n")
         }
 
-        message.append("\n")
-            .append(
-                Component.literal(getChatBreak()).withStyle {
-                    it.withClickEvent(ClickEvent.SuggestCommand(passedRoomsText))
-                        .withHoverEvent(HoverEvent.ShowText(Component.literal(passedRoomsText)))
-                }
-            )
+        message.append("\n").append(Component.literal(getChatBreak()))
 
         modMessage(message, "")
     }
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
new file mode 100644
index 0000000..8a72d9d
--- /dev/null
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DoorHighlight.kt
@@ -0,0 +1,112 @@
+package com.odtheking.odin.features.impl.dungeon
+
+import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
+import com.odtheking.odin.clickgui.settings.impl.ColorSetting
+import com.odtheking.odin.events.ChatPacketEvent
+import com.odtheking.odin.events.LevelEvent
+import com.odtheking.odin.events.RenderEvent
+import com.odtheking.odin.events.core.on
+import com.odtheking.odin.events.core.onReceive
+import com.odtheking.odin.features.Module
+import com.odtheking.odin.features.impl.dungeon.map.DungeonScan
+import com.odtheking.odin.features.impl.dungeon.map.tile.DoorType
+import com.odtheking.odin.utils.Color
+import com.odtheking.odin.utils.Color.Companion.withAlpha
+import com.odtheking.odin.utils.Colors
+import com.odtheking.odin.utils.alert
+import com.odtheking.odin.utils.equalsOneOf
+import com.odtheking.odin.utils.render.drawStyledBox
+import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
+import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
+import net.minecraft.world.entity.Entity
+import net.minecraft.world.entity.decoration.ArmorStand
+import net.minecraft.world.phys.AABB
+
+object DoorHighlight : Module(
+    name = "Door Highlight",
+    description = "Highlights wither and blood doors and keys in dungeons."
+) {
+    private val announceKeySpawn by BooleanSetting("Announce Key Spawn", true, desc = "Announces when a key is spawned.")
+    private val doorHighlightColor by ColorSetting("Door Highlight Color", Colors.MINECRAFT_RED.withAlpha(0.8f), true, desc = "Color for locked doors.")
+    private val openableColor by ColorSetting("Openable Door Color", Colors.MINECRAFT_GREEN.withAlpha(0.8f), true, desc = "Color for doors that can be opened with a held key.")
+    private val witherColor by ColorSetting("Wither Color", Colors.BLACK.withAlpha(0.8f), true, desc = "The color of the box.")
+    private val bloodColor by ColorSetting("Blood Color", Colors.MINECRAFT_RED.withAlpha(0.8f), true, desc = "The color of the box.")
+
+    private var currentKey: KeyType? = null
+    private var witherKeys = 0
+    private var bloodKey = false
+    private var bloodOpened = false
+
+    private val witherKeyObtainRegex = Regex("^(\\[[^]]*?])? ?(\\w{1,16}) has obtained Wither Key!?$")
+    private val witherKeyPickedUpRegex = Regex("^A Wither Key was picked up!$")
+    private val witherDoorOpenRegex = Regex("^(\\[[^]]*?])? ?(\\w{1,16}) opened a WITHER door!$")
+    private val bloodKeyObtainRegex = Regex("^(\\[[^]]*?])? ?(\\w{1,16}) has obtained Blood Key!$")
+    private val bloodKeyPickedUpRegex = Regex("^A Blood Key was picked up!$")
+    private val bloodDoorOpenRegex = Regex("^The BLOOD DOOR has been opened!$")
+
+    init {
+        on<ChatPacketEvent> {
+            if (!DungeonUtils.inClear) return@on
+            when {
+                witherKeyObtainRegex.matches(value) || witherKeyPickedUpRegex.matches(value) -> witherKeys++
+                witherDoorOpenRegex.matches(value) -> witherKeys = (witherKeys - 1).coerceAtLeast(0)
+                bloodKeyObtainRegex.matches(value) || bloodKeyPickedUpRegex.matches(value) -> bloodKey = true
+                bloodDoorOpenRegex.matches(value) -> { bloodKey = false; bloodOpened = true }
+            }
+        }
+
+        onReceive<ClientboundSetEntityDataPacket> {
+            if (!DungeonUtils.inClear) return@onReceive
+            val entity = mc.level?.getEntity(id) as? ArmorStand ?: return@onReceive
+            if (currentKey?.entity == entity) return@onReceive
+            currentKey = KeyType.entries.find { it.displayName == entity.name.string } ?: return@onReceive
+            currentKey?.entity = entity
+
+            if (announceKeySpawn) alert("§${currentKey?.colorCode}${entity.name.string}§7 spawned!")
+        }
+
+        on<RenderEvent.Extract> {
+            if (!DungeonUtils.inClear) return@on
+
+            DungeonScan.viewableDoors.forEach { (door, _) ->
+                if (!door.type.equalsOneOf(DoorType.Wither, DoorType.Blood)) return@forEach
+                if (door.type == DoorType.Blood && bloodOpened) return@forEach
+
+                val worldX = (door.position.x - 6) * 32 + 7 + door.rotation.offset.x * 16
+                val worldZ = (door.position.z - 6) * 32 + 7 + door.rotation.offset.z * 16
+                val box = AABB(worldX - 1.0, 69.0, worldZ - 1.0, worldX + 2.0, 73.0, worldZ + 2.0)
+
+                val isOpenable = when (door.type) {
+                    DoorType.Wither -> witherKeys > 0
+                    DoorType.Blood -> bloodKey
+                    else -> false
+                }
+                drawStyledBox(box, if (isOpenable) openableColor else doorHighlightColor, 2, false)
+            }
+
+            if (currentKey == null || currentKey?.entity == null) return@on
+            currentKey?.let { keyType ->
+                if (keyType.entity?.isAlive == false) {
+                    currentKey = null
+                    return@on
+                }
+                val position = keyType.entity?.position() ?: return@on
+                drawStyledBox(AABB.unitCubeFromLowerCorner(position.add(-0.5, 1.0, -0.5)), keyType.color(), 2, true)
+            }
+        }
+
+        on<LevelEvent.Load> {
+            currentKey = null
+            witherKeys = 0
+            bloodKey = false
+            bloodOpened = false
+        }
+    }
+
+    private enum class KeyType(val displayName: String, val color: () -> Color, val colorCode: Char) {
+        Wither("Wither Key", { witherColor }, '8'),
+        Blood("Blood Key", { bloodColor }, 'c');
+
+        var entity: Entity? = null
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonMap.kt
deleted file mode 100644
index d171430..0000000
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/DungeonMap.kt
+++ /dev/null
@@ -1,189 +0,0 @@
-package com.odtheking.odin.features.impl.dungeon
-
-import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
-import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.clickgui.settings.impl.DropdownSetting
-import com.odtheking.odin.clickgui.settings.impl.NumberSetting
-import com.odtheking.odin.events.TickEvent
-import com.odtheking.odin.events.LevelEvent
-import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
-import com.odtheking.odin.features.Module
-import com.odtheking.odin.features.impl.dungeon.map.DungMap
-import com.odtheking.odin.features.impl.dungeon.map.MapScanner
-import com.odtheking.odin.features.impl.dungeon.map.SpecialColumn
-import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.equalsOneOf
-import com.odtheking.odin.utils.itemId
-import com.odtheking.odin.utils.render.hollowFill
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.RoomType
-import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientChunkEvents
-import net.minecraft.client.gui.GuiGraphicsExtractor
-import net.minecraft.client.gui.components.PlayerFaceExtractor
-import net.minecraft.client.renderer.RenderPipelines
-import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket
-import net.minecraft.resources.Identifier
-import java.awt.Color as AwtColor
-
-object DungeonMap : Module(
-    name = "Dungeon Map",
-    description = "Customizable dungeon map with room colors, door colors, and player names."
-) {
-    private val disableBoss by BooleanSetting("Disable in Boss", true, desc = "Disables the map during boss fights.")
-    val disablePred by BooleanSetting("Disable Predictions", false, desc = "Disables map predictions for unseen rooms.")
-
-    private val backgroundColor by ColorSetting("Background Color", Color(0, 0, 0, 0.7f), true, desc = "The background color of the map.")
-    private val backgroundOutline by ColorSetting("Background Outline", Colors.gray26, false, desc = "The color of the background border.").withDependency { backgroundSize != 0f }
-    private val backgroundSize by NumberSetting("Background Size", 5f, 0f, 20f, 1f, desc = "The size of the background border.")
-
-    val textScaling by NumberSetting("Text Scaling", 0.45f, 0.1f, 1f, 0.05f, desc = "Scale of room names.")
-
-    private val playerDropdown by DropdownSetting("Player Settings")
-    private val playerHeadBackgroundSize by NumberSetting("Player Head BG Size", 1, 0, 10, 1, desc = "Size of player head background.").withDependency { playerDropdown }
-    private val playerNamesScaling by NumberSetting("Player Names Scaling", 0.75f, 0.1f, 2f, 0.05f, desc = "Scale of player names.").withDependency { playerDropdown }
-    private val playerNameColor by ColorSetting("Player Name Color", Color(70, 70, 70), false, desc = "Color of player names.").withDependency { playerDropdown }
-
-    private val doorDropdown by DropdownSetting("Door Settings")
-    val doorThickness by NumberSetting("Door Thickness", 9, 1, 20, 1, desc = "Thickness of doors on map.").withDependency { doorDropdown }
-    val unopenedDoorColor by ColorSetting("Unopened Door", Color(30, 30, 30), false, desc = "Color of unopened doors.").withDependency { doorDropdown }
-    val bloodDoorColor by ColorSetting("Blood Door", Colors.MINECRAFT_RED, false, desc = "Color of blood room doors.").withDependency { doorDropdown }
-    val witherDoorColor by ColorSetting("Wither Door", Colors.BLACK, false, desc = "Color of wither doors.").withDependency { doorDropdown }
-    val normalDoorColor by ColorSetting("Normal Door", Color(107, 58, 17), false, desc = "Color of normal doors.").withDependency { doorDropdown }
-    val puzzleDoorColor by ColorSetting("Puzzle Door", Color(117, 0, 133), false, desc = "Color of puzzle doors.").withDependency { doorDropdown }
-    val championDoorColor by ColorSetting("Champion Door", Color(254, 223, 0), false, desc = "Color of champion doors.").withDependency { doorDropdown }
-    val trapDoorColor by ColorSetting("Trap Door", Color(216, 127, 51), false, desc = "Color of trap doors.").withDependency { doorDropdown }
-    val entranceDoorColor by ColorSetting("Entrance Door", Color(20, 133, 0), false, desc = "Color of entrance doors.").withDependency { doorDropdown }
-    val fairyDoorColor by ColorSetting("Fairy Door", Color(244, 19, 139), false, desc = "Color of fairy room doors.").withDependency { doorDropdown }
-    val rareDoorColor by ColorSetting("Rare Door", Color(255, 203, 89), false, desc = "Color of rare doors.").withDependency { doorDropdown }
-
-    // Room Settings
-    private val roomDropdown by DropdownSetting("Room Settings")
-    val darkenMultiplier by NumberSetting("Darken Multiplier", 0.4f, 0f, 1f, 0.05f, desc = "Multiplier for darkening rooms.").withDependency { roomDropdown }
-    val unopenedRoomColor by ColorSetting("Unopened Room", Color(30, 30, 30), false, desc = "Color of unopened rooms.").withDependency { roomDropdown }
-    val bloodRoomColor by ColorSetting("Blood Room", Color(255, 0, 0), false, desc = "Color of blood rooms.").withDependency { roomDropdown }
-    val normalRoomColor by ColorSetting("Normal Room", Color(107, 58, 17), false, desc = "Color of normal rooms.").withDependency { roomDropdown }
-    val puzzleRoomColor by ColorSetting("Puzzle Room", Color(117, 0, 133), false, desc = "Color of puzzle rooms.").withDependency { roomDropdown }
-    val championRoomColor by ColorSetting("Champion Room", Color(254, 223, 0), false, desc = "Color of champion rooms.").withDependency { roomDropdown }
-    val trapRoomColor by ColorSetting("Trap Room", Color(216, 127, 51), false, desc = "Color of trap rooms.").withDependency { roomDropdown }
-    val entranceRoomColor by ColorSetting("Entrance Room", Color(20, 133, 0), false, desc = "Color of entrance rooms.").withDependency { roomDropdown }
-    val fairyRoomColor by ColorSetting("Fairy Room", Color(244, 19, 139), false, desc = "Color of fairy rooms.").withDependency { roomDropdown }
-    val rareRoomColor by ColorSetting("Rare Room", Color(255, 203, 89), false, desc = "Color of rare rooms.").withDependency { roomDropdown }
-
-    private val mapHud by HUD("Dungeon Map", "Displays the dungeon map with customizable colors.", false) { example ->
-        when {
-            (!DungeonUtils.inDungeons || (disableBoss && DungeonUtils.inBoss)) && !example -> 0 to 0
-            example -> renderExampleMap()
-            else -> renderDungeonMap()
-        }
-    }
-
-    private val marker = Identifier.fromNamespaceAndPath("odin", "map/marker.png")
-    val x = Identifier.fromNamespaceAndPath("odin", "map/x.png")
-    val green = Identifier.fromNamespaceAndPath("odin", "map/green.png")
-    val white = Identifier.fromNamespaceAndPath("odin", "map/white.png")
-    val question = Identifier.fromNamespaceAndPath("odin", "map/question.png")
-
-    private fun GuiGraphicsExtractor.renderExampleMap(): Pair<Int, Int> {
-        val roomsX = 116
-        val roomsZ = 116
-        val offset = backgroundSize.toInt()
-
-        fill(0, 0, roomsX + offset * 2, roomsZ + offset * 2, backgroundColor.rgba)
-        centeredText(mc.font, "MAP", roomsX / 2 + offset, roomsZ / 2 + offset - mc.font.lineHeight, AwtColor.WHITE.rgb)
-
-        return (roomsX + offset * 2) to (roomsZ + offset * 2)
-    }
-
-    private fun GuiGraphicsExtractor.renderDungeonMap(): Pair<Int, Int> {
-        val matrices = pose()
-        val mapSize = DungMap.calculateMapSize()
-        val roomsX = mapSize.x * 16 + (mapSize.x - 1) * 4
-        val roomsZ = mapSize.z * 16 + (mapSize.z - 1) * 4
-        val offset = backgroundSize.toInt() * 2
-
-        matrices.pushMatrix()
-
-        fill(0, 0, roomsX + offset, roomsZ + offset, backgroundColor.rgba)
-        hollowFill(0, 0, roomsX + offset, roomsZ + offset, 1, backgroundOutline)
-        matrices.translate(backgroundSize, backgroundSize)
-
-        MapScanner.rooms.forEach { it.render(this) }
-
-        for (door in MapScanner.doors) {
-            if (!door.seen) continue
-            door.render(this)
-        }
-
-        val textFactor = 1 / textScaling
-
-        MapScanner.rooms.forEach { room ->
-            if (room.type == RoomType.ENTRANCE || room.type == RoomType.BLOOD) return@forEach
-            room.renderName(this, textFactor)
-        }
-
-        if (!DungeonUtils.inBoss) {
-            val renderNames = mc.player?.mainHandItem?.itemId?.equalsOneOf("INFINITE_SPIRIT_LEAP", "SPIRIT_LEAP") == true
-
-            for (player in DungeonUtils.dungeonTeammates) {
-                if (player.isDead) continue
-
-                val (posX, posZ) = player.mapRenderPosition()
-
-                matrices.pushMatrix()
-                matrices.translate(posX, posZ)
-
-                if (renderNames) {
-                    matrices.pushMatrix()
-                    matrices.scale(playerNamesScaling)
-                    centeredText(mc.font, player.name, 0, 8, playerNameColor.rgba)
-                    matrices.popMatrix()
-                }
-
-                player.playerSkin?.let { skin ->
-                    matrices.rotate(Math.toRadians(180.0 + player.mapRenderYaw()).toFloat())
-
-                    if (player.playerSkin == mc.player?.skin) {
-                        blit(RenderPipelines.GUI_TEXTURED, marker, 5, 5, 10f, 10f, -10, -10, 10, 10)
-                        return@let
-                    }
-
-                    if (playerHeadBackgroundSize != 0) {
-                        val size = 5 + playerHeadBackgroundSize
-                        fill(-size, -size, size, size, player.clazz.color.rgba)
-                    }
-
-                    PlayerFaceExtractor.extractRenderState(this, skin, -5, -5, 10)
-                }
-
-                matrices.popMatrix()
-            }
-        }
-
-        matrices.popMatrix()
-
-        return (roomsX + offset) to (roomsZ + offset)
-    }
-
-    init {
-        on<LevelEvent.Load> {
-            SpecialColumn.unload()
-            MapScanner.unload()
-            DungMap.unload()
-        }
-
-        on<TickEvent.End> {
-            MapScanner.scan(world)
-        }
-
-        ClientChunkEvents.CHUNK_LOAD.register { _, _ ->
-            DungMap.onChunkLoad()
-        }
-
-        onReceive<ClientboundMapItemDataPacket> {
-            DungMap.rescanMapItem(this)
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/KeyHighlight.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/KeyHighlight.kt
deleted file mode 100644
index c71bd87..0000000
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/KeyHighlight.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package com.odtheking.odin.features.impl.dungeon
-
-import com.odtheking.odin.clickgui.settings.impl.BooleanSetting
-import com.odtheking.odin.clickgui.settings.impl.ColorSetting
-import com.odtheking.odin.events.RenderEvent
-import com.odtheking.odin.events.LevelEvent
-import com.odtheking.odin.events.core.on
-import com.odtheking.odin.events.core.onReceive
-import com.odtheking.odin.features.Module
-import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.Color.Companion.withAlpha
-import com.odtheking.odin.utils.Colors
-import com.odtheking.odin.utils.alert
-import com.odtheking.odin.utils.render.drawStyledBox
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
-import net.minecraft.world.entity.Entity
-import net.minecraft.world.entity.decoration.ArmorStand
-import net.minecraft.world.phys.AABB
-
-object KeyHighlight : Module(
-    name = "Key Highlight",
-    description = "Highlights wither and blood keys in dungeons."
-) {
-    private val announceKeySpawn by BooleanSetting("Announce Key Spawn", true, desc = "Announces when a key is spawned.")
-    private val witherColor by ColorSetting("Wither Color", Colors.BLACK.withAlpha(0.8f), true, desc = "The color of the box.")
-    private val bloodColor by ColorSetting("Blood Color", Colors.MINECRAFT_RED.withAlpha(0.8f), true, desc = "The color of the box.")
-
-    private var currentKey: KeyType? = null
-
-    init {
-        onReceive<ClientboundSetEntityDataPacket> {
-            if (!DungeonUtils.inClear) return@onReceive
-            val entity = mc.level?.getEntity(id) as? ArmorStand ?: return@onReceive
-            if (currentKey?.entity == entity) return@onReceive
-            currentKey = KeyType.entries.find { it.displayName == entity.name.string } ?: return@onReceive
-            currentKey?.entity = entity
-
-            if (announceKeySpawn) alert("§${currentKey?.colorCode}${entity.name.string}§7 spawned!")
-        }
-
-        on<RenderEvent.Extract> {
-            if (!DungeonUtils.inClear) return@on
-            if (currentKey == null || currentKey?.entity == null) return@on
-            currentKey?.let { keyType ->
-                if (keyType.entity?.isAlive == false) {
-                    currentKey = null
-                    return@on
-                }
-                val position = keyType.entity?.position() ?: return@on
-                drawStyledBox(AABB.unitCubeFromLowerCorner(position.add(-0.5, 1.0, -0.5)), keyType.color(), 2, true)
-            }
-        }
-
-        on<LevelEvent.Load> {
-            currentKey = null
-        }
-    }
-
-    private enum class KeyType(val displayName: String, val color: () -> Color, val colorCode: Char) {
-        Wither("Wither Key", { witherColor }, '8'),
-        Blood("Blood Key", { bloodColor }, 'c');
-
-        var entity: Entity? = null
-    }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
index f6a0082..94d8c80 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/MapInfo.kt
@@ -2,12 +2,13 @@ package com.odtheking.odin.features.impl.dungeon
 
 import com.odtheking.odin.clickgui.settings.Setting.Companion.withDependency
 import com.odtheking.odin.clickgui.settings.impl.*
+import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.events.RoomEnterEvent
-import com.odtheking.odin.events.LevelEvent
 import com.odtheking.odin.events.core.on
 import com.odtheking.odin.events.core.onReceive
 import com.odtheking.odin.features.Module
+import com.odtheking.odin.features.impl.dungeon.map.tile.RoomType
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.Colors
 import com.odtheking.odin.utils.alert
@@ -19,8 +20,6 @@ import com.odtheking.odin.utils.render.getStringWidth
 import com.odtheking.odin.utils.render.text
 import com.odtheking.odin.utils.render.textDim
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils.getRealCoords
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.RoomType
 import net.minecraft.core.BlockPos
 import net.minecraft.network.protocol.game.ClientboundSystemChatPacket
 import net.minecraft.world.phys.AABB
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/RoomClear.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/RoomClear.kt
new file mode 100644
index 0000000..89cb4ed
--- /dev/null
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/RoomClear.kt
@@ -0,0 +1,28 @@
+package com.odtheking.odin.features.impl.dungeon
+
+import com.odtheking.odin.clickgui.settings.impl.SelectorSetting
+import com.odtheking.odin.events.CheckmarkUpdateEvent
+import com.odtheking.odin.events.core.on
+import com.odtheking.odin.features.Module
+import com.odtheking.odin.features.impl.dungeon.map.tile.MapCheckmark
+import com.odtheking.odin.features.impl.dungeon.map.tile.RoomType
+import com.odtheking.odin.utils.alert
+import com.odtheking.odin.utils.equalsOneOf
+import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
+
+object RoomClear : Module(
+    name = "Room Clear",
+    description = "Notifies you when a room is cleared in dungeons."
+) {
+    private val mode by SelectorSetting("Mode", "Both", listOf("Both", "Green", "White"), desc = "Notification for when a room turns green or white.")
+
+    init {
+        on<CheckmarkUpdateEvent> {
+            if (DungeonUtils.currentRoom != room || !checkmark.equalsOneOf(MapCheckmark.GREEN, MapCheckmark.WHITE)) return@on
+            if (room.type == RoomType.FAIRY || room.type == RoomType.ENTRANCE) return@on
+
+            if (mode == 0 || (mode == 1 && checkmark == MapCheckmark.GREEN) || (mode == 2 && checkmark == MapCheckmark.WHITE))
+                alert(if (checkmark == MapCheckmark.GREEN) "§aRoom Complete!" else "Room Cleared!")
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt
index 4957257..a5a71ce 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointEditor.kt
@@ -6,6 +6,7 @@ import com.odtheking.odin.events.InputEvent
 import com.odtheking.odin.events.RenderEvent
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints.DungeonWaypoint
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints.WaypointType
+import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
 import com.odtheking.odin.features.impl.render.Etherwarp
 import com.odtheking.odin.utils.Color.Companion.withAlpha
 import com.odtheking.odin.utils.devMessage
@@ -16,8 +17,6 @@ import com.odtheking.odin.utils.render.drawBoxes
 import com.odtheking.odin.utils.render.drawStyledBox
 import com.odtheking.odin.utils.render.drawText
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils.getRelativeCoords
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.Room
 import kotlinx.coroutines.launch
 import net.minecraft.core.BlockPos
 import net.minecraft.world.phys.AABB
@@ -94,7 +93,7 @@ private fun DungeonWaypoints.cacheEtherwarpTarget() {
 }
 
 private fun DungeonWaypoints.openWaypointTitlePrompt(
-    room: Room,
+    room: DungeonRoom,
     blockPos: BlockPos,
     aabb: AABB,
     editableWaypoints: MutableList<DungeonWaypoint>,
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
index 5bc3795..9c45673 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/DungeonWaypointPacks.kt
@@ -4,10 +4,9 @@ import com.odtheking.odin.config.WaypointPackFileUtils
 import com.odtheking.odin.config.WaypointPackState
 import com.odtheking.odin.config.normalized
 import com.odtheking.odin.features.ModuleManager
+import com.odtheking.odin.features.impl.dungeon.map.tile.DungeonRoom
 import com.odtheking.odin.utils.modMessage
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils.getRealCoords
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.Room
 import net.minecraft.world.phys.AABB
 
 suspend fun DungeonWaypoints.loadWaypoints() {
@@ -79,23 +78,25 @@ internal fun DungeonWaypoints.resetClickedWaypoints() {
     DungeonUtils.currentRoom?.setWaypoints()
 }
 
-fun Room.setWaypoints() {
-    waypoints = DungeonWaypoints.allActiveWaypoints[data.name]
+fun DungeonRoom.setWaypoints() {
+    val name = data?.name ?: return
+    waypoints = DungeonWaypoints.allActiveWaypoints[name]
         ?.mapTo(mutableSetOf()) { waypoint ->
             waypoint.copy(blockPos = getRealCoords(waypoint.blockPos))
         } ?: mutableSetOf()
 }
 
-fun DungeonWaypoints.getWaypoints(room: Room): MutableList<DungeonWaypoints.DungeonWaypoint> =
-    allActiveWaypoints.getOrPut(room.data.name) { mutableListOf() }
+fun DungeonWaypoints.getWaypoints(room: DungeonRoom): MutableList<DungeonWaypoints.DungeonWaypoint> =
+    allActiveWaypoints.getOrPut(room.data?.name ?: return mutableListOf()) { mutableListOf() }
 
-fun DungeonWaypoints.getEditableWaypoints(room: Room): MutableList<DungeonWaypoints.DungeonWaypoint> =
-    loadedPacks.getOrPut(editPackId) { mutableMapOf() }.getOrPut(room.data.name) { mutableListOf() }
+fun DungeonWaypoints.getEditableWaypoints(room: DungeonRoom): MutableList<DungeonWaypoints.DungeonWaypoint> =
+    loadedPacks.getOrPut(editPackId) { mutableMapOf() }.getOrPut(room.data?.name ?: return mutableListOf()) { mutableListOf() }
 
-fun DungeonWaypoints.syncRoomToActive(room: Room) {
-    val mergedRoom = mergeRoomWaypoints(room.data.name)
-    if (mergedRoom.isEmpty()) allActiveWaypoints.remove(room.data.name)
-    else allActiveWaypoints[room.data.name] = mergedRoom
+fun DungeonWaypoints.syncRoomToActive(room: DungeonRoom) {
+    val name = room.data?.name ?: return@syncRoomToActive
+    val mergedRoom = mergeRoomWaypoints(name)
+    if (mergedRoom.isEmpty()) allActiveWaypoints.remove(name)
+    else allActiveWaypoints[name] = mergedRoom
     room.setWaypoints()
 }
 
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/SecretWaypoints.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/SecretWaypoints.kt
index 03d2641..7c2b2e1 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/SecretWaypoints.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/dungeonwaypoints/SecretWaypoints.kt
@@ -5,7 +5,6 @@ import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoint
 import com.odtheking.odin.features.impl.dungeon.dungeonwaypoints.DungeonWaypoints.WaypointType
 import com.odtheking.odin.utils.devMessage
 import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils.getRelativeCoords
 import net.minecraft.core.BlockPos
 import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket
 import net.minecraft.world.phys.Vec3
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/Door.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/Door.kt
deleted file mode 100644
index 2a3fe48..0000000
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/Door.kt
+++ /dev/null
@@ -1,93 +0,0 @@
-package com.odtheking.odin.features.impl.dungeon.map
-
-import com.odtheking.odin.features.impl.dungeon.DungeonMap
-import com.odtheking.odin.utils.Color
-import com.odtheking.odin.utils.Color.Companion.darker
-import com.odtheking.odin.utils.equalsOneOf
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.RoomState
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.RoomType
-import net.minecraft.client.gui.GuiGraphicsExtractor
-
-class Door (val pos: Vec2i, var type: Type, val rooms: MutableList<MapRoom.Tile>) {
-
-    init {
-        rooms.forEach { it.owner?.doors?.add(this) }
-    }
-
-    enum class Type { BLOOD, NORMAL, WITHER }
-
-    var locked = type.equalsOneOf(Type.WITHER, Type.BLOOD)
-    val seen get() = rooms.any { it.owner?.state !in setOf(RoomState.UNDISCOVERED, RoomState.UNOPENED) }
-
-    private fun size(): Vec2i {
-        val xOffset = ((pos.x + 185) shr 4) % 2
-        val zOffset = ((pos.z + 185) shr 4) % 2
-        return Vec2i(
-            (xOffset xor 1) * DungeonMap.doorThickness + xOffset * 4,
-            (zOffset xor 1) * DungeonMap.doorThickness + zOffset * 4
-        )
-    }
-
-    inline val placement: Vec2i
-        get() {
-            val x = (pos.x + 185) shr 4
-            val z = (pos.z + 185) shr 4
-            val xEven = x % 2
-            val zEven = z % 2
-            val thicknessBasedOffset = (16 - DungeonMap.doorThickness) / 2
-            val xOffset = (x shr 1) * 20 + xEven * 16 + (xEven xor 1) * thicknessBasedOffset
-            val yOffset = (z shr 1) * 20 + zEven * 16 + (zEven xor 1) * thicknessBasedOffset
-
-            return Vec2i(xOffset, yOffset)
-        }
-
-    fun render(graphics: GuiGraphicsExtractor) {
-        val size = size()
-        if (size != Vec2i(0, 0)) {
-            val matrices = graphics.pose()
-            matrices.pushMatrix()
-            matrices.translate(placement.x.toFloat(), placement.z.toFloat())
-
-            graphics.fill(0, 0, size.x, size.z, color().rgba)
-
-            matrices.popMatrix()
-        }
-    }
-
-    private fun color(): Color {
-        val hasUnopenedRoom = rooms.any { it.owner?.state in setOf(RoomState.UNDISCOVERED, RoomState.UNOPENED) }
-
-        return when {
-            hasUnopenedRoom && type != Type.NORMAL -> getLockedDoorColor()
-            hasUnopenedRoom -> DungeonMap.unopenedDoorColor
-            else -> getOpenDoorColor()
-        }
-    }
-
-    private fun getLockedDoorColor(): Color = when (type) {
-        Type.BLOOD -> if (locked) DungeonMap.bloodDoorColor.darker(DungeonMap.darkenMultiplier) else DungeonMap.bloodDoorColor
-        Type.WITHER -> if (locked) DungeonMap.witherDoorColor.darker(DungeonMap.darkenMultiplier) else DungeonMap.witherDoorColor
-        Type.NORMAL -> DungeonMap.unopenedDoorColor
-    }
-
-    private fun getOpenDoorColor(): Color = when (type) {
-        Type.BLOOD -> DungeonMap.bloodDoorColor
-        Type.WITHER -> if (locked) DungeonMap.witherDoorColor
-                      else rooms.firstOrNull { it.owner?.data?.type == RoomType.FAIRY }?.let { DungeonMap.fairyDoorColor }
-                           ?: DungeonMap.normalDoorColor
-        Type.NORMAL -> getDoorColorByRoomType()
-    }
-
-    private fun getDoorColorByRoomType(): Color {
-        val specialRoom = rooms.firstOrNull { it.owner?.data?.type !in setOf(RoomType.NORMAL, RoomType.FAIRY) }
-        return when (specialRoom?.owner?.data?.type) {
-            RoomType.ENTRANCE -> DungeonMap.entranceDoorColor
-            RoomType.BLOOD -> DungeonMap.bloodDoorColor
-            RoomType.CHAMPION -> DungeonMap.championDoorColor
-            RoomType.PUZZLE -> DungeonMap.puzzleDoorColor
-            RoomType.RARE -> DungeonMap.rareDoorColor
-            RoomType.TRAP -> DungeonMap.trapDoorColor
-            else -> DungeonMap.normalDoorColor
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungMap.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungMap.kt
deleted file mode 100644
index 1fd54a1..0000000
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/map/DungMap.kt
+++ /dev/null
@@ -1,322 +0,0 @@
-package com.odtheking.odin.features.impl.dungeon.map
-
-import com.odtheking.odin.utils.equalsOneOf
-import com.odtheking.odin.utils.skyblock.dungeon.DungeonUtils
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.RoomShape
-import com.odtheking.odin.utils.skyblock.dungeon.tiles.RoomType
-import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket
-import net.minecraft.world.level.saveddata.maps.MapDecorationTypes
-import kotlin.jvm.optionals.getOrNull
-
-object DungMap {
-    var mapCenter: Vec2i = Vec2i(0, 0)
-    var startCoords: Vec2i? = null
-    var mapSize: Vec2i? = null
-    var roomSize: Int? = null
-    var shouldScan = false
-
-    private const val MAP_SIZE = 128
-    private const val EMPTY: Byte = 0
-
-    fun unload() {
-        mapCenter = Vec2i(0, 0)
-        shouldScan = false
-        startCoords = null
-        mapSize = null
-        roomSize = null
-    }
-
-    fun onChunkLoad() {
-        shouldScan = true
-        if (mapSize == null) {
-            mapSize = when (DungeonUtils.floor?.floorNumber) {
-                0 -> Vec2i(4, 4)
-                1 -> Vec2i(4, 5)
-                2, 3 -> Vec2i(5, 5)
-                else -> Vec2i(6, 6)
-            }
-        }
-    }
-
-    fun rescanMapItem(packet: ClientboundMapItemDataPacket) {
-        if (!DungeonUtils.inClear || packet.mapId().id and 1000 != 0) return
-        updatePlayerPositions(packet)
-
-        val colors = packet.colorPatch.getOrNull()?.mapColors() ?: return
-        if (colors.size < MAP_SIZE * MAP_SIZE || colors[0] != EMPTY) return
-
-        if (startCoords == null && !initializeMapCoordinates(colors)) return
-
-        updateRoomTiles(colors)
-        updateRoomStates(colors)
-        updateDoorStates(colors)
-
-        if (mapSize != null) SpecialColumn.updateSpecialColumn()
-    }
-
-    private fun initializeMapCoordinates(colors: ByteArray): Boolean {
-        val (greenStart, greenLength) = findGreenRoom(colors)
-        if (!greenLength.equalsOneOf(16, 18)) return false
-
-        val (start, center, size) = when (DungeonUtils.floor?.floorNumber) {
-            0 -> Triple(Vec2i(22, 22), Vec2i(-137, -137), Vec2i(4, 4))
-            1 -> Triple(Vec2i(22, 11), Vec2i(-137, -121), Vec2i(4, 5))
-            2, 3 -> Triple(Vec2i(11, 11), Vec2i(-121, -121), Vec2i(5, 5))
-            else -> calculateDynamicMapSize(greenStart, greenLength)
-        }
-
-        roomSize = greenLength
-        startCoords = start
-        mapCenter = center
-        mapSize = size
-
-        if ((DungeonUtils.isFloor(6, 5) && size.x == 6 && size.z == 6) || (DungeonUtils.isFloor(4) && size.x == 6 && size.z == 5))
-            SpecialColumn.column = 5
-
-        return true
-    }
-
-    private fun calculateDynamicMapSize(greenStart: Int, greenLength: Int): Triple<Vec2i, Vec2i, Vec2i> {
-        val start = Vec2i((greenStart and 127) % (greenLength + 4), (greenStart shr 7) % (greenLength + 4))
-
-        val extra = Vec2i(if (start.x == 5) 1 else 0, if (start.z == 5) 1 else 0)
-        val size = Vec2i(5, 5).add(extra)
-        val center = Vec2i(-121, -121).add(Vec2i(extra.x * 16, extra.z * 16))
-
-        return Triple(start, center, size)
-    }
-
-    private fun updatePlayerPositions(packet: ClientboundMapItemDataPacket) {
-        packet.decorations.getOrNull()?.let { decorations ->
-            val playerIterator = DungeonUtils.dungeonTeammatesNoSelf.iterator()
-
-            decorations.forEach { decoration ->
-                if (decoration.type.value() == MapDecorationTypes.FRAME.value()) return@forEach
-
-                val player = playerIterator.asSequence().firstOrNull { !it.isDead } ?: return@forEach
-                player.mapPos = Vec2i(decoration.x.toInt(), decoration.y.toInt())
-                player.yaw = decoration.rot() * 360 / 16f
-            }
-        }
-    }
-
-    private fun updateRoomTiles(colors: ByteArray) {
-        val rs = roomSize ?: return
-        val sc = startCoords ?: return
-        val ms = mapSize ?: return
-
-        val tileGrid = Array(ms.x) { Array(ms.z) { -1 } }
-        var nextId = 0
-        for (i in 0 until ms.x) {
-            for (j in 0 until ms.z) {
-                val idx = Vec2i(i, j).multiply(rs + 4).add(sc).mapIndex()
-                if (idx < colors.size && colors[idx].toInt() != 0) {
-                    tileGrid[i][j] = nextId++
-                }
-            }
-        }
-
-        val queue = ArrayDeque<Vec2i>()
-        for (i in 0 until ms.x) for (j in 0 until ms.z)
-            if (tileGrid[i][j] != -1) queue += Vec2i(i, j)
-
-        val visited = Array(ms.x) { BooleanArray(ms.z) }
-        while (queue.isNotEmpty()) {
-            val cur = queue.removeFirst()
-            if (visited[cur.x][cur.z]) continue
-            visited[cur.x][cur.z] = true
-            val id = tileGrid[cur.x][cur.z]
-
-            val nr = cur.x + 1
-            if (nr < ms.x && tileGrid[nr][cur.z] != -1 && !visited[nr][cur.z]) {
-                val di = sc.add(Vec2i(rs + 1 + cur.x * (rs + 4), cur.z * (rs + 4) + 1)).mapIndex()
-                if (di < colors.size && colors[di].toInt() != 0) {
-                    tileGrid[nr][cur.z] = id
-                    queue += Vec2i(nr, cur.z)
-                }
-            }
-
-            val nb = cur.z + 1
-            if (nb < ms.z && tileGrid[cur.x][nb] != -1 && !visited[cur.x][nb]) {
-                val di = sc.add(Vec2i(cur.x * (rs + 4) + 1, rs + 1 + cur.z * (rs + 4))).mapIndex()
-                if (di < colors.size && colors[di].toInt() != 0) {
-                    tileGrid[cur.x][nb] = id
-                    queue += Vec2i(cur.x, nb)
-                }
-            }
-        }
-
-        data class UniqueRoom(val id: Int, val coords: Vec2i, val tiles: MutableList<Vec2i>)
-        val uniques = mutableListOf<UniqueRoom>()
-        tileGrid.forEachIndexed { i, col ->
-            col.forEachIndexed { j, id ->
-                if (id == -1) return@forEachIndexed
-                (uniques.find { it.id == id } ?: run {
-                    val u = UniqueRoom(id, Vec2i(i, j), mutableListOf())
-                    uniques.add(u)
-                    u
-                }).tiles.add(Vec2i(i, j))
-            }
-        }
-
-        uniques.forEach { unique ->
-            val idx = unique.coords.multiply(rs + 4).add(sc).mapIndex()
-            val type = when (colors[idx].toInt()) {
-                18 -> RoomType.BLOOD
-                30 -> RoomType.ENTRANCE
-                85 -> RoomType.UNKNOWN
-                63 -> RoomType.NORMAL
-                62 -> RoomType.TRAP
-                66 -> RoomType.PUZZLE
-                74 -> RoomType.CHAMPION
-                82 -> RoomType.FAIRY
-                else -> return@forEach
-            }
-
-            val shape = if (type == RoomType.UNKNOWN) RoomShape.UNKNOWN
-            else when (unique.tiles.size) {
-                1 -> RoomShape.S1x1
-                2 -> RoomShape.S2x1
-                3 -> {
-                    val (a, b, c) = unique.tiles
-                    if ((a.x == b.x && a.x == c.x) || (a.z == b.z && a.z == c.z)) RoomShape.S3x1 else RoomShape.L
-                }
-                4 -> {
-                    val (a, b, c) = unique.tiles
-                    if ((a.x == b.x && a.x == c.x) || (a.z == b.z && a.z == c.z)) RoomShape.S4x1 else RoomShape.S2x2
-                }
-                else -> return@forEach
-            }
-
-            val found = unique.tiles.firstNotNullOfOrNull { tile ->
-                MapScanner.roomsList[tile.x * 6 + tile.z].owner
-            }
-
-            if (found != null) {
-                if (type != RoomType.UNKNOWN && found.type == RoomType.UNKNOWN) {
-                    found.type = type
-                    found.shape = shape
-                }
-                unique.tiles.forEach { tile ->
-                    val other = MapScanner.roomsList[tile.x * 6 + tile.z].owner
-                    if (other != null && other != found) {
-                        other.doors.forEach { found.doors.add(it) }
-                        MapScanner.rooms.remove(other)
-                    }
-                    if (!found.places.contains(tile)) {
-                        found.roomTile(tile.multiply(32).add(-185, -185))?.let { roomTile ->
-                            MapScanner.list[tile.roomListIndex()] = roomTile
-                        }
-                    }
-                }
-                return@forEach
-            }
-
-            val room = MapRoom(type, shape)
-            MapScanner.rooms.add(room)
-            unique.tiles.forEach { tile ->
-                room.roomTile(tile.multiply(32).add(-185, -185))?.let { roomTile ->
-                    MapScanner.list[tile.roomListIndex()] = roomTile
-                }
-            }
-        }
-    }
-
-    private fun updateRoomStates(colors: ByteArray) {
-        val rs = roomSize ?: return
-        val halfRoomSize = rs / 2
-        val startCenter = startCoords?.add(Vec2i(halfRoomSize, halfRoomSize)) ?: return
-        val tileSize = rs + 4
-
-        MapScanner.rooms.forEach { room ->
-            val topLeftPlacement = room.places.minWith { a, b ->
-                a.x * 1000 + a.z - b.x * 1000 - b.z
-            }
-
-            var color = getColorAtPlacement(topLeftPlacement, startCenter, tileSize, colors).toInt()
-            var placement = topLeftPlacement
-
-            if (color == 0) {
-                room.places.firstNotNullOfOrNull { testPlacement ->
-                    val testColor = getColorAtPlacement(testPlacement, startCenter, tileSize, colors)
-                    if (testColor != 0.toByte()) Pair(testPlacement, testColor) else null
-                }?.let { (visiblePlacement, visibleColor) ->
-                    placement = visiblePlacement
-                    color = visibleColor.toInt()
-                }
-            }
-
-            room.updateState(placement, color)
-        }
-    }
-
-    private fun updateDoorStates(colors: ByteArray) {
-        val rs = roomSize ?: return
-        val halfRoomSize = rs / 2
-        val startCenter = startCoords?.add(Vec2i(halfRoomSize, halfRoomSize)) ?: return
-
-        for (a in 0..4) {
-            for (b in 0..5) {
-                val doorOffset = halfRoomSize + a * (rs + 4)
-                val midRoomOffset = b * (rs + 4)
-
-                val horizontalDoorIndex = startCenter.add(Vec2i(doorOffset, midRoomOffset)).mapIndex()
-                if (horizontalDoorIndex < colors.size) {
-                    (MapScanner.list[(a * 2 + 1) * 11 + b * 2] as? Door)?.let { door ->
-                        updateDoorFromColor(door, colors[horizontalDoorIndex])
-                    }
-                }
-
-                val verticalDoorIndex = startCenter.add(Vec2i(midRoomOffset, doorOffset)).mapIndex()
-                if (verticalDoorIndex < colors.size) {
-                    (MapScanner.list[(b * 2) * 11 + (a * 2 + 1)] as? Door)?.let { door ->
-                        updateDoorFromColor(door, colors[verticalDoorIndex])
-                    }
-                }
-            }
-        }
-    }
-
-    fun findGreenRoom(mapData: ByteArray): Pair<Int, Int> {
-        var start = -1
-        var length = 0
-
-        for (i in mapData.indices) {
-            if (mapData[i].toInt() == 30) {
-                if (length++ == 0) start = i
-            } else {
-                if (length >= 16) return start to length
-                length = 0
-            }
-        }
-
-        return start to length
-    }
-
-    private fun getColorAtPlacement(placement: Vec2i, startCenter: Vec2i, tileSize: Int, colors: ByteArray): Byte {
-        val mapIndex = startCenter.add(placement.multiply(tileSize)).mapIndex()
-        return if (mapIndex < colors.size) colors[mapIndex] else 0
-    }
-
-    private fun updateDoorFromColor(door: Door, color: Byte) {
-        door.locked = when (color.toInt()) {
-            119 -> {
-                door.rooms.forEach { it.owner?.rushRoom = true }
-                door.type = Door.Type.WITHER
-                true
-            }
-            82 -> {
-                door.rooms.forEach { it.owner?.rushRoom = true }
-                false
-            }
-   
…… diff 过长已截断 ……
Fixed Petkeybinds not working with 8th and 9th keysodtheking · 2026-07-04 · 4e95954
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
index 8f92d1a..976d285 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/skyblock/PetKeybinds.kt
@@ -46,6 +46,8 @@ object PetKeybinds : Module(
         }
     }
 
+    private val petSlots = intArrayOf(10, 11, 12, 13, 14, 15, 16, 19, 20)
+
     private fun onClick(screen: AbstractContainerScreen<*>, keyCode: Int): Boolean {
         val (current, total) = petsRegex.find(screen.title.string)?.destructured?.let {
             (it.component1().toIntOrNull() ?: 1) to (it.component2().toIntOrNull() ?: 1)
@@ -60,8 +62,10 @@ object PetKeybinds : Module(
                     .takeIf { it != -1 }?.plus(10) ?: return false.also { modMessage("§cCouldn't find equipped pet") }
 
             else -> {
-                arrayOf(pet1, pet2, pet3, pet4, pet5, pet6, pet7, pet8, pet9).indexOfFirst { it.value == keyCode }
-                    .takeIf { it != -1 }?.plus(10) ?: return false
+                val petIndex = arrayOf(pet1, pet2, pet3, pet4, pet5, pet6, pet7, pet8, pet9).indexOfFirst { it.value == keyCode }
+                if (petIndex == -1) return false
+
+                petSlots.getOrNull(petIndex) ?: return false
             }
         }
 
Removed duplicate error messageodtheking · 2026-07-04 · 79dc06e
查看变更
src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt
index 0278fd9..a3bed0a 100644
--- a/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt
+++ b/src/main/kotlin/com/odtheking/odin/features/impl/dungeon/BetterPartyFinder.kt
@@ -68,7 +68,7 @@ object BetterPartyFinder : Module(
 
                     val kickedReasons = mutableListOf<String>()
 
-                    val currentProfile = profile.getOrElse { return@launch modMessage(it.message) }.memberData ?: return@launch modMessage("Could not find member data for $name")
+                    val currentProfile = profile.getOrNull()?.memberData ?: return@launch modMessage("Could not find member data for $name")
 
                     val dungeon = if (!mmToggle) currentProfile.dungeons.dungeonTypes.catacombs else currentProfile.dungeons.dungeonTypes.mastermode
                     dungeon.fastestTimeSPlus["$floor"]?.let {
chore: add Lotus Atoll island (#107)jcnlk · 2026-07-04 · 03ad1bc
查看变更
src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt | 1 +
 1 file changed, 1 insertion(+)
diff --git a/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt b/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt
index 6540c52..7d02d5b 100644
--- a/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt
+++ b/src/main/kotlin/com/odtheking/odin/utils/skyblock/Island.kt
@@ -23,5 +23,6 @@ enum class Island(val displayName: String) {
     Rift("The Rift"),
     BackwaterBayou("Backwater Bayou"),
     Galatea("Galatea"),
+    LotusAtoll("Lotus Atoll"),
     Unknown("(Unknown)");
 }
\ No newline at end of file
implement separate version range property (#106)Marie · 2026-07-04 · 087833a
查看变更
gradle.properties                  | 1 +
 src/main/resources/fabric.mod.json | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/gradle.properties b/gradle.properties
index 7bc2d78..19ce37b 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -3,6 +3,7 @@ org.gradle.jvmargs=-Xmx4G
 org.gradle.parallel=true
 #Fabric Properties
 minecraft_version=26.1.2
+minecraft_version_range=~26.1
 loader_version=0.19.3
 loom_version=1.16-SNAPSHOT
 # Mod Properties
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index e796fea..b3232b5 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -30,7 +30,7 @@
   "depends": {
     "fabricloader": ">=${loader_version}",
     "fabric-api": ">=${fabric_api_version}",
-    "minecraft": "~${minecraft_version}",
+    "minecraft": "${minecraft_version_range}",
     "fabric-language-kotlin": ">=${fabric_kotlin_version}"
   },
   "custom": {