Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions common/src/main/kotlin/com/lambda/config/Configurable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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].
Expand Down Expand Up @@ -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<Duration>,
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].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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<Duration>,
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<Duration>(
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}"
Comment thread
emyfops marked this conversation as resolved.
Outdated
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import kotlin.reflect.KProperty
/**
* @see [com.lambda.config.Configurable]
*/
abstract class NumericSetting<T>(
abstract class NumericSetting<T : Comparable<T>>(
value: T,
open val range: ClosedRange<T>,
open val step: T,
Expand All @@ -38,7 +38,7 @@ abstract class NumericSetting<T>(
TypeToken.get(value::class.java).type,
description,
visibility
) where T : Number, T : Comparable<T> {
) {
private val formatter = NumberFormat.getNumberInstance(Locale.getDefault())

override fun toString() = "${formatter.format(value)}$unit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,16 @@ 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",
description = "Discord Rich Presence configuration",
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 */
Expand Down Expand Up @@ -119,7 +121,7 @@ object CrystalAura : Module(
private val predictionTimer = Timer()
private var lastEntityId = 0

private val decay = LimitedDecayQueue<Int>(10000, 3000L)
private val decay = LimitedDecayQueue<Int>(10000, 3.seconds)

private val collidingOffsets = mutableListOf<BlockPos>().apply {
for (x in -1..1) {
Expand Down Expand Up @@ -293,7 +295,7 @@ object CrystalAura : Module(
}

private fun SafeContext.updateBlueprint(target: LivingEntity) =
updateTimer.runIfPassed(updateDelay.milliseconds) {
updateTimer.runIfPassed(updateDelay) {
resetBlueprint()

// Build damage info
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<ClientPacket>()
private var inboundPool = ConcurrentLinkedDeque<ServerPacket>()
private var outboundLastUpdate = 0L
private var inboundLastUpdate = 0L
private var outboundLastUpdate = Timer()
private var inboundLastUpdate = Timer()

init {
listen<RenderEvent.World> {
if (mode != Mode.STATIC) return@listen

flushPools(System.currentTimeMillis())
flushPools()
}

listen<PacketEvent.Send.Pre>(Int.MIN_VALUE) { event ->
Expand Down Expand Up @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,20 @@ 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(
name = "PacketLimiter",
description = "Limits the amount of packets sent to the server",
defaultTags = setOf(ModuleTag.NETWORK)
) {
private var packetQueue = LimitedDecayQueue<PacketEvent.Send.Pre>(99, 1000)
private var packetQueue = LimitedDecayQueue<PacketEvent.Send.Pre>(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(
Expand Down
3 changes: 2 additions & 1 deletion common/src/main/kotlin/com/lambda/task/tasks/BuildTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<BuildContext>(
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<BreakContext>()
Expand Down
3 changes: 2 additions & 1 deletion common/src/main/kotlin/com/lambda/util/ServerTPS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long>(61, 60000)
private val updateHistory = LimitedDecayQueue<Long>(61, 60.seconds)
private var lastUpdate = 0L

val averageMSPerTick: Double
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,25 @@

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
* older than a specified time interval. The elements are stored with the timestamp of their addition to the collection.
*
* @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<E>(
private var sizeLimit: Int,
private var maxAge: Long,
private var maxDuration: Duration,
private val onDecay: (E) -> Unit = {}
) : AbstractMutableCollection<E>() {
private val queue: ConcurrentLinkedQueue<Pair<E, Instant>> = ConcurrentLinkedQueue()
private val queue = ConcurrentLinkedQueue<Pair<E, TimeSource.Monotonic.ValueTimeMark>>()

override val size: Int
@Synchronized
Expand Down Expand Up @@ -63,7 +64,7 @@ class LimitedDecayQueue<E>(
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
Expand All @@ -75,7 +76,7 @@ class LimitedDecayQueue<E>(
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 }
}

Expand Down Expand Up @@ -120,21 +121,23 @@ class LimitedDecayQueue<E>(
}

/**
* 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
}
}
}
Loading