diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/Clicker.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/Clicker.kt index bc804060278..b65215bc482 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/Clicker.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/Clicker.kt @@ -18,30 +18,25 @@ */ package net.ccbluex.liquidbounce.utils.clicking +import net.ccbluex.liquidbounce.config.types.CurveValue.Axis.Companion.axis import net.ccbluex.liquidbounce.config.types.Value import net.ccbluex.liquidbounce.config.types.group.ValueGroup -import net.ccbluex.liquidbounce.config.types.list.Tagged import net.ccbluex.liquidbounce.event.EventListener +import net.ccbluex.liquidbounce.event.events.GameRenderEvent import net.ccbluex.liquidbounce.event.events.GameTickEvent import net.ccbluex.liquidbounce.event.events.KeybindIsPressedEvent import net.ccbluex.liquidbounce.event.handler import net.ccbluex.liquidbounce.features.module.modules.render.ModuleDebug.debugParameter -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern -import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.ButterflyPattern -import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.DoubleClickPattern -import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.DragPattern -import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.EfficientPattern -import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.NormalDistributionPattern -import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.SpammingPattern -import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.StabilizedPattern import net.ccbluex.liquidbounce.utils.client.mc -import net.ccbluex.liquidbounce.utils.client.player -import net.ccbluex.liquidbounce.utils.entity.hasCooldown +import net.ccbluex.liquidbounce.utils.client.vector2f +import net.ccbluex.liquidbounce.utils.input.InputTracker +import net.ccbluex.liquidbounce.utils.input.InputTracker.timeSinceComboStart +import net.ccbluex.liquidbounce.utils.input.InputTracker.timeSinceLastPress +import net.ccbluex.liquidbounce.utils.input.InputTracker.updateInputPress import net.ccbluex.liquidbounce.utils.kotlin.EventPriorityConvention import net.minecraft.client.KeyMapping import net.minecraft.client.Minecraft -import java.util.Arrays -import java.util.Random +import kotlin.math.roundToInt /** * An attack scheduler @@ -62,26 +57,42 @@ open class Clicker( val itemCooldown: ItemCooldown? = ItemCooldown(), maxCps: Int = 60, name: String = "Clicker" -) : ValueGroup(name, aliases = listOf("ClickScheduler")), EventListener where T : EventListener { +) : ValueGroup(name), EventListener where T : EventListener { companion object { - internal val RNG = Random() - private const val DEFAULT_CYCLE_LENGTH = 20 - private var lastClickTime = 0L - private val lastClickPassed - get() = System.currentTimeMillis() - lastClickTime + private const val TICK_DURATION_MS = 50L + private const val TICKS_IN_A_SECOND = 20 + private const val DEFAULT_CYCLE_LENGTH_MS = (TICKS_IN_A_SECOND * TICK_DURATION_MS).toInt() + private const val DEFAULT_CURVE_WINDOW_SECONDS = 10f + private const val CLICK_TOLERANCE_MS = 5L } - // Options - private val cps by intRange("CPS", 5..8, 1..maxCps, "clicks") - .onChanged { - fill() - } + private val cps: IntRange by intRange("CPS", 11..14, 1..maxCps, "clicks").onChanged { + currentCps = cps.random() + nextClickDelayMs = sampleIntervalMs() + } - private val pattern by enumChoice("Technique", ClickPatterns.STABILIZED) - .onChanged { - fill() + private val fatigue = curve( + "Fatigue", + mutableListOf( + 0f vector2f 2f, + DEFAULT_CURVE_WINDOW_SECONDS / 2 vector2f -2f, + DEFAULT_CURVE_WINDOW_SECONDS vector2f 1f, + ), + xAxis = "Seconds" axis 0f..DEFAULT_CURVE_WINDOW_SECONDS, + yAxis = "CPS" axis -5f..5f, + ).apply { + onChanged { + nextClickDelayMs = sampleIntervalMs() } + } + + /** + * When we break the combo, we reset our fatigue. If set to 0, we stay in the combo indefinitely. + */ + private val breakCombo by intRange("BreakCombo", 0..0, 0..20, "ticks") + + private var pauseTicks = 0 init { itemCooldown?.let(this::tree) @@ -94,146 +105,144 @@ open class Clicker( * This is useful for anti-cheats that detect if you are ignoring this cooldown. * Applies to the FailSwing feature as well. */ - private val attackCooldown: Value? = if (keyBinding == mc.options.keyAttack) { - boolean("AttackCooldown", true) + private val missCooldown: Value? = if (keyBinding == mc.options.keyAttack) { + boolean("MissCooldown", true) } else { null } - private val passesAttackCooldown - get() = !(attackCooldown?.get() == true && mc.missTime > 0) + private val passesMissCooldown + get() = !(missCooldown?.get() == true && mc.missTime > 0) - private val clickArray = RollingClickArray(DEFAULT_CYCLE_LENGTH, 2) + /** + * With each combo, we start with a random CPS value. + * This allows us to have a different CPS value for each combo. + */ + private var currentCps = cps.random() + private var nextClickDelayMs = sampleIntervalMs() - init { - fill() - } + // Clicks that are possible by the next tick + var possibleClicks: Int = 0 + private set // Clicks that were executed by [click] in the current tick var clickAmount: Int? = null private set + // todo: find better name val isClickTick: Boolean - get() = willClickAt(0) - - val ticksUntilClick: Int - get() { - for (i in 0 until clickArray.iterations) { - if (willClickAt(i)) { - return i - } - } + get() = timeUntilNextClickMs(0) <= 0 - return clickArray.iterations - } + // todo: find better name + fun willClickAt(tick: Int = 1) = timeUntilNextClickMs(1 + tick) <= 0 - fun willClickAt(tick: Int = 1) = getClickAmount(tick) > 0 + /** + * Clicks on a curve-driven schedule per tick. If the cooldown is not passed, it will not click. + * [block] should return true if the click was successful. Otherwise, it will not count as a click. + */ + fun click(block: () -> Boolean): Boolean { + debugParameter("Current CPS") { currentCps } + debugParameter("Time Since Last Click") { timeSinceLastClickMs() } + debugParameter("Time Since Combo Start") { keyBinding.timeSinceComboStart } + debugParameter("Time Until Next Click") { timeUntilNextClickMs(0) } + debugParameter("Miss Cooldown") { mc.missTime } + debugParameter("Item Cooldown") { itemCooldown?.cooldownProgress() ?: 0.0f } - fun getClickAmount(tick: Int = 0): Int { - if (isEnforcedClick()) { - return 1 + var clicks = 0 + var elapsedMs = timeSinceLastClickMs() + // todo: make double clicking work + while (pauseTicks <= 0 && elapsedMs + CLICK_TOLERANCE_MS >= nextClickDelayMs) { +// if (!passesMissCooldown) { +// break +// } +// +// if (itemCooldown?.isCooldownPassed() == false) { +// break +// } + + if (block()) { + itemCooldown?.newCooldown() + updateInputPress(keyBinding.key) + elapsedMs -= nextClickDelayMs + nextClickDelayMs = sampleIntervalMs() + clicks++ + } else { + break + } } - return clickArray.get(tick) - } - private fun isEnforcedClick(tick: Int = 0): Boolean { - val hasCooldown = player.hasCooldown - debugParameter("HasCooldown") { hasCooldown } - if (hasCooldown && itemCooldown?.isCooldownPassed(tick) == true) { - return true - } + this.clickAmount = clicks + debugParameter("Next Click Delay") { nextClickDelayMs } + debugParameter("Current Clicks") { clicks } + debugParameter("Time Until Next Click (Post)") { timeUntilNextClickMs(0) } - return lastClickPassed + (tick * 50L) >= 1000L + return clicks > 0 } @Suppress("unused") private val keybindIsPressedHandler = handler { event -> val clickAmount = this.clickAmount ?: return@handler - // It turns out, we only want to do this with [attackKey], otherwise + // It turns out we only want to do this with [attackKey]; otherwise // [useKey] will do unexpected things. if (keyBinding == mc.options.keyAttack && event.keyBinding == keyBinding) { - // We want to simulate the click in order to - // allow the game to handle the logic as if we clicked + // We want to simulate the click to allow the game to handle the logic as if we clicked. event.isPressed = clickAmount > 0 } } - /** - * Clicks [cps] times per call (tick). If the cooldown is not passed, it will not click. - * [block] should return true if the click was successful. Otherwise, it will not count as a click. - */ - fun click(block: () -> Boolean) { - val clicks = getClickAmount() - - debugParameter("Current Clicks") { clicks } - debugParameter("Peek Clicks") { clickArray.get(1) } - debugParameter("Last Click Passed") { lastClickPassed } - debugParameter("Attack Cooldown") { mc.missTime } - debugParameter("Item Cooldown") { itemCooldown?.cooldownProgress() ?: 0.0f } - - var clickAmount = 0 - - repeat(clicks) { - if (!passesAttackCooldown) { - return@repeat - } - - if (itemCooldown?.isCooldownPassed() != false && block()) { - clickAmount++ - itemCooldown?.newCooldown() - lastClickTime = System.currentTimeMillis() - } - } + @Suppress("handler") + private val renderHandler = handler { - this.clickAmount = clickAmount } @Suppress("unused") - private val gameHandler = handler( - priority = EventPriorityConvention.FIRST_PRIORITY - ) { + private val gameHandler = handler(priority = EventPriorityConvention.FIRST_PRIORITY) { clickAmount = null - if (clickArray.advance()) { - val cycleArray = IntArray(DEFAULT_CYCLE_LENGTH) - pattern.pattern.fill(cycleArray, cps, this) - clickArray.push(cycleArray) + if (pauseTicks > 0) { + pauseTicks-- } + } - debugParameter("Click Technique") { pattern.tag } - debugParameter("Click Array") { - clickArray.array.withIndex().joinToString { (i, v) -> - if (i == clickArray.head) "*$v" else v.toString() - } - } + override fun parent() = parent + + private fun timeUntilNextClickMs(tickOffset: Int): Long { + val timeSince = timeSinceLastClickMs() + val offsetMs = tickOffset * TICK_DURATION_MS + val baseRemainingMs = nextClickDelayMs.toLong() - (timeSince + offsetMs) - CLICK_TOLERANCE_MS + val pauseRemainingMs = (pauseTicks - tickOffset).coerceAtLeast(0) * TICK_DURATION_MS + return if (pauseRemainingMs > 0) pauseRemainingMs else baseRemainingMs } - private fun fill() { - clickArray.clear() - val cycleArray = IntArray(DEFAULT_CYCLE_LENGTH) - repeat(clickArray.iterations) { - Arrays.fill(cycleArray, 0) - pattern.pattern.fill(cycleArray, cps, this) - clickArray.push(cycleArray) - clickArray.advance(DEFAULT_CYCLE_LENGTH) - } + private fun timeSinceLastClickMs(): Long { + val timeSince = keyBinding.timeSinceLastPress + return if (timeSince == Long.MAX_VALUE) DEFAULT_CYCLE_LENGTH_MS.toLong() else timeSince } - override fun parent() = parent + private fun sampleIntervalMs(): Int { + var comboMs = keyBinding.timeSinceComboStart + if (comboMs == Long.MAX_VALUE) { + currentCps = cps.random() + comboMs = 0L + } - @Suppress("unused") - enum class ClickPatterns( - override val tag: String, - val pattern: ClickPattern - ) : Tagged { - STABILIZED("Stabilized", StabilizedPattern), - EFFICIENT("Efficient", EfficientPattern), - SPAMMING("Spamming", SpammingPattern), - DOUBLE_CLICK("DoubleClick", DoubleClickPattern), - DRAG("Drag", DragPattern), - BUTTERFLY("Butterfly", ButterflyPattern), - NORMAL_DISTRIBUTION("NormalDistribution", NormalDistributionPattern); + val maxComboSeconds = fatigue.xAxis.range.endInclusive + val elapsedSeconds = comboMs / 1000f + + // If we exceeded the max combo, we break the combo and reset fatigue. + if (elapsedSeconds > maxComboSeconds) { + pauseTicks = breakCombo.random() + if (pauseTicks > 0) { + InputTracker.resetCombo(keyBinding.key) + currentCps = cps.random() + } + } + + val cpsValue = (currentCps + fatigue.transform(elapsedSeconds.coerceIn(0f, maxComboSeconds))) + .coerceAtLeast(1f) + val intervalMs = 1000f / cpsValue + return intervalMs.roundToInt().coerceIn(1, DEFAULT_CYCLE_LENGTH_MS) } } diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/RollingClickArray.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/RollingClickArray.kt deleted file mode 100644 index b48b0715a02..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/RollingClickArray.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ - -package net.ccbluex.liquidbounce.utils.clicking - -/** - * A circular buffer that maintains double the cycle length and regenerates the second half - * when reaching the midpoint - */ -class RollingClickArray( - private val cycleLength: Int, - val iterations: Int, -) { - - internal val array = IntArray(cycleLength * iterations) - var head = 0 - private set - private val size get() = array.size - - /** - * Gets value at relative index from current head - */ - fun get(relativeIndex: Int): Int { - val actualIndex = (head + relativeIndex) % size - return array[actualIndex] - } - - /** - * Sets value at relative index from current head - */ - fun set(relativeIndex: Int, value: Int) { - val actualIndex = (head + relativeIndex) % size - array[actualIndex] = value - } - - /** - * Advances the head position and returns true if halfway point reached - */ - fun advance(amount: Int = 1): Boolean { - head = (head + amount) % size - return head % cycleLength == 0 - } - - /** - * Clears the array - */ - fun clear() { - array.fill(0) - head = 0 - } - - fun push(cycleArray: IntArray) { - require(cycleArray.size == cycleLength) { "Array size must match cycle length" } - - when (head) { - 0 -> System.arraycopy(cycleArray, 0, array, cycleLength, cycleLength) - cycleLength -> System.arraycopy(cycleArray, 0, array, 0, cycleLength) - else -> error("Head must be at 0 or cycle length") - } - } - -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/ClickPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/ClickPattern.kt deleted file mode 100644 index 4d6675fe704..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/ClickPattern.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern - -import net.ccbluex.liquidbounce.utils.clicking.Clicker - -interface ClickPattern { - fun fill(clickArray: IntArray, cps: IntRange, clicker: Clicker<*>) -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/ButterflyPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/ButterflyPattern.kt deleted file mode 100644 index 9ab54f4d601..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/ButterflyPattern.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern.patterns - -import net.ccbluex.liquidbounce.utils.clicking.Clicker -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern - -/** - * Butterfly clicking is a method that is used to bypass the CPS limit of 20. - * - * It will often result in double click (very similar to the double click technique - but randomized). - */ -object ButterflyPattern : ClickPattern { - override fun fill( - clickArray: IntArray, - cps: IntRange, - clicker: Clicker<*> - ) { - val clicks = cps.random() - - while (clickArray.sum() < clicks) { - // Increase random index inside click array by 1 - val indices = clickArray.indices.filter { clickArray[it] == 0 } - - if (indices.isNotEmpty()) { - // Increase a random index which is not yet clicked - indices.random().let { index -> - clickArray[index] = Clicker.RNG.nextInt(1, 3) - } - } else { - // Randomly increase an index - clickArray.indices.random().let { index -> - clickArray[index]++ - } - } - } - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/DoubleClickPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/DoubleClickPattern.kt deleted file mode 100644 index 2a6ac89a202..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/DoubleClickPattern.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern.patterns - -import net.ccbluex.liquidbounce.utils.clicking.Clicker -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern - -/** - * Double-clicking is NOT a method but a button on a few cheating mice. - * This button is called the FIRE button and will result in two clicks when pressed once. - * - * This is a method that is not allowed on most servers and is considered cheating. - * Unlikely to bypass and will result in twice the CPS (!!!). - * - * @note In the past I had a mouse with this feature and I always used it. @1zuna - */ -object DoubleClickPattern : ClickPattern { - override fun fill( - clickArray: IntArray, - cps: IntRange, - clicker: Clicker<*> - ) { - val clicks = cps.random() - - repeat(clicks) { - // Increase random index inside click array by 1 - clickArray.indices.random().let { index -> - clickArray[index] += 2 - } - } - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/DragPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/DragPattern.kt deleted file mode 100644 index caa9cf2a192..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/DragPattern.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern.patterns - -import net.ccbluex.liquidbounce.utils.clicking.Clicker -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern - -/** - * Drag clicking is a method that is used to bypass the CPS limit of 20. - * - * It can be done by gliding your finger over the mouse button and causing friction - * to click very fast. - * - * Is not very easy to do as it requires a lot of practice and a good mouse, - * as well as a good grip on the mouse. Sweaty hands are a big no-no. - * - * This is very hard to implement as I am not able to do this method myself, - * so I will simply guess how it works. - */ -object DragPattern : ClickPattern { - override fun fill( - clickArray: IntArray, - cps: IntRange, - clicker: Clicker<*> - ) { - val clicks = cps.random() - - /** - * The travel time is the time it takes to move the finger - * from the top of the mouse to the bottom. - * - * After this travel time we need to move the finger back to the top and cannot click. - * This is more consistent usually. - */ - val travelTime = Clicker.RNG.nextInt(17, 19) - - // Fit the clicks into the travel time of the - while (clickArray.sum() < clicks) { - // Fill the travel time area in the click array with clicks - - // Get index with the lowest clicks on the click array - val index = clickArray.copyOf(travelTime).indices.minByOrNull { clickArray[it] }!! - - // Increase the click count at the index - clickArray[index]++ - } - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/EfficientPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/EfficientPattern.kt deleted file mode 100644 index 55ff1a2cd73..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/EfficientPattern.kt +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern.patterns - -import net.ccbluex.liquidbounce.utils.clicking.Clicker -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern - -/** - * Keeps at least one-tick interval between each click. - */ -object EfficientPattern : ClickPattern { - - override fun fill( - clickArray: IntArray, - cps: IntRange, - clicker: Clicker<*> - ) { - val clicks = cps.random() - - // Efficient will introduce wide gaps when the CPS is lower than half of the cycle length, - // so we will use StabilizedPattern instead. - if (clicks < 10) { - return StabilizedPattern.fill(clickArray, cps, clicker) - } - - for (i in 0 until clicks) { - clickArray[i * 2 % clickArray.size]++ - } - } - -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/NormalDistributionPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/NormalDistributionPattern.kt deleted file mode 100644 index b6b1c53fd66..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/NormalDistributionPattern.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern.patterns - -import net.ccbluex.liquidbounce.utils.clicking.Clicker -import net.ccbluex.liquidbounce.utils.clicking.Clicker.Companion.RNG -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern - -/** - * Normal distribution clicking pattern. - */ -object NormalDistributionPattern : ClickPattern { - - override fun fill( - clickArray: IntArray, - cps: IntRange, - clicker: Clicker<*> - ) { - data class Band(val top: Double, val mean: Double, val std: Double) - - val frequencyBands = arrayOf( - Band(10.0 / 110.0, 179.5242718446602, 20.416937885616676), - Band(0.0, 87.88, 13.420088130563776) - ) - - var t = 0.0 - - while (true) { - val v = RNG.nextDouble() - - val band = frequencyBands.first { v >= it.top } - - t += RNG.nextGaussian(band.mean, band.std) * 20.0 / 1000.0 - - // Second is over - if (t > 20.0) { - break - } - - clickArray[t.toInt()]++ - } - } - -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/SpammingPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/SpammingPattern.kt deleted file mode 100644 index b5fc3244412..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/SpammingPattern.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern.patterns - -import net.ccbluex.liquidbounce.utils.clicking.Clicker -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern - -/** - * Normal clicking is the most common clicking method and usually - * results in a CPS of 5-8 and sometimes when aggressive 10-12. - * - * - * - * It is when clicking normally with your finger. - * - * @note I was not able to press faster than 8 CPS. @1zuna - */ -object SpammingPattern : ClickPattern { - override fun fill( - clickArray: IntArray, - cps: IntRange, - clicker: Clicker<*> - ) { - val clicks = cps.random() - - repeat(clicks) { - // Increase random index inside click array by 1 - clickArray.indices.random().let { index -> - clickArray[index]++ - } - } - } -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/StabilizedPattern.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/StabilizedPattern.kt deleted file mode 100644 index 9837773d563..00000000000 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/pattern/patterns/StabilizedPattern.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce) - * - * Copyright (c) 2015 - 2026 CCBlueX - * - * LiquidBounce 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. - * - * LiquidBounce 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 LiquidBounce. If not, see . - */ -package net.ccbluex.liquidbounce.utils.clicking.pattern.patterns - -import net.ccbluex.liquidbounce.utils.clicking.Clicker -import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern -import kotlin.math.max - -/** - * Normal clicking but with a stabilized click cycle. - */ -object StabilizedPattern : ClickPattern { - - override fun fill( - clickArray: IntArray, - cps: IntRange, - clicker: Clicker<*> - ) { - val clicks = cps.random() - - // Calculate the interval and distribute the remainder to spread evenly - val interval = if (clicks > 0) clickArray.size / clicks else 0 - var remainder = if (clicks > 0) clickArray.size % clicks else 0 - - var currentIndex = 0 - - repeat(clicks) { - clickArray[currentIndex % clickArray.size]++ - currentIndex += max(interval, 1) - if (remainder > 0) { - currentIndex++ - remainder-- - } - } - } - -} diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/utils/input/InputTracker.kt b/src/main/kotlin/net/ccbluex/liquidbounce/utils/input/InputTracker.kt index 41b1bf4cb40..48645f53fb4 100644 --- a/src/main/kotlin/net/ccbluex/liquidbounce/utils/input/InputTracker.kt +++ b/src/main/kotlin/net/ccbluex/liquidbounce/utils/input/InputTracker.kt @@ -21,37 +21,39 @@ package net.ccbluex.liquidbounce.utils.input import com.mojang.blaze3d.platform.InputConstants import net.ccbluex.liquidbounce.event.EventListener +import net.ccbluex.liquidbounce.event.events.GameTickEvent +import net.ccbluex.liquidbounce.event.events.KeyboardKeyEvent import net.ccbluex.liquidbounce.event.events.MouseButtonEvent import net.ccbluex.liquidbounce.event.handler import net.minecraft.client.KeyMapping import org.lwjgl.glfw.GLFW /** - * Singleton object that tracks the state of mouse buttons and key presses. - * It listens for mouse button events and provides utility functions to check if - * a key or mouse button is currently pressed or was recently pressed. + * Tracking if an input constant is pressed, released or repeated, + * and when it was last pressed. Also tracks how long an input has been pressed repeatedly. */ object InputTracker : EventListener { + private const val COMBO_TIMEOUT_MS = 250L + /** * Tracks the state of each mouse button. * * [GLFW.GLFW_RELEASE], [GLFW.GLFW_PRESS] or [GLFW.GLFW_REPEAT] * @see GLFW */ - private val mouseStates = IntArray(32) + private val inputStates = mutableMapOf() /** - * Tracks the last time each mouse button was pressed. - * Array indices correspond to GLFW mouse button codes. + * Tracks the last time an input was pressed. + * Map key is the [InputConstants.Key] with value being the last time it was pressed in milliseconds. */ - private val mouseLastPressed = LongArray(32) + private val inputLastPressed = mutableMapOf() /** - * Tracks the last time each keyboard key was pressed. - * Map key is the GLFW key code, value is the timestamp. + * Tracks how long we have been pressing a key continuously. */ - private val keyLastPressed = mutableMapOf() + private val inputComboStartTime = mutableMapOf() /** * Extension property that checks if a key binding is pressed on either the keyboard or mouse. @@ -76,7 +78,7 @@ object InputTracker : EventListener { * @return True if the mouse button is pressed, false otherwise. */ val KeyMapping.pressedOnMouse: Boolean - get() = this.key.type == InputConstants.Type.MOUSE && isMouseButtonPressed(this.key.value) + get() = this.key.type == InputConstants.Type.MOUSE && isMouseButtonPressed(this.key) /** * Extension property that checks if a key binding was pressed recently. @@ -84,13 +86,7 @@ object InputTracker : EventListener { * @param withinMs The time window in milliseconds to check within. * @return True if the key binding was pressed within the specified time, false otherwise. */ - fun KeyMapping.wasPressedRecently(withinMs: Long): Boolean { - return when (this.key.type) { - InputConstants.Type.KEYSYM -> wasKeyPressedRecently(this.key.value, withinMs) - InputConstants.Type.MOUSE -> wasMouseButtonPressedRecently(this.key.value, withinMs) - else -> false - } - } + fun KeyMapping.wasPressedRecently(withinMs: Long) = wasInputPressedRecently(this.key, withinMs) /** * Extension property that gets the time elapsed since the key binding was last pressed. @@ -98,13 +94,15 @@ object InputTracker : EventListener { * @return Milliseconds since last press, or Long.MAX_VALUE if never pressed. */ val KeyMapping.timeSinceLastPress: Long - get() { - return when (this.key.type) { - InputConstants.Type.KEYSYM -> getTimeSinceKeyPress(this.key.value) - InputConstants.Type.MOUSE -> getTimeSinceMousePress(this.key.value) - else -> Long.MAX_VALUE - } - } + get() = getTimeSinceInputPress(this.key) + + /** + * Extension property that gets the time elapsed since the key binding combo started. + * + * @return Milliseconds since combo start, or Long.MAX_VALUE if no combo is tracked. + */ + val KeyMapping.timeSinceComboStart: Long + get() = getTimeSinceComboStart(this.key) /** * Event handler for mouse button actions. It updates the mouseStates map @@ -112,11 +110,27 @@ object InputTracker : EventListener { */ @Suppress("unused") private val handleMouseAction = handler { event -> - mouseStates[event.button] = event.action + inputStates[event.key] = event.action // Track when the button was pressed if (event.action == GLFW.GLFW_PRESS) { - mouseLastPressed[event.button] = System.currentTimeMillis() + updateInputPress(event.key) + } + } + + @Suppress("unused") + private val handleKeyAction = handler { event -> + if (event.action == GLFW.GLFW_PRESS) { + updateInputPress(event.key) + } + } + + @Suppress("unused") + private val handleTick = handler { + val now = System.currentTimeMillis() + inputComboStartTime.entries.removeIf { entry -> + val lastPressed = inputLastPressed[entry.key] + lastPressed == null || now - lastPressed > COMBO_TIMEOUT_MS } } @@ -126,7 +140,7 @@ object InputTracker : EventListener { * @param button The GLFW code of the mouse button. * @return True if the mouse button is pressed, false otherwise. */ - fun isMouseButtonPressed(button: Int): Boolean = mouseStates[button] == GLFW.GLFW_PRESS + fun isMouseButtonPressed(button: InputConstants.Key): Boolean = inputStates[button] == GLFW.GLFW_PRESS /** * Checks if the specified mouse button was pressed recently. @@ -135,19 +149,19 @@ object InputTracker : EventListener { * @param withinMs The time window in milliseconds to check within. * @return True if the mouse button was pressed within the specified time, false otherwise. */ - fun wasMouseButtonPressedRecently(button: Int, withinMs: Long): Boolean { - val lastPressed = mouseLastPressed[button] + fun wasInputPressedRecently(keyCode: InputConstants.Key, withinMs: Long): Boolean { + val lastPressed = inputLastPressed[keyCode] ?: return false return lastPressed > 0 && (System.currentTimeMillis() - lastPressed) <= withinMs } /** - * Gets the time elapsed since the specified mouse button was last pressed. + * Gets the time elapsed since the specified input was last pressed. * - * @param button The GLFW code of the mouse button. + * @param keyCode The GLFW key code. * @return Milliseconds since last press, or Long.MAX_VALUE if never pressed. */ - fun getTimeSinceMousePress(button: Int): Long { - val lastPressed = mouseLastPressed[button] + fun getTimeSinceInputPress(keyCode: InputConstants.Key): Long { + val lastPressed = inputLastPressed[keyCode] ?: return Long.MAX_VALUE return if (lastPressed > 0) { System.currentTimeMillis() - lastPressed } else { @@ -156,31 +170,32 @@ object InputTracker : EventListener { } /** - * Checks if the specified keyboard key was pressed recently. - * Note: This requires manual tracking via updateKeyPress() since we don't have a keyboard event handler. + * Gets the time elapsed since the specified input combo started. * * @param keyCode The GLFW key code. - * @param withinMs The time window in milliseconds to check within. - * @return True if the key was pressed within the specified time, false otherwise. + * @return Milliseconds since combo start, or Long.MAX_VALUE if no combo is tracked. */ - fun wasKeyPressedRecently(keyCode: Int, withinMs: Long): Boolean { - val lastPressed = keyLastPressed[keyCode] ?: return false - return (System.currentTimeMillis() - lastPressed) <= withinMs + fun getTimeSinceComboStart(keyCode: InputConstants.Key): Long { + val comboStart = inputComboStartTime[keyCode] ?: return Long.MAX_VALUE + return if (comboStart > 0) { + System.currentTimeMillis() - comboStart + } else { + Long.MAX_VALUE + } } /** - * Gets the time elapsed since the specified keyboard key was last pressed. - * - * @param keyCode The GLFW key code. - * @return Milliseconds since last press, or Long.MAX_VALUE if never pressed. + * Updates the last pressed time for the specified input. */ - fun getTimeSinceKeyPress(keyCode: Int): Long { - val lastPressed = keyLastPressed[keyCode] - return if (lastPressed != null) { - System.currentTimeMillis() - lastPressed - } else { - Long.MAX_VALUE + fun updateInputPress(keyCode: InputConstants.Key) { + inputLastPressed[keyCode] = System.currentTimeMillis() + if (!inputComboStartTime.containsKey(keyCode)) { + inputComboStartTime[keyCode] = System.currentTimeMillis() } } + fun resetCombo(keyCode: InputConstants.Key) { + inputComboStartTime.remove(keyCode) + } + }