Skip to content

Commit

Permalink
Nextgen ScriptAPI (CCBlueX#1552)
Browse files Browse the repository at this point in the history
An extension to the built-in JS ScriptAPI. This will make it much easier and longer lasting to use the client features via script. This means that anything we add to the API, we expect to support for as long as possible.

- Import of commonly used Minecraft classes
- Access to commonly used LiquidBounce features
- Added script command
- Improved error handling

`api.utilName` features:
- [x] Rotation Util
- [x] Item Util
- [x] Network Util
- [x] Interaction Util
- [x] Block Util
- [x] Reflection Util
- [x] Movement Util
  • Loading branch information
1zun4 authored Dec 18, 2023
1 parent 5f2a5f7 commit 8d97ef3
Show file tree
Hide file tree
Showing 19 changed files with 589 additions and 49 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ plugins {
id 'com.gorylenko.gradle-git-properties' version '2.4.1'
id "io.gitlab.arturbosch.detekt" version "1.23.4"
id "com.github.node-gradle.node" version "7.0.1"
id 'org.jetbrains.dokka' version '1.9.10'
}

ktlint {
Expand Down
56 changes: 30 additions & 26 deletions config/detekt/baseline.xml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import net.ccbluex.liquidbounce.features.command.commands.creative.CommandItemRe
import net.ccbluex.liquidbounce.features.command.commands.creative.CommandItemSkull
import net.ccbluex.liquidbounce.features.command.commands.utility.CommandPosition
import net.ccbluex.liquidbounce.features.command.commands.utility.CommandUsername
import net.ccbluex.liquidbounce.script.CommandScript
import net.ccbluex.liquidbounce.script.RequiredByScript
import net.ccbluex.liquidbounce.utils.client.chat
import net.ccbluex.liquidbounce.utils.client.outputString
import net.minecraft.text.MutableText
Expand Down Expand Up @@ -142,6 +144,7 @@ object CommandManager : Iterable<Command> {
addCommand(CommandConfig.createCommand())
addCommand(CommandLocalConfig.createCommand())
addCommand(CommandAutoDisable.createCommand())
addCommand(CommandScript.createCommand())

// creative commands
addCommand(CommandItemRename.createCommand())
Expand Down Expand Up @@ -212,6 +215,8 @@ object CommandManager : Iterable<Command> {
*
* @param cmd The command. If there is no command in it (it is empty or only whitespaces), this method is a no op
*/
@RequiredByScript
@JvmName("execute")
fun execute(cmd: String) {
val args = tokenizeCommand(cmd).first

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ object CommandClient {
// TODO: links
// TODO: instructions
// TODO: reset
// TODO: script manager
// TODO: theme manager
// .. other client base commands
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import net.ccbluex.liquidbounce.features.module.modules.render.nametags.ModuleNa
import net.ccbluex.liquidbounce.features.module.modules.world.*
import net.ccbluex.liquidbounce.features.module.modules.world.crystalAura.ModuleCrystalAura
import net.ccbluex.liquidbounce.features.module.modules.world.scaffold.ModuleScaffold
import net.ccbluex.liquidbounce.script.RequiredByScript
import org.lwjgl.glfw.GLFW

private val modules = mutableListOf<Module>()
Expand Down Expand Up @@ -269,6 +270,12 @@ object ModuleManager : Listenable, Iterable<Module> by modules {
/**
* This is being used by UltralightJS for the implementation of the ClickGUI. DO NOT REMOVE!
*/
@JvmName("getCategories")
@RequiredByScript
fun getCategories() = Category.values().map { it.readableName }.toTypedArray()

@JvmName("getModuleByName")
@RequiredByScript
fun getModuleByName(module: String) = find { it.name.equals(module, true) }

}
105 changes: 105 additions & 0 deletions src/main/kotlin/net/ccbluex/liquidbounce/script/CommandScript.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package net.ccbluex.liquidbounce.script

import net.ccbluex.liquidbounce.features.command.Command
import net.ccbluex.liquidbounce.features.command.builder.CommandBuilder
import net.ccbluex.liquidbounce.features.command.builder.ParameterBuilder
import net.ccbluex.liquidbounce.utils.client.chat
import net.ccbluex.liquidbounce.utils.client.regular
import net.ccbluex.liquidbounce.utils.client.variable
import net.minecraft.util.Util

object CommandScript {

fun createCommand(): Command {
return CommandBuilder.begin("script")
.hub()
.subcommand(CommandBuilder.begin("reload").handler { command, _ ->
runCatching {
ScriptManager.reloadScripts()
}.onSuccess {
chat(regular(command.result("reloaded")))
}.onFailure {
chat(regular(command.result("reloadFailed", variable(it.message ?: "unknown"))))
}
}.build())
.subcommand(CommandBuilder.begin("load").parameter(
ParameterBuilder.begin<String>("name").verifiedBy(ParameterBuilder.STRING_VALIDATOR).required()
.build()
).handler { command, args ->
val name = args[0] as String
val scriptFile = ScriptManager.scriptsRoot.resolve("$name.js")

if (!scriptFile.exists()) {
chat(regular(command.result("notFound", variable(name))))
return@handler
}

// Check if script is already loaded
if (ScriptManager.loadedScripts.any { it.scriptFile == scriptFile }) {
chat(regular(command.result("alreadyLoaded", variable(name))))
return@handler
}

runCatching {
ScriptManager.loadScript(scriptFile)
}.onSuccess {
chat(regular(command.result("loaded", variable(name))))
}.onFailure {
chat(regular(command.result("failedToLoad", variable(it.message ?: "unknown"))))
}

}.build())
.subcommand(CommandBuilder.begin("unload").parameter(
ParameterBuilder.begin<String>("name").verifiedBy(ParameterBuilder.STRING_VALIDATOR).required()
.build()
).handler { command, args ->
val name = args[0] as String

val script = ScriptManager.loadedScripts.find { it.scriptName.equals(name, true) }

if (script == null) {
chat(regular(command.result("notFound", variable(name))))
return@handler
}

runCatching {
ScriptManager.unloadScript(script)
}.onSuccess {
chat(regular(command.result("unloaded", variable(name))))
}.onFailure {
chat(regular(command.result("failedToUnload", variable(it.message ?: "unknown"))))
}
}.build())
.subcommand(CommandBuilder.begin("list").handler { command, _ ->
val scripts = ScriptManager.loadedScripts
val scriptNames = scripts.map { it.scriptName }

if (scriptNames.isEmpty()) {
chat(regular(command.result("noScripts")))
return@handler
}

chat(regular(command.result("scripts", variable(scriptNames.joinToString(", ")))))
}.build())
.subcommand(CommandBuilder.begin("directory").handler { command, _ ->
Util.getOperatingSystem().open(ScriptManager.scriptsRoot)
chat(regular(command.result("scriptsDirectory", variable(ScriptManager.scriptsRoot.absolutePath))))
}.build())
.subcommand(CommandBuilder.begin("edit").parameter(
ParameterBuilder.begin<String>("name").verifiedBy(ParameterBuilder.STRING_VALIDATOR).required()
.build()
).handler { command, args ->
val name = args[0] as String
val scriptFile = ScriptManager.scriptsRoot.resolve("$name.js")

if (!scriptFile.exists()) {
chat(regular(command.result("notFound", variable(name))))
return@handler
}

Util.getOperatingSystem().open(scriptFile)
chat(regular(command.result("opened", variable(name))))
}.build())
.build()
}
}
17 changes: 7 additions & 10 deletions src/main/kotlin/net/ccbluex/liquidbounce/script/Script.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ import net.ccbluex.liquidbounce.features.command.builder.CommandBuilder
import net.ccbluex.liquidbounce.features.command.builder.ParameterBuilder
import net.ccbluex.liquidbounce.features.module.Module
import net.ccbluex.liquidbounce.features.module.ModuleManager
import net.ccbluex.liquidbounce.script.bindings.api.JsApiProvider
import net.ccbluex.liquidbounce.script.bindings.features.JsModule
import net.ccbluex.liquidbounce.script.bindings.features.JsSetting
import net.ccbluex.liquidbounce.script.bindings.globals.JsClient
import net.ccbluex.liquidbounce.script.bindings.globals.JsItem
import net.ccbluex.liquidbounce.utils.client.logger
import net.ccbluex.liquidbounce.utils.client.mc
import org.graalvm.polyglot.Context
Expand All @@ -47,16 +47,8 @@ class Script(val scriptFile: File) {
.build().apply {
// Global instances
val jsBindings = getBindings("js")
jsBindings.putMember("Setting", JsSetting)
jsBindings.putMember("Item", JsItem)

// Direct access to CommandBuilder and ParameterBuilder required for commands
// todo: remove this as soon we figured out a more JS-like way to create commands
jsBindings.putMember("CommandBuilder", CommandBuilder)
jsBindings.putMember("ParameterBuilder", ParameterBuilder)

jsBindings.putMember("mc", mc)
jsBindings.putMember("client", JsClient)
JsApiProvider.setupUsefulContext(jsBindings)

// Global functions
jsBindings.putMember("registerScript", RegisterScript())
Expand Down Expand Up @@ -88,6 +80,11 @@ class Script(val scriptFile: File) {
// Call load event
callGlobalEvent("load")

if (!::scriptName.isInitialized || !::scriptVersion.isInitialized || !::scriptAuthors.isInitialized) {
logger.error("[ScriptAPI] Script '${scriptFile.name}' is missing required information!")
error("Script '${scriptFile.name}' is missing required information!")
}

logger.info("[ScriptAPI] Successfully loaded script '${scriptFile.name}'.")
}

Expand Down
26 changes: 17 additions & 9 deletions src/main/kotlin/net/ccbluex/liquidbounce/script/ScriptManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import java.io.FileFilter

object ScriptManager {

// todo: add fabricmc mappings nashorn remapper

// Loaded scripts
val loadedScripts = mutableListOf<Script>()

Expand All @@ -41,7 +39,7 @@ object ScriptManager {
* Loads all scripts inside the scripts folder.
*/
fun loadScripts() {
scriptsRoot.listFiles(FileFilter { it.name.endsWith(".js") })?.forEach(ScriptManager::loadScript)
scriptsRoot.listFiles(FileFilter { it.name.endsWith(".js") })?.forEach(ScriptManager::loadSafely)
}

/**
Expand All @@ -52,17 +50,27 @@ object ScriptManager {
loadedScripts.clear()
}

fun loadSafely(file: File) = runCatching {
loadScript(file)
}.onFailure {
logger.error("Unable to load script ${file.name}.", it)
}.getOrNull()

/**
* Loads a script from a file.
*/
fun loadScript(file: File) = runCatching {
val script = Script(file).also { loadedScripts += it }
fun loadScript(file: File): Script {
val script = Script(file)
script.initScript()

script
}.onFailure {
logger.error("Unable to load script ${file.name}.", it)
}.getOrNull()
loadedScripts += script
return script
}

fun unloadScript(script: Script) {
script.disable()
loadedScripts.remove(script)
}

/**
* Enables all scripts.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package net.ccbluex.liquidbounce.script.bindings.api

import net.ccbluex.liquidbounce.features.command.builder.CommandBuilder
import net.ccbluex.liquidbounce.features.command.builder.ParameterBuilder
import net.ccbluex.liquidbounce.script.bindings.features.JsSetting
import net.ccbluex.liquidbounce.script.bindings.globals.JsClient
import net.ccbluex.liquidbounce.utils.client.mc
import net.minecraft.util.Hand
import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3i
import org.graalvm.polyglot.Value

/**
* The main hub of the ScriptAPI that provides access to all kind of useful APIs.
*/
object JsApiProvider {

internal fun setupUsefulContext(context: Value) = context.apply {
// Class bindings
// -> Client API
putMember("Setting", JsSetting)
putMember("CommandBuilder", CommandBuilder)
putMember("ParameterBuilder", ParameterBuilder)
// -> Minecraft API
// todo: test if this works
putMember("Vec3i", Vec3i::class.java)
putMember("BlockPos", BlockPos::class.java)
putMember("Hand", Hand::class.java)

// Variable bindings
putMember("mc", mc)
putMember("client", JsClient)
putMember("api", JsApiProvider)
}

/**
* A collection of useful rotation utilities for the ScriptAPI.
* This SHOULD not be changed in a way that breaks backwards compatibility.
*
* This is a singleton object, so it can be accessed from the script API like this:
* ```js
* api.rotationUtil.newRaytracedRotationEntity(entity, 4.2, 0.0)
* api.rotationUtil.newRotationEntity(entity)
* api.rotationUtil.aimAtRotation(rotation, true)
* ```
*/
@JvmField
val rotationUtil = JsRotationUtil

/**
* Object used by the script API to provide an idiomatic way of creating module values.
*/
@JvmField
val itemUtil = JsItemUtil

@JvmField
val networkUtil = JsNetworkUtil

@JvmField
val interactionUtil = JsInteractionUtil

@JvmField
val blockUtil = JsBlockUtil

@JvmField
val reflectionUtil = JsReflectionUtil

@JvmField
val movementUtil = JsMovementUtil

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.ccbluex.liquidbounce.script.bindings.api

import net.ccbluex.liquidbounce.utils.block.getBlock
import net.ccbluex.liquidbounce.utils.block.getState
import net.ccbluex.liquidbounce.utils.item.createItem
import net.minecraft.block.BlockState
import net.minecraft.item.ItemStack
import net.minecraft.util.math.BlockPos

/**
* Object used by the script API to provide an
*/
object JsBlockUtil {

@JvmName("newBlockPos")
fun newBlockPos(x: Int, y: Int, z: Int): BlockPos = BlockPos(x, y, z)

@JvmName("toBlock")
fun toBlock(blockPos: BlockPos) = blockPos.getBlock()

@JvmName("toState")
fun toState(blockPos: BlockPos): BlockState? = blockPos.getState()

}
Loading

0 comments on commit 8d97ef3

Please sign in to comment.