From 8a9d1e95e144b430ecfea9d65dcd4b3386717e78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E6=9C=A8=E8=91=89=20Scarlet?=
<93977077+mukjepscarlet@users.noreply.github.com>
Date: Tue, 25 Feb 2025 00:59:03 +0800
Subject: [PATCH] rewrite
---
.../liquidbounce/script/PolyglotScript.kt | 10 +-
.../liquidbounce/script/ScriptManager.kt | 3 +
.../script/bindings/api/ScriptAsyncUtil.kt | 122 ++++++++++++++++++
.../bindings/api/ScriptContextProvider.kt | 75 +++++++----
.../bindings/async/JsSequenceHandler.kt | 92 -------------
.../script/bindings/features/ScriptModule.kt | 14 +-
6 files changed, 176 insertions(+), 140 deletions(-)
create mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptAsyncUtil.kt
delete mode 100644 src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/async/JsSequenceHandler.kt
diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/script/PolyglotScript.kt b/src/main/kotlin/net/ccbluex/liquidbounce/script/PolyglotScript.kt
index 88e0370b24b..7b6e2b5581a 100644
--- a/src/main/kotlin/net/ccbluex/liquidbounce/script/PolyglotScript.kt
+++ b/src/main/kotlin/net/ccbluex/liquidbounce/script/PolyglotScript.kt
@@ -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
@@ -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())
@@ -119,12 +119,6 @@ class PolyglotScript(
private val scriptText: String = file.readText()
- internal val promiseConstructor: Value? = if (language.equals("js", true)) {
- context.getBindings(language).getMember("Promise")
- } else {
- null
- }
-
// Script information
lateinit var scriptName: String
lateinit var scriptVersion: String
diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/script/ScriptManager.kt b/src/main/kotlin/net/ccbluex/liquidbounce/script/ScriptManager.kt
index 7e0b96306bb..bf93a99f355 100644
--- a/src/main/kotlin/net/ccbluex/liquidbounce/script/ScriptManager.kt
+++ b/src/main/kotlin/net/ccbluex/liquidbounce/script/ScriptManager.kt
@@ -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
@@ -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()
diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptAsyncUtil.kt b/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptAsyncUtil.kt
new file mode 100644
index 00000000000..7e5e4b377cb
--- /dev/null
+++ b/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptAsyncUtil.kt
@@ -0,0 +1,122 @@
+/*
+ * 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 .
+ */
+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()
+
+ @Suppress("unused")
+ private val tickHandler = handler(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`
+ */
+ @ScriptApiRequired
+ fun ticks(n: Int): Value {
+ if (n == 0) {
+ return defaultPromise
+ }
+
+ var remains = n
+ return until { --remains == 0 }
+ }
+
+ /**
+ * Example: `await seconds(1)`
+ *
+ * @return `Promise`
+ */
+ @ScriptApiRequired
+ fun seconds(n: Int): Value = ticks(n * 20)
+
+ /**
+ * Example: `const duration = await until(() => mc.player.isOnGround())`
+ *
+ * @return `Promise`
+ */
+ @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
+ }
+ }
+ }
+ )
+
+ /**
+ * Example: `const result = await conditional(20, () => mc.player.isOnGround())`
+ *
+ * @return `Promise`
+ */
+ @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 }
+ }
+
+}
+
diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptContextProvider.kt b/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptContextProvider.kt
index ded2eebcc74..dea41618deb 100644
--- a/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptContextProvider.kt
+++ b/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/api/ScriptContextProvider.kt
@@ -22,40 +22,61 @@ 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) {
+ if (!::scriptAsyncUtil.isInitialized && language.equals("js", true)) {
+ // 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) {
+ putMember("ticks", IntFunction(scriptAsyncUtil::ticks))
+ putMember("seconds", IntFunction(scriptAsyncUtil::seconds))
+ putMember("until", Function(scriptAsyncUtil::until))
+ putMember("conditional", BiFunction(scriptAsyncUtil::conditional))
+ }
+ }
}
}
diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/async/JsSequenceHandler.kt b/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/async/JsSequenceHandler.kt
deleted file mode 100644
index b8a6f47069b..00000000000
--- a/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/async/JsSequenceHandler.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package net.ccbluex.liquidbounce.script.bindings.async
-
-import kotlinx.coroutines.launch
-import net.ccbluex.liquidbounce.event.Event
-import net.ccbluex.liquidbounce.event.Sequence
-import net.ccbluex.liquidbounce.event.SequenceManager
-import net.ccbluex.liquidbounce.script.ScriptApiRequired
-import net.ccbluex.liquidbounce.script.bindings.features.ScriptModule
-import org.graalvm.polyglot.Value
-import org.graalvm.polyglot.proxy.ProxyExecutable
-import java.util.function.BooleanSupplier
-import java.util.function.Consumer
-import kotlin.coroutines.resume
-import kotlin.coroutines.resumeWithException
-import kotlin.coroutines.suspendCoroutine
-
-/**
- * Transform Kotlin suspend functions to JS async functions (returns `Promise`)
- *
- * @author MukjepScarlet
- */
-internal class JsSequenceHandler(
- private val owner: ScriptModule,
- private val promiseConstructor: Value,
- private val asyncFunction: Value,
-) {
-
- private var current: Sequence? = null
-
- internal fun startWith(eventInstance: Event?) {
- current = Sequence(owner) {
- /**
- * TODO
- * without this, the `asyncFunction.execute` will cause NPE
- * because the execution requires [current] to be initialized
- */
- sync()
-
- suspendCoroutine { continuation ->
- // Promise
- asyncFunction.execute(this@JsSequenceHandler, eventInstance).invokeMember(
- "then",
- Consumer(continuation::resume), // onResolve
- Consumer(continuation::resumeWithException) // onRejected
- )
- }
- }
- }
-
- private inline fun wrap(
- crossinline suspendableHandler: suspend Sequence.() -> T
- ): Value = promiseConstructor.newInstance(ProxyExecutable { (onResolve, onReject) ->
- SequenceManager.launch {
- try {
- val result = current!!.suspendableHandler()
- onResolve.execute(result)
- } catch (e: Throwable) {
- onReject.execute(e)
- }
- }
- })
-
- /**
- * Example: `await seq.ticks(10)`
- */
- @ScriptApiRequired
- fun ticks(n: Int) =
- wrap { waitTicks(n) }
-
- /**
- * Example: `await seq.seconds(1)`
- */
- @ScriptApiRequired
- fun seconds(n: Int) =
- wrap { waitSeconds(n) }
-
- /**
- * Example: `await seq.until(() => mc.player.isOnGround())`
- */
- @ScriptApiRequired
- fun until(condition: BooleanSupplier) =
- wrap { waitUntil(condition) }
-
- /**
- * Example: `const result = await seq.conditional(20, () => mc.player.isOnGround())`
- */
- @JvmOverloads
- @ScriptApiRequired
- fun conditional(ticks: Int, breakLoop: BooleanSupplier = BooleanSupplier { false }) =
- wrap { waitConditional(ticks, breakLoop) }
-
-}
diff --git a/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/features/ScriptModule.kt b/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/features/ScriptModule.kt
index 18992a2f9d4..959614f4931 100644
--- a/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/features/ScriptModule.kt
+++ b/src/main/kotlin/net/ccbluex/liquidbounce/script/bindings/features/ScriptModule.kt
@@ -23,7 +23,6 @@ import net.ccbluex.liquidbounce.event.*
import net.ccbluex.liquidbounce.features.module.Category
import net.ccbluex.liquidbounce.features.module.ClientModule
import net.ccbluex.liquidbounce.script.PolyglotScript
-import net.ccbluex.liquidbounce.script.bindings.async.JsSequenceHandler
import net.ccbluex.liquidbounce.utils.client.*
import java.util.function.Supplier
import kotlin.reflect.KClass
@@ -34,7 +33,6 @@ class ScriptModule(val script: PolyglotScript, moduleObject: Map) :
) {
private val events = hashMapOf()
- private val eventSequences = hashMapOf()
private val _values = linkedMapOf>()
private var _tag: String? = null
override val tag: String?
@@ -80,16 +78,7 @@ class ScriptModule(val script: PolyglotScript, moduleObject: Map) :
return
}
- when (handler.getMember("constructor").getMember("name").asString()) {
- "Function" -> {
- events[eventName] = handler
- }
- "AsyncFunction" -> script.promiseConstructor?.let {
- eventSequences[eventName] = JsSequenceHandler(this, it, handler)
- }
- else -> { /* ??? */ }
- }
-
+ events[eventName] = handler
hookHandler(eventName)
}
@@ -105,7 +94,6 @@ class ScriptModule(val script: PolyglotScript, moduleObject: Map) :
private fun callEvent(event: String, payload: Event? = null) {
try {
events[event]?.executeVoid(payload)
- eventSequences[event]?.startWith(payload)
} catch (throwable: Throwable) {
if (inGame) {
chat(