1818 */
1919package net.ccbluex.liquidbounce.utils.clicking
2020
21+ import net.ccbluex.liquidbounce.config.types.CurveValue.Axis.Companion.axis
2122import net.ccbluex.liquidbounce.config.types.Value
2223import net.ccbluex.liquidbounce.config.types.group.ValueGroup
23- import net.ccbluex.liquidbounce.config.types.list.Tagged
2424import net.ccbluex.liquidbounce.event.EventListener
2525import net.ccbluex.liquidbounce.event.events.GameTickEvent
2626import net.ccbluex.liquidbounce.event.events.KeybindIsPressedEvent
2727import net.ccbluex.liquidbounce.event.handler
2828import 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
3729import 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
4035import net.ccbluex.liquidbounce.utils.kotlin.EventPriorityConvention
4136import net.minecraft.client.KeyMapping
4237import 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