Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
249 changes: 122 additions & 127 deletions src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/Clicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,24 @@
*/
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.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
Expand All @@ -62,26 +56,41 @@ open class Clicker<T>(
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
}

// 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)
Expand All @@ -94,146 +103,132 @@ open class Clicker<T>(
* 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<Boolean>? = if (keyBinding == mc.options.keyAttack) {
boolean("AttackCooldown", true)
private val missCooldown: Value<Boolean>? = 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)

init {
fill()
}
/**
* 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()

// 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(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 Press") { keyBinding.timeSinceLastPress }
debugParameter("Time Since Combo Start") { keyBinding.timeSinceComboStart }
debugParameter("Time Until Next Click") { timeUntilNextClickMs(1) }
debugParameter("Miss Cooldown") { mc.missTime }
debugParameter("Item Cooldown") { itemCooldown?.cooldownProgress() ?: 0.0f }

fun getClickAmount(tick: Int = 0): Int {
if (isEnforcedClick()) {
return 1
}
return clickArray.get(tick)
}
var clicks = 0
// todo: make double clicking work
while (timeUntilNextClickMs(1) <= 0) {
if (!passesMissCooldown) {
break
}

private fun isEnforcedClick(tick: Int = 0): Boolean {
val hasCooldown = player.hasCooldown
debugParameter("HasCooldown") { hasCooldown }
if (hasCooldown && itemCooldown?.isCooldownPassed(tick) == true) {
return true
if (itemCooldown?.isCooldownPassed() == false) {
break
}

if (block()) {
itemCooldown?.newCooldown()
updateInputPress(keyBinding.key)
nextClickDelayMs = sampleIntervalMs()
clicks++
} else {
break
}
}

return lastClickPassed + (tick * 50L) >= 1000L
this.clickAmount = clicks
debugParameter("Next Click Delay") { nextClickDelayMs }
debugParameter("Current Clicks") { clicks }

return clicks > 0
}

@Suppress("unused")
private val keybindIsPressedHandler = handler<KeybindIsPressedEvent> { 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
}
@Suppress("unused")
private val gameHandler = handler<GameTickEvent>(priority = EventPriorityConvention.FIRST_PRIORITY) {
clickAmount = null

if (itemCooldown?.isCooldownPassed() != false && block()) {
clickAmount++
itemCooldown?.newCooldown()
lastClickTime = System.currentTimeMillis()
}
if (pauseTicks > 0) {
pauseTicks--
}

this.clickAmount = clickAmount
}

@Suppress("unused")
private val gameHandler = handler<GameTickEvent>(
priority = EventPriorityConvention.FIRST_PRIORITY
) {
clickAmount = null
override fun parent() = parent

if (clickArray.advance()) {
val cycleArray = IntArray(DEFAULT_CYCLE_LENGTH)
pattern.pattern.fill(cycleArray, cps, this)
clickArray.push(cycleArray)
}
private fun timeUntilNextClickMs(tickOffset: Int): Long {
val timeSince = timeSinceLastClickMs()
val offsetMs = tickOffset * TICK_DURATION_MS
val baseRemainingMs = nextClickDelayMs.toLong() - (timeSince + offsetMs)
val pauseRemainingMs = (pauseTicks - tickOffset).coerceAtLeast(0) * TICK_DURATION_MS
return if (pauseRemainingMs > 0) pauseRemainingMs else baseRemainingMs
}

debugParameter("Click Technique") { pattern.tag }
debugParameter("Click Array") {
clickArray.array.withIndex().joinToString { (i, v) ->
if (i == clickArray.head) "*$v" else v.toString()
}
}
private fun timeSinceLastClickMs(): Long {
val timeSince = keyBinding.timeSinceLastPress
return if (timeSince == Long.MAX_VALUE) DEFAULT_CYCLE_LENGTH_MS.toLong() else timeSince
}

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 sampleIntervalMs(): Int {
var comboMs = keyBinding.timeSinceComboStart
if (comboMs == Long.MAX_VALUE) {
currentCps = cps.random()
comboMs = 0L
}
}

override fun parent() = parent
val maxComboSeconds = fatigue.xAxis.range.endInclusive
val elapsedSeconds = comboMs / 1000f

@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);
// 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)
}

}
Loading