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(ScriptAPI): async API for tick scheduling #5613

Draft
wants to merge 9 commits into
base: nextgen
Choose a base branch
from
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
4 changes: 2 additions & 2 deletions src/main/kotlin/net/ccbluex/liquidbounce/event/Sequence.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import kotlin.coroutines.suspendCoroutine
typealias SuspendableEventHandler<T> = suspend Sequence.(T) -> Unit
typealias SuspendableHandler = suspend Sequence.() -> Unit

object SequenceManager : EventListener {
object SequenceManager : EventListener, CoroutineScope by CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) {

// Running sequences
internal val sequences = CopyOnWriteArrayList<Sequence>()
Expand Down Expand Up @@ -92,7 +92,7 @@ open class Sequence(val owner: EventListener, val handler: SuspendableHandler) {
// otherwise there is an edge case where the first time a time-dependent suspension occurs it will be
// overwritten by the initialization of the `totalTicks` field which results in one or less ticks of actual wait
// time.
this.coroutine = GlobalScope.launch(Dispatchers.Unconfined) {
this.coroutine = SequenceManager.launch {
SequenceManager.sequences += this@Sequence
coroutineRun()
SequenceManager.sequences -= this@Sequence
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import net.ccbluex.liquidbounce.features.command.CommandManager
import net.ccbluex.liquidbounce.features.module.ClientModule
import net.ccbluex.liquidbounce.features.module.ModuleManager
import net.ccbluex.liquidbounce.lang.translation
import net.ccbluex.liquidbounce.script.bindings.api.ScriptContextProvider
import net.ccbluex.liquidbounce.script.bindings.api.ScriptContextProvider.setupContext
import net.ccbluex.liquidbounce.script.bindings.features.ScriptChoice
import net.ccbluex.liquidbounce.script.bindings.features.ScriptCommandBuilder
import net.ccbluex.liquidbounce.script.bindings.features.ScriptModule
Expand Down Expand Up @@ -56,7 +56,7 @@ class PolyglotScript(
.currentWorkingDirectory(file.parentFile.toPath())
.allowIO(IOAccess.ALL) // Allow access to all IO operations
.allowCreateProcess(false) // Disable process creation
.allowCreateThread(true) // Disable thread creation
.allowCreateThread(true) // Enable thread creation
.allowNativeAccess(false) // Disable native access
.allowExperimentalOptions(true) // Allow experimental options
.option("js.nashorn-compat", "true") // Enable Nashorn compatibility
Expand Down Expand Up @@ -111,7 +111,7 @@ class PolyglotScript(
// Global instances
val bindings = getBindings(language)

ScriptContextProvider.setupContext(bindings)
this.setupContext(language, bindings)

// Global functions
bindings.putMember("registerScript", RegisterScript())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package net.ccbluex.liquidbounce.script
import com.mojang.blaze3d.systems.RenderSystem
import net.ccbluex.liquidbounce.config.ConfigSystem
import net.ccbluex.liquidbounce.features.module.modules.render.ModuleClickGui
import net.ccbluex.liquidbounce.script.bindings.api.ScriptAsyncUtil
import net.ccbluex.liquidbounce.utils.client.logger
import org.graalvm.polyglot.Engine
import org.graalvm.polyglot.Source
Expand Down Expand Up @@ -50,6 +51,8 @@ object ScriptManager {
}

init {
ScriptAsyncUtil.TickScheduler

try {
// Initialize the script engine and log its version and supported languages.
val engine = Engine.create()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* This file is part of LiquidBounce (https://github.com/CCBlueX/LiquidBounce)
*
* Copyright (c) 2015 - 2025 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 <https://www.gnu.org/licenses/>.
*/
package net.ccbluex.liquidbounce.script.bindings.api

import net.ccbluex.liquidbounce.event.EventListener
import net.ccbluex.liquidbounce.event.events.GameTickEvent
import net.ccbluex.liquidbounce.event.handler
import net.ccbluex.liquidbounce.script.ScriptApiRequired
import net.ccbluex.liquidbounce.utils.client.mc
import net.ccbluex.liquidbounce.utils.kotlin.EventPriorityConvention.FIRST_PRIORITY
import org.graalvm.polyglot.Value
import org.graalvm.polyglot.proxy.ProxyExecutable
import java.util.function.BooleanSupplier

/**
* @author MukjepScarlet
*/
class ScriptAsyncUtil(
private val jsPromiseConstructor: Value
) {

companion object TickScheduler : EventListener {
private val schedules = arrayListOf<BooleanSupplier>()

@Suppress("unused")
private val tickHandler = handler<GameTickEvent>(priority = FIRST_PRIORITY) {
schedules.removeIf { it.asBoolean }
}

private fun schedule(breakLoop: BooleanSupplier) {
mc.execute { schedules += breakLoop }
}
}

private val defaultPromise: Value = jsPromiseConstructor.invokeMember("resolve", 0);

/**
* Example: `await ticks(10)`
*
* @return `Promise<number>`
*/
@ScriptApiRequired
fun ticks(n: Int): Value {
if (n == 0) {
return defaultPromise
}

var remains = n
return until { --remains == 0 }
}

/**
* Example: `await seconds(1)`
*
* @return `Promise<number>`
*/
@ScriptApiRequired
fun seconds(n: Int): Value = ticks(n * 20)

/**
* Example: `const duration = await until(() => mc.player.isOnGround())`
*
* @return `Promise<number>`
*/
@ScriptApiRequired
fun until(condition: BooleanSupplier): Value = jsPromiseConstructor.newInstance(
ProxyExecutable { (onResolve, onReject) ->
var waitingTick = 0
schedule {
waitingTick++
try {
if (condition.asBoolean) {
onResolve.executeVoid(waitingTick)
true
} else {
false
}
} catch (e: Throwable) {
onReject.executeVoid(e)
true
}
}

null
}
)

/**
* Example: `const result = await conditional(20, () => mc.player.isOnGround())`
*
* @return `Promise<number>`
*/
@JvmOverloads
@ScriptApiRequired
fun conditional(
ticks: Int,
breakLoop: BooleanSupplier = BooleanSupplier { false }
): Value {
if (ticks == 0) {
return defaultPromise
}

var remains = ticks
return until { --remains == 0 || breakLoop.asBoolean }
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,62 @@ import net.ccbluex.liquidbounce.script.bindings.features.ScriptSetting
import net.ccbluex.liquidbounce.utils.client.mc
import net.minecraft.util.Hand
import net.minecraft.util.math.*
import org.graalvm.polyglot.Context
import org.graalvm.polyglot.Value
import java.util.function.BiFunction
import java.util.function.Function
import java.util.function.IntFunction

/**
* The main hub of the ScriptAPI that provides access to a useful set of members.
*/
object ScriptContextProvider {

internal fun setupContext(bindings: Value) = bindings.apply {
// Class bindings
// -> Client API
putMember("Setting", ScriptSetting)

// -> Minecraft API
putMember("Vec3i", Vec3i::class.java)
putMember("Vec3d", Vec3d::class.java)
putMember("MathHelper", MathHelper::class.java)
putMember("BlockPos", BlockPos::class.java)
putMember("Hand", Hand::class.java)
putMember("RotationAxis", RotationAxis::class.java)

// Variable bindings
putMember("mc", mc)
putMember("Client", ScriptClient)

// Register utilities
putMember("RotationUtil", ScriptRotationUtil)
putMember("ItemUtil", ScriptItemUtil)
putMember("NetworkUtil", ScriptNetworkUtil)
putMember("InteractionUtil", ScriptInteractionUtil)
putMember("BlockUtil", ScriptBlockUtil)
putMember("MovementUtil", ScriptMovementUtil)
putMember("ReflectionUtil", ScriptReflectionUtil())
putMember("ParameterValidator", ScriptParameterValidator(bindings))
putMember("UnsafeThread", ScriptUnsafeThread)
private lateinit var scriptAsyncUtil: ScriptAsyncUtil

internal fun Context.setupContext(language: String, bindings: Value) {
val isJs = language.equals("js", true)
if (!::scriptAsyncUtil.isInitialized && isJs) {
// Init Promise constructor
scriptAsyncUtil = ScriptAsyncUtil(this.getBindings(language).getMember("Promise"))
}

bindings.apply {
// Class bindings
// -> Client API
putMember("Setting", ScriptSetting)

// -> Minecraft API
putMember("Vec3i", Vec3i::class.java)
putMember("Vec3d", Vec3d::class.java)
putMember("MathHelper", MathHelper::class.java)
putMember("BlockPos", BlockPos::class.java)
putMember("Hand", Hand::class.java)
putMember("RotationAxis", RotationAxis::class.java)

// Variable bindings
putMember("mc", mc)
putMember("Client", ScriptClient)

// Register utilities
putMember("RotationUtil", ScriptRotationUtil)
putMember("ItemUtil", ScriptItemUtil)
putMember("NetworkUtil", ScriptNetworkUtil)
putMember("InteractionUtil", ScriptInteractionUtil)
putMember("BlockUtil", ScriptBlockUtil)
putMember("MovementUtil", ScriptMovementUtil)
putMember("ReflectionUtil", ScriptReflectionUtil())
putMember("ParameterValidator", ScriptParameterValidator(bindings))
putMember("UnsafeThread", ScriptUnsafeThread)

// Async support
if (::scriptAsyncUtil.isInitialized && isJs) {
putMember("ticks", IntFunction(scriptAsyncUtil::ticks))
putMember("seconds", IntFunction(scriptAsyncUtil::seconds))
putMember("until", Function(scriptAsyncUtil::until))
putMember("conditional", BiFunction(scriptAsyncUtil::conditional))
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class ScriptModule(val script: PolyglotScript, moduleObject: Map<String, Any>) :
category = Category.fromReadableName(moduleObject["category"] as String)!!
) {

private val events = hashMapOf<String, (Any?) -> Unit>()
private val events = hashMapOf<String, org.graalvm.polyglot.Value>()
private val _values = linkedMapOf<String, Value<*>>()
private var _tag: String? = null
override val tag: String?
Expand Down Expand Up @@ -68,8 +68,16 @@ class ScriptModule(val script: PolyglotScript, moduleObject: Map<String, Any>) :
* Called from inside the script to register a new event handler.
* @param eventName Name of the event.
* @param handler JavaScript function used to handle the event.
* 1. `() => void` (enable/disable)
* 2. `(Event) => void` (handler<T>)
* 3. `async (Event) => void` (sequenceHandler<T>)
*/
fun on(eventName: String, handler: (Any?) -> Unit) {
fun on(eventName: String, handler: org.graalvm.polyglot.Value) {
if (!handler.canExecute()) {
logger.error("Invalid event handler for $eventName")
return
}

events[eventName] = handler
hookHandler(eventName)
}
Expand All @@ -79,11 +87,13 @@ class ScriptModule(val script: PolyglotScript, moduleObject: Map<String, Any>) :
override fun disable() = callEvent("disable")

/**
* Calls the function of the [event] with the [payload] of the event.
* Calls the function of the [event] with the [payload] of the event.
*
* @param payload when event is "enable" or "disable", it will be null
*/
private fun callEvent(event: String, payload: Event? = null) {
try {
events[event]?.invoke(payload)
events[event]?.executeVoid(payload)
} catch (throwable: Throwable) {
if (inGame) {
chat(
Expand Down
Loading