From deaf0916ccb3a57954c0c78aa6213c720b135cff Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:18:03 +0200 Subject: [PATCH 01/21] refactor: extract shared failsafe activity guard into base class Every failsafe duplicated the same `isScriptRunning() || isDevelopmentEnvironment` check as the first line of its event handler. Hoist it into a single `Failsafe.shouldReactToEvents()` so the condition has one source of truth. --- .../org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt | 3 +-- .../org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt | 4 +--- .../org/cobalt/module/impl/failsafes/RotationFailsafe.kt | 4 +--- .../org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt | 4 +--- .../org/cobalt/module/impl/failsafes/TeleportFailsafe.kt | 4 +--- .../org/cobalt/module/impl/failsafes/VelocityFailsafe.kt | 4 +--- src/main/kotlin/org/cobalt/module/type/Failsafe.kt | 6 ++++++ 7 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt index a5305672..21a5168c 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt @@ -1,6 +1,5 @@ package org.cobalt.module.impl.failsafes -import net.fabricmc.loader.api.FabricLoader import net.minecraft.network.chat.Component import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket import net.minecraft.network.protocol.game.ClientboundSystemChatPacket @@ -32,7 +31,7 @@ object ChatMentionFailsafe : Failsafe("Chat Mention", 10, false) { @SubscribeEvent fun onChatMessage(event: PacketEvent.Receive) { - if (!ModuleManager.isScriptRunning() && !FabricLoader.getInstance().isDevelopmentEnvironment) return + if (!shouldReactToEvents()) return val packet = event.packet as? ClientboundSystemChatPacket ?: return val message = packet.content.string val sender = getSenderFromComponent(packet.content) diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt index ca9851af..1459ab51 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt @@ -1,13 +1,11 @@ package org.cobalt.module.impl.failsafes -import net.fabricmc.loader.api.FabricLoader import net.minecraft.core.BlockPos import net.minecraft.world.phys.EntityHitResult import net.minecraft.world.phys.HitResult import org.cobalt.Cobalt import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.TickEvent -import org.cobalt.module.ModuleManager import org.cobalt.module.type.Failsafe import org.cobalt.util.client.PlayerUtils import org.cobalt.util.failsafe.FailsafeManager @@ -17,7 +15,7 @@ object PlayerCheckFailsafe : Failsafe("Player Check", 10, false) { @SubscribeEvent fun onTick(ignored: TickEvent.End) { - if (!ModuleManager.isScriptRunning() && !FabricLoader.getInstance().isDevelopmentEnvironment) return + if (!shouldReactToEvents()) return val player = PlayerUtils.player ?: return val level = Cobalt.minecraft.level ?: return diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/RotationFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/RotationFailsafe.kt index e138924e..2ec70207 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/RotationFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/RotationFailsafe.kt @@ -1,15 +1,13 @@ package org.cobalt.module.impl.failsafes -import net.fabricmc.loader.api.FabricLoader import org.cobalt.Cobalt -import org.cobalt.module.ModuleManager import org.cobalt.module.type.Failsafe import org.cobalt.util.failsafe.FailsafeManager import org.cobalt.util.rotation.data.Rotation object RotationFailsafe : Failsafe("Rotation", 10, false) { fun onRotation(currentRot: Rotation, newRot: Rotation) { - if (!ModuleManager.isScriptRunning() && !FabricLoader.getInstance().isDevelopmentEnvironment) return + if (!shouldReactToEvents()) return val player = Cobalt.minecraft.player ?: return if (currentRot == newRot) return // I think this is needed? not sure diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt index 5cd28c97..40750a59 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt @@ -1,11 +1,9 @@ package org.cobalt.module.impl.failsafes -import net.fabricmc.loader.api.FabricLoader import net.minecraft.network.protocol.game.ClientboundSetHeldSlotPacket import org.cobalt.Cobalt import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.PacketEvent -import org.cobalt.module.ModuleManager import org.cobalt.module.type.Failsafe import org.cobalt.util.failsafe.FailsafeManager import org.cobalt.util.inventory.InventoryUtils @@ -13,7 +11,7 @@ import org.cobalt.util.inventory.InventoryUtils object SlotChangeFailsafe : Failsafe("Slot Change", 10, false) { @SubscribeEvent fun onServerItemChange(event: PacketEvent.Receive) { - if (!ModuleManager.isScriptRunning() && !FabricLoader.getInstance().isDevelopmentEnvironment) return + if (!shouldReactToEvents()) return val player = Cobalt.minecraft.player ?: return val packet = event.packet as? ClientboundSetHeldSlotPacket ?: return diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt index e1619768..640e9725 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt @@ -1,6 +1,5 @@ package org.cobalt.module.impl.failsafes -import net.fabricmc.loader.api.FabricLoader import net.minecraft.core.BlockPos import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket import net.minecraft.network.protocol.game.ServerboundChatCommandPacket @@ -8,7 +7,6 @@ import net.minecraft.network.protocol.game.ServerboundUseItemPacket import org.cobalt.Cobalt.minecraft import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.PacketEvent -import org.cobalt.module.ModuleManager import org.cobalt.module.type.Failsafe import org.cobalt.util.chat.ChatUtils import org.cobalt.util.chat.MessageType @@ -21,7 +19,7 @@ import org.cobalt.util.rotation.data.Rotation object TeleportFailsafe : Failsafe("Teleport", 10, false) { @SubscribeEvent fun onTeleport(event: PacketEvent.Any) { - if (!ModuleManager.isScriptRunning() && !FabricLoader.getInstance().isDevelopmentEnvironment) return + if (!shouldReactToEvents()) return if (minecraft.level == null) return when (val packet = event.packet) { is ClientboundPlayerPositionPacket -> { diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/VelocityFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/VelocityFailsafe.kt index 77c50196..8dae5d5a 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/VelocityFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/VelocityFailsafe.kt @@ -1,13 +1,11 @@ package org.cobalt.module.impl.failsafes -import net.fabricmc.loader.api.FabricLoader import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket import net.minecraft.world.level.block.Blocks import net.minecraft.world.phys.Vec3 import org.cobalt.Cobalt import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.PacketEvent -import org.cobalt.module.ModuleManager import org.cobalt.module.type.Failsafe import org.cobalt.util.client.PlayerUtils import org.cobalt.util.failsafe.FailsafeManager @@ -27,7 +25,7 @@ object VelocityFailsafe : Failsafe("Velocity", 10, false) { @SubscribeEvent fun onVelo(event: PacketEvent.Receive) { - if (!ModuleManager.isScriptRunning() && !FabricLoader.getInstance().isDevelopmentEnvironment) return + if (!shouldReactToEvents()) return val player = Cobalt.minecraft.player ?: return val packet = event.packet as? ClientboundSetEntityMotionPacket ?: return diff --git a/src/main/kotlin/org/cobalt/module/type/Failsafe.kt b/src/main/kotlin/org/cobalt/module/type/Failsafe.kt index 98721a0e..0cd7af52 100644 --- a/src/main/kotlin/org/cobalt/module/type/Failsafe.kt +++ b/src/main/kotlin/org/cobalt/module/type/Failsafe.kt @@ -1,7 +1,9 @@ package org.cobalt.module.type +import net.fabricmc.loader.api.FabricLoader import org.cobalt.module.Module import org.cobalt.module.ModuleCategory +import org.cobalt.module.ModuleManager abstract class Failsafe @JvmOverloads constructor( name: String, @@ -13,6 +15,10 @@ abstract class Failsafe @JvmOverloads constructor( startValue = startValue ) { + /** Failsafes only need to react while a script is running, or during manual testing in a dev environment. */ + protected fun shouldReactToEvents(): Boolean = + ModuleManager.isScriptRunning() || FabricLoader.getInstance().isDevelopmentEnvironment + abstract fun resetStates() abstract fun performReaction(): ReactionResult? From bc9684732de96caa488275cbceb706c39a8bbb4a Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:19:15 +0200 Subject: [PATCH 02/21] build: make detekt fail the build again on warnings The codebase is currently detekt-clean (0 findings after the failsafe guard-clause dedup), so there is no longer a backlog blocking this. Restores failOnSeverity = Warning so future violations get caught by `check` instead of accumulating silently. --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index b935bc71..1f4dfa18 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,7 +18,7 @@ base { detekt { buildUponDefaultConfig = true config.setFrom(rootProject.file("config/detekt/detekt.yml")) - failOnSeverity = FailOnSeverity.Never + failOnSeverity = FailOnSeverity.Warning allRules = false ignoredBuildTypes = listOf() } From 30a937853005afffdf40f74435db8503d9444bfe Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:19:52 +0200 Subject: [PATCH 03/21] fix: close the stream opened by ResourceUtils.read() for remote URLs The HTTP InputStream was never closed, leaking a connection on every remote font/image fetch. UpdateChecker already closes the same getInputStream() result correctly via .use{} -- apply the same pattern here. Also drops the runBlocking wrapper around what was already a blocking call. --- src/main/kotlin/org/cobalt/util/web/ResourceUtils.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/kotlin/org/cobalt/util/web/ResourceUtils.kt b/src/main/kotlin/org/cobalt/util/web/ResourceUtils.kt index 38d103ac..4b0c9eb9 100644 --- a/src/main/kotlin/org/cobalt/util/web/ResourceUtils.kt +++ b/src/main/kotlin/org/cobalt/util/web/ResourceUtils.kt @@ -6,7 +6,6 @@ import java.io.InputStream import java.net.HttpURLConnection import java.net.URI import java.nio.file.Files -import kotlinx.coroutines.runBlocking import org.cobalt.Cobalt object ResourceUtils { @@ -16,9 +15,7 @@ object ResourceUtils { val trimmedPath = location.trim() return if (trimmedPath.startsWith("http")) { - runBlocking { - getInputStream(trimmedPath).readBytes() - } + getInputStream(trimmedPath).use { it.readBytes() } } else { val file = File(trimmedPath) From 85489ddbf51bda10f3bd4eb6e7330f2a24994e17 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:21:31 +0200 Subject: [PATCH 04/21] fix: use thread-safe collections for state shared across threads ModuleManager.modules, AddonManager.addons, AudioManager.clips, TickScheduler.taskQueue, and FailsafeManager.ignoreGens were all plain non-thread-safe collections despite being touched from handlers that can run off the client thread (Java Sound line-event callbacks, Multithreading's worker pool). EventBus already established CopyOnWriteArrayList/ ConcurrentHashMap as this codebase's pattern for that; apply it consistently. --- src/main/kotlin/org/cobalt/addon/AddonManager.kt | 3 ++- src/main/kotlin/org/cobalt/module/ModuleManager.kt | 3 ++- src/main/kotlin/org/cobalt/util/audio/AudioManager.kt | 5 ++++- .../kotlin/org/cobalt/util/failsafe/FailsafeManager.kt | 2 +- .../kotlin/org/cobalt/util/scheduling/TickScheduler.kt | 8 ++++++-- 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/org/cobalt/addon/AddonManager.kt b/src/main/kotlin/org/cobalt/addon/AddonManager.kt index 6e5b7d60..3c0ec493 100644 --- a/src/main/kotlin/org/cobalt/addon/AddonManager.kt +++ b/src/main/kotlin/org/cobalt/addon/AddonManager.kt @@ -3,6 +3,7 @@ package org.cobalt.addon import com.google.gson.Gson import java.nio.file.Files import java.nio.file.Path +import java.util.concurrent.CopyOnWriteArrayList import java.util.zip.ZipFile import kotlin.io.path.extension import net.fabricmc.loader.api.FabricLoader @@ -16,7 +17,7 @@ object AddonManager { private val gson = Gson() @JvmStatic - val addons = mutableListOf>() + val addons = CopyOnWriteArrayList>() @JvmStatic internal fun onPreLaunch() { diff --git a/src/main/kotlin/org/cobalt/module/ModuleManager.kt b/src/main/kotlin/org/cobalt/module/ModuleManager.kt index b2e460e5..d0bb9a9c 100644 --- a/src/main/kotlin/org/cobalt/module/ModuleManager.kt +++ b/src/main/kotlin/org/cobalt/module/ModuleManager.kt @@ -1,5 +1,6 @@ package org.cobalt.module +import java.util.concurrent.CopyOnWriteArraySet import net.minecraft.client.gui.screens.LevelLoadingScreen import net.minecraft.client.gui.screens.ProgressScreen import org.cobalt.Cobalt.minecraft @@ -27,7 +28,7 @@ import org.cobalt.util.render.skia.SkiaPIP object ModuleManager { - val modules = mutableSetOf() + val modules = CopyOnWriteArraySet() var lastScript: Script? = null var currentScript: Script? = null diff --git a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt index b6736a66..f39c8251 100644 --- a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt +++ b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt @@ -1,6 +1,7 @@ package org.cobalt.util.audio import java.io.File +import java.util.concurrent.CopyOnWriteArrayList import javax.sound.sampled.AudioSystem import javax.sound.sampled.Clip import javax.sound.sampled.FloatControl @@ -11,7 +12,9 @@ import org.slf4j.LoggerFactory object AudioManager { private val logger = LoggerFactory.getLogger(this::class.java) - private val clips = mutableListOf() + + // clip.addLineListener callbacks fire on the Java Sound line-event thread, not the caller's thread + private val clips = CopyOnWriteArrayList() // YES I KNOW THIS PLAYS THRU SPEAKERS ISNTEAD OF HEADPHONES IDK WHY fun play(file: File, volume: Float = 1.0f) { diff --git a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt b/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt index 214512c3..6fb05cbf 100644 --- a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt +++ b/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt @@ -26,7 +26,7 @@ object FailsafeManager { var failsafes = mutableListOf() private val tempIgnored = ConcurrentHashMap.newKeySet() - private val ignoreGens = mutableMapOf() + private val ignoreGens = ConcurrentHashMap() private var triggeredFailsafe: Failsafe? = null private var queueTimer = 0 diff --git a/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt b/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt index a05b5425..e6e94797 100644 --- a/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt +++ b/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt @@ -1,6 +1,6 @@ package org.cobalt.util.scheduling -import java.util.PriorityQueue +import java.util.concurrent.PriorityBlockingQueue import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.TickEvent @@ -8,7 +8,11 @@ import org.cobalt.event.impl.TickEvent object TickScheduler { private var currentTick: Long = 0 - private val taskQueue = PriorityQueue( + + // PriorityBlockingQueue rather than PriorityQueue: schedule() can be called from Multithreading's worker + // threads while onClientTick concurrently drains this on the client thread. + private val taskQueue = PriorityBlockingQueue( + 11, Comparator.comparingLong(ScheduledTask::executeTick) ) From 87dd1129bdc11230a7c8acc4f9b900ec1d33a681 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:22:35 +0200 Subject: [PATCH 05/21] chore: remove unused imports --- src/main/kotlin/org/cobalt/command/impl/MainCommand.kt | 5 ----- .../org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt | 2 -- .../org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt | 2 -- .../org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt | 1 - .../org/cobalt/module/impl/failsafes/TeleportFailsafe.kt | 1 - src/main/kotlin/org/cobalt/util/audio/AudioManager.kt | 1 - 6 files changed, 12 deletions(-) diff --git a/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt b/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt index 578efdd4..aec3a0ce 100644 --- a/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt +++ b/src/main/kotlin/org/cobalt/command/impl/MainCommand.kt @@ -1,7 +1,5 @@ package org.cobalt.command.impl -import java.io.File -import net.minecraft.world.entity.Entity import net.minecraft.world.entity.player.Player import net.minecraft.world.phys.EntityHitResult import net.minecraft.world.phys.HitResult @@ -12,11 +10,8 @@ import org.cobalt.command.annotation.SubCommand import org.cobalt.ui.screen.ConfigScreen import org.cobalt.ui.screen.HudEditorScreen import org.cobalt.event.EventBus -import org.cobalt.module.impl.failsafes.TeleportFailsafe import org.cobalt.module.impl.misc.Rotations -import org.cobalt.util.audio.AudioManager import org.cobalt.util.chat.ChatUtils -import org.cobalt.util.failsafe.FailsafeManager import org.cobalt.util.rotation.data.Rotation import org.cobalt.util.scheduling.TickScheduler import org.cobalt.util.server.Scoreboard diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt index 21a5168c..ab9d9c67 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/ChatMentionFailsafe.kt @@ -1,14 +1,12 @@ package org.cobalt.module.impl.failsafes import net.minecraft.network.chat.Component -import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket import net.minecraft.network.protocol.game.ClientboundSystemChatPacket import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.PacketEvent import org.cobalt.module.ModuleManager import org.cobalt.module.type.Failsafe import org.cobalt.ui.component.setting.impl.CheckboxSetting -import org.cobalt.ui.component.setting.impl.ModeSetting import org.cobalt.ui.component.setting.impl.TextSetting import org.cobalt.util.chat.ChatUtils import org.cobalt.util.chat.MessageType diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt index 1459ab51..3f6fac94 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/PlayerCheckFailsafe.kt @@ -1,8 +1,6 @@ package org.cobalt.module.impl.failsafes import net.minecraft.core.BlockPos -import net.minecraft.world.phys.EntityHitResult -import net.minecraft.world.phys.HitResult import org.cobalt.Cobalt import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.TickEvent diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt index 40750a59..9ace42de 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/SlotChangeFailsafe.kt @@ -6,7 +6,6 @@ import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.PacketEvent import org.cobalt.module.type.Failsafe import org.cobalt.util.failsafe.FailsafeManager -import org.cobalt.util.inventory.InventoryUtils object SlotChangeFailsafe : Failsafe("Slot Change", 10, false) { @SubscribeEvent diff --git a/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt b/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt index 640e9725..7427b18e 100644 --- a/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt +++ b/src/main/kotlin/org/cobalt/module/impl/failsafes/TeleportFailsafe.kt @@ -13,7 +13,6 @@ import org.cobalt.util.chat.MessageType import org.cobalt.util.client.PlayerUtils.player import org.cobalt.util.failsafe.FailsafeManager import org.cobalt.util.inventory.InventoryUtils -import org.cobalt.util.inventory.ItemUtils import org.cobalt.util.rotation.data.Rotation object TeleportFailsafe : Failsafe("Teleport", 10, false) { diff --git a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt index f39c8251..2683e667 100644 --- a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt +++ b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt @@ -6,7 +6,6 @@ import javax.sound.sampled.AudioSystem import javax.sound.sampled.Clip import javax.sound.sampled.FloatControl import kotlin.math.log10 -import net.minecraft.client.Minecraft import org.slf4j.LoggerFactory object AudioManager { From 02e5f0561e91c4dad2855b503fd56fa750561be5 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:23:41 +0200 Subject: [PATCH 06/21] chore: remove dead code - SubCommand.name was declared but never read; buildSubCommand() always uses the Kotlin function name for the brigadier literal. - ButtonSetting.renderSetting() re-bound buttonWidth to itself with no effect. - ModeSetting.mouseClicked()'s options.isEmpty() check was unreachable, since the init block already requires options to be non-empty at construction. --- src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt | 2 +- .../org/cobalt/ui/component/setting/impl/ButtonSetting.kt | 1 - .../org/cobalt/ui/component/setting/impl/ModeSetting.kt | 4 ---- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt b/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt index 9339d0d9..76d74315 100644 --- a/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt +++ b/src/main/kotlin/org/cobalt/command/annotation/SubCommand.kt @@ -2,4 +2,4 @@ package org.cobalt.command.annotation @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) -annotation class SubCommand(val name: String = "") +annotation class SubCommand diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt index 7b800244..1dd04661 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt @@ -27,7 +27,6 @@ class ButtonSetting( SkiaRenderer.textWidth(SkiaRenderer.regularFont, buttonLabel, FONT_SIZE) + 30f override fun renderSetting() { - val buttonWidth = buttonWidth val startX = xPos + width - buttonWidth - PADDING val startY = yPos + (height - BUTTON_HEIGHT) / 2 diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt index 2d7c74b4..56dc63a5 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt @@ -66,10 +66,6 @@ class ModeSetting( } override fun mouseClicked(button: Int): Boolean { - if (options.isEmpty()) { - return false - } - val display = options.getOrNull(value).orEmpty() val buttonWidth = SkiaRenderer.textWidth(SkiaRenderer.regularFont, display, FONT_SIZE) + 30f val startX = xPos + width - buttonWidth - PADDING From da1be14dd3a45080f3589d02e88650b3ee823954 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:25:03 +0200 Subject: [PATCH 07/21] refactor: replace positional Int mode branching with enums AutoClicker.attackMode and PlayerESP.espType were raw Ints matched positionally against a ModeSetting options array, so reordering the array would silently desync the branches with no compiler warning (unlike Rotations.RotationType, which already models the same kind of concept as an enum). ModeSetting itself still persists a plain Int (its UI/serialization format is unchanged); the enums are a typed view over that Int at the consuming site, making the when-branches exhaustive and named. --- .../cobalt/module/impl/combat/AutoClicker.kt | 19 +++++++++---- .../cobalt/module/impl/visual/PlayerESP.kt | 27 ++++++++++++------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt b/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt index eb56c17a..6e5cfebb 100644 --- a/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt +++ b/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt @@ -56,6 +56,9 @@ object AutoClicker : Module( options = arrayOf("All", "Entity Only", "No Blocks"), ) + private val attackModeType: AttackMode + get() = AttackMode.entries.getOrElse(attackMode) { AttackMode.ALL } + private val mobFilter by TextSetting( name = "Mob Filter", description = "Only attack listed names (comma-separated)", @@ -104,11 +107,10 @@ object AutoClicker : Module( val player = minecraft.player ?: return false val hit = minecraft.hitResult - val attackAllowed = when (attackMode) { - 0 -> true - 1 -> hit?.type == HitResult.Type.ENTITY - 2 -> hit?.type != HitResult.Type.BLOCK - else -> false + val attackAllowed = when (attackModeType) { + AttackMode.ALL -> true + AttackMode.ENTITY_ONLY -> hit?.type == HitResult.Type.ENTITY + AttackMode.NO_BLOCKS -> hit?.type != HitResult.Type.BLOCK } if (!attackAllowed || (waitForReload && player.getAttackStrengthScale(0f) < 1.0f)) { @@ -134,4 +136,11 @@ object AutoClicker : Module( return (base + jitter).coerceAtLeast(1.0).toLong() } + // Ordinal must match the "Attack Mode" ModeSetting options order above. + private enum class AttackMode { + ALL, + ENTITY_ONLY, + NO_BLOCKS, + } + } diff --git a/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt b/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt index f93963b4..e593f18f 100644 --- a/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt +++ b/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt @@ -21,8 +21,19 @@ object PlayerESP : Module(name = "PlayerESP", category = ModuleCategory.VISUAL) options = arrayOf("Box", "Target Beam", "Outline", "Beam & Outline") ) + // Ordinal must match the "ESP Type" ModeSetting options order above. + private enum class EspType { + BOX, + TARGET_BEAM, + OUTLINE, + BEAM_AND_OUTLINE, + } + + private val espTypeMode: EspType + get() = EspType.entries.getOrElse(espType) { EspType.BOX } + fun shouldOutline(entity: Entity): Boolean { - if (espType != 2 && espType != 3) { + if (espTypeMode != EspType.OUTLINE && espTypeMode != EspType.BEAM_AND_OUTLINE) { return false } @@ -45,7 +56,7 @@ object PlayerESP : Module(name = "PlayerESP", category = ModuleCategory.VISUAL) @SubscribeEvent fun onWorldRender(ignored: WorldEvent.BeforeGizmos) { - if (espType == 2) { + if (espTypeMode == EspType.OUTLINE) { return } @@ -56,24 +67,20 @@ object PlayerESP : Module(name = "PlayerESP", category = ModuleCategory.VISUAL) if (player.uuid.version() == 2) return@forEach - when (espType) { - 0 -> GizmoRenderer.drawEntityOutline( + when (espTypeMode) { + EspType.BOX -> GizmoRenderer.drawEntityOutline( entity = player, color = ThemeManager.activeTheme.accentPrimary, esp = true ) - 1 -> GizmoRenderer.drawTargetBeam( + EspType.TARGET_BEAM, EspType.BEAM_AND_OUTLINE -> GizmoRenderer.drawTargetBeam( entity = player, color = ThemeManager.activeTheme.accentPrimary, esp = true ) - 3 -> GizmoRenderer.drawTargetBeam( // both beam and esp - entity = player, - color = ThemeManager.activeTheme.accentPrimary, - esp = true - ) + EspType.OUTLINE -> Unit // handled by shouldOutline(), not drawn in this loop } } } From 639cebe1009bd801e74f4dfa825c3a408469e412 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:25:51 +0200 Subject: [PATCH 08/21] build: move DevAuth-fabric into the version catalog Every other dependency is declared via libs.versions.toml; DevAuth was the only one hardcoded as a raw coordinate string in build.gradle.kts. --- build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1f4dfa18..3e69764e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -55,7 +55,7 @@ dependencies { jij(libs.skija.shared) jij(libs.discordIpc) - runtimeOnly("me.djtheredstoner:DevAuth-fabric:1.2.2") + runtimeOnly(libs.devAuth) } addResolvedDependencies(jij, "compileOnly", "include", "api") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5639e0e4..d68e9604 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,6 +8,7 @@ fabric-kotlin = "1.13.12+kotlin.2.4.0" skija-version = "0.143.11" discordIpc = "0.6.0-SNAPSHOT" +devAuth = "1.2.2" # Plugins Versions kotlin = "2.4.0" @@ -23,6 +24,7 @@ fabric-kotlin = { group = "net.fabricmc", name = "fabric-language-kotlin", versi skija-shared = { group = "io.github.humbleui", name = "skija-shared", version.ref = "skija-version" } discordIpc = { group = "com.jagrosh", name = "DiscordIPC", version.ref = "discordIpc" } +devAuth = { group = "me.djtheredstoner", name = "DevAuth-fabric", version.ref = "devAuth" } [plugins] kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } From be3103b26f5d3c9c08db4562cebc0f9cefc7ede6 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:27:19 +0200 Subject: [PATCH 09/21] refactor: fold FailsafeUtils into FailsafeManager FailsafeUtils was a single init() with no independent responsibility beyond calling FailsafeManager.ignoreFailsafe() -- an arbitrary Manager/Utils split for what is really one piece of FailsafeManager's own startup behavior. --- src/main/kotlin/org/cobalt/Cobalt.kt | 2 -- .../cobalt/util/failsafe/FailsafeManager.kt | 14 +++++++++++ .../org/cobalt/util/failsafe/FailsafeUtils.kt | 23 ------------------- 3 files changed, 14 insertions(+), 25 deletions(-) delete mode 100644 src/main/kotlin/org/cobalt/util/failsafe/FailsafeUtils.kt diff --git a/src/main/kotlin/org/cobalt/Cobalt.kt b/src/main/kotlin/org/cobalt/Cobalt.kt index a7cb1ad7..fb9ade0b 100644 --- a/src/main/kotlin/org/cobalt/Cobalt.kt +++ b/src/main/kotlin/org/cobalt/Cobalt.kt @@ -12,7 +12,6 @@ import org.cobalt.command.CommandManager import org.cobalt.module.ModuleManager import org.cobalt.ui.theme.ThemeManager import org.cobalt.util.failsafe.FailsafeManager -import org.cobalt.util.failsafe.FailsafeUtils import org.cobalt.util.input.KeyMappingHandler import org.cobalt.util.render.skia.SkiaPIP import org.cobalt.util.scheduling.Multithreading @@ -58,7 +57,6 @@ object Cobalt : ClientModInitializer { CommandManager.registerCommands() FailsafeManager.initialize() - FailsafeUtils.init() KeyMappingHandler.registerKeyMappings() Multithreading.runAsync { diff --git a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt b/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt index 6fb05cbf..7bea481a 100644 --- a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt +++ b/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt @@ -1,6 +1,7 @@ package org.cobalt.util.failsafe import java.util.concurrent.ConcurrentHashMap +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent import org.cobalt.event.impl.TickEvent @@ -54,9 +55,22 @@ object FailsafeManager { fun initialize() { registerDefaultFailsafe() + registerWorldChangeIgnores() EventBus.register(this) } + private fun registerWorldChangeIgnores() { + ClientPlayConnectionEvents.JOIN.register { _, _, _ -> + val falseFlagOnWorldChange = listOf( + TeleportFailsafe, + SlotChangeFailsafe, + RotationFailsafe + ) + + falseFlagOnWorldChange.forEach { ignoreFailsafe(it) } + } + } + fun ignoreFailsafe(failsafe: Failsafe) { val gen = (ignoreGens[failsafe] ?: 0L) + 1L ignoreGens[failsafe] = gen diff --git a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeUtils.kt b/src/main/kotlin/org/cobalt/util/failsafe/FailsafeUtils.kt deleted file mode 100644 index 4e6b179a..00000000 --- a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeUtils.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.cobalt.util.failsafe - -import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents -import org.cobalt.module.impl.failsafes.RotationFailsafe -import org.cobalt.module.impl.failsafes.SlotChangeFailsafe -import org.cobalt.module.impl.failsafes.TeleportFailsafe -import org.cobalt.module.type.Failsafe - -object FailsafeUtils { - fun init() { - ClientPlayConnectionEvents.JOIN.register { _, _, _ -> - val falseFlagOnWorldChange = listOf( - TeleportFailsafe, - SlotChangeFailsafe, - RotationFailsafe - ) - - falseFlagOnWorldChange.forEach { - FailsafeManager.ignoreFailsafe(it) - } - } - } -} From b05fd60914002110dba7aa92593634a91375b79e Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:28:50 +0200 Subject: [PATCH 10/21] fix: correct the declared import layout to match actual practice .editorconfig declared a grouped import layout (non-JDK first, then java/ javax/kotlin/static last) that nothing ever enforced -- the project's own IntelliJ code style is KOTLIN_OFFICIAL (plain alphabetical, single group), which is what every file in the codebase already follows. The declared rule was the stale one; reformatting ~150 files to match a rule that contradicts the project's own configured code style would just fight the IDE on every future edit. Point the config at reality instead. --- .editorconfig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 17418e66..c3aca48c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,12 +12,12 @@ indent_size = 2 [*.kt] max_line_length = 120 -ij_kotlin_imports_layout = *, java, javax, kotlin, static * +ij_kotlin_imports_layout = * ij_kotlin_allow_trailing_comma = true [*.java] max_line_length = 120 -ij_java_imports_layout = *, java, javax, static * +ij_java_imports_layout = * [*.{kts,xml,yml,json,md}] indent_style = space From 876b985ab3d1a23d315343fd2c18393123baddca Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:32:56 +0200 Subject: [PATCH 11/21] refactor: consolidate the repeated 20f UI edge padding into one constant PADDING (and TopbarComponent's differently-named INNER_PADDING) was independently redeclared at the same 20f value in 7 files. Define it once as a top-level constant in the ui package and have every component/page/setting reference that instead. Purely a constant extraction -- every value stays 20f, so rendering output is unchanged. --- src/main/kotlin/org/cobalt/ui/UIComponent.kt | 3 +++ src/main/kotlin/org/cobalt/ui/component/ModuleComponent.kt | 2 +- src/main/kotlin/org/cobalt/ui/component/ScriptComponent.kt | 2 +- src/main/kotlin/org/cobalt/ui/component/TopbarComponent.kt | 6 +++--- src/main/kotlin/org/cobalt/ui/component/setting/Setting.kt | 2 +- .../org/cobalt/ui/component/setting/impl/ButtonSetting.kt | 1 + .../org/cobalt/ui/component/setting/impl/CheckboxSetting.kt | 1 + .../org/cobalt/ui/component/setting/impl/InfoSetting.kt | 1 + .../org/cobalt/ui/component/setting/impl/KeyBindSetting.kt | 1 + .../org/cobalt/ui/component/setting/impl/ModeSetting.kt | 1 + .../org/cobalt/ui/component/setting/impl/RangeSetting.kt | 1 + .../org/cobalt/ui/component/setting/impl/SliderSetting.kt | 1 + .../org/cobalt/ui/component/setting/impl/TextSetting.kt | 1 + src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt | 2 +- src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt | 2 +- src/main/kotlin/org/cobalt/ui/page/impl/ThemesPage.kt | 2 +- 16 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/org/cobalt/ui/UIComponent.kt b/src/main/kotlin/org/cobalt/ui/UIComponent.kt index 05b831e0..6824b19f 100644 --- a/src/main/kotlin/org/cobalt/ui/UIComponent.kt +++ b/src/main/kotlin/org/cobalt/ui/UIComponent.kt @@ -5,6 +5,9 @@ import net.minecraft.client.input.KeyEvent import org.cobalt.ui.theme.Theme import org.cobalt.ui.theme.ThemeManager +/** Standard edge padding shared by components, pages, and settings throughout the UI. */ +const val PADDING = 20f + abstract class UIComponent( var xPos: Float = 0f, var yPos: Float = 0f, diff --git a/src/main/kotlin/org/cobalt/ui/component/ModuleComponent.kt b/src/main/kotlin/org/cobalt/ui/component/ModuleComponent.kt index 6a2f1c81..b98848df 100644 --- a/src/main/kotlin/org/cobalt/ui/component/ModuleComponent.kt +++ b/src/main/kotlin/org/cobalt/ui/component/ModuleComponent.kt @@ -1,6 +1,7 @@ package org.cobalt.ui.component import org.cobalt.module.Module +import org.cobalt.ui.PADDING import org.cobalt.ui.UIComponent import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.util.render.SkiaRenderer @@ -126,7 +127,6 @@ class ModuleComponent( private const val BASE_HEIGHT = 60f private const val FONT_SIZE = 16f - private const val PADDING = 20f } } diff --git a/src/main/kotlin/org/cobalt/ui/component/ScriptComponent.kt b/src/main/kotlin/org/cobalt/ui/component/ScriptComponent.kt index d057dcfd..3fa5f7c9 100644 --- a/src/main/kotlin/org/cobalt/ui/component/ScriptComponent.kt +++ b/src/main/kotlin/org/cobalt/ui/component/ScriptComponent.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.component import org.cobalt.module.ModuleManager import org.cobalt.module.type.Script +import org.cobalt.ui.PADDING import org.cobalt.ui.UIComponent import org.cobalt.ui.animation.ColorAnimation import org.cobalt.ui.animation.EaseOutAnimation @@ -123,7 +124,6 @@ class ScriptComponent(val script: Script) : UIComponent( val WIDTH = (TopbarComponent.width - 60) / 2f val pauseIcon = SkiaRenderer.createImage("/assets/cobalt/ui/pause.svg") - private const val PADDING = 20f private const val FONT_SIZE = 20f private const val ICON_SIZE = 30f } diff --git a/src/main/kotlin/org/cobalt/ui/component/TopbarComponent.kt b/src/main/kotlin/org/cobalt/ui/component/TopbarComponent.kt index 5331b12b..4b7e3ab0 100644 --- a/src/main/kotlin/org/cobalt/ui/component/TopbarComponent.kt +++ b/src/main/kotlin/org/cobalt/ui/component/TopbarComponent.kt @@ -1,6 +1,7 @@ package org.cobalt.ui.component import org.cobalt.Cobalt.minecraft +import org.cobalt.ui.PADDING import org.cobalt.ui.UIComponent import org.cobalt.ui.component.button.IconButton import org.cobalt.ui.page.impl.ScriptsPage @@ -61,7 +62,7 @@ object TopbarComponent : UIComponent() { corners = arrayOf(SkiaCorner.TOP_RIGHT) ) - val textX = xPos + INNER_PADDING + 10f + val textX = xPos + PADDING + 10f val textY = yPos + (height - CURRENT_PAGE_TITLE_FONT) / 2 SkiaRenderer.text( @@ -73,7 +74,7 @@ object TopbarComponent : UIComponent() { color = theme.textPrimary ) - val searchBarX = xPos + width - SEARCHBAR_WIDTH - INNER_PADDING + val searchBarX = xPos + width - SEARCHBAR_WIDTH - PADDING val searchBarY = yPos + (height - SEARCHBAR_HEIGHT) / 2 searchBar @@ -97,7 +98,6 @@ object TopbarComponent : UIComponent() { private const val SEARCHBAR_WIDTH = 250f private const val SEARCHBAR_HEIGHT = 40f private const val CURRENT_PAGE_TITLE_FONT = 19f - private const val INNER_PADDING = 20f private const val BUTTON_SPACING = 10f } diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/Setting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/Setting.kt index 91649389..0ef7bd35 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/Setting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/Setting.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonElement import kotlin.properties.PropertyDelegateProvider import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty +import org.cobalt.ui.PADDING import org.cobalt.ui.UIComponent import org.cobalt.ui.component.ModuleComponent import org.cobalt.ui.component.setting.impl.InfoSetting @@ -76,7 +77,6 @@ abstract class Setting( companion object { protected const val BASE_HEIGHT = 60f - protected const val PADDING = 20f protected const val NAME_SIZE = 14f protected const val DESCRIPTION_SIZE = 12f protected const val TEXT_SPACING_Y = 5f diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt index 1dd04661..48aaa352 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonPrimitive +import org.cobalt.ui.PADDING import org.cobalt.ui.animation.ColorAnimation import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.ui.component.setting.Setting diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/CheckboxSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/CheckboxSetting.kt index d70d6c6e..cf42c42a 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/CheckboxSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/CheckboxSetting.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonPrimitive +import org.cobalt.ui.PADDING import org.cobalt.ui.animation.ColorAnimation import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.ui.component.setting.Setting diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/InfoSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/InfoSetting.kt index f7c25022..808c4c21 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/InfoSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/InfoSetting.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonPrimitive +import org.cobalt.ui.PADDING import org.cobalt.ui.component.setting.Setting import org.cobalt.util.color.updateAlpha import org.cobalt.util.render.SkiaRenderer diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/KeyBindSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/KeyBindSetting.kt index 77312339..15607391 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/KeyBindSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/KeyBindSetting.kt @@ -4,6 +4,7 @@ import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import com.mojang.blaze3d.platform.InputConstants import net.minecraft.client.input.KeyEvent +import org.cobalt.ui.PADDING import org.cobalt.ui.component.setting.Setting import org.cobalt.util.input.Mouse import org.cobalt.util.render.SkiaRenderer diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt index 56dc63a5..ed0d0743 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ModeSetting.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonPrimitive +import org.cobalt.ui.PADDING import org.cobalt.ui.component.setting.Setting import org.cobalt.util.input.Mouse import org.cobalt.util.render.SkiaRenderer diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt index 9b82b1c6..8606d79d 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt @@ -3,6 +3,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import org.cobalt.ui.PADDING import org.cobalt.ui.component.setting.Setting import org.cobalt.util.input.Mouse import org.cobalt.util.render.SkiaRenderer diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt index 49bb194c..5372731f 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonPrimitive +import org.cobalt.ui.PADDING import org.cobalt.ui.component.setting.Setting import org.cobalt.util.input.Mouse import org.cobalt.util.render.SkiaRenderer diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/TextSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TextSetting.kt index 3235bd84..60f04694 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/TextSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TextSetting.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonPrimitive +import org.cobalt.ui.PADDING import org.cobalt.ui.component.TextInputComponent import org.cobalt.ui.component.setting.Setting diff --git a/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt b/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt index 5076c7c4..d57c3967 100644 --- a/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt +++ b/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt @@ -3,6 +3,7 @@ package org.cobalt.ui.page.impl import org.cobalt.module.Module import org.cobalt.module.ModuleCategory import org.cobalt.module.ModuleManager +import org.cobalt.ui.PADDING import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.ui.component.ModuleComponent import org.cobalt.ui.helper.ScrollHelper @@ -109,7 +110,6 @@ object ModulesPage : Page() { return false } - private const val PADDING = 20f private const val COLUMN_GAP = 20f } diff --git a/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt b/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt index b78d882a..f98cea11 100644 --- a/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt +++ b/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt @@ -2,6 +2,7 @@ package org.cobalt.ui.page.impl import org.cobalt.module.ModuleManager import org.cobalt.module.type.Script +import org.cobalt.ui.PADDING import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.ui.component.ScriptComponent import org.cobalt.ui.helper.ScrollHelper @@ -109,7 +110,6 @@ object ScriptsPage : Page() { return false } - private const val PADDING = 20f private const val COLUMN_GAP = 20f private const val GROUP_GAP = 28f private const val CATEGORY_FONT_SIZE = 12f diff --git a/src/main/kotlin/org/cobalt/ui/page/impl/ThemesPage.kt b/src/main/kotlin/org/cobalt/ui/page/impl/ThemesPage.kt index c7b95233..f26f3d8f 100644 --- a/src/main/kotlin/org/cobalt/ui/page/impl/ThemesPage.kt +++ b/src/main/kotlin/org/cobalt/ui/page/impl/ThemesPage.kt @@ -1,6 +1,7 @@ package org.cobalt.ui.page.impl import kotlin.math.ceil +import org.cobalt.ui.PADDING import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.ui.component.ThemeComponent import org.cobalt.ui.component.button.IconButton @@ -95,7 +96,6 @@ object ThemesPage : Page() { return false } - private const val PADDING = 20f private const val COLUMNS = 3 private const val SPACING = 20f From 0a30432282b9b6efbe506b23e800f0ddcecf2c97 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:34:28 +0200 Subject: [PATCH 12/21] refactor: extract shared HoverFade helper from IconButton and ButtonSetting Both components duplicated the exact same hover-transition idiom: track wasHovering, restart a ColorAnimation + EaseOutAnimation pair on transition, then derive an overlay alpha and a border/text color from it. Pull that into one HoverFade class. SidebarButton and CheckboxSetting use the same two animation primitives but are driven by different state (selection, toggled value) with different draw structure, so they were left alone rather than forced into this abstraction. --- .../org/cobalt/ui/animation/HoverFade.kt | 26 +++++++++++++++++++ .../cobalt/ui/component/button/IconButton.kt | 21 +++++---------- .../component/setting/impl/ButtonSetting.kt | 20 +++++--------- 3 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt diff --git a/src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt b/src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt new file mode 100644 index 00000000..45a651ea --- /dev/null +++ b/src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt @@ -0,0 +1,26 @@ +package org.cobalt.ui.animation + +import java.awt.Color + +/** Shared hover-transition animation: restarts a color/alpha fade whenever the hover state flips. */ +class HoverFade(duration: Long = 150L) { + + private val colorAnimation = ColorAnimation(duration) + private val alphaAnimation = EaseOutAnimation(duration) + private var wasHovering = false + + fun update(hovering: Boolean) { + if (hovering != wasHovering) { + colorAnimation.start() + alphaAnimation.start() + wasHovering = hovering + } + } + + fun overlayAlpha(hovering: Boolean, from: Float = 0f, to: Float = 40f): Int = + alphaAnimation.get(from, to, !hovering).toInt() + + fun color(from: Color, to: Color, hovering: Boolean): Color = + colorAnimation.get(from, to, !hovering) + +} diff --git a/src/main/kotlin/org/cobalt/ui/component/button/IconButton.kt b/src/main/kotlin/org/cobalt/ui/component/button/IconButton.kt index 4724be58..c1e5d9ab 100644 --- a/src/main/kotlin/org/cobalt/ui/component/button/IconButton.kt +++ b/src/main/kotlin/org/cobalt/ui/component/button/IconButton.kt @@ -1,8 +1,7 @@ package org.cobalt.ui.component.button import org.cobalt.ui.UIComponent -import org.cobalt.ui.animation.ColorAnimation -import org.cobalt.ui.animation.EaseOutAnimation +import org.cobalt.ui.animation.HoverFade import org.cobalt.util.color.updateAlpha import org.cobalt.util.input.Mouse import org.cobalt.util.render.SkiaRenderer @@ -16,23 +15,15 @@ class IconButton( ) { private val icon = SkiaRenderer.createImage(resourcePath) - private val colorAnimation = ColorAnimation(150L) - private val alphaAnimation = EaseOutAnimation(150L) - - private var wasHovering = false + private val hoverFade = HoverFade() override fun renderComponent() { val hovering = Mouse.isHoveringOver(xPos, yPos, width, height) + hoverFade.update(hovering) - if (hovering != wasHovering) { - colorAnimation.start() - alphaAnimation.start() - wasHovering = hovering - } - - val alpha = alphaAnimation.get(0f, 40f, !hovering).toInt() - val borderColor = colorAnimation.get(theme.border, theme.accentPrimary, !hovering) - val iconColor = colorAnimation.get(theme.textMuted, theme.accentPrimary, !hovering) + val alpha = hoverFade.overlayAlpha(hovering) + val borderColor = hoverFade.color(theme.border, theme.accentPrimary, hovering) + val iconColor = hoverFade.color(theme.textMuted, theme.accentPrimary, hovering) SkiaRenderer.roundedRect( x = xPos, diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt index 48aaa352..21399bb4 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/ButtonSetting.kt @@ -3,8 +3,7 @@ package org.cobalt.ui.component.setting.impl import com.google.gson.JsonElement import com.google.gson.JsonPrimitive import org.cobalt.ui.PADDING -import org.cobalt.ui.animation.ColorAnimation -import org.cobalt.ui.animation.EaseOutAnimation +import org.cobalt.ui.animation.HoverFade import org.cobalt.ui.component.setting.Setting import org.cobalt.util.color.updateAlpha import org.cobalt.util.input.Mouse @@ -20,9 +19,7 @@ class ButtonSetting( override fun read(element: JsonElement) = Unit override fun write(): JsonElement = JsonPrimitive("") - private val colorAnim = ColorAnimation(150L) - private val alphaAnim = EaseOutAnimation(150L) - private var wasHovering = false + private val hoverFade = HoverFade() private val buttonWidth = SkiaRenderer.textWidth(SkiaRenderer.regularFont, buttonLabel, FONT_SIZE) + 30f @@ -32,16 +29,11 @@ class ButtonSetting( val startY = yPos + (height - BUTTON_HEIGHT) / 2 val hovering = Mouse.isHoveringOver(startX, startY, buttonWidth, BUTTON_HEIGHT) + hoverFade.update(hovering) - if (hovering != wasHovering) { - colorAnim.start() - alphaAnim.start() - wasHovering = hovering - } - - val alpha = alphaAnim.get(0f, 40f, !hovering).toInt() - val borderColor = colorAnim.get(theme.border, theme.accentPrimary, !hovering) - val textColor = colorAnim.get(theme.textPrimary, theme.accentPrimary, !hovering) + val alpha = hoverFade.overlayAlpha(hovering) + val borderColor = hoverFade.color(theme.border, theme.accentPrimary, hovering) + val textColor = hoverFade.color(theme.textPrimary, theme.accentPrimary, hovering) SkiaRenderer.roundedRect( x = startX, From b2bce36b4ca1759f2182a4c312efa465504b4f63 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:36:58 +0200 Subject: [PATCH 13/21] refactor: extract shared TrackSettingSupport from RangeSetting/SliderSetting RangeSetting (two knobs) and SliderSetting (one knob) duplicated identical value-box and track rendering code and six identically-valued constants. Extract the shared rendering (value box, track background/fill/knobs) into TrackSettingSupport, parameterized by knob positions so both the one-knob and two-knob cases go through the same draw calls. Drag-state handling (Knob enum vs plain Boolean) stays in each class since that genuinely differs between them. --- .../ui/component/setting/impl/RangeSetting.kt | 103 +++++------------- .../component/setting/impl/SliderSetting.kt | 85 ++++----------- .../setting/impl/TrackSettingSupport.kt | 85 +++++++++++++++ 3 files changed, 134 insertions(+), 139 deletions(-) create mode 100644 src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingSupport.kt diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt index 8606d79d..9679a785 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt @@ -6,7 +6,6 @@ import com.google.gson.JsonPrimitive import org.cobalt.ui.PADDING import org.cobalt.ui.component.setting.Setting import org.cobalt.util.input.Mouse -import org.cobalt.util.render.SkiaRenderer class RangeSetting( name: String, @@ -36,72 +35,36 @@ class RangeSetting( private var rawEnd: Float = defaultValue.second.toFloat() override val height: Float - get() = BASE_HEIGHT + TRACK_ROW_HEIGHT + get() = BASE_HEIGHT + TrackSettingSupport.TRACK_ROW_HEIGHT override fun renderSetting() { val text = "${value.first} – ${value.second}" - val boxWidth = boxWidth(text) + val boxWidth = TrackSettingSupport.valueBoxWidth(text) val boxX = xPos + width - PADDING - boxWidth - val boxY = yPos + (BASE_HEIGHT - VALUE_BOX_HEIGHT) / 2 + val boxY = yPos + (BASE_HEIGHT - TrackSettingSupport.VALUE_BOX_HEIGHT) / 2 - SkiaRenderer.roundedRect( + TrackSettingSupport.drawValueBox( x = boxX, y = boxY, width = boxWidth, - height = VALUE_BOX_HEIGHT, - radius = 5f, - color = theme.backgroundPrimary - ) - - SkiaRenderer.roundedOutline( - x = boxX, - y = boxY, - width = boxWidth, - height = VALUE_BOX_HEIGHT, - thickness = 1f, - radius = 5f, - color = theme.border - ) - - val textWidth = SkiaRenderer.textWidth(SkiaRenderer.regularFont, text, FONT_SIZE) - - SkiaRenderer.text( - font = SkiaRenderer.regularFont, text = text, - x = boxX + (boxWidth - textWidth) / 2, - y = boxY + (VALUE_BOX_HEIGHT - FONT_SIZE) / 2, - size = FONT_SIZE, - color = theme.textPrimary + background = theme.backgroundPrimary, + border = theme.border, + textColor = theme.textPrimary ) val geometry = trackGeometry() - SkiaRenderer.roundedRect( - x = geometry.startX, - y = geometry.trackY - 2f, - width = geometry.trackWidth, - height = 4f, - radius = 3f, - color = theme.backgroundPrimary - ) - - SkiaRenderer.roundedRect( - x = geometry.startKnobX, - y = geometry.trackY - 2f, - width = (geometry.endKnobX - geometry.startKnobX).coerceAtLeast(0f), - height = 4f, - radius = 3f, - color = theme.accentPrimary - ) - - SkiaRenderer.circle( - geometry.startKnobX, geometry.trackY, - KNOB_RADIUS, theme.textPrimary - ) - - SkiaRenderer.circle( - geometry.endKnobX, geometry.trackY, - KNOB_RADIUS, theme.textPrimary + TrackSettingSupport.drawTrack( + startX = geometry.startX, + trackWidth = geometry.trackWidth, + trackY = geometry.trackY, + fillStartX = geometry.startKnobX, + fillEndX = geometry.endKnobX, + knobXs = listOf(geometry.startKnobX, geometry.endKnobX), + trackBackground = theme.backgroundPrimary, + fillColor = theme.accentPrimary, + knobColor = theme.textPrimary ) } @@ -112,19 +75,21 @@ class RangeSetting( val geometry = trackGeometry() + val knobRadius = TrackSettingSupport.KNOB_RADIUS + dragging = when { Mouse.isHoveringOver( - geometry.startKnobX - KNOB_RADIUS, - geometry.trackY - KNOB_RADIUS, - KNOB_RADIUS * 2, - KNOB_RADIUS * 2 + geometry.startKnobX - knobRadius, + geometry.trackY - knobRadius, + knobRadius * 2, + knobRadius * 2 ) -> Knob.START Mouse.isHoveringOver( - geometry.endKnobX - KNOB_RADIUS, - geometry.trackY - KNOB_RADIUS, - KNOB_RADIUS * 2, - KNOB_RADIUS * 2 + geometry.endKnobX - knobRadius, + geometry.trackY - knobRadius, + knobRadius * 2, + knobRadius * 2 ) -> Knob.END else -> return false @@ -198,7 +163,7 @@ class RangeSetting( private fun trackGeometry(): TrackGeometry { val startX = xPos + PADDING val trackWidth = width - PADDING * 2 - val trackY = yPos + BASE_HEIGHT + TRACK_MARGIN + val trackY = yPos + BASE_HEIGHT + TrackSettingSupport.TRACK_MARGIN val range = (max - min).toFloat().takeIf { it != 0f } ?: 1f val startKnobX = startX + (rawStart - min) / range * trackWidth val endKnobX = startX + (rawEnd - min) / range * trackWidth @@ -212,9 +177,6 @@ class RangeSetting( ) } - private fun boxWidth(text: String): Float = - SkiaRenderer.textWidth(SkiaRenderer.regularFont, text, FONT_SIZE) + VALUE_BOX_PADDING_X * 2f - private data class TrackGeometry( val startX: Float, val trackWidth: Float, @@ -227,13 +189,4 @@ class RangeSetting( START, END, NONE } - companion object { - private const val KNOB_RADIUS = 5f - private const val FONT_SIZE = 12f - private const val VALUE_BOX_HEIGHT = 30f - private const val VALUE_BOX_PADDING_X = 14f - private const val TRACK_ROW_HEIGHT = 15f - private const val TRACK_MARGIN = 5f - } - } diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt index 5372731f..a7a7dc13 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt @@ -5,7 +5,6 @@ import com.google.gson.JsonPrimitive import org.cobalt.ui.PADDING import org.cobalt.ui.component.setting.Setting import org.cobalt.util.input.Mouse -import org.cobalt.util.render.SkiaRenderer class SliderSetting( name: String, @@ -16,7 +15,7 @@ class SliderSetting( ) : Setting(name, description, defaultValue) { override val height: Float - get() = BASE_HEIGHT + TRACK_ROW_HEIGHT + get() = BASE_HEIGHT + TrackSettingSupport.TRACK_ROW_HEIGHT override fun read(element: JsonElement) { value = element.asInt.coerceIn(min, max) @@ -30,63 +29,32 @@ class SliderSetting( override fun renderSetting() { val text = value.toString() - val boxWidth = valueBoxWidth(text) + val boxWidth = TrackSettingSupport.valueBoxWidth(text) val boxX = xPos + width - PADDING - boxWidth - val boxY = yPos + (BASE_HEIGHT - VALUE_BOX_HEIGHT) / 2 + val boxY = yPos + (BASE_HEIGHT - TrackSettingSupport.VALUE_BOX_HEIGHT) / 2 - SkiaRenderer.roundedRect( + TrackSettingSupport.drawValueBox( x = boxX, y = boxY, width = boxWidth, - height = VALUE_BOX_HEIGHT, - radius = 5f, - color = theme.backgroundPrimary - ) - - SkiaRenderer.roundedOutline( - x = boxX, - y = boxY, - width = boxWidth, - height = VALUE_BOX_HEIGHT, - thickness = 1f, - radius = 5f, - color = theme.border - ) - - val textWidth = SkiaRenderer.textWidth(SkiaRenderer.regularFont, text, FONT_SIZE) - - SkiaRenderer.text( - font = SkiaRenderer.regularFont, text = text, - x = boxX + (boxWidth - textWidth) / 2, - y = boxY + (VALUE_BOX_HEIGHT - FONT_SIZE) / 2, - size = FONT_SIZE, - color = theme.textPrimary + background = theme.backgroundPrimary, + border = theme.border, + textColor = theme.textPrimary ) val (startX, trackWidth, trackY, knobX) = trackGeometry() - SkiaRenderer.roundedRect( - x = startX, - y = trackY - 2f, - width = trackWidth, - height = 4f, - radius = 3f, - color = theme.backgroundPrimary - ) - - SkiaRenderer.roundedRect( - x = startX, - y = trackY - 2f, - width = (knobX - startX).coerceAtLeast(0f), - height = 4f, - radius = 3f, - color = theme.accentPrimary - ) - - SkiaRenderer.circle( - knobX, trackY, - KNOB_RADIUS, theme.textPrimary + TrackSettingSupport.drawTrack( + startX = startX, + trackWidth = trackWidth, + trackY = trackY, + fillStartX = startX, + fillEndX = knobX, + knobXs = listOf(knobX), + trackBackground = theme.backgroundPrimary, + fillColor = theme.accentPrimary, + knobColor = theme.textPrimary ) } @@ -96,11 +64,12 @@ class SliderSetting( } val (_, _, trackY, knobX) = trackGeometry() + val knobRadius = TrackSettingSupport.KNOB_RADIUS if ( !Mouse.isHoveringOver( - knobX - KNOB_RADIUS, trackY - KNOB_RADIUS, - KNOB_RADIUS * 2, KNOB_RADIUS * 2 + knobX - knobRadius, trackY - knobRadius, + knobRadius * 2, knobRadius * 2 ) ) { return false @@ -142,16 +111,13 @@ class SliderSetting( private fun trackGeometry(): TrackGeometry { val startX = xPos + PADDING val trackWidth = width - PADDING * 2 - val trackY = yPos + BASE_HEIGHT + TRACK_MARGIN + val trackY = yPos + BASE_HEIGHT + TrackSettingSupport.TRACK_MARGIN val range = (max - min).toFloat().takeIf { it != 0f } ?: 1f val displayValue = if (dragging) rawValue else value.toFloat() val knobX = startX + (displayValue - min) / range * trackWidth return TrackGeometry(startX, trackWidth, trackY, knobX) } - private fun valueBoxWidth(text: String): Float = - SkiaRenderer.textWidth(SkiaRenderer.regularFont, text, FONT_SIZE) + VALUE_BOX_PADDING_X * 2f - private data class TrackGeometry( val startX: Float, val trackWidth: Float, @@ -159,13 +125,4 @@ class SliderSetting( val knobX: Float, ) - companion object { - private const val KNOB_RADIUS = 5f - private const val FONT_SIZE = 12f - private const val VALUE_BOX_HEIGHT = 30f - private const val VALUE_BOX_PADDING_X = 14f - private const val TRACK_ROW_HEIGHT = 15f - private const val TRACK_MARGIN = 5f - } - } diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingSupport.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingSupport.kt new file mode 100644 index 00000000..a0784663 --- /dev/null +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingSupport.kt @@ -0,0 +1,85 @@ +package org.cobalt.ui.component.setting.impl + +import java.awt.Color +import org.cobalt.util.render.SkiaRenderer + +/** Value-box and track rendering shared by RangeSetting (two knobs) and SliderSetting (one knob). */ +internal object TrackSettingSupport { + + const val KNOB_RADIUS = 5f + const val FONT_SIZE = 12f + const val VALUE_BOX_HEIGHT = 30f + const val VALUE_BOX_PADDING_X = 14f + const val TRACK_ROW_HEIGHT = 15f + const val TRACK_MARGIN = 5f + + fun valueBoxWidth(text: String): Float = + SkiaRenderer.textWidth(SkiaRenderer.regularFont, text, FONT_SIZE) + VALUE_BOX_PADDING_X * 2f + + fun drawValueBox(x: Float, y: Float, width: Float, text: String, background: Color, border: Color, textColor: Color) { + SkiaRenderer.roundedRect( + x = x, + y = y, + width = width, + height = VALUE_BOX_HEIGHT, + radius = 5f, + color = background + ) + + SkiaRenderer.roundedOutline( + x = x, + y = y, + width = width, + height = VALUE_BOX_HEIGHT, + thickness = 1f, + radius = 5f, + color = border + ) + + val textWidth = SkiaRenderer.textWidth(SkiaRenderer.regularFont, text, FONT_SIZE) + + SkiaRenderer.text( + font = SkiaRenderer.regularFont, + text = text, + x = x + (width - textWidth) / 2, + y = y + (VALUE_BOX_HEIGHT - FONT_SIZE) / 2, + size = FONT_SIZE, + color = textColor + ) + } + + fun drawTrack( + startX: Float, + trackWidth: Float, + trackY: Float, + fillStartX: Float, + fillEndX: Float, + knobXs: List, + trackBackground: Color, + fillColor: Color, + knobColor: Color, + ) { + SkiaRenderer.roundedRect( + x = startX, + y = trackY - 2f, + width = trackWidth, + height = 4f, + radius = 3f, + color = trackBackground + ) + + SkiaRenderer.roundedRect( + x = fillStartX, + y = trackY - 2f, + width = (fillEndX - fillStartX).coerceAtLeast(0f), + height = 4f, + radius = 3f, + color = fillColor + ) + + knobXs.forEach { knobX -> + SkiaRenderer.circle(knobX, trackY, KNOB_RADIUS, knobColor) + } + } + +} From d9fad2466bc0461420df324ce7b1a41e937933d9 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:38:27 +0200 Subject: [PATCH 14/21] refactor: extract shared masonry column layout from ModulesPage/ScriptsPage Both pages flowed items into 2 independently-growing columns (item i in column i % 2, each column advancing its own running Y), but implemented it two different ways -- named leftX/leftY/rightX/rightY pairs with an if/else in one, a FloatArray indexed by index % 2 in the other. Extract the shared algorithm into layoutMasonryColumns(). ThemesPage was left alone: it lays out a fixed-size uniform grid (no per-item height tracking needed), which is a different problem already solved in two lines -- forcing it through this helper would add indirection without removing any real duplication. --- .../cobalt/ui/helper/MasonryColumnLayout.kt | 26 +++++++++++++ .../org/cobalt/ui/page/impl/ModulesPage.kt | 37 ++++++------------- .../org/cobalt/ui/page/impl/ScriptsPage.kt | 25 +++++-------- 3 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt diff --git a/src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt b/src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt new file mode 100644 index 00000000..f858c070 --- /dev/null +++ b/src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt @@ -0,0 +1,26 @@ +package org.cobalt.ui.helper + +/** + * Lays out [items] into [columns] independently-flowing columns (item i goes to column i % columns), each + * advancing its own running Y by the item's height + [gap]. Returns the tallest column's final Y, so callers + * can size scroll content from it. + */ +fun layoutMasonryColumns( + items: List, + columns: Int, + startY: Float, + gap: Float, + columnX: (col: Int) -> Float, + heightOf: (T) -> Float, + place: (item: T, x: Float, y: Float) -> Unit, +): Float { + val columnY = FloatArray(columns) { startY } + + items.forEachIndexed { index, item -> + val col = index % columns + place(item, columnX(col), columnY[col]) + columnY[col] += heightOf(item) + gap + } + + return columnY.max() +} diff --git a/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt b/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt index d57c3967..0b700904 100644 --- a/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt +++ b/src/main/kotlin/org/cobalt/ui/page/impl/ModulesPage.kt @@ -7,6 +7,7 @@ import org.cobalt.ui.PADDING import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.ui.component.ModuleComponent import org.cobalt.ui.helper.ScrollHelper +import org.cobalt.ui.helper.layoutMasonryColumns import org.cobalt.ui.page.Page import org.cobalt.ui.screen.ConfigScreen import org.cobalt.util.failsafe.FailsafeManager @@ -66,37 +67,23 @@ object ModulesPage : Page() { super.renderComponent() val pageOffset = openingOffset.get(-30f, 0f) - - val leftX = xPos + PADDING - var leftY = yPos + PADDING + pageOffset - scrollHelper.scrollOffset - val rightX = xPos + PADDING + ModuleComponent.WIDTH + COLUMN_GAP - var rightY = yPos + PADDING + pageOffset - scrollHelper.scrollOffset + val startY = yPos + PADDING + pageOffset - scrollHelper.scrollOffset SkiaRenderer.pushScissor(xPos, yPos, width, height) - moduleComponents.forEachIndexed { index, component -> - if (index % 2 == 0) { - component - .updateBounds(leftX, leftY) - .renderComponent() - - leftY += component.height + PADDING - } else { - component - .updateBounds(rightX, rightY) - .renderComponent() - - rightY += component.height + PADDING - } - } + val maxColumnY = layoutMasonryColumns( + items = moduleComponents, + columns = 2, + startY = startY, + gap = PADDING, + columnX = { col -> xPos + PADDING + col * (ModuleComponent.WIDTH + COLUMN_GAP) }, + heightOf = { it.height }, + place = { component, x, y -> component.updateBounds(x, y).renderComponent() } + ) SkiaRenderer.popScissor() - val contentHeight = maxOf( - leftY + scrollHelper.scrollOffset, - rightY + scrollHelper.scrollOffset - ) - yPos - + val contentHeight = maxColumnY + scrollHelper.scrollOffset - yPos scrollHelper.updateMaxScroll(contentHeight, height) } diff --git a/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt b/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt index f98cea11..3cd3aa4b 100644 --- a/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt +++ b/src/main/kotlin/org/cobalt/ui/page/impl/ScriptsPage.kt @@ -6,6 +6,7 @@ import org.cobalt.ui.PADDING import org.cobalt.ui.animation.EaseOutAnimation import org.cobalt.ui.component.ScriptComponent import org.cobalt.ui.helper.ScrollHelper +import org.cobalt.ui.helper.layoutMasonryColumns import org.cobalt.ui.page.Page import org.cobalt.util.input.Mouse import org.cobalt.util.render.SkiaRenderer @@ -76,23 +77,17 @@ object ScriptsPage : Page() { currentY += CATEGORY_FONT_SIZE + CATEGORY_MARGIN - val columnY = floatArrayOf(currentY, currentY) - val columnX = floatArrayOf( - xPos + PADDING, - xPos + PADDING + ScriptComponent.WIDTH + COLUMN_GAP + val maxColumnY = layoutMasonryColumns( + items = components, + columns = 2, + startY = currentY, + gap = PADDING, + columnX = { col -> xPos + PADDING + col * (ScriptComponent.WIDTH + COLUMN_GAP) }, + heightOf = { (component, _) -> component.height }, + place = { (component, _), x, y -> component.updateBounds(x, y).renderComponent() } ) - components.forEachIndexed { index, (component, _) -> - val col = index % 2 - - component - .updateBounds(columnX[col], columnY[col]) - .renderComponent() - - columnY[col] += component.height + PADDING - } - - currentY = columnY.max() + GROUP_GAP + currentY = maxColumnY + GROUP_GAP } SkiaRenderer.popScissor() From c100d6466dfddc914be8f6e7564b6b54d78d6b3a Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 12:40:12 +0200 Subject: [PATCH 15/21] refactor: unify GlSurface/VulkanSurface teardown into SkijaSurfaceCache Both backends duplicated the same close-and-null-out logic for their DirectContext/BackendRenderTarget/Surface triple, both in close() and in the cache-invalidation branch of surfaceFor(). Extract that into a small SkijaSurfaceCache owned by each backend. GL's extra native cleanup (FBO, depth/stencil renderbuffer) has no Vulkan equivalent and stays in GlSurface.close() -- only the genuinely shared teardown moved. Backend- specific surface construction (BackendRenderTarget.makeGL vs .makeVulkan, cache-key comparisons) is untouched. --- .../util/render/skia/surface/GlSurface.kt | 26 ++++----------- .../render/skia/surface/SkijaSurfaceCache.kt | 32 +++++++++++++++++++ .../util/render/skia/surface/VulkanSurface.kt | 23 +++---------- 3 files changed, 44 insertions(+), 37 deletions(-) create mode 100644 src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt diff --git a/src/main/kotlin/org/cobalt/util/render/skia/surface/GlSurface.kt b/src/main/kotlin/org/cobalt/util/render/skia/surface/GlSurface.kt index f6ca228c..2c522454 100644 --- a/src/main/kotlin/org/cobalt/util/render/skia/surface/GlSurface.kt +++ b/src/main/kotlin/org/cobalt/util/render/skia/surface/GlSurface.kt @@ -12,9 +12,7 @@ import org.lwjgl.opengl.GL33C internal class GlSurface : SkiaSurface { - private var context: DirectContext? = null - private var renderTarget: BackendRenderTarget? = null - private var surface: Surface? = null + private val skija = SkijaSurfaceCache() private var fbo = 0 private var depthStencil = 0 @@ -39,8 +37,8 @@ internal class GlSurface : SkiaSurface { GlStateManager._viewport(0, 0, width, height) GL33C.glBindSampler(0, 0) - val directContext = context ?: DirectContext.makeGL().also { - context = it + val directContext = skija.context ?: DirectContext.makeGL().also { + skija.context = it } directContext.resetGLAll() @@ -99,16 +97,13 @@ internal class GlSurface : SkiaSurface { } private fun surfaceFor(width: Int, height: Int, textureId: Int): Surface { - val existing = surface + val existing = skija.surface if (existing != null && existing.width == width && existing.height == height && lastTextureId == textureId) { return existing } - surface?.close() - renderTarget?.close() - - val directContext = context ?: DirectContext.makeGL().also { context = it } + val directContext = skija.context ?: DirectContext.makeGL().also { skija.context = it } val target = BackendRenderTarget.makeGL(width, height, 0, 8, fbo, GL30C.GL_RGBA8) val created = Surface.wrapBackendRenderTarget( directContext, @@ -118,18 +113,14 @@ internal class GlSurface : SkiaSurface { ColorSpace.getSRGB() ) - renderTarget = target - surface = created + skija.replaceSurface(target, created) lastTextureId = textureId return created } override fun close() { - surface?.close() - surface = null - renderTarget?.close() - renderTarget = null + skija.close() if (depthStencil != 0) { GL30C.glDeleteRenderbuffers(depthStencil) @@ -140,9 +131,6 @@ internal class GlSurface : SkiaSurface { GlStateManager._glDeleteFramebuffers(fbo) fbo = 0 } - - context?.close() - context = null } } diff --git a/src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt b/src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt new file mode 100644 index 00000000..095b8679 --- /dev/null +++ b/src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt @@ -0,0 +1,32 @@ +package org.cobalt.util.render.skia.surface + +import io.github.humbleui.skija.BackendRenderTarget +import io.github.humbleui.skija.DirectContext +import io.github.humbleui.skija.Surface + +/** Owns the DirectContext/BackendRenderTarget/Surface triple every SkiaSurface backend caches, and their teardown. */ +internal class SkijaSurfaceCache { + + var context: DirectContext? = null + var renderTarget: BackendRenderTarget? = null + private set + var surface: Surface? = null + private set + + fun replaceSurface(renderTarget: BackendRenderTarget, surface: Surface) { + this.surface?.close() + this.renderTarget?.close() + this.renderTarget = renderTarget + this.surface = surface + } + + fun close() { + surface?.close() + surface = null + renderTarget?.close() + renderTarget = null + context?.close() + context = null + } + +} diff --git a/src/main/kotlin/org/cobalt/util/render/skia/surface/VulkanSurface.kt b/src/main/kotlin/org/cobalt/util/render/skia/surface/VulkanSurface.kt index 6dce69b3..95f2228e 100644 --- a/src/main/kotlin/org/cobalt/util/render/skia/surface/VulkanSurface.kt +++ b/src/main/kotlin/org/cobalt/util/render/skia/surface/VulkanSurface.kt @@ -13,9 +13,7 @@ import org.lwjgl.vulkan.VK12.* internal class VulkanSurface : SkiaSurface { - private var context: DirectContext? = null - private var renderTarget: BackendRenderTarget? = null - private var surface: Surface? = null + private val skija = SkijaSurfaceCache() private var cachedWidth = 0 private var cachedHeight = 0 @@ -31,7 +29,7 @@ internal class VulkanSurface : SkiaSurface { val vkImage = vkTexture.vkImage() val vkFormat = gpuFormatToVkFormat(vkTexture.format) - val directContext = context ?: makeContext().also { context = it } + val directContext = skija.context ?: makeContext().also { skija.context = it } RenderSystem.getDevice().createCommandEncoder().submit() directContext.resetAll() @@ -87,15 +85,12 @@ internal class VulkanSurface : SkiaSurface { vkImage: Long, vkFormat: Int, ): Surface { - val existing = surface + val existing = skija.surface if (existing != null && cachedWidth == width && cachedHeight == height && cachedVkImage == vkImage) { return existing } - surface?.close() - renderTarget?.close() - val rt = BackendRenderTarget.makeVulkan( width, height, vkImage, @@ -116,8 +111,7 @@ internal class VulkanSurface : SkiaSurface { ColorSpace.getSRGB(), ) - renderTarget = rt - surface = surf + skija.replaceSurface(rt, surf) cachedWidth = width cachedHeight = height cachedVkImage = vkImage @@ -126,14 +120,7 @@ internal class VulkanSurface : SkiaSurface { } override fun close() { - surface?.close() - surface = null - - renderTarget?.close() - renderTarget = null - - context?.close() - context = null + skija.close() } companion object { From fa45f253d6c1da80365ab516ada3bbbc3728466b Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 13:22:21 +0200 Subject: [PATCH 16/21] revert: keep AddonManager.addons as a plain mutableListOf No evidence of concurrent access: addons is only ever written during the single-threaded PreLaunch phase (loadAddon/loadDevelopmentAddons, both called synchronously from onPreLaunch) and read once from loadAddons() at init. Unlike AudioManager (Java Sound callback thread) or ModuleManager (read from Multithreading.runAsync in ConfigScreen/HudEditorScreen), there's no actual cross-thread access here, so CopyOnWriteArrayList was unjustified churn. --- src/main/kotlin/org/cobalt/addon/AddonManager.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/org/cobalt/addon/AddonManager.kt b/src/main/kotlin/org/cobalt/addon/AddonManager.kt index 3c0ec493..6e5b7d60 100644 --- a/src/main/kotlin/org/cobalt/addon/AddonManager.kt +++ b/src/main/kotlin/org/cobalt/addon/AddonManager.kt @@ -3,7 +3,6 @@ package org.cobalt.addon import com.google.gson.Gson import java.nio.file.Files import java.nio.file.Path -import java.util.concurrent.CopyOnWriteArrayList import java.util.zip.ZipFile import kotlin.io.path.extension import net.fabricmc.loader.api.FabricLoader @@ -17,7 +16,7 @@ object AddonManager { private val gson = Gson() @JvmStatic - val addons = CopyOnWriteArrayList>() + val addons = mutableListOf>() @JvmStatic internal fun onPreLaunch() { From e753fed300f44bd0a2213bbd6d25423bfab3f746 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 13:22:31 +0200 Subject: [PATCH 17/21] refactor: give AttackMode/EspType explicit option indices instead of relying on ordinal entries.getOrElse(index) mapped the ModeSetting's stored Int to an enum by declaration order, which is exactly the kind of implicit position-coupling that made the raw-Int version risky in the first place: reordering the enum constants would silently desync from the ModeSetting options array again, just one layer removed. Store each constant's intended option index explicitly and look up by that value instead of by position. --- .../cobalt/module/impl/combat/AutoClicker.kt | 15 +++++++++------ .../org/cobalt/module/impl/visual/PlayerESP.kt | 17 ++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt b/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt index 6e5cfebb..a7c496d7 100644 --- a/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt +++ b/src/main/kotlin/org/cobalt/module/impl/combat/AutoClicker.kt @@ -57,7 +57,7 @@ object AutoClicker : Module( ) private val attackModeType: AttackMode - get() = AttackMode.entries.getOrElse(attackMode) { AttackMode.ALL } + get() = AttackMode.fromOptionIndex(attackMode) private val mobFilter by TextSetting( name = "Mob Filter", @@ -136,11 +136,14 @@ object AutoClicker : Module( return (base + jitter).coerceAtLeast(1.0).toLong() } - // Ordinal must match the "Attack Mode" ModeSetting options order above. - private enum class AttackMode { - ALL, - ENTITY_ONLY, - NO_BLOCKS, + private enum class AttackMode(val optionIndex: Int) { + ALL(0), + ENTITY_ONLY(1), + NO_BLOCKS(2); + + companion object { + fun fromOptionIndex(index: Int): AttackMode = entries.firstOrNull { it.optionIndex == index } ?: ALL + } } } diff --git a/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt b/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt index e593f18f..5aacdcba 100644 --- a/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt +++ b/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt @@ -21,16 +21,19 @@ object PlayerESP : Module(name = "PlayerESP", category = ModuleCategory.VISUAL) options = arrayOf("Box", "Target Beam", "Outline", "Beam & Outline") ) - // Ordinal must match the "ESP Type" ModeSetting options order above. - private enum class EspType { - BOX, - TARGET_BEAM, - OUTLINE, - BEAM_AND_OUTLINE, + private enum class EspType(val optionIndex: Int) { + BOX(0), + TARGET_BEAM(1), + OUTLINE(2), + BEAM_AND_OUTLINE(3); + + companion object { + fun fromOptionIndex(index: Int): EspType = entries.firstOrNull { it.optionIndex == index } ?: BOX + } } private val espTypeMode: EspType - get() = EspType.entries.getOrElse(espType) { EspType.BOX } + get() = EspType.fromOptionIndex(espType) fun shouldOutline(entity: Entity): Boolean { if (espTypeMode != EspType.OUTLINE && espTypeMode != EspType.BEAM_AND_OUTLINE) { From d1abc426bfec3746ae31f78413c2fe8abb519991 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 13:22:39 +0200 Subject: [PATCH 18/21] rename: TrackSettingSupport -> TrackSettingHelper This codebase already has an established Helper naming convention for this exact kind of shared UI logic (ScrollHelper, DragHandler, SnapHelper, TextInputHelper). "Support" was an inconsistent one-off; rename to match. --- .../ui/component/setting/impl/RangeSetting.kt | 14 +++++++------- .../ui/component/setting/impl/SliderSetting.kt | 14 +++++++------- ...rackSettingSupport.kt => TrackSettingHelper.kt} | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) rename src/main/kotlin/org/cobalt/ui/component/setting/impl/{TrackSettingSupport.kt => TrackSettingHelper.kt} (98%) diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt index 9679a785..1c218f49 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/RangeSetting.kt @@ -35,15 +35,15 @@ class RangeSetting( private var rawEnd: Float = defaultValue.second.toFloat() override val height: Float - get() = BASE_HEIGHT + TrackSettingSupport.TRACK_ROW_HEIGHT + get() = BASE_HEIGHT + TrackSettingHelper.TRACK_ROW_HEIGHT override fun renderSetting() { val text = "${value.first} – ${value.second}" - val boxWidth = TrackSettingSupport.valueBoxWidth(text) + val boxWidth = TrackSettingHelper.valueBoxWidth(text) val boxX = xPos + width - PADDING - boxWidth - val boxY = yPos + (BASE_HEIGHT - TrackSettingSupport.VALUE_BOX_HEIGHT) / 2 + val boxY = yPos + (BASE_HEIGHT - TrackSettingHelper.VALUE_BOX_HEIGHT) / 2 - TrackSettingSupport.drawValueBox( + TrackSettingHelper.drawValueBox( x = boxX, y = boxY, width = boxWidth, @@ -55,7 +55,7 @@ class RangeSetting( val geometry = trackGeometry() - TrackSettingSupport.drawTrack( + TrackSettingHelper.drawTrack( startX = geometry.startX, trackWidth = geometry.trackWidth, trackY = geometry.trackY, @@ -75,7 +75,7 @@ class RangeSetting( val geometry = trackGeometry() - val knobRadius = TrackSettingSupport.KNOB_RADIUS + val knobRadius = TrackSettingHelper.KNOB_RADIUS dragging = when { Mouse.isHoveringOver( @@ -163,7 +163,7 @@ class RangeSetting( private fun trackGeometry(): TrackGeometry { val startX = xPos + PADDING val trackWidth = width - PADDING * 2 - val trackY = yPos + BASE_HEIGHT + TrackSettingSupport.TRACK_MARGIN + val trackY = yPos + BASE_HEIGHT + TrackSettingHelper.TRACK_MARGIN val range = (max - min).toFloat().takeIf { it != 0f } ?: 1f val startKnobX = startX + (rawStart - min) / range * trackWidth val endKnobX = startX + (rawEnd - min) / range * trackWidth diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt index a7a7dc13..73b71d2e 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/SliderSetting.kt @@ -15,7 +15,7 @@ class SliderSetting( ) : Setting(name, description, defaultValue) { override val height: Float - get() = BASE_HEIGHT + TrackSettingSupport.TRACK_ROW_HEIGHT + get() = BASE_HEIGHT + TrackSettingHelper.TRACK_ROW_HEIGHT override fun read(element: JsonElement) { value = element.asInt.coerceIn(min, max) @@ -29,11 +29,11 @@ class SliderSetting( override fun renderSetting() { val text = value.toString() - val boxWidth = TrackSettingSupport.valueBoxWidth(text) + val boxWidth = TrackSettingHelper.valueBoxWidth(text) val boxX = xPos + width - PADDING - boxWidth - val boxY = yPos + (BASE_HEIGHT - TrackSettingSupport.VALUE_BOX_HEIGHT) / 2 + val boxY = yPos + (BASE_HEIGHT - TrackSettingHelper.VALUE_BOX_HEIGHT) / 2 - TrackSettingSupport.drawValueBox( + TrackSettingHelper.drawValueBox( x = boxX, y = boxY, width = boxWidth, @@ -45,7 +45,7 @@ class SliderSetting( val (startX, trackWidth, trackY, knobX) = trackGeometry() - TrackSettingSupport.drawTrack( + TrackSettingHelper.drawTrack( startX = startX, trackWidth = trackWidth, trackY = trackY, @@ -64,7 +64,7 @@ class SliderSetting( } val (_, _, trackY, knobX) = trackGeometry() - val knobRadius = TrackSettingSupport.KNOB_RADIUS + val knobRadius = TrackSettingHelper.KNOB_RADIUS if ( !Mouse.isHoveringOver( @@ -111,7 +111,7 @@ class SliderSetting( private fun trackGeometry(): TrackGeometry { val startX = xPos + PADDING val trackWidth = width - PADDING * 2 - val trackY = yPos + BASE_HEIGHT + TrackSettingSupport.TRACK_MARGIN + val trackY = yPos + BASE_HEIGHT + TrackSettingHelper.TRACK_MARGIN val range = (max - min).toFloat().takeIf { it != 0f } ?: 1f val displayValue = if (dragging) rawValue else value.toFloat() val knobX = startX + (displayValue - min) / range * trackWidth diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingSupport.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt similarity index 98% rename from src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingSupport.kt rename to src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt index a0784663..d2f24dd8 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingSupport.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt @@ -4,7 +4,7 @@ import java.awt.Color import org.cobalt.util.render.SkiaRenderer /** Value-box and track rendering shared by RangeSetting (two knobs) and SliderSetting (one knob). */ -internal object TrackSettingSupport { +internal object TrackSettingHelper { const val KNOB_RADIUS = 5f const val FONT_SIZE = 12f From c9ab465315d08f2d02dfce3baba7e9243430bcc7 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 13:37:45 +0200 Subject: [PATCH 19/21] chore: remove redundant comments --- src/main/kotlin/org/cobalt/module/type/Failsafe.kt | 1 - src/main/kotlin/org/cobalt/ui/UIComponent.kt | 1 - src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt | 1 - src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt | 5 ----- src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt | 3 ++- .../org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt | 1 - src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt | 5 +---- 7 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/org/cobalt/module/type/Failsafe.kt b/src/main/kotlin/org/cobalt/module/type/Failsafe.kt index 0cd7af52..5b49c867 100644 --- a/src/main/kotlin/org/cobalt/module/type/Failsafe.kt +++ b/src/main/kotlin/org/cobalt/module/type/Failsafe.kt @@ -15,7 +15,6 @@ abstract class Failsafe @JvmOverloads constructor( startValue = startValue ) { - /** Failsafes only need to react while a script is running, or during manual testing in a dev environment. */ protected fun shouldReactToEvents(): Boolean = ModuleManager.isScriptRunning() || FabricLoader.getInstance().isDevelopmentEnvironment diff --git a/src/main/kotlin/org/cobalt/ui/UIComponent.kt b/src/main/kotlin/org/cobalt/ui/UIComponent.kt index 6824b19f..4423c13b 100644 --- a/src/main/kotlin/org/cobalt/ui/UIComponent.kt +++ b/src/main/kotlin/org/cobalt/ui/UIComponent.kt @@ -5,7 +5,6 @@ import net.minecraft.client.input.KeyEvent import org.cobalt.ui.theme.Theme import org.cobalt.ui.theme.ThemeManager -/** Standard edge padding shared by components, pages, and settings throughout the UI. */ const val PADDING = 20f abstract class UIComponent( diff --git a/src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt b/src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt index 45a651ea..9999cbd6 100644 --- a/src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt +++ b/src/main/kotlin/org/cobalt/ui/animation/HoverFade.kt @@ -2,7 +2,6 @@ package org.cobalt.ui.animation import java.awt.Color -/** Shared hover-transition animation: restarts a color/alpha fade whenever the hover state flips. */ class HoverFade(duration: Long = 150L) { private val colorAnimation = ColorAnimation(duration) diff --git a/src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt b/src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt index f858c070..ff330f39 100644 --- a/src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt +++ b/src/main/kotlin/org/cobalt/ui/helper/MasonryColumnLayout.kt @@ -1,10 +1,5 @@ package org.cobalt.ui.helper -/** - * Lays out [items] into [columns] independently-flowing columns (item i goes to column i % columns), each - * advancing its own running Y by the item's height + [gap]. Returns the tallest column's final Y, so callers - * can size scroll content from it. - */ fun layoutMasonryColumns( items: List, columns: Int, diff --git a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt b/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt index 7bea481a..ad57017b 100644 --- a/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt +++ b/src/main/kotlin/org/cobalt/util/failsafe/FailsafeManager.kt @@ -1,6 +1,7 @@ package org.cobalt.util.failsafe import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents import org.cobalt.event.EventBus import org.cobalt.event.annotation.SubscribeEvent @@ -24,7 +25,7 @@ import org.lwjgl.glfw.GLFW object FailsafeManager { - var failsafes = mutableListOf() + var failsafes = CopyOnWriteArrayList() private val tempIgnored = ConcurrentHashMap.newKeySet() private val ignoreGens = ConcurrentHashMap() diff --git a/src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt b/src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt index 095b8679..ff6b9dbc 100644 --- a/src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt +++ b/src/main/kotlin/org/cobalt/util/render/skia/surface/SkijaSurfaceCache.kt @@ -4,7 +4,6 @@ import io.github.humbleui.skija.BackendRenderTarget import io.github.humbleui.skija.DirectContext import io.github.humbleui.skija.Surface -/** Owns the DirectContext/BackendRenderTarget/Surface triple every SkiaSurface backend caches, and their teardown. */ internal class SkijaSurfaceCache { var context: DirectContext? = null diff --git a/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt b/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt index e6e94797..52e76cfd 100644 --- a/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt +++ b/src/main/kotlin/org/cobalt/util/scheduling/TickScheduler.kt @@ -8,10 +8,7 @@ import org.cobalt.event.impl.TickEvent object TickScheduler { private var currentTick: Long = 0 - - // PriorityBlockingQueue rather than PriorityQueue: schedule() can be called from Multithreading's worker - // threads while onClientTick concurrently drains this on the client thread. - private val taskQueue = PriorityBlockingQueue( + private val taskQueue = PriorityBlockingQueue( 11, Comparator.comparingLong(ScheduledTask::executeTick) ) From 64e324be990f8cae393309f03d667184a1527143 Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 14:06:51 +0200 Subject: [PATCH 20/21] chore: remove some more redundant comments --- src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt | 2 +- .../org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt | 1 - src/main/kotlin/org/cobalt/util/audio/AudioManager.kt | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt b/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt index 5aacdcba..05e693e9 100644 --- a/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt +++ b/src/main/kotlin/org/cobalt/module/impl/visual/PlayerESP.kt @@ -83,7 +83,7 @@ object PlayerESP : Module(name = "PlayerESP", category = ModuleCategory.VISUAL) esp = true ) - EspType.OUTLINE -> Unit // handled by shouldOutline(), not drawn in this loop + EspType.OUTLINE -> Unit } } } diff --git a/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt index d2f24dd8..62dab825 100644 --- a/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt +++ b/src/main/kotlin/org/cobalt/ui/component/setting/impl/TrackSettingHelper.kt @@ -3,7 +3,6 @@ package org.cobalt.ui.component.setting.impl import java.awt.Color import org.cobalt.util.render.SkiaRenderer -/** Value-box and track rendering shared by RangeSetting (two knobs) and SliderSetting (one knob). */ internal object TrackSettingHelper { const val KNOB_RADIUS = 5f diff --git a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt index 2683e667..5de13d5b 100644 --- a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt +++ b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt @@ -12,7 +12,6 @@ object AudioManager { private val logger = LoggerFactory.getLogger(this::class.java) - // clip.addLineListener callbacks fire on the Java Sound line-event thread, not the caller's thread private val clips = CopyOnWriteArrayList() // YES I KNOW THIS PLAYS THRU SPEAKERS ISNTEAD OF HEADPHONES IDK WHY From 886ad1755ad298c02385e003576efb18bfbab66c Mon Sep 17 00:00:00 2001 From: Nick Date: Tue, 22 Sep 2026 14:21:48 +0200 Subject: [PATCH 21/21] fix(audio): play clips on Minecraft's configured sound device --- .../org/cobalt/util/audio/AudioManager.kt | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt index 5de13d5b..41384b0b 100644 --- a/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt +++ b/src/main/kotlin/org/cobalt/util/audio/AudioManager.kt @@ -5,7 +5,9 @@ import java.util.concurrent.CopyOnWriteArrayList import javax.sound.sampled.AudioSystem import javax.sound.sampled.Clip import javax.sound.sampled.FloatControl +import javax.sound.sampled.Mixer import kotlin.math.log10 +import org.cobalt.Cobalt import org.slf4j.LoggerFactory object AudioManager { @@ -14,13 +16,43 @@ object AudioManager { private val clips = CopyOnWriteArrayList() - // YES I KNOW THIS PLAYS THRU SPEAKERS ISNTEAD OF HEADPHONES IDK WHY + private const val OPENAL_SOFT_PREFIX = "OpenAL Soft on " + + private fun resolveMixerInfo(): Mixer.Info? { + + val rawDeviceName = Cobalt.minecraft.options.soundDevice().get().takeIf { it.isNotBlank() } ?: return null + + val targetName = rawDeviceName.removePrefix(OPENAL_SOFT_PREFIX) + val mixers = AudioSystem.getMixerInfo() + + mixers.firstOrNull { it.name.equals(targetName, ignoreCase = true) }?.let { return it } + + val loose = mixers.firstOrNull { + it.name.contains(targetName, ignoreCase = true) || targetName.contains(it.name, ignoreCase = true) + } + if (loose == null) { + logger.warn("No javax.sound.sampled mixer matched Minecraft's sound device '{}'", rawDeviceName) + } + return loose + } + fun play(file: File, volume: Float = 1.0f) { if (!file.exists()) return try { val audioStream = AudioSystem.getAudioInputStream(file) - val clip = AudioSystem.getClip() + + val mixerInfo = resolveMixerInfo() + val clip: Clip = if (mixerInfo != null) { + try { + AudioSystem.getClip(mixerInfo) + } catch (e: Exception) { + logger.warn("Failed to open clip on Minecraft's sound device, falling back to default mixer", e) + AudioSystem.getClip() + } + } else { + AudioSystem.getClip() + } clip.open(audioStream)