Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(legacy): Async tick support #5693

Open
wants to merge 6 commits into
base: legacy
Choose a base branch
from
Open
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
2 changes: 0 additions & 2 deletions src/main/java/net/ccbluex/liquidbounce/LiquidBounce.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ import net.ccbluex.liquidbounce.utils.render.MiniMapRegister
import net.ccbluex.liquidbounce.utils.render.shader.Background
import net.ccbluex.liquidbounce.utils.rotation.RotationUtils
import net.ccbluex.liquidbounce.utils.timing.TickedActions
import net.ccbluex.liquidbounce.utils.timing.WaitMsUtils
import net.ccbluex.liquidbounce.utils.timing.WaitTickUtils
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Future
Expand Down Expand Up @@ -181,7 +180,6 @@ object LiquidBounce {
BPSUtils
WaitTickUtils
SilentHotbar
WaitMsUtils
BlinkUtils

// Load settings
Expand Down
23 changes: 20 additions & 3 deletions src/main/java/net/ccbluex/liquidbounce/event/EventHook.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,33 @@ sealed class EventHook<T : Event>(
val always: Boolean,
val priority: Byte,
) {
val isActive: Boolean
get() = this.owner.handleEvents() || this.always

class Blocking<T : Event>(
owner: Listenable,
always: Boolean = false,
priority: Byte = 0,
val action: (T) -> Unit
) : EventHook<T>(owner, always, priority)

class Terminate<T : Event>(
owner: Listenable,
always: Boolean = false,
priority: Byte = 0,
maxExecutionTime: Int = 1,
val action: (T) -> Unit
) : EventHook<T>(owner, always, priority) {
init {
require(maxExecutionTime > 0)
}

var remaining = maxExecutionTime
private set

fun shouldStop(): Boolean = !isActive || remaining-- > 0
}

class Async<T : Event>(
owner: Listenable,
/**
Expand All @@ -32,6 +52,3 @@ sealed class EventHook<T : Event>(
val action: suspend CoroutineScope.(T) -> Unit
) : EventHook<T>(owner, always, priority)
}

val EventHook<*>.isActive: Boolean
get() = this.owner.handleEvents() || this.always
95 changes: 48 additions & 47 deletions src/main/java/net/ccbluex/liquidbounce/event/EventManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import net.ccbluex.liquidbounce.event.async.LoopManager
import net.ccbluex.liquidbounce.event.async.TickScheduler
import net.ccbluex.liquidbounce.event.async.waitTicks
import net.ccbluex.liquidbounce.utils.client.ClientUtils
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.coroutines.cancellation.CancellationException

/**
Expand Down Expand Up @@ -40,15 +45,44 @@ private fun List<EventHook<*>>.findIndexByPriority(item: EventHook<*>): Int {
*/
object EventManager : CoroutineScope by CoroutineScope(SupervisorJob()) {
private val registry = ALL_EVENT_CLASSES.associateWithTo(IdentityHashMap(ALL_EVENT_CLASSES.size)) {
ArrayList<EventHook<in Event>>()
CopyOnWriteArrayList<EventHook<in Event>>()
}

private class AsyncTask(val owner: EventHook<*>, val job: Job)

private val jobs = CopyOnWriteArrayList<AsyncTask>()

init {
LoopManager
TickScheduler

LoopManager.loopHandler {
jobs.removeIf { !it.owner.isActive || !it.job.isActive }
waitTicks(1)
}
}

fun Listenable.cancelAsyncJobs() {
jobs.removeIf {
if (it.owner.owner === this) {
it.job.cancel()
true
} else {
false
}
}
}

fun <T : Event> unregisterEventHook(eventClass: Class<out T>, eventHook: EventHook<in T>) {
registry[eventClass]!!.remove(eventHook)
jobs.removeIf {
if (it.owner === eventHook) {
it.job.cancel()
true
} else {
false
}
}
}

fun <T : Event> registerEventHook(eventClass: Class<out T>, eventHook: EventHook<T>): EventHook<T> {
Expand Down Expand Up @@ -85,14 +119,26 @@ object EventManager : CoroutineScope by CoroutineScope(SupervisorJob()) {
}
}

is EventHook.Terminate -> {
try {
action(event)
} catch (e: Exception) {
ClientUtils.LOGGER.error("Exception during call event (terminate, remaining=${this.remaining})", e)
}
if (this.shouldStop()) {
unregisterEventHook(event::class.java, this)
}
}

is EventHook.Async -> {
launch(dispatcher) {
val job = launch(dispatcher) {
try {
action(this, event)
} catch (e: Exception) {
ClientUtils.LOGGER.error("Exception during call event (async)", e)
}
}
jobs += AsyncTask(this, job)
}
}
}
Expand Down Expand Up @@ -120,48 +166,3 @@ object EventManager : CoroutineScope by CoroutineScope(SupervisorJob()) {
}

}

/**
* This manager will start a job for each hook.
*
* Once the job is finished, the next [UpdateEvent] (any stateless event is OK for this) will start a new one.
*
* This is designed to run **asynchronous** tasks instead of tick loops.
*
* @author MukjepScarlet
*/
internal object LoopManager : Listenable, CoroutineScope by CoroutineScope(SupervisorJob()) {
private val registry = IdentityHashMap<EventHook.Async<UpdateEvent>, Job?>()

operator fun plusAssign(eventHook: EventHook.Async<UpdateEvent>) {
registry[eventHook] = null
}

operator fun minusAssign(eventHook: EventHook.Async<UpdateEvent>) {
registry.remove(eventHook)
}

init {
handler<UpdateEvent>(priority = Byte.MAX_VALUE) {
for ((eventHook, job) in registry) {
if (eventHook.isActive) {
if (job == null || !job.isActive) {
registry[eventHook] = launch(eventHook.dispatcher) {
try {
eventHook.action(this, UpdateEvent)
} catch (e: CancellationException) {
// The job is canceled due to handler is no longer active
return@launch
} catch (e: Exception) {
ClientUtils.LOGGER.error("Exception during loop in", e)
}
}
}
} else if (job != null) {
job.cancel()
registry[eventHook] = null
}
}
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/net/ccbluex/liquidbounce/event/Listenable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package net.ccbluex.liquidbounce.event
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import net.ccbluex.liquidbounce.event.async.LoopManager

interface Listenable {
fun handleEvents(): Boolean = parent?.handleEvents() ?: true
Expand All @@ -32,6 +33,21 @@ inline fun <reified T : Event> Listenable.handler(
EventManager.registerEventHook(T::class.java, EventHook.Blocking(this, always, priority, action))
}

inline fun <reified T : Event> Listenable.terminateHandler(
always: Boolean = false,
priority: Byte = 0,
maxExecutionTime: Int,
noinline action: (T) -> Unit
) {
EventManager.registerEventHook(T::class.java, EventHook.Terminate(this, always, priority, maxExecutionTime, action))
}

inline fun <reified T : Event> Listenable.once(
always: Boolean = false,
priority: Byte = 0,
noinline action: (T) -> Unit
) = terminateHandler(always, priority, maxExecutionTime = 1, action)

inline fun <reified T : Event> Listenable.handler(
dispatcher: CoroutineDispatcher,
always: Boolean = false,
Expand Down
57 changes: 57 additions & 0 deletions src/main/java/net/ccbluex/liquidbounce/event/async/LoopManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* LiquidBounce Hacked Client
* A free open source mixin-based injection hacked client for Minecraft using Minecraft Forge.
* https://github.com/CCBlueX/LiquidBounce/
*/
package net.ccbluex.liquidbounce.event.async

import kotlinx.coroutines.*
import net.ccbluex.liquidbounce.event.*

import net.ccbluex.liquidbounce.utils.client.ClientUtils
import java.util.*

/**
* This manager will start a job for each hook.
*
* Once the job is finished, the next [UpdateEvent] (any stateless event is OK for this) will start a new one.
*
* This is designed to run **asynchronous** tasks instead of tick loops.
*
* @author MukjepScarlet
*/
internal object LoopManager : Listenable, CoroutineScope by CoroutineScope(SupervisorJob()) {
private val registry = IdentityHashMap<EventHook.Async<UpdateEvent>, Job?>()

operator fun plusAssign(eventHook: EventHook.Async<UpdateEvent>) {
registry[eventHook] = null
}

operator fun minusAssign(eventHook: EventHook.Async<UpdateEvent>) {
registry.remove(eventHook)
}

init {
handler<UpdateEvent>(priority = Byte.MAX_VALUE) {
for ((eventHook, job) in registry) {
if (eventHook.isActive) {
if (job == null || !job.isActive) {
registry[eventHook] = launch(eventHook.dispatcher) {
try {
eventHook.action(this, UpdateEvent)
} catch (e: CancellationException) {
// The job is canceled due to handler is no longer active
return@launch
} catch (e: Exception) {
ClientUtils.LOGGER.error("Exception during loop in", e)
}
}
}
} else if (job != null) {
job.cancel()
registry[eventHook] = null
}
}
}
}
}
115 changes: 115 additions & 0 deletions src/main/java/net/ccbluex/liquidbounce/event/async/TickScheduler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* LiquidBounce Hacked Client
* A free open source mixin-based injection hacked client for Minecraft using Minecraft Forge.
* https://github.com/CCBlueX/LiquidBounce/
*/
package net.ccbluex.liquidbounce.event.async

import kotlinx.coroutines.*
import net.ccbluex.liquidbounce.event.GameTickEvent
import net.ccbluex.liquidbounce.event.Listenable
import net.ccbluex.liquidbounce.event.PacketEvent
import net.ccbluex.liquidbounce.event.handler
import net.ccbluex.liquidbounce.utils.client.MinecraftInstance
import java.util.function.BooleanSupplier
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.RestrictsSuspension
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
* This manager is for suspend tick functions.
*
* **ANY** scopes without [RestrictsSuspension] annotation can use wait actions.
*
* Note: These functions will be called on [Dispatchers.Main] (the Render thread).
*
* Most of the game events are called from the Render thread, except of [PacketEvent], it's called from the Netty client thread.
* You should carefully use this to prevent unexpected thread issue.
*
* @author MukjepScarlet
*/
object TickScheduler : Listenable, MinecraftInstance {

private val schedules = arrayListOf<BooleanSupplier>()

init {
handler<GameTickEvent>(priority = Byte.MAX_VALUE) {
schedules.removeIf { it.asBoolean }
}
}

/**
* Add a task for scheduling.
*
* @param breakLoop Stop tick the body when it returns `true`
*/
fun schedule(breakLoop: BooleanSupplier) {
if (mc.isCallingFromMinecraftThread) {
schedules += breakLoop
} else {
mc.addScheduledTask { schedules += breakLoop }
}
}

}

/**
* Wait until given [condition] returns true.
*
* @param condition It will be called on [Dispatchers.Main] (the Render thread)
* @return Total ticks during waiting
*/
suspend inline fun waitUntil(crossinline condition: () -> Boolean): Int =
suspendCancellableCoroutine { cont ->
var waitingTick = -1
TickScheduler.schedule {
waitingTick++
try {
if (condition()) {
cont.resume(waitingTick)
true
} else {
false
}
} catch (e: Throwable) {
cont.resumeWithException(e)
true
}
}
}

/**
* Wait for given [ticks].
*/
suspend fun waitTicks(ticks: Int) {
require(ticks >= 0) { "Negative tick: $ticks" }

if (ticks == 0) {
return
}

var remainingTick = ticks
waitUntil { --remainingTick == 0 }
}

/**
* Start a tick sequence for given [Listenable]
* which will be cancelled if [Listenable.handleEvents] of the owner returns false
*/
fun Listenable.tickSequence(
context: CoroutineContext = Dispatchers.Unconfined,
body: suspend CoroutineScope.() -> Unit
) {
val job = GlobalScope.launch(context, block = body)

TickScheduler.schedule {
when {
[email protected]() -> {
job.cancel()
true
}
else -> job.isCompleted
}
}
}
Loading