From abd6a3f25bc38d1f890d4e6e6e84424ad016eae4 Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Fri, 30 May 2025 16:02:48 -0400 Subject: [PATCH 1/2] durations --- .../kotlin/com/lambda/config/Configurable.kt | 34 ++++++++++++ .../com/lambda/config/groups/BuildSettings.kt | 2 +- .../lambda/config/settings/DurationSetting.kt | 46 ++++++++++++++++ .../lambda/config/settings/NumericSetting.kt | 4 +- .../lambda/module/modules/client/Discord.kt | 4 +- .../module/modules/combat/CrystalAura.kt | 10 ++-- .../module/modules/network/PacketDelay.kt | 28 +++++----- .../module/modules/network/PacketLimiter.kt | 6 ++- .../kotlin/com/lambda/task/tasks/BuildTask.kt | 3 +- .../main/kotlin/com/lambda/util/ServerTPS.kt | 3 +- .../util/collections/LimitedDecayQueue.kt | 29 +++++----- .../kotlin/com/lambda/util/extension/Time.kt | 53 +++++++++++++++++++ .../src/test/kotlin/LimitedDecayQueueTest.kt | 6 ++- 13 files changed, 185 insertions(+), 43 deletions(-) create mode 100644 common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt create mode 100644 common/src/main/kotlin/com/lambda/util/extension/Time.kt diff --git a/common/src/main/kotlin/com/lambda/config/Configurable.kt b/common/src/main/kotlin/com/lambda/config/Configurable.kt index b17e6f5b7..b4e3ebdd1 100644 --- a/common/src/main/kotlin/com/lambda/config/Configurable.kt +++ b/common/src/main/kotlin/com/lambda/config/Configurable.kt @@ -23,6 +23,7 @@ import com.google.gson.reflect.TypeToken import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.config.settings.CharSetting +import com.lambda.config.settings.DurationSetting import com.lambda.config.settings.FunctionSetting import com.lambda.config.settings.StringSetting import com.lambda.config.settings.collections.ListSetting @@ -35,10 +36,13 @@ import com.lambda.config.settings.numeric.* import com.lambda.util.Communication.logError import com.lambda.util.KeyCode import com.lambda.util.Nameable +import com.lambda.util.extension.highestUnit import net.minecraft.block.Block import net.minecraft.util.math.BlockPos import net.minecraft.util.math.Vec3d import java.awt.Color +import kotlin.time.Duration +import kotlin.time.toDuration /** * Represents a set of [AbstractSetting]s that are associated with the [name] of the [Configurable]. @@ -354,6 +358,36 @@ abstract class Configurable( visibility: () -> Boolean = { true }, ) = LongSetting(name, defaultValue, range, step, description, visibility, unit).register() + /** + * Creates a [DurationSetting] with the provided parameters and adds it to the [settings]. + * + * The value of the setting is coerced into the specified [range] and rounded to the nearest [step]. + * + * The unit of the duration is inferred automatically by [Duration.highestUnit] + * + * Example: + * ```kotlin + * val duration by setting("Duration", 10.microseconds, 420.nanoseconds..80.minutes, 69420.microseconds) + * ``` + * + * @param name The unique identifier for the setting. + * @param defaultValue The default [Duration] value of the setting. + * @param range The range within which the setting's value must fall. + * @param step The step to which the setting's value is rounded. + * @param description A brief explanation of the setting's purpose and behavior. + * @param visibility A lambda expression that determines the visibility status of the setting. + * + * @return The created [Duration]. + */ + fun setting( + name: String, + defaultValue: Duration, + range: ClosedRange, + step: Duration = 1.toDuration(defaultValue.highestUnit), + description: String = "", + visibility: () -> Boolean = { true }, + ) = DurationSetting(name, defaultValue, range, step, description, visibility).register() + /** * Creates a [KeyBindSetting] with the provided parameters and adds it to the [settings]. * diff --git a/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt b/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt index 88251114b..17a287700 100644 --- a/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt +++ b/common/src/main/kotlin/com/lambda/config/groups/BuildSettings.kt @@ -49,5 +49,5 @@ class BuildSettings( override val placeConfirmation by c.setting("Place Confirmation", true, "Wait for block placement confirmation") { vis() && page == Page.Place } override val placementsPerTick by c.setting("Instant Places Per Tick", 1, 1..30, 1, "Maximum instant block places per tick") { vis() && page == Page.Place } - override val interactionTimeout by c.setting("Interaction Timeout", 10, 1..30, 1, "Timeout for block breaks in ticks", unit = " ticks") { vis() && (page == Page.Place && placeConfirmation || page == Page.Break && breakConfirmation) } + override val interactionTimeout by c.setting("Interaction Timeout", 10, 1..30, 1,"Timeout for block breaks in ticks", unit = " ticks") { vis() && (page == Page.Place && placeConfirmation || page == Page.Break && breakConfirmation) } } diff --git a/common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt new file mode 100644 index 000000000..00852d2c7 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.settings + +import com.lambda.util.extension.highestUnit +import com.lambda.util.extension.symbol +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +class DurationSetting( + override val name: String, + private val defaultValue: Duration, + override val range: ClosedRange, + override val step: Duration = 1.toDuration(defaultValue.highestUnit), // FixMe: Causes issues if the lower bound of the range is less than the step duration unit + // Ex: 60.microseconds..10.minutes will have a step of 1 minute + // Maybe we should leave this as is for the default behavior + description: String, + visibility: () -> Boolean, +) : NumericSetting( + defaultValue, + range, + step, + description, + visibility, + "", +) { + // ToDo: + // Should we determine the unit from the step since this is the in/decrement value within the range so it would make sense to use that for the unit inference + override fun toString() = "${value.toInt(value.highestUnit)} ${value.highestUnit.symbol}" +} diff --git a/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt index e7ea70a99..6ea791ab5 100644 --- a/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt @@ -26,7 +26,7 @@ import kotlin.reflect.KProperty /** * @see [com.lambda.config.Configurable] */ -abstract class NumericSetting( +abstract class NumericSetting>( value: T, open val range: ClosedRange, open val step: T, @@ -38,7 +38,7 @@ abstract class NumericSetting( TypeToken.get(value::class.java).type, description, visibility -) where T : Number, T : Comparable { +) { private val formatter = NumberFormat.getNumberInstance(Locale.getDefault()) override fun toString() = "${formatter.format(value)}$unit" diff --git a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt index 7820c6c44..b56460f36 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/client/Discord.kt @@ -37,6 +37,8 @@ import dev.cbyrne.kdiscordipc.KDiscordIPC import dev.cbyrne.kdiscordipc.core.packet.inbound.impl.AuthenticatePacket import dev.cbyrne.kdiscordipc.data.activity.* import kotlinx.coroutines.delay +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds object Discord : Module( name = "Discord", @@ -44,7 +46,7 @@ object Discord : Module( defaultTags = setOf(ModuleTag.CLIENT), //enabledByDefault = true, // ToDo: Bring this back on beta release ) { - private val delay by setting("Update Delay", 5000L, 5000L..30000L, 100L, unit = "ms") + private val delay by setting("Update Delay", 5.seconds, 5.seconds..30.seconds, 100.milliseconds) private val showTime by setting("Show Time", true, description = "Show how long you have been playing for.") private val line1Left by setting("Line 1 Left", LineInfo.WORLD) private val line1Right by setting("Line 1 Right", LineInfo.USERNAME) diff --git a/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt b/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt index 01c626dcc..27f5fecbd 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/combat/CrystalAura.kt @@ -60,6 +60,8 @@ import net.minecraft.util.math.* import kotlin.concurrent.fixedRateTimer import kotlin.math.max import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.nanoseconds +import kotlin.time.Duration.Companion.seconds object CrystalAura : Module( name = "CrystalAura", @@ -74,9 +76,9 @@ object CrystalAura : Module( private val placeDelay by setting("Place Delay", 50L, 0L..1000L, 1L, "Delay between placement attempts", " ms") { page == Page.General } private val explodeDelay by setting("Explode Delay", 10L, 0L..1000L, 1L, "Delay between explosion attempts", " ms") { page == Page.General } private val updateMode by setting("Update Mode", UpdateMode.Async) { page == Page.General } - private val updateDelaySetting by setting("Update Delay", 25L, 5L..200L, 5L, unit = " ms") { page == Page.General && updateMode == UpdateMode.Async } + private val updateDelaySetting by setting("Update Delay", 25.milliseconds, 5.milliseconds..200.milliseconds, 5.milliseconds) { page == Page.General && updateMode == UpdateMode.Async } private val maxUpdatesPerFrame by setting("Max Updates Per Frame", 5, 1..20, 1) { page == Page.General && updateMode == UpdateMode.Async } - private val updateDelay get() = if (updateMode == UpdateMode.Async) updateDelaySetting else 0L + private val updateDelay get() = if (updateMode == UpdateMode.Async) updateDelaySetting else 0.nanoseconds private val debug by setting("Debug", false) { page == Page.General } /* Placement */ @@ -119,7 +121,7 @@ object CrystalAura : Module( private val predictionTimer = Timer() private var lastEntityId = 0 - private val decay = LimitedDecayQueue(10000, 3000L) + private val decay = LimitedDecayQueue(10000, 3.seconds) private val collidingOffsets = mutableListOf().apply { for (x in -1..1) { @@ -293,7 +295,7 @@ object CrystalAura : Module( } private fun SafeContext.updateBlueprint(target: LivingEntity) = - updateTimer.runIfPassed(updateDelay.milliseconds) { + updateTimer.runIfPassed(updateDelay) { resetBlueprint() // Build damage info diff --git a/common/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt b/common/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt index 2c1673991..ccd30bf7e 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/network/PacketDelay.kt @@ -29,12 +29,13 @@ import com.lambda.util.ClientPacket import com.lambda.util.PacketUtils.handlePacketSilently import com.lambda.util.PacketUtils.sendPacketSilently import com.lambda.util.ServerPacket +import com.lambda.util.Timer import kotlinx.coroutines.delay -import net.minecraft.network.listener.ClientPacketListener -import net.minecraft.network.listener.ServerPacketListener import net.minecraft.network.packet.Packet import net.minecraft.network.packet.c2s.common.KeepAliveC2SPacket import java.util.concurrent.ConcurrentLinkedDeque +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds object PacketDelay : Module( name = "PacketDelay", @@ -44,19 +45,18 @@ object PacketDelay : Module( private val mode by setting("Mode", Mode.STATIC) private val networkScope by setting("Network Scope", Direction.BOTH) private val packetScope by setting("Packet Scope", PacketType.ANY) - private val inboundDelay by setting("Inbound Delay", 250L, 0L..5000L, 10L, unit = "ms") { networkScope != Direction.OUTBOUND } - private val outboundDelay by setting("Outbound Delay", 250L, 0L..5000L, 10L, unit = "ms") { networkScope != Direction.INBOUND } + private val inboundDelay by setting("Inbound Delay", 250.milliseconds, 1.milliseconds..5.seconds, 10.milliseconds) { networkScope != Direction.OUTBOUND } + private val outboundDelay by setting("Outbound Delay", 250.milliseconds, 1.milliseconds..5.seconds, 10.milliseconds) { networkScope != Direction.INBOUND } private var outboundPool = ConcurrentLinkedDeque() private var inboundPool = ConcurrentLinkedDeque() - private var outboundLastUpdate = 0L - private var inboundLastUpdate = 0L + private var outboundLastUpdate = Timer() + private var inboundLastUpdate = Timer() init { listen { if (mode != Mode.STATIC) return@listen - - flushPools(System.currentTimeMillis()) + flushPools() } listen(Int.MIN_VALUE) { event -> @@ -104,29 +104,25 @@ object PacketDelay : Module( } onDisable { - flushPools(System.currentTimeMillis()) + flushPools() } } - private fun SafeContext.flushPools(time: Long) { - if (time - outboundLastUpdate >= outboundDelay) { + private fun SafeContext.flushPools() { + outboundLastUpdate.runIfPassed(outboundDelay) { while (outboundPool.isNotEmpty()) { outboundPool.poll().let { packet -> connection.sendPacketSilently(packet) } } - - outboundLastUpdate = time } - if (time - inboundLastUpdate >= inboundDelay) { + inboundLastUpdate.runIfPassed(inboundDelay) { while (inboundPool.isNotEmpty()) { inboundPool.poll().let { packet -> connection.handlePacketSilently(packet) } } - - inboundLastUpdate = time } } diff --git a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt index 729e8a718..c44b6a109 100644 --- a/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt +++ b/common/src/main/kotlin/com/lambda/module/modules/network/PacketLimiter.kt @@ -26,6 +26,8 @@ import com.lambda.util.collections.LimitedDecayQueue import net.minecraft.network.packet.c2s.common.CommonPongC2SPacket import net.minecraft.network.packet.c2s.play.PlayerMoveC2SPacket.* import net.minecraft.network.packet.c2s.play.TeleportConfirmC2SPacket +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds // ToDo: HUD info object PacketLimiter : Module( @@ -33,11 +35,11 @@ object PacketLimiter : Module( description = "Limits the amount of packets sent to the server", defaultTags = setOf(ModuleTag.NETWORK) ) { - private var packetQueue = LimitedDecayQueue(99, 1000) + private var packetQueue = LimitedDecayQueue(99, 1.seconds) private val limit by setting("Limit", 99, 1..100, 1, "The maximum amount of packets to send per given time interval", unit = " packets") .onValueChange { _, to -> packetQueue.setSizeLimit(to) } - private val interval by setting("Duration", 4000L, 1L..10000L, 50L, "The interval / duration in milliseconds to limit packets for", unit = " ms") + private val interval by setting("Duration", 4.seconds, 1.milliseconds..10.seconds, 50.milliseconds, "The interval / duration in milliseconds to limit packets for") .onValueChange { _, to -> packetQueue.setDecayTime(to) } private val defaultIgnorePackets = setOf( diff --git a/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt b/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt index aa8fac874..ea097c695 100644 --- a/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt +++ b/common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt @@ -53,6 +53,7 @@ import com.lambda.util.Formatting.string import com.lambda.util.collections.LimitedDecayQueue import com.lambda.util.extension.Structure import com.lambda.util.extension.inventorySlots +import com.lambda.util.extension.ticks import com.lambda.util.item.ItemUtils.block import com.lambda.util.player.SlotUtils.hotbarAndStorage import net.minecraft.entity.ItemEntity @@ -70,7 +71,7 @@ class BuildTask @Ta5kBuilder constructor( override val name: String get() = "Building $blueprint with ${(breaks / (age / 20.0 + 0.001)).string} b/s ${(placements / (age / 20.0 + 0.001)).string} p/s" private val pendingInteractions = LimitedDecayQueue( - build.maxPendingInteractions, build.interactionTimeout * 50L + build.maxPendingInteractions, build.interactionTimeout.ticks ) { info("${it::class.simpleName} at ${it.expectedPos.toShortString()} timed out") } private var currentInteraction: BuildContext? = null private val instantBreaks = mutableSetOf() diff --git a/common/src/main/kotlin/com/lambda/util/ServerTPS.kt b/common/src/main/kotlin/com/lambda/util/ServerTPS.kt index b29903082..d04d22f67 100644 --- a/common/src/main/kotlin/com/lambda/util/ServerTPS.kt +++ b/common/src/main/kotlin/com/lambda/util/ServerTPS.kt @@ -22,10 +22,11 @@ import com.lambda.event.events.PacketEvent import com.lambda.event.listener.SafeListener.Companion.listen import com.lambda.util.collections.LimitedDecayQueue import net.minecraft.network.packet.s2c.play.WorldTimeUpdateS2CPacket +import kotlin.time.Duration.Companion.seconds object ServerTPS { // Server sends exactly one world time update every 20 server ticks (one per second). - private val updateHistory = LimitedDecayQueue(61, 60000) + private val updateHistory = LimitedDecayQueue(61, 60.seconds) private var lastUpdate = 0L val averageMSPerTick: Double diff --git a/common/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt b/common/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt index 7bf1d0534..1fd9a4b4b 100644 --- a/common/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt +++ b/common/src/main/kotlin/com/lambda/util/collections/LimitedDecayQueue.kt @@ -17,8 +17,9 @@ package com.lambda.util.collections -import java.time.Instant import java.util.concurrent.ConcurrentLinkedQueue +import kotlin.time.Duration +import kotlin.time.TimeSource /** * A thread-safe collection that limits the number of elements it can hold and automatically removes elements @@ -26,15 +27,15 @@ import java.util.concurrent.ConcurrentLinkedQueue * * @param E The type of elements held in this collection. * @property sizeLimit The maximum number of elements the queue can hold at any given time. - * @property maxAge The age (in milliseconds) after which elements are considered expired and are removed from the queue. + * @property maxDuration The duration after which elements are considered expired and are removed from the queue. * @property onDecay Lambda function that is executed on decay of element [E]. */ class LimitedDecayQueue( private var sizeLimit: Int, - private var maxAge: Long, + private var maxDuration: Duration, private val onDecay: (E) -> Unit = {} ) : AbstractMutableCollection() { - private val queue: ConcurrentLinkedQueue> = ConcurrentLinkedQueue() + private val queue = ConcurrentLinkedQueue>() override val size: Int @Synchronized @@ -63,7 +64,7 @@ class LimitedDecayQueue( override fun add(element: E): Boolean { cleanUp() return if (queue.size < sizeLimit) { - queue.add(element to Instant.now()) + queue.add(element to TimeSource.Monotonic.markNow()) true } else { false @@ -75,7 +76,7 @@ class LimitedDecayQueue( cleanUp() val spaceAvailable = sizeLimit - queue.size val elementsToAdd = elements.take(spaceAvailable) - val added = elementsToAdd.map { queue.add(it to Instant.now()) } + val added = elementsToAdd.map { queue.add(it to TimeSource.Monotonic.markNow()) } return added.any { it } } @@ -120,21 +121,23 @@ class LimitedDecayQueue( } /** - * Sets the decay time for the elements in the queue. The decay time determines the + * Sets the decay duration for the elements in the queue. The decay determines the * maximum age that any element in the queue can have before being considered expired * and removed. Updates the internal state and triggers a cleanup of expired elements. * - * @param decayTime The decay time in milliseconds. Must be a non-negative value. + * @param decay The decay time [Duration]. */ - fun setDecayTime(decayTime: Long) { - maxAge = decayTime + fun setDecayTime(decay: Duration) { + maxDuration = decay cleanUp() } private fun cleanUp() { - val now = Instant.now() - while (queue.isNotEmpty() && now.minusMillis(maxAge).isAfter(queue.peek().second)) { - onDecay(queue.poll().first) + while (queue.isNotEmpty()) { + val (_, time) = queue.peek() + + if (time.elapsedNow() >= maxDuration) onDecay(queue.poll().first) + else break } } } diff --git a/common/src/main/kotlin/com/lambda/util/extension/Time.kt b/common/src/main/kotlin/com/lambda/util/extension/Time.kt new file mode 100644 index 000000000..5ae5979cf --- /dev/null +++ b/common/src/main/kotlin/com/lambda/util/extension/Time.kt @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.util.extension + +import kotlin.ranges.contains +import kotlin.time.Duration +import kotlin.time.DurationUnit +import kotlin.time.toDuration + +val Int.ticks: Duration get() = (this*50).toDuration(DurationUnit.MILLISECONDS) +val Long.ticks: Duration get() = (this*50).toDuration(DurationUnit.MILLISECONDS) +val Double.ticks: Duration get() = (this*50).toDuration(DurationUnit.MILLISECONDS) + +/** + * Returns the highest unit present in the duration's nano + */ +val Duration.highestUnit: DurationUnit + get() = when { + inWholeDays > 0 -> DurationUnit.DAYS + inWholeHours in 1..24 -> DurationUnit.HOURS + inWholeMinutes in 1..60 -> DurationUnit.MINUTES + inWholeSeconds in 1..60 -> DurationUnit.SECONDS + inWholeMilliseconds in 1..1000 -> DurationUnit.MILLISECONDS + inWholeMicroseconds in 1..1000 -> DurationUnit.MICROSECONDS + inWholeNanoseconds in 0..1000 -> DurationUnit.NANOSECONDS + else -> DurationUnit.NANOSECONDS // We need to please the almighty compiler + } + +val DurationUnit.symbol: String + get() = when (this) { + DurationUnit.NANOSECONDS -> "ns" + DurationUnit.MICROSECONDS -> "μs" + DurationUnit.MILLISECONDS -> "ms" + DurationUnit.SECONDS -> "s" + DurationUnit.MINUTES -> "min" + DurationUnit.HOURS -> "hr" + DurationUnit.DAYS -> "d" + } diff --git a/common/src/test/kotlin/LimitedDecayQueueTest.kt b/common/src/test/kotlin/LimitedDecayQueueTest.kt index 0f48bcdc7..c823d77d8 100644 --- a/common/src/test/kotlin/LimitedDecayQueueTest.kt +++ b/common/src/test/kotlin/LimitedDecayQueueTest.kt @@ -5,6 +5,8 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds /* * Copyright 2025 Lambda @@ -32,7 +34,7 @@ class LimitedDecayQueueTest { fun setUp() { // Initialize the onDecay callback onDecayCalled = mutableListOf() - queue = LimitedDecayQueue(3, 1000) { onDecayCalled.add(it) } // 1 second decay time + queue = LimitedDecayQueue(3, 1.seconds) { onDecayCalled.add(it) } // 1 second decay time } @Test @@ -145,7 +147,7 @@ class LimitedDecayQueueTest { queue.add("Element1") queue.add("Element2") - queue.setDecayTime(500) // Set a shorter decay time of 500 ms + queue.setDecayTime(500.milliseconds) // Set a shorter decay time of 500 ms // Simulate passage of time (greater than decay time) TimeUnit.MILLISECONDS.sleep(600) From 88ed39e1defdb0ed5704ea0739747196dea3854c Mon Sep 17 00:00:00 2001 From: Edouard127 <46357922+Edouard127@users.noreply.github.com> Date: Sat, 31 May 2025 13:20:43 -0400 Subject: [PATCH 2/2] Added gui component --- .../kotlin/com/lambda/config/Configurable.kt | 2 +- .../lambda/config/settings/DurationSetting.kt | 46 ----------- .../lambda/config/settings/NumericSetting.kt | 4 +- .../settings/comparable/DurationSetting.kt | 43 ++++++++++ .../main/kotlin/com/lambda/gui/GuiManager.kt | 14 ++++ .../module/setting/settings/DurationSlider.kt | 82 +++++++++++++++++++ 6 files changed, 142 insertions(+), 49 deletions(-) delete mode 100644 common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt create mode 100644 common/src/main/kotlin/com/lambda/config/settings/comparable/DurationSetting.kt create mode 100644 common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/setting/settings/DurationSlider.kt diff --git a/common/src/main/kotlin/com/lambda/config/Configurable.kt b/common/src/main/kotlin/com/lambda/config/Configurable.kt index b4e3ebdd1..b94f0fc88 100644 --- a/common/src/main/kotlin/com/lambda/config/Configurable.kt +++ b/common/src/main/kotlin/com/lambda/config/Configurable.kt @@ -23,7 +23,7 @@ import com.google.gson.reflect.TypeToken import com.lambda.Lambda import com.lambda.Lambda.LOG import com.lambda.config.settings.CharSetting -import com.lambda.config.settings.DurationSetting +import com.lambda.config.settings.comparable.DurationSetting import com.lambda.config.settings.FunctionSetting import com.lambda.config.settings.StringSetting import com.lambda.config.settings.collections.ListSetting diff --git a/common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt deleted file mode 100644 index 00852d2c7..000000000 --- a/common/src/main/kotlin/com/lambda/config/settings/DurationSetting.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2025 Lambda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.lambda.config.settings - -import com.lambda.util.extension.highestUnit -import com.lambda.util.extension.symbol -import kotlin.time.Duration -import kotlin.time.DurationUnit -import kotlin.time.toDuration - -class DurationSetting( - override val name: String, - private val defaultValue: Duration, - override val range: ClosedRange, - override val step: Duration = 1.toDuration(defaultValue.highestUnit), // FixMe: Causes issues if the lower bound of the range is less than the step duration unit - // Ex: 60.microseconds..10.minutes will have a step of 1 minute - // Maybe we should leave this as is for the default behavior - description: String, - visibility: () -> Boolean, -) : NumericSetting( - defaultValue, - range, - step, - description, - visibility, - "", -) { - // ToDo: - // Should we determine the unit from the step since this is the in/decrement value within the range so it would make sense to use that for the unit inference - override fun toString() = "${value.toInt(value.highestUnit)} ${value.highestUnit.symbol}" -} diff --git a/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt index 6ea791ab5..e7ea70a99 100644 --- a/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt +++ b/common/src/main/kotlin/com/lambda/config/settings/NumericSetting.kt @@ -26,7 +26,7 @@ import kotlin.reflect.KProperty /** * @see [com.lambda.config.Configurable] */ -abstract class NumericSetting>( +abstract class NumericSetting( value: T, open val range: ClosedRange, open val step: T, @@ -38,7 +38,7 @@ abstract class NumericSetting>( TypeToken.get(value::class.java).type, description, visibility -) { +) where T : Number, T : Comparable { private val formatter = NumberFormat.getNumberInstance(Locale.getDefault()) override fun toString() = "${formatter.format(value)}$unit" diff --git a/common/src/main/kotlin/com/lambda/config/settings/comparable/DurationSetting.kt b/common/src/main/kotlin/com/lambda/config/settings/comparable/DurationSetting.kt new file mode 100644 index 000000000..eb9f47f14 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/config/settings/comparable/DurationSetting.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.config.settings.comparable + +import com.google.gson.reflect.TypeToken +import com.lambda.config.AbstractSetting +import kotlin.reflect.KProperty +import kotlin.time.Duration + +class DurationSetting( + override val name: String, + defaultValue: Duration, + val range: ClosedRange, + val step: Duration, + description: String, + visibility: () -> Boolean, +) : AbstractSetting( + defaultValue, + TypeToken.get(defaultValue::class.java).type, + description, + visibility, +) { + override fun toString() = value.toString() + + override operator fun setValue(thisRef: Any?, property: KProperty<*>, valueIn: Duration) { + value = valueIn.coerceIn(range) + } +} diff --git a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt index c2efba21e..796038d11 100644 --- a/common/src/main/kotlin/com/lambda/gui/GuiManager.kt +++ b/common/src/main/kotlin/com/lambda/gui/GuiManager.kt @@ -20,6 +20,7 @@ package com.lambda.gui import com.lambda.config.settings.comparable.BooleanSetting import com.lambda.config.settings.comparable.EnumSetting import com.lambda.config.settings.FunctionSetting +import com.lambda.config.settings.comparable.DurationSetting import com.lambda.config.settings.complex.ColorSetting import com.lambda.config.settings.complex.KeyBindSetting import com.lambda.config.settings.numeric.DoubleSetting @@ -31,6 +32,7 @@ import com.lambda.gui.component.core.UIBuilder import com.lambda.gui.component.layout.Layout import com.lambda.gui.impl.clickgui.module.setting.settings.BooleanButton.Companion.booleanSetting import com.lambda.gui.impl.clickgui.module.setting.settings.ColorPicker.Companion.colorPicker +import com.lambda.gui.impl.clickgui.module.setting.settings.DurationSlider.Companion.durationSlider import com.lambda.gui.impl.clickgui.module.setting.settings.EnumSlider.Companion.enumSetting import com.lambda.gui.impl.clickgui.module.setting.settings.KeybindPicker.Companion.keybindSetting import com.lambda.gui.impl.clickgui.module.setting.settings.NumberSlider.Companion.numberSlider @@ -115,6 +117,18 @@ object GuiManager : Loadable { } } + typeAdapter { owner, ref -> + owner.durationSlider( + ref.name, + ref.range.start, + ref.range.endInclusive, + ref.step, + ref::value + ).apply { + visibility { ref.visibility() } + } + } + return "Loaded ${typeMap.size} gui type adapters." } diff --git a/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/setting/settings/DurationSlider.kt b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/setting/settings/DurationSlider.kt new file mode 100644 index 000000000..a61dd2780 --- /dev/null +++ b/common/src/main/kotlin/com/lambda/gui/impl/clickgui/module/setting/settings/DurationSlider.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2025 Lambda + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.lambda.gui.impl.clickgui.module.setting.settings + +import com.lambda.gui.component.core.UIBuilder +import com.lambda.gui.component.layout.Layout +import com.lambda.gui.impl.clickgui.module.setting.SettingSlider +import com.lambda.util.math.MathUtils.roundToStep +import com.lambda.util.math.lerp +import com.lambda.util.math.transform +import kotlin.reflect.KMutableProperty0 +import kotlin.time.Duration +import com.lambda.config.settings.comparable.DurationSetting +import com.lambda.util.extension.highestUnit +import kotlin.time.toDuration + +class DurationSlider( + owner: Layout, + name: String, + min: Duration, + max: Duration, + step: Duration, + field: KMutableProperty0, +) : SettingSlider(owner, name, field) { + override val settingValue: String + get() = settingDelegate.toString() + + private val unit = step.highestUnit + + private val minNanos = min.toDouble(unit) + private val maxNanos = max.toDouble(unit) + private val stepNanos = step.toDouble(unit) + + init { + // Slider logic + slider.progress { + transform( + settingDelegate.toDouble(unit), + minNanos, + maxNanos, + 0.0, + 1.0, + ) + } + + slider.onSlide { + settingDelegate = lerp(it, minNanos, maxNanos) + .roundToStep(stepNanos) + .toDuration(unit) + .coerceIn(min..max) + } + } + + companion object { + /** + * Creates an [DurationSlider] - visual representation of the [DurationSetting] + */ + @UIBuilder + fun Layout.durationSlider( + name: String, + min: Duration, + max: Duration, + step: Duration, + field: KMutableProperty0 + ) = DurationSlider(this, name, min, max, step, field).apply(children::add) + } +}