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(