Skip to content

Commit 5ebea95

Browse files
1zun4MukjepScarlet
authored andcommitted
feat(Clicker): global input tracking and fatigue
Removes all patterns.
1 parent 081c307 commit 5ebea95

11 files changed

Lines changed: 188 additions & 656 deletions

File tree

src/main/kotlin/net/ccbluex/liquidbounce/utils/clicking/Clicker.kt

Lines changed: 122 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -18,30 +18,24 @@
1818
*/
1919
package net.ccbluex.liquidbounce.utils.clicking
2020

21+
import net.ccbluex.liquidbounce.config.types.CurveValue.Axis.Companion.axis
2122
import net.ccbluex.liquidbounce.config.types.Value
2223
import net.ccbluex.liquidbounce.config.types.group.ValueGroup
23-
import net.ccbluex.liquidbounce.config.types.list.Tagged
2424
import net.ccbluex.liquidbounce.event.EventListener
2525
import net.ccbluex.liquidbounce.event.events.GameTickEvent
2626
import net.ccbluex.liquidbounce.event.events.KeybindIsPressedEvent
2727
import net.ccbluex.liquidbounce.event.handler
2828
import net.ccbluex.liquidbounce.features.module.modules.render.ModuleDebug.debugParameter
29-
import net.ccbluex.liquidbounce.utils.clicking.pattern.ClickPattern
30-
import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.ButterflyPattern
31-
import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.DoubleClickPattern
32-
import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.DragPattern
33-
import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.EfficientPattern
34-
import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.NormalDistributionPattern
35-
import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.SpammingPattern
36-
import net.ccbluex.liquidbounce.utils.clicking.pattern.patterns.StabilizedPattern
3729
import net.ccbluex.liquidbounce.utils.client.mc
38-
import net.ccbluex.liquidbounce.utils.client.player
39-
import net.ccbluex.liquidbounce.utils.entity.hasCooldown
30+
import net.ccbluex.liquidbounce.utils.client.vector2f
31+
import net.ccbluex.liquidbounce.utils.input.InputTracker
32+
import net.ccbluex.liquidbounce.utils.input.InputTracker.timeSinceComboStart
33+
import net.ccbluex.liquidbounce.utils.input.InputTracker.timeSinceLastPress
34+
import net.ccbluex.liquidbounce.utils.input.InputTracker.updateInputPress
4035
import net.ccbluex.liquidbounce.utils.kotlin.EventPriorityConvention
4136
import net.minecraft.client.KeyMapping
4237
import net.minecraft.client.Minecraft
43-
import java.util.Arrays
44-
import java.util.Random
38+
import kotlin.math.roundToInt
4539

4640
/**
4741
* An attack scheduler
@@ -62,26 +56,41 @@ open class Clicker<T>(
6256
val itemCooldown: ItemCooldown? = ItemCooldown(),
6357
maxCps: Int = 60,
6458
name: String = "Clicker"
65-
) : ValueGroup(name, aliases = listOf("ClickScheduler")), EventListener where T : EventListener {
59+
) : ValueGroup(name), EventListener where T : EventListener {
6660

6761
companion object {
68-
internal val RNG = Random()
69-
private const val DEFAULT_CYCLE_LENGTH = 20
70-
private var lastClickTime = 0L
71-
private val lastClickPassed
72-
get() = System.currentTimeMillis() - lastClickTime
62+
private const val TICK_DURATION_MS = 50L
63+
private const val TICKS_IN_A_SECOND = 20
64+
private const val DEFAULT_CYCLE_LENGTH_MS = (TICKS_IN_A_SECOND * TICK_DURATION_MS).toInt()
65+
private const val DEFAULT_CURVE_WINDOW_SECONDS = 10f
7366
}
7467

75-
// Options
76-
private val cps by intRange("CPS", 5..8, 1..maxCps, "clicks")
77-
.onChanged {
78-
fill()
79-
}
68+
private val cps: IntRange by intRange("CPS", 11..14, 1..maxCps, "clicks").onChanged {
69+
currentCps = cps.random()
70+
nextClickDelayMs = sampleIntervalMs()
71+
}
8072

81-
private val pattern by enumChoice("Technique", ClickPatterns.STABILIZED)
82-
.onChanged {
83-
fill()
73+
private val fatigue = curve(
74+
"Fatigue",
75+
mutableListOf(
76+
0f vector2f 2f,
77+
DEFAULT_CURVE_WINDOW_SECONDS / 2 vector2f -2f,
78+
DEFAULT_CURVE_WINDOW_SECONDS vector2f 1f,
79+
),
80+
xAxis = "Seconds" axis 0f..DEFAULT_CURVE_WINDOW_SECONDS,
81+
yAxis = "CPS" axis -5f..5f,
82+
).apply {
83+
onChanged {
84+
nextClickDelayMs = sampleIntervalMs()
8485
}
86+
}
87+
88+
/**
89+
* When we break the combo, we reset our fatigue. If set to 0, we stay in the combo indefinitely.
90+
*/
91+
private val breakCombo by intRange("BreakCombo", 0..0, 0..20, "ticks")
92+
93+
private var pauseTicks = 0
8594

8695
init {
8796
itemCooldown?.let(this::tree)
@@ -94,146 +103,132 @@ open class Clicker<T>(
94103
* This is useful for anti-cheats that detect if you are ignoring this cooldown.
95104
* Applies to the FailSwing feature as well.
96105
*/
97-
private val attackCooldown: Value<Boolean>? = if (keyBinding == mc.options.keyAttack) {
98-
boolean("AttackCooldown", true)
106+
private val missCooldown: Value<Boolean>? = if (keyBinding == mc.options.keyAttack) {
107+
boolean("MissCooldown", true)
99108
} else {
100109
null
101110
}
102111

103-
private val passesAttackCooldown
104-
get() = !(attackCooldown?.get() == true && mc.missTime > 0)
112+
private val passesMissCooldown
113+
get() = !(missCooldown?.get() == true && mc.missTime > 0)
105114

106-
private val clickArray = RollingClickArray(DEFAULT_CYCLE_LENGTH, 2)
107-
108-
init {
109-
fill()
110-
}
115+
/**
116+
* With each combo, we start with a random CPS value.
117+
* This allows us to have a different CPS value for each combo.
118+
*/
119+
private var currentCps = cps.random()
120+
private var nextClickDelayMs = sampleIntervalMs()
111121

112122
// Clicks that were executed by [click] in the current tick
113123
var clickAmount: Int? = null
114124
private set
115125

126+
// todo: find better name
116127
val isClickTick: Boolean
117-
get() = willClickAt(0)
118-
119-
val ticksUntilClick: Int
120-
get() {
121-
for (i in 0 until clickArray.iterations) {
122-
if (willClickAt(i)) {
123-
return i
124-
}
125-
}
128+
get() = timeUntilNextClickMs(0) <= 0
126129

127-
return clickArray.iterations
128-
}
130+
// todo: find better name
131+
fun willClickAt(tick: Int = 1) = timeUntilNextClickMs(tick) <= 0
129132

130-
fun willClickAt(tick: Int = 1) = getClickAmount(tick) > 0
133+
/**
134+
* Clicks on a curve-driven schedule per tick. If the cooldown is not passed, it will not click.
135+
* [block] should return true if the click was successful. Otherwise, it will not count as a click.
136+
*/
137+
fun click(block: () -> Boolean): Boolean {
138+
debugParameter("Current CPS") { currentCps }
139+
debugParameter("Time Since Last Press") { keyBinding.timeSinceLastPress }
140+
debugParameter("Time Since Combo Start") { keyBinding.timeSinceComboStart }
141+
debugParameter("Time Until Next Click") { timeUntilNextClickMs(1) }
142+
debugParameter("Miss Cooldown") { mc.missTime }
143+
debugParameter("Item Cooldown") { itemCooldown?.cooldownProgress() ?: 0.0f }
131144

132-
fun getClickAmount(tick: Int = 0): Int {
133-
if (isEnforcedClick()) {
134-
return 1
135-
}
136-
return clickArray.get(tick)
137-
}
145+
var clicks = 0
146+
// todo: make double clicking work
147+
while (timeUntilNextClickMs(1) <= 0) {
148+
if (!passesMissCooldown) {
149+
break
150+
}
138151

139-
private fun isEnforcedClick(tick: Int = 0): Boolean {
140-
val hasCooldown = player.hasCooldown
141-
debugParameter("HasCooldown") { hasCooldown }
142-
if (hasCooldown && itemCooldown?.isCooldownPassed(tick) == true) {
143-
return true
152+
if (itemCooldown?.isCooldownPassed() == false) {
153+
break
154+
}
155+
156+
if (block()) {
157+
itemCooldown?.newCooldown()
158+
updateInputPress(keyBinding.key)
159+
nextClickDelayMs = sampleIntervalMs()
160+
clicks++
161+
} else {
162+
break
163+
}
144164
}
145165

146-
return lastClickPassed + (tick * 50L) >= 1000L
166+
this.clickAmount = clicks
167+
debugParameter("Next Click Delay") { nextClickDelayMs }
168+
debugParameter("Current Clicks") { clicks }
169+
170+
return clicks > 0
147171
}
148172

149173
@Suppress("unused")
150174
private val keybindIsPressedHandler = handler<KeybindIsPressedEvent> { event ->
151175
val clickAmount = this.clickAmount ?: return@handler
152176

153-
// It turns out, we only want to do this with [attackKey], otherwise
177+
// It turns out we only want to do this with [attackKey]; otherwise
154178
// [useKey] will do unexpected things.
155179
if (keyBinding == mc.options.keyAttack && event.keyBinding == keyBinding) {
156-
// We want to simulate the click in order to
157-
// allow the game to handle the logic as if we clicked
180+
// We want to simulate the click to allow the game to handle the logic as if we clicked.
158181
event.isPressed = clickAmount > 0
159182
}
160183
}
161184

162-
/**
163-
* Clicks [cps] times per call (tick). If the cooldown is not passed, it will not click.
164-
* [block] should return true if the click was successful. Otherwise, it will not count as a click.
165-
*/
166-
fun click(block: () -> Boolean) {
167-
val clicks = getClickAmount()
168-
169-
debugParameter("Current Clicks") { clicks }
170-
debugParameter("Peek Clicks") { clickArray.get(1) }
171-
debugParameter("Last Click Passed") { lastClickPassed }
172-
debugParameter("Attack Cooldown") { mc.missTime }
173-
debugParameter("Item Cooldown") { itemCooldown?.cooldownProgress() ?: 0.0f }
174-
175-
var clickAmount = 0
176-
177-
repeat(clicks) {
178-
if (!passesAttackCooldown) {
179-
return@repeat
180-
}
185+
@Suppress("unused")
186+
private val gameHandler = handler<GameTickEvent>(priority = EventPriorityConvention.FIRST_PRIORITY) {
187+
clickAmount = null
181188

182-
if (itemCooldown?.isCooldownPassed() != false && block()) {
183-
clickAmount++
184-
itemCooldown?.newCooldown()
185-
lastClickTime = System.currentTimeMillis()
186-
}
189+
if (pauseTicks > 0) {
190+
pauseTicks--
187191
}
188-
189-
this.clickAmount = clickAmount
190192
}
191193

192-
@Suppress("unused")
193-
private val gameHandler = handler<GameTickEvent>(
194-
priority = EventPriorityConvention.FIRST_PRIORITY
195-
) {
196-
clickAmount = null
194+
override fun parent() = parent
197195

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

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

212-
private fun fill() {
213-
clickArray.clear()
214-
val cycleArray = IntArray(DEFAULT_CYCLE_LENGTH)
215-
repeat(clickArray.iterations) {
216-
Arrays.fill(cycleArray, 0)
217-
pattern.pattern.fill(cycleArray, cps, this)
218-
clickArray.push(cycleArray)
219-
clickArray.advance(DEFAULT_CYCLE_LENGTH)
209+
private fun sampleIntervalMs(): Int {
210+
var comboMs = keyBinding.timeSinceComboStart
211+
if (comboMs == Long.MAX_VALUE) {
212+
currentCps = cps.random()
213+
comboMs = 0L
220214
}
221-
}
222215

223-
override fun parent() = parent
216+
val maxComboSeconds = fatigue.xAxis.range.endInclusive
217+
val elapsedSeconds = comboMs / 1000f
224218

225-
@Suppress("unused")
226-
enum class ClickPatterns(
227-
override val tag: String,
228-
val pattern: ClickPattern
229-
) : Tagged {
230-
STABILIZED("Stabilized", StabilizedPattern),
231-
EFFICIENT("Efficient", EfficientPattern),
232-
SPAMMING("Spamming", SpammingPattern),
233-
DOUBLE_CLICK("DoubleClick", DoubleClickPattern),
234-
DRAG("Drag", DragPattern),
235-
BUTTERFLY("Butterfly", ButterflyPattern),
236-
NORMAL_DISTRIBUTION("NormalDistribution", NormalDistributionPattern);
219+
// If we exceeded the max combo, we break the combo and reset fatigue.
220+
if (elapsedSeconds > maxComboSeconds) {
221+
pauseTicks = breakCombo.random()
222+
if (pauseTicks > 0) {
223+
InputTracker.resetCombo(keyBinding.key)
224+
currentCps = cps.random()
225+
}
226+
}
227+
228+
val cpsValue = (currentCps + fatigue.transform(elapsedSeconds.coerceIn(0f, maxComboSeconds)))
229+
.coerceAtLeast(1f)
230+
val intervalMs = 1000f / cpsValue
231+
return intervalMs.roundToInt().coerceIn(1, DEFAULT_CYCLE_LENGTH_MS)
237232
}
238233

239234
}

0 commit comments

Comments
 (0)