diff --git a/.gitignore b/.gitignore index 556e22d..cb2678c 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,6 @@ bin/ .vscode/ ### Mac OS ### -.DS_Store \ No newline at end of file +.DS_Store + +run/ \ No newline at end of file diff --git a/Rocket.md b/Rocket.md new file mode 100644 index 0000000..a63d40d --- /dev/null +++ b/Rocket.md @@ -0,0 +1,14 @@ +# Module Rocket Java API + +This is the documentation for the Rocket Java API. +This documentation is generated using Dokka, and is not useful to script developers. + +## Who is this for? + +This documentation is for developers who are working on the Rocket Java API. If you are a script developer, you should refer to the [Rocket Scripting API](https://example.com). + +## Copyright + +This documentation is Copyright (c) 2025 znci. Licensed under the Apache License, Version 2.0. + +You may obtain a copy of the License at [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0). diff --git a/build.gradle.kts b/build.gradle.kts index f480d46..9a0919c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.dokka.gradle.DokkaTask + /** * Copyright 2025 znci * @@ -16,7 +18,9 @@ plugins { kotlin("jvm") version "2.1.20-RC" + id("org.jetbrains.dokka") version "2.0.0" id("com.github.johnrengelman.shadow") version "8.1.1" + id("xyz.jpenilla.run-paper") version "2.3.1" } group = "dev.znci" @@ -37,6 +41,7 @@ dependencies { implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") implementation("org.luaj:luaj-jse:3.0.1") implementation("net.luckperms:api:5.4") + implementation("org.jetbrains.kotlin:kotlin-reflect") } val targetJavaVersion = 21 @@ -56,3 +61,28 @@ tasks.processResources { expand(props) } } + +tasks.withType().configureEach { + dokkaSourceSets { + named("main") { + moduleName.set("Rocket Java API") + includes.from("Rocket.md") + + } + } +} + + +tasks.withType(xyz.jpenilla.runtask.task.AbstractRun::class) { + javaLauncher = javaToolchains.launcherFor { + @Suppress("UnstableApiUsage") + vendor = JvmVendorSpec.JETBRAINS // use JetBrains JVM + languageVersion = JavaLanguageVersion.of(21) + } + jvmArgs("-XX:+AllowEnhancedClassRedefinition") +} + +tasks.runServer { + minecraftVersion("1.21.4") + jvmArgs("-Dcom.mojang.eula.agree=true") +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/Rocket.kt b/src/main/kotlin/dev/znci/rocket/Rocket.kt index e2ac822..70ffbe4 100644 --- a/src/main/kotlin/dev/znci/rocket/Rocket.kt +++ b/src/main/kotlin/dev/znci/rocket/Rocket.kt @@ -19,6 +19,7 @@ import dev.znci.rocket.commands.RocketCommand import dev.znci.rocket.i18n.LocaleManager import dev.znci.rocket.scripting.ScriptManager import dev.znci.rocket.scripting.events.EventListener +import dev.znci.rocket.scripting.GlobalInitializer import org.bukkit.plugin.java.JavaPlugin import java.io.File @@ -52,8 +53,17 @@ class Rocket : JavaPlugin() { this.getCommand("rocket")?.setExecutor(RocketCommand(this)) // Register all events - logger.info("Rocket plugin enabled") EventListener.registerAllEvents() + + // Register globals + val globalInitialized = GlobalInitializer.init() + if (globalInitialized) { + logger.info("Globals successfully initialized") + } else { + logger.severe("Globals failed to initialize") + } + + logger.info("Rocket plugin enabled") } override fun onDisable() { diff --git a/src/main/kotlin/dev/znci/rocket/i18n/LocaleManager.kt b/src/main/kotlin/dev/znci/rocket/i18n/LocaleManager.kt index 6d052cf..294816e 100644 --- a/src/main/kotlin/dev/znci/rocket/i18n/LocaleManager.kt +++ b/src/main/kotlin/dev/znci/rocket/i18n/LocaleManager.kt @@ -21,23 +21,54 @@ import org.bukkit.configuration.file.YamlConfiguration import org.bukkit.plugin.java.JavaPlugin import java.io.File +/** + * LocaleManager is responsible for managing internationalization support in the plugin. + * It loads language files from YAML configurations and provides the translated messages. + */ object LocaleManager { + /** + * A map storing language keys and their corresponding translations. + */ private val messages = mutableMapOf>() + + /** + * Default language code. + */ private const val DEFAULT_LANG: String = "en_GB" + /** + * The plugin instance. + */ private var plugin: JavaPlugin? = null + + /** + * Current language being used. + */ private var lang: String = DEFAULT_LANG + /** + * Sets the plugin instance for LocaleManager. + * + * @param plugin The JavaPlugin instance. + */ fun setPlugin(plugin: JavaPlugin) { this.plugin = plugin } + /** + * Sets the current locale. + * + * @param lang The language code to switch to. + */ fun setLocale(lang: String) { if (messages.containsKey(lang)) { this.lang = lang } } + /** + * Loads all available language files from the plugin's locales directory. + */ fun loadLanguages() { val langFolder = File(plugin?.dataFolder, "locales") if (!langFolder.exists()) langFolder.mkdirs() @@ -56,6 +87,13 @@ object LocaleManager { } } + /** + * Retrieves a translated message for the given key. + * If the message contains placeholders in {key} format, they will be replaced recursively. + * + * @param key The translation key. + * @return The translated message or a fallback "MISSING_TRANSLATION" string. + */ private fun getMessage(key: String): String { val message = messages[lang]?.get(key) @@ -72,6 +110,13 @@ object LocaleManager { return message ?: "MISSING_TRANSLATION" } + /** + * Retrieves a translated message as a Component with optional formatting arguments. + * + * @param key The translation key. + * @param formatArgs Optional formatting arguments. + * @return The translated message as a Component + */ fun getMessageAsComponent(key: String, vararg formatArgs: Any): Component { val message = getMessage(key) diff --git a/src/main/kotlin/dev/znci/rocket/scripting/GlobalInitializer.kt b/src/main/kotlin/dev/znci/rocket/scripting/GlobalInitializer.kt new file mode 100644 index 0000000..cdaf3ce --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/GlobalInitializer.kt @@ -0,0 +1,30 @@ +package dev.znci.rocket.scripting + +import dev.znci.rocket.scripting.globals.tables.LuaLocations +import dev.znci.rocket.scripting.globals.tables.LuaPlayers +import dev.znci.rocket.scripting.globals.tables.SimpleTest +import dev.znci.rocket.scripting.globals.values.TestValue + +/** + * The `GlobalInitializer` object is responsible for initializing and registering global objects + * with the `ScriptManager`. It ensures that necessary components are properly registered for use + * for the server admins. + */ +object GlobalInitializer { + /** + * Initializes the global objects and registers them with the `ScriptManager`. + * + * It returns `true` upon successful registration of these objects. + * + * @return `true` if the global objects were successfully initialized and registered. + */ + fun init(): Boolean { + ScriptManager.registerGlobal(SimpleTest()) + ScriptManager.registerGlobal(TestValue()) + ScriptManager.registerGlobal(LuaPlayers()) + ScriptManager.registerGlobal(LuaLocations()) + //ScriptManager.registerGlobal(GamemodeEnum()) + + return true + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/PermissionsManager.kt b/src/main/kotlin/dev/znci/rocket/scripting/PermissionsManager.kt index 46d291b..2a091af 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/PermissionsManager.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/PermissionsManager.kt @@ -17,11 +17,33 @@ package dev.znci.rocket.scripting import org.bukkit.entity.Player +/** + * The `PermissionsManager` object is responsible for managing player permissions and group memberships. + * It provides utility functions to check if a player belongs to a specific group and if they have specific permissions. + */ object PermissionsManager { + /** + * Checks if a player belongs to a specific group. + * + * This method checks whether the player has the permission for the specified group in the format `group.`. + * + * @param player The player whose group is being checked. + * @param group The name of the group to check. + * @return `true` if the player belongs to the specified group, `false` otherwise. + */ fun isPlayerInGroup(player: Player, group: String): Boolean { return player.hasPermission("group.$group") } + /** + * Retrieves all the groups the player belongs to. + * + * This method checks all the player's effective permissions, filtering out those that are related to groups + * (permissions starting with `group.`), and returns a list of group names. + * + * @param player The player whose groups are being retrieved. + * @return A list of group names that the player is part of. + */ @Suppress("unused") // TODO: This will be used in the future. Remove this decorator when it's used. fun getPlayerGroups(player: Player): List { return player.effectivePermissions @@ -29,6 +51,15 @@ object PermissionsManager { .map { it.permission.substring(6) } } + /** + * Checks if a player has a specific permission. + * + * This method checks whether the player has the specified permission. + * + * @param player The player whose permissions are being checked. + * @param permission The permission to check. + * @return `true` if the player has the specified permission, `false` otherwise. + */ fun hasPermission(player: Player, permission: String): Boolean { return player.hasPermission(permission) } diff --git a/src/main/kotlin/dev/znci/rocket/scripting/PlayerManager.kt b/src/main/kotlin/dev/znci/rocket/scripting/PlayerManager.kt index b29b862..f50d9cd 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/PlayerManager.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/PlayerManager.kt @@ -15,8 +15,8 @@ */ package dev.znci.rocket.scripting -import dev.znci.rocket.scripting.functions.LuaLocation.Companion.fromBukkit -import dev.znci.rocket.scripting.functions.toBukkitLocation +import dev.znci.rocket.scripting.globals.tables.LuaLocation.Companion.fromBukkit +import dev.znci.rocket.scripting.globals.tables.toBukkitLocation import dev.znci.rocket.scripting.util.defineProperty import dev.znci.rocket.util.MessageFormatter import net.kyori.adventure.text.Component diff --git a/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt b/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt index 6206ab8..3fc86a6 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt @@ -15,29 +15,70 @@ */ package dev.znci.rocket.scripting +import dev.znci.rocket.scripting.api.RocketError +import dev.znci.rocket.scripting.api.RocketLuaValue +import dev.znci.rocket.scripting.api.RocketProperty +import dev.znci.rocket.scripting.api.RocketTable +import dev.znci.rocket.scripting.api.RocketValueBase import dev.znci.rocket.scripting.classes.Command -import dev.znci.rocket.scripting.functions.* -import dev.znci.rocket.util.RocketEnums import org.bukkit.event.Event import java.io.File import org.luaj.vm2.Globals import org.luaj.vm2.LuaError +import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.jse.JsePlatform import java.util.ArrayList +/** + * The `ScriptManager` object is responsible for managing Lua scripts, global values, and event handling in the plugin. + * It allows for the loading and running of scripts, registering and unregistering global values. + */ object ScriptManager { + /** + * The global environment for Lua scripts. + * This is used to load and execute Lua code with a standard Lua environment. + */ private val globals: Globals = JsePlatform.standardGlobals() + /** + * A list of global values (properties and tables) that have been registered for Lua scripting. + * These globals are made available to Lua scripts during their execution. + */ + private var enabledGlobals: MutableList = mutableListOf() + + /** + * The folder where scripts are located. + * This can be set to a custom folder to load Lua scripts from a specific directory. + */ var scriptsFolder: File = File("") + + /** + * A map of events and their associated Lua handlers. + * It stores the events triggered in the system and the corresponding Lua functions that handle them. + */ val usedEvents = mutableMapOf, LuaValue>() + + /** + * A map of enabled commands by their names. + * It associates command names with their respective command executors. + */ val enabledCommands = mutableMapOf() + /** + * Sets the folder where scripts are located. + * + * @param folder The folder containing the Lua scripts. + */ @Suppress("unused") // TODO: Will be used in the future when custom configuration folders are implemented fun setFolder(folder: File) { scriptsFolder = folder } + /** + * Loads all scripts from the `scriptsFolder` directory. + * This method currently prints the content of the scripts, but is planned for future use when custom folder configurations are implemented. + */ @Suppress("unused") // TODO: Is this still required? fun loadScripts() { scriptsFolder.walkTopDown().forEach { file -> @@ -49,6 +90,13 @@ object ScriptManager { } } + /** + * Retrieves a list of all scripts available in the `scriptsFolder` directory. + * Optionally, can include or exclude disabled scripts based on their file name (files starting with "-"). + * + * @param includeDisabled If `true`, disabled scripts (starting with '-') are included. + * @return A list of script file paths relative to the plugin directory. + */ fun getAllScripts(includeDisabled: Boolean = true): List { val list = ArrayList() scriptsFolder.walkTopDown().forEach { file -> @@ -60,18 +108,16 @@ object ScriptManager { return list } + /** + * Runs a Lua script provided as a string. + * The script is executed within the global Lua environment, and any errors are caught and returned as a string message. + * + * @param text The Lua script content to execute. + * @return An error message if execution fails, or an empty string if the script ran successfully. + */ fun runScript(text: String): String? { try { - globals.set("players", LuaPlayers()) - globals.set("events", LuaEvents()) - globals.set("commands", LuaCommands()) - globals.set("http", LuaHTTPClient()) - globals.set("location", LuaLocations()) - - globals.set("Material", RocketEnums.RocketMaterial.getLuaTable()) - globals.set("WorldType", RocketEnums.RocketWorldType.getLuaTable()) - globals.set("Environment", RocketEnums.RocketEnvironment.getLuaTable()) - + applyGlobals(globals) val scriptResult = globals.load(text, "script", globals) scriptResult.call() @@ -81,4 +127,63 @@ object ScriptManager { return "" } + + /** + * Retrieves a global value by its table name. + * + * @param valueName The name of the global value to retrieve. + * @return The global value if found, or `null` if it is not registered. + */ + private fun getGlobalByTableName(valueName: String): RocketValueBase? { + return enabledGlobals.find { it.valueName == valueName } + } + + /** + * Registers a global value, making it available for use in Lua. + * + * @param global The global value to register. + * @throws RocketError If a global with the same table name is already registered. + */ + fun registerGlobal(global: RocketValueBase) { + if (getGlobalByTableName(global.valueName) != null) { + throw RocketError("A global of the same table name ('${global.valueName}') is already registered.") + } + + enabledGlobals.add(global) + } + + /** + * Unregisters a global value, making it unavailable for use in Lua. + * + * @param global The global value to unregister. + * @throws RocketError If no global with the given table name is registered. + */ + @Suppress("unused") // TODO: more management of globals by the end-user + private fun unregisterGlobal(global: RocketValueBase) { + if (getGlobalByTableName(global.valueName) == null) { + throw RocketError("A global with the table name ('${global.valueName}') is not registered and cannot be unregistered.") + } + + enabledGlobals.remove(global) + } + + /** + * Applies the registered global values to a Lua table. + * This makes all registered globals available to Lua scripts via the provided Lua table. + * + * @param table The Lua table to which the globals will be applied. + */ + private fun applyGlobals(table: LuaTable) { + enabledGlobals.forEach { + println("Processing: ${it::class.qualifiedName}") + when (it) { + is RocketTable -> { + table.set(it.valueName, it.table) + } + is RocketProperty -> { + table.set(it.valueName, RocketLuaValue.valueOf(it.value)) + } + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/RocketEnum.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketEnum.kt new file mode 100644 index 0000000..3be9016 --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketEnum.kt @@ -0,0 +1,44 @@ +package dev.znci.rocket.scripting.api + +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue +import kotlin.reflect.KClass + +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class RocketEnum(name: String) : RocketTable(name) { + fun toLuaTable(enum: Enum<*>): LuaTable { + val table = this.table + for (enumConstant in enum.javaClass.enumConstants) { + table.set(enumConstant.name, RocketLuaValue(LuaValue.valueOf(enumConstant.ordinal))) + } + return table + } + + fun fromLuaTable(luaTable: LuaTable, enum: KClass): Enum<*> { + val enumConstants = enum.java.enumConstants + for (i in 0 until luaTable.length()) { + val name = luaTable[i + 1].toString() + for (enumConstant in enumConstants as Array>) { + if (enumConstant.name == name) { + return enumConstant + } + } + } + throw RocketError("Enum constant not found") + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/RocketError.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketError.kt new file mode 100644 index 0000000..80c9057 --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketError.kt @@ -0,0 +1,18 @@ +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.znci.rocket.scripting.api + +class RocketError(message: String) : Exception(message) \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/RocketLuaValue.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketLuaValue.kt new file mode 100644 index 0000000..d0beff8 --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketLuaValue.kt @@ -0,0 +1,68 @@ +package dev.znci.rocket.scripting.api + +import org.luaj.vm2.LuaValue + +/** + * A wrapper class for `LuaValue` that provides additional functionality and type safety. + * This class extends `LuaValue` and delegates method calls to the wrapped `luaValue` instance. + */ +open class RocketLuaValue(val luaValue: LuaValue = LuaValue.TRUE) : LuaValue() { + + /** + * Returns the type of the wrapped `LuaValue`. + * + * @return The integer type code of the `LuaValue`. + */ + override fun type(): Int { + return luaValue.type() + } + + /** + * Returns the type name of the wrapped `LuaValue`. + * + * @return The name of the Lua type as a string. + */ + override fun typename(): String? { + return luaValue.typename() + } + + companion object { + /** + * Represents a `nil` Lua value. + */ + val NIL = RocketLuaValue(LuaValue.NIL) + + /** + * Represents a `true` Lua value. + */ + val TRUE = RocketLuaValue(LuaValue.TRUE) + + /** + * Represents a `false` Lua value. + */ + val FALSE = RocketLuaValue(LuaValue.FALSE) + + /** + * Converts a given Kotlin value to a `RocketLuaValue` instance. + * + * @param value The value to be converted. + * @return A corresponding `RocketLuaValue` representing the input value. + */ + fun valueOf(value: Any?): RocketLuaValue { + return when (value) { + is String -> RocketLuaValue(LuaValue.valueOf(value)) + is Boolean -> RocketLuaValue(LuaValue.valueOf(value)) + is Int -> RocketLuaValue(LuaValue.valueOf(value)) + is Double -> RocketLuaValue(LuaValue.valueOf(value)) + is RocketTable -> RocketLuaValue(value.table) + is LuaValue -> RocketLuaValue(value) + else -> { + throw RocketError("Unsupported type: ${value?.javaClass?.simpleName ?: "null"}") + NIL + } + } + } + } + + override fun toString(): String = luaValue.toString() +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/RocketNative.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketNative.kt new file mode 100644 index 0000000..d538e1e --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketNative.kt @@ -0,0 +1,238 @@ +package dev.znci.rocket.scripting.api + +import dev.znci.rocket.scripting.api.annotations.RocketNativeFunction +import dev.znci.rocket.scripting.api.annotations.RocketNativeProperty +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue +import org.luaj.vm2.Varargs +import org.luaj.vm2.lib.ThreeArgFunction +import org.luaj.vm2.lib.TwoArgFunction +import org.luaj.vm2.lib.VarArgFunction +import java.util.ArrayList +import kotlin.reflect.* +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.functions +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.primaryConstructor + +/** + * Abstract class RocketNative serves as a bridge between Kotlin and Lua, allowing functions and properties + * to be dynamically registered in a Lua table. + * + * Code is written as Kotlin, and is converted to Lua if the appropriate function/property has the correct annotation. + * + * Functions with the {@code RocketNativeFunction} annotation will be registered, and properties with the {@code RocketNativeProperty} annotation. + */ +@Suppress("unused") +abstract class RocketNative( + /** The name of the Lua table/property for this object. */ + override var valueName: String +) : RocketTable(valueName) { + + /** + * Initializes the RocketNative instance by registering its functions and properties. + */ + init { + registerFunctions(this.table) + registerProperties(this.table) + } + + /** + * Registers functions annotated with {@code RocketNativeFunction} into the Lua table. + * + * @param table The LuaTable instance to register functions to. + */ + private fun registerFunctions(table: LuaTable) { + this::class.functions.forEach { function -> + if (function.findAnnotation() == null) { + return@forEach + } + val annotation = function.findAnnotation() + var annotatedFunctionName = annotation?.name ?: function.name + + if (annotatedFunctionName == "INHERIT_FROM_DEFINITION" ) { + annotatedFunctionName = function.name + } + + table.set(annotatedFunctionName, object : VarArgFunction() { + override fun invoke(args: Varargs): Varargs { + return try { + val kotlinArgs = args.toKotlinArgs(function) + val result = function.call(this@RocketNative, *kotlinArgs) + result.toLuaValue() + } catch (e: Exception) { + error("Error calling ${function.name}: ${e.message}") + } + } + }) + } + } + + /** + * Registers properties annotated with {@code RocketNativeProperty} into the Lua table. + * + * @param table The LuaTable instance to register properties to. + */ + private fun registerProperties(table: LuaTable) { + val properties = this::class.memberProperties + .mapNotNull { prop -> + prop.findAnnotation()?.let { annotation -> + val customName = annotation.name.takeIf { it != "INHERIT_FROM_DEFINITION" } ?: prop.name + customName to prop + } + }.toMap() + + val metatable = LuaTable() + + // Handle property getting + metatable.set("__index", object : TwoArgFunction() { + override fun call(self: LuaValue, key: LuaValue): LuaValue { + val prop = properties[key.tojstring()] as? KProperty<*> + ?: return error("No property '${key.tojstring()}'") + + return try { + val value = prop.getter.call(this@RocketNative) + value.toLuaValue() + } catch (e: Exception) { + error("Error getting '${prop.name}': ${e.message}") + } + } + }) + + // Handle property setting + metatable.set("__newindex", object : ThreeArgFunction() { + override fun call(self: LuaValue, key: LuaValue, value: LuaValue): LuaValue { + val prop = properties[key.tojstring()] as? KMutableProperty<*> + ?: return error("No property '${key.tojstring()}'") + + return try { + prop.setter.call(this@RocketNative, value.toKotlinValue(prop.returnType.classifier)) + TRUE + } catch (e: Exception) { + error("Error setting '${prop.name}': ${e.message}") + } + } + }) + + table.setmetatable(metatable) + } + + /** + * Converts Lua arguments to Kotlin arguments based on function parameter types. + * + * @param func The function whose parameters should be converted. + * @return An array of Kotlin compatible arguments. + */ + private fun Varargs.toKotlinArgs(func: KFunction<*>): Array { + val params = func.parameters.drop(1) // Skip `this` + return params.mapIndexed { index, param -> + this.arg(index + 1).let { arg -> + if (arg.istable()) { + arg.checktable().toClass(func) + } else { + arg.toKotlinValue(param.type.classifier) + } + } + }.toTypedArray() + } + + /** + * Converts a LuaValue to a Kotlin compatible value based on its type. + * + * @param type The expected Kotlin type. + * @return The converted value in Kotlin. + */ + private fun LuaValue.toKotlinValue(type: KClassifier?): Any? { + return when (type) { + String::class -> if (isnil()) null else tojstring() + Boolean::class -> toboolean() + Int::class -> toint() + Double::class -> todouble() + Float::class -> tofloat() + Long::class -> tolong() + else -> this + } + } + + /** + * Converts a Kotlin value to a LuaValue. + * + * @return The corresponding LuaValue. + */ + private fun Any?.toLuaValue(): LuaValue { + return when (this) { + is String -> LuaValue.valueOf(this) + is Boolean -> LuaValue.valueOf(this) + is Int -> LuaValue.valueOf(this) + is Double -> LuaValue.valueOf(this) + is Float -> LuaValue.valueOf(this.toDouble()) + is Long -> LuaValue.valueOf(this.toDouble()) + is RocketTable -> { + val table = this.table + set("__javaClass", TableSetOptions(getter = { LuaValue.valueOf(javaClass.name) })) + table + } + is RocketLuaValue -> { + throw RocketError("RocketLuaValue should not be used as a return type.") + } + is ArrayList<*> -> { + val table = LuaTable() + this.forEachIndexed { index, value -> + table.set(index + 1, value.toLuaValue()) + } + table + } + is Enum<*> -> { + val enumClass = this::class + val enumTable = RocketEnum(enumClass.simpleName!!) + enumTable.toLuaTable(this) + } + else -> { + throw RocketError("Unsupported type: ${this?.javaClass?.simpleName ?: "null"}") + } + } + } + + /** + * Converts a LuaTable into a given class. + */ + private fun LuaTable.toClass(func: KFunction<*>): Any { // XXX: stupid trick to also return enums. + try { + var className = get("__javaClass").tojstring() + if (className == "nil") { + var classes = func.parameters.map { it.type.classifier } + classes = classes.drop(1) + val tableProps = keys().asSequence().map { it.tojstring() }.toList() + for (clazz in classes) { + val constructor = (clazz as KClass<*>).primaryConstructor + if (constructor != null) { + val constructorProps = constructor.parameters.map { it.name } + if (constructorProps.containsAll(tableProps)) { + className = clazz.qualifiedName!! + break + } + } + } + + } + val clazz = Class.forName(className).kotlin + + // if enum class + if (clazz.java.isEnum) { + val renum = RocketEnum(clazz.simpleName!!) + return renum.fromLuaTable(this, clazz) + } else { + val constructor = + clazz.primaryConstructor + ?: throw IllegalArgumentException("No primary constructor found for $className") + val args = constructor.parameters.map { param -> + val value = get(param.name) + value.toKotlinValue(param.type.classifier) + }.toTypedArray() + return constructor.call(*args) as RocketTable + } + } catch (e: Exception) { + throw e + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/RocketProperty.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketProperty.kt new file mode 100644 index 0000000..ba83eab --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketProperty.kt @@ -0,0 +1,34 @@ +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.znci.rocket.scripting.api + +/** + * Represents a property in the Rocket framework, extending the `RocketValueBase` class. + * This class provides a basic structure for holding a property with a `valueName` and a `value`. + * It is used to represent properties that can be accessed and modified (getters/setters). + * + * @param valueName The name of the property. + */ +open class RocketProperty( + override var valueName: String, +) : RocketValueBase(valueName) { + /** + * The value of the property. + * This is a generic any field that can hold any type of data. + * It must be assigned before use. + */ + lateinit var value: Any +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/RocketTable.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketTable.kt new file mode 100644 index 0000000..202e94a --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketTable.kt @@ -0,0 +1,105 @@ +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.znci.rocket.scripting.api + +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue +import org.luaj.vm2.lib.ThreeArgFunction +import org.luaj.vm2.lib.TwoArgFunction + +/** + * Data class that provides the options for setting properties on a `LuaTable`. + * It provides functionality for defining a getter, setter, and validator for properties in a table. + * + * @param getter The getter function for retrieving the property's value. It returns a `LuaValue` and can be `null`. + * @param setter The setter function for setting the property's value. It takes a `LuaValue` and can be `null`. + * @param validator The validator function that checks the validity of the value before setting it. It returns a `Boolean` and can be `null`. + */ +data class TableSetOptions( + val getter: (() -> LuaValue)?, + val setter: ((LuaValue) -> Unit)? = null, + val validator: ((LuaValue) -> Boolean)? = null +) + +/** + * Represents a table, which is a wrapper around a `LuaTable`. + * This class provides functionality for managing Lua properties and exposing them with custom getter, setter, and validation logic. + * It allows you to set properties dynamically on the table, with custom logic for getting, setting, and validating property values. + * + * @param valueName The name of the table, used for specifying the name of the table if it becomes a global. + */ +open class RocketTable( + override var valueName: String +): RocketValueBase(valueName) { + /** + * The internal `LuaTable` instance that holds the table's data. + */ + val table: LuaTable = LuaTable() + + + /** + * Sets a property on the table with custom getter, setter, and validator options. + * + * @param propertyName The name of the property to set on the table. + * @param options The options that define how the property should behave, including getter, setter, and validator. + */ + fun set( + propertyName: String, + options: TableSetOptions + ) { + // FIXME: this null chuck is redundant + val meta = table.getmetatable() ?: LuaTable() + val indexFunction = meta.get("__index") as? TwoArgFunction + val newIndexFunction = meta.get("__newindex") as? ThreeArgFunction + + // Define the getter + if (options.getter != null) { + meta.set("__index", object : TwoArgFunction() { + override fun call(table: LuaValue, key: LuaValue): LuaValue { + if (key.tojstring() == propertyName) { + return options.getter() + } + return indexFunction?.call(table, key) ?: NIL + } + }) + } + + // Define the setter + meta.set("__newindex", object : ThreeArgFunction() { + override fun call(table: LuaValue, key: LuaValue, value: LuaValue): LuaValue { + if (key.tojstring() == propertyName) { + if (options.setter != null) { + if (options.validator != null && !options.validator(value)) { + error("Invalid input for '$propertyName', got ${value.typename()} (value: ${value.tojstring()})") + } + options.setter(value) + } else { + return NIL + } + } else { + newIndexFunction?.call(table, key, value) + } + return NIL + } + }) + + table.setmetatable(meta) + } + + fun setSimple(propertyName: String, value: Any) { + table.set(propertyName, valueOf(value).luaValue) + } +} diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/RocketValueBase.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketValueBase.kt new file mode 100644 index 0000000..8eead85 --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/RocketValueBase.kt @@ -0,0 +1,13 @@ +package dev.znci.rocket.scripting.api + +/** + * Base class for Rocket API values, extending `RocketLuaValue`. + * This class provides a common structure for all Rocket values, including a `valueName` + * that identifies the value within the Rocket API. + * It serves as a superclass for other classes that represent values in the Rocket API. + * + * @param valueName The name of the value, used for specifying the name of the value if it becomes a global. + */ +open class RocketValueBase( + open var valueName: String +): RocketLuaValue() \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/api/annotations/RocketNativeFunction.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/annotations/RocketNativeFunction.kt new file mode 100644 index 0000000..d1651bf --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/annotations/RocketNativeFunction.kt @@ -0,0 +1,30 @@ + +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.znci.rocket.scripting.api.annotations + +/** + * Annotation to mark a function as a native function in the Rocket framework. + * This allows functions to be registered as callable from Lua. + * + * @param name The name of the function when it is added to a LuaTable. + * If not specified, the default name is "INHERIT_FROM_DEFINITION", + * which inherits the function name. + */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@Suppress("unused") +annotation class RocketNativeFunction(val name: String = "INHERIT_FROM_DEFINITION") \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/functions/Players.kt b/src/main/kotlin/dev/znci/rocket/scripting/api/annotations/RocketNativeProperty.kt similarity index 51% rename from src/main/kotlin/dev/znci/rocket/scripting/functions/Players.kt rename to src/main/kotlin/dev/znci/rocket/scripting/api/annotations/RocketNativeProperty.kt index 816f408..be8548b 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/functions/Players.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/api/annotations/RocketNativeProperty.kt @@ -1,3 +1,4 @@ + /** * Copyright 2025 znci * @@ -13,22 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.znci.rocket.scripting.functions - -import dev.znci.rocket.scripting.PlayerManager -import org.bukkit.Bukkit -import org.luaj.vm2.LuaTable -import org.luaj.vm2.LuaValue -import org.luaj.vm2.lib.OneArgFunction - -class LuaPlayers : LuaTable() { - init { - set("get", object : OneArgFunction() { - override fun call(playerName: LuaValue): LuaValue { - val player = Bukkit.getPlayer(playerName.tojstring()) ?: return LuaValue.NIL +package dev.znci.rocket.scripting.api.annotations - return PlayerManager.getPlayerTable(player) - } - }) - } -} \ No newline at end of file +/** + * Annotation to mark a property as a native property in the Rocket framework. + * This allows the property to be exposed to Lua, making it accessible for manipulation from Lua scripts. + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@Suppress("unused") +annotation class RocketNativeProperty(val name: String = "INHERIT_FROM_DEFINITION") \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/classes/Command.kt b/src/main/kotlin/dev/znci/rocket/scripting/classes/Command.kt index 5ff304e..efed6c1 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/classes/Command.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/classes/Command.kt @@ -17,6 +17,22 @@ package dev.znci.rocket.scripting.classes import org.bukkit.command.CommandExecutor +/** + * Represents a command in the Rocket API, containing information about the command's name, + * description, usage, permission, permission message, aliases, argument count, and the executor that handles the command. + * This class encapsulates all necessary details to define and execute a command within the API. + * + * @param name The name of the command. + * @param description A brief description of what the command does. Not shown to the player. + * @param usage The usage information for the command, explaining how to use it. + * The message should be in MiniMessage format. + * @param permission The permission required to execute the command. + * @param permissionMessage The message shown when the user does not have the required permission to use the command. + * The message should be in MiniMessage format. + * @param aliases A list of alternative names for the command. + * @param argCount The number of arguments that the command expects. + * @param executor The `CommandExecutor` responsible for executing the command logic. + */ data class Command( val name: String, var description: String, diff --git a/src/main/kotlin/dev/znci/rocket/scripting/functions/Location.kt b/src/main/kotlin/dev/znci/rocket/scripting/functions/Location.kt deleted file mode 100644 index 20f74e3..0000000 --- a/src/main/kotlin/dev/znci/rocket/scripting/functions/Location.kt +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright 2025 znci - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.znci.rocket.scripting.functions - -import dev.znci.rocket.scripting.util.defineProperty -import dev.znci.rocket.scripting.util.getWorldByNameOrUUID -import org.bukkit.Bukkit -import org.bukkit.Location -import org.bukkit.World -import org.luaj.vm2.LuaTable -import org.luaj.vm2.LuaValue -import org.luaj.vm2.Varargs -import org.luaj.vm2.lib.VarArgFunction -import java.util.* - -class LuaLocations : LuaTable() { - init { - set("new", object : VarArgFunction() { - override fun invoke(args: Varargs): LuaValue { - if (args.narg() < 3) return LuaValue.FALSE - val x = args.arg(1).todouble() - val y = args.arg(2).todouble() - val z = args.arg(3).todouble() - val worldUUID = if (args.narg() >= 4) { - val worldNameOrUUID = args.arg(4).checkjstring() - getWorldByNameOrUUID(worldNameOrUUID).uid.toString() - } else { - Bukkit.getWorlds().first().uid.toString() - } - val yaw = if (args.narg() >= 5) args.arg(5).tofloat() else 0f - val pitch = if (args.narg() >= 6) args.arg(6).tofloat() else 0f - return LuaLocation(x, y, z, worldUUID, yaw, pitch).getLocationTable() - } - }) - } -} - -class LuaLocation( - x: Double, - y: Double, - z: Double, - worldUUID: String, - yaw: Float = 0f, - pitch: Float = 0f -) : LuaTable() { - private var world: World? = Bukkit.getWorld(UUID.fromString(worldUUID)) - private var location: Location = Location(world, x, y, z, yaw, pitch) - - companion object { - fun fromBukkit(location: Location): LuaTable { - return LuaLocation( - location.x, - location.y, - location.z, - location.world.uid.toString(), - location.yaw, - location.pitch - ).getLocationTable() - } - } - - fun getLocationTable(): LuaTable { - val table = LuaTable() - - defineProperty(table, "x", { LuaValue.valueOf(location.x) }, { value -> location.x = value.todouble() }) - defineProperty(table, "y", { LuaValue.valueOf(location.y) }, { value -> location.y = value.todouble() }) - defineProperty(table, "z", { LuaValue.valueOf(location.z) }, { value -> location.z = value.todouble() }) - defineProperty(table, "world", { LuaValue.valueOf(location.world.name) }, { value -> location.world = Bukkit.getWorld(UUID.fromString(value.tojstring())) }) - defineProperty(table, "worldUUID", { LuaValue.valueOf(location.world.uid.toString()) }, { value -> location.world = Bukkit.getWorld(UUID.fromString(value.tojstring())) }) - defineProperty(table, "yaw", { LuaValue.valueOf(location.yaw.toDouble()) }, { value -> location.yaw = value.tofloat() }) - defineProperty(table, "pitch", { LuaValue.valueOf(location.pitch.toDouble()) }, { value -> location.pitch = value.tofloat() }) - - return table - } -} - -fun LuaValue.toBukkitLocation(): Location { - if (this !is LuaTable) { - error("Expected a LuaTable, got ${this.typename()} (value: ${this.tojstring()})") - } - - return try { - val x = this.get("x").todouble() - val y = this.get("y").todouble() - val z = this.get("z").todouble() - val worldUUIDStr = this.get("worldUUID").tojstring() - val worldUUID = try { - UUID.fromString(worldUUIDStr) - } catch (e: IllegalArgumentException) { - error("Invalid 'worldUUID': Not a valid UUID (value: $worldUUIDStr)") - } - val world = Bukkit.getWorld(worldUUID) - val yaw = this.get("yaw").tofloat() - val pitch = this.get("pitch").tofloat() - Location(world, x, y, z, yaw, pitch) - } catch (e: Exception) { - error("LuaTable does not represent a valid location: ${e.message}") - } -} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/functions/Commands.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Commands.kt similarity index 93% rename from src/main/kotlin/dev/znci/rocket/scripting/functions/Commands.kt rename to src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Commands.kt index 00c0980..d8c702f 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/functions/Commands.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Commands.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.znci.rocket.scripting.functions +package dev.znci.rocket.scripting.globals.tables import dev.znci.rocket.i18n.LocaleManager import dev.znci.rocket.scripting.PermissionsManager @@ -24,6 +24,7 @@ import dev.znci.rocket.util.MessageFormatter import org.bukkit.Bukkit import org.bukkit.command.Command import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandMap import org.bukkit.command.CommandSender import org.bukkit.command.defaults.BukkitCommand import org.bukkit.entity.Player @@ -82,7 +83,7 @@ class LuaCommands : LuaTable() { registerCommand(customCommand) - return LuaValue.NIL + return NIL } }) @@ -94,7 +95,7 @@ class LuaCommands : LuaTable() { ScriptManager.enabledCommands.remove(commandStr) - return LuaValue.NIL + return NIL } }) @@ -207,15 +208,15 @@ class LuaCommands : LuaTable() { val aliases = commandReference.aliases for (i in 1..aliases.size) { - luaAliases.set(i, LuaValue.valueOf(aliases[i - 1])) + luaAliases.set(i, valueOf(aliases[i - 1])) } defineProperty(commandTable, "aliases", { luaAliases }) - defineProperty(commandTable, "description", { LuaValue.valueOf(commandReference.description) }) - defineProperty(commandTable, "usage", { LuaValue.valueOf(commandReference.usage) }) - defineProperty(commandTable, "permission", { LuaValue.valueOf(commandReference.permission) }) - defineProperty(commandTable, "permissionMessage", { LuaValue.valueOf(commandReference.permissionMessage) }) - defineProperty(commandTable, "argCount", { LuaValue.valueOf(commandReference.argCount) }) + defineProperty(commandTable, "description", { valueOf(commandReference.description) }) + defineProperty(commandTable, "usage", { valueOf(commandReference.usage) }) + defineProperty(commandTable, "permission", { valueOf(commandReference.permission) }) + defineProperty(commandTable, "permissionMessage", { valueOf(commandReference.permissionMessage) }) + defineProperty(commandTable, "argCount", { valueOf(commandReference.argCount) }) return commandTable } @@ -229,7 +230,7 @@ class LuaCommands : LuaTable() { private fun registerCommand(command: Command) { val commandMap = Bukkit.getServer().javaClass.getDeclaredField("commandMap").apply { isAccessible = true - }.get(Bukkit.getServer()) as org.bukkit.command.CommandMap + }.get(Bukkit.getServer()) as CommandMap commandMap.register(command.name, command) } @@ -237,7 +238,7 @@ class LuaCommands : LuaTable() { private fun unregisterCommand(commandName: String) { val commandMap = Bukkit.getServer().javaClass.getDeclaredField("commandMap").apply { isAccessible = true - }.get(Bukkit.getServer()) as org.bukkit.command.CommandMap + }.get(Bukkit.getServer()) as CommandMap val existingCommand = commandMap.getCommand(commandName) if (existingCommand != null) { @@ -249,7 +250,7 @@ class LuaCommands : LuaTable() { private fun convertArgsToLua(args: Array): LuaTable { val luaArgs = LuaTable() for ((index, arg) in args.withIndex()) { - luaArgs.set(index + 1, LuaValue.valueOf(arg)) + luaArgs.set(index + 1, valueOf(arg)) } return luaArgs } diff --git a/src/main/kotlin/dev/znci/rocket/scripting/functions/Events.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Events.kt similarity index 93% rename from src/main/kotlin/dev/znci/rocket/scripting/functions/Events.kt rename to src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Events.kt index 1d0ac0e..d3488a8 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/functions/Events.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Events.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.znci.rocket.scripting.functions +package dev.znci.rocket.scripting.globals.tables import dev.znci.rocket.scripting.ScriptManager import dev.znci.rocket.scripting.events.EventListener @@ -31,7 +31,7 @@ class LuaEvents : LuaTable() { ScriptManager.usedEvents[eventClass] = callback.checkfunction() } - return LuaValue.NIL + return NIL } }) } diff --git a/src/main/kotlin/dev/znci/rocket/scripting/functions/HTTPClient.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/HTTPClient.kt similarity index 92% rename from src/main/kotlin/dev/znci/rocket/scripting/functions/HTTPClient.kt rename to src/main/kotlin/dev/znci/rocket/scripting/globals/tables/HTTPClient.kt index 6cf5372..7122d69 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/functions/HTTPClient.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/HTTPClient.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package dev.znci.rocket.scripting.functions +package dev.znci.rocket.scripting.globals.tables import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue @@ -30,9 +30,9 @@ class LuaHTTPClient : LuaTable() { return try { val response = httpGet(url.tojstring()) - LuaValue.valueOf(response) + valueOf(response) } catch (e: Exception) { - LuaValue.NIL + NIL } } }) diff --git a/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Location.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Location.kt new file mode 100644 index 0000000..ace971f --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Location.kt @@ -0,0 +1,188 @@ +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.znci.rocket.scripting.globals.tables + +import dev.znci.rocket.scripting.api.RocketNative +import dev.znci.rocket.scripting.api.RocketTable +import dev.znci.rocket.scripting.api.annotations.RocketNativeFunction +import dev.znci.rocket.scripting.api.annotations.RocketNativeProperty +import dev.znci.rocket.scripting.util.getWorldByNameOrUUID +import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.World +import org.luaj.vm2.LuaTable +import org.luaj.vm2.LuaValue +import java.util.* + +class LuaLocations : RocketNative("location") { + @RocketNativeFunction + fun new(x: Double, y: Double, z: Double, worldUUID: String, yaw: Float = 0f, pitch: Float = 0f): LuaLocation { + return LuaLocation(x, y, z, worldUUID, yaw, pitch) + } +} + +@Suppress("unused") +class LuaLocation( + x: Double, + y: Double, + z: Double, + worldUUID: String, + yaw: Float = 0f, + pitch: Float = 0f +) : RocketNative("") { + private var world: World? = getWorldByNameOrUUID(worldUUID) + private var location: Location = Location(world, x, y, z, yaw, pitch) + + @RocketNativeProperty("x") + var xProperty: Double + get() = location.x + set(value) { + location.x = value + } + + @RocketNativeProperty("y") + var yProperty: Double + get() = location.y + set(value) { + location.y = value + } + + @RocketNativeProperty("z") + var zProperty: Double + get() = location.z + set(value) { + location.z = value + } + + @RocketNativeProperty("world") + var worldProperty: String + get() = location.world.name + set(value) { + location.world = getWorldByNameOrUUID(value) + } + + @RocketNativeProperty("worldUUID") + var worldUUIDProperty: String + get() = location.world.uid.toString() + set(value) { + location.world = getWorldByNameOrUUID(value) + } + + @RocketNativeProperty("yaw") + var yawProperty: Float + get() = location.yaw + set(value) { + location.yaw = value + } + + @RocketNativeProperty("pitch") + var pitchProperty: Float + get() = location.pitch + set(value) { + location.pitch = value + } + + override fun get(name: LuaValue): LuaValue { + return when (name.tojstring()) { + "x" -> LuaValue.valueOf(location.x) + "y" -> LuaValue.valueOf(location.y) + "z" -> LuaValue.valueOf(location.z) + "world" -> LuaValue.valueOf(location.world.name) + "worldUUID" -> LuaValue.valueOf(location.world.uid.toString()) + "yaw" -> LuaValue.valueOf(location.yaw.toDouble()) + "pitch" -> LuaValue.valueOf(location.pitch.toDouble()) + else -> super.get(name) + } + } + + companion object { + fun fromBukkit(location: Location): RocketTable { + return LuaLocation( + location.x, + location.y, + location.z, + location.world.uid.toString(), + location.yaw, + location.pitch + ) + } + } +} + + +// +//class LuaLocation( +// x: Double, +// y: Double, +// z: Double, +// worldUUID: String, +// yaw: Float = 0f, +// pitch: Float = 0f +//) : LuaTable() { +// private var world: World? = Bukkit.getWorld(UUID.fromString(worldUUID)) +// private var location: Location = Location(world, x, y, z, yaw, pitch) +// +// companion object { +// fun fromBukkit(location: Location): LuaTable { +// return LuaLocation( +// location.x, +// location.y, +// location.z, +// location.world.uid.toString(), +// location.yaw, +// location.pitch +// ).getLocationTable() +// } +// } +// +// fun getLocationTable(): LuaTable { +// val table = LuaTable() +// +// defineProperty(table, "x", { valueOf(location.x) }, { value -> location.x = value.todouble() }) +// defineProperty(table, "y", { valueOf(location.y) }, { value -> location.y = value.todouble() }) +// defineProperty(table, "z", { valueOf(location.z) }, { value -> location.z = value.todouble() }) +// defineProperty(table, "world", { valueOf(location.world.name) }, { value -> location.world = Bukkit.getWorld(UUID.fromString(value.tojstring())) }) +// defineProperty(table, "worldUUID", { valueOf(location.world.uid.toString()) }, { value -> location.world = Bukkit.getWorld(UUID.fromString(value.tojstring())) }) +// defineProperty(table, "yaw", { valueOf(location.yaw.toDouble()) }, { value -> location.yaw = value.tofloat() }) +// defineProperty(table, "pitch", { valueOf(location.pitch.toDouble()) }, { value -> location.pitch = value.tofloat() }) +// +// return table +// } +//} + +fun LuaValue.toBukkitLocation(): Location { + if (this !is LuaTable) { + error("Expected a LuaTable, got ${this.typename()} (value: ${this.tojstring()})") + } + + return try { + val x = this.get("x").todouble() + val y = this.get("y").todouble() + val z = this.get("z").todouble() + val worldUUIDStr = this.get("worldUUID").tojstring() + val worldUUID = try { + UUID.fromString(worldUUIDStr) + } catch (e: IllegalArgumentException) { + error("Invalid 'worldUUID': Not a valid UUID (value: $worldUUIDStr)") + } + val world = Bukkit.getWorld(worldUUID) + val yaw = this.get("yaw").tofloat() + val pitch = this.get("pitch").tofloat() + Location(world, x, y, z, yaw, pitch) + } catch (e: Exception) { + error("LuaTable does not represent a valid location: ${e.message}") + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Players.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Players.kt new file mode 100644 index 0000000..4c73149 --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Players.kt @@ -0,0 +1,240 @@ +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.znci.rocket.scripting.globals.tables + +import dev.znci.rocket.scripting.PermissionsManager +import dev.znci.rocket.scripting.api.RocketError +import dev.znci.rocket.scripting.api.RocketNative +import dev.znci.rocket.scripting.api.RocketTable +import dev.znci.rocket.scripting.api.annotations.RocketNativeFunction +import dev.znci.rocket.scripting.api.annotations.RocketNativeProperty +import dev.znci.rocket.util.MessageFormatter +import net.kyori.adventure.title.Title +import net.kyori.adventure.title.TitlePart +import org.bukkit.Bukkit +import org.bukkit.GameMode +import org.bukkit.Location +import org.bukkit.entity.Player +import java.time.Duration + +@Suppress("unused") +class LuaPlayers : RocketNative("players") { + @RocketNativeFunction("get") + fun getPlayerByName(playerName: String): LuaPlayer { + val player = Bukkit.getPlayer(playerName) ?: throw RocketError("Player not found") + + return LuaPlayer(player) + } + + @RocketNativeFunction("getByUUID") + fun getPlayerByUUID(playerUUID: String): LuaPlayer { + val player = Bukkit.getPlayer(playerUUID) ?: throw RocketError("Player not found") + + return LuaPlayer(player) + } + + @RocketNativeFunction("getAll") + fun getAllPlayers(): List { + return Bukkit.getOnlinePlayers().map { LuaPlayer(it) } + } +} + +data class TitleTimeTable( + val fadeIn: Long, + val stay: Long, + val fadeOut: Long +) : RocketTable("") + +@Suppress("unused") +class LuaPlayer( + val player: Player +) : RocketNative("") { + @RocketNativeFunction + fun send(message: Any): Boolean { + val messageComponent = MessageFormatter.formatMessage(message.toString()) + player.sendMessage(messageComponent) + return true + } + + @RocketNativeFunction + fun sendActionbar(message: String): Boolean { + val messageComponent = MessageFormatter.formatMessage(message) + player.sendActionBar(messageComponent) + return true + } + + @RocketNativeFunction + fun sendTitle(message: String, timeTable: TitleTimeTable): Boolean { + val messageComponent = MessageFormatter.formatMessage(message) + + val times = Title.Times.times( + Duration.ofMillis(timeTable.fadeIn * 50), + Duration.ofMillis(timeTable.stay * 50), + Duration.ofMillis(timeTable.fadeOut * 50) + ) + player.sendTitlePart(TitlePart.TITLE, messageComponent) + player.sendTitlePart(TitlePart.TIMES, times) + return true + } + + @RocketNativeFunction + fun sendSubtitle(message: String, timeTable: TitleTimeTable): Boolean { + val messageComponent = MessageFormatter.formatMessage(message) + val times = Title.Times.times( + Duration.ofMillis(timeTable.fadeIn * 50), + Duration.ofMillis(timeTable.stay * 50), + Duration.ofMillis(timeTable.fadeOut * 50) + ) + player.sendTitlePart(TitlePart.SUBTITLE, messageComponent) + player.sendTitlePart(TitlePart.TIMES, times) + return true + } + + @RocketNativeFunction + fun setPlayerTime(value: Long, relative: Boolean): Boolean { + player.setPlayerTime(value, relative) + return true + } + + @RocketNativeFunction + fun addPermission(value: String): Boolean { + player.addAttachment( + Bukkit.getPluginManager().getPlugin("Rocket")!! + ).setPermission(value, true) + return true + } + + @RocketNativeFunction + fun op(): Boolean { + player.isOp = true + return true + } + + @RocketNativeFunction + fun deop(): Boolean { + player.isOp = false + return true + } + + @RocketNativeFunction + fun teleport(location: LuaLocation): Boolean { + player.teleport(location.toBukkitLocation()) + return true + } + + @RocketNativeFunction + fun hasPermission(value: String): Boolean { + println(PermissionsManager.hasPermission(player, value)) + return PermissionsManager.hasPermission(player, value) + } + + @RocketNativeFunction + fun isInGroup(value: String): Boolean { + return PermissionsManager.isPlayerInGroup(player, value) + } + + @RocketNativeFunction + fun setGamemode(value: String): Boolean { + if (false) { // TODO: enum + throw RocketError("Invalid gamemode") + } + + player.gameMode = GameMode.valueOf(value) + return true + } + + @RocketNativeProperty + val name: String + get() { + return player.name + } + + + @RocketNativeProperty + val uuid: String + get() { + return player.uniqueId.toString() + } + + @RocketNativeProperty + val world: String + get() { + return player.world.name + } + + @RocketNativeProperty + val ip: String? + get() { + return player.address?.hostString + } + + @RocketNativeProperty + val isFlying: Boolean + get() { + return player.isFlying + } + + @RocketNativeProperty + val isSneaking: Boolean + get() { + return player.isSneaking + } + + @RocketNativeProperty + val isSprinting: Boolean + get() { + return player.isSprinting + } + + @RocketNativeProperty + val isBlocking: Boolean + get() { + return player.isBlocking + } + + @RocketNativeProperty + val isSleeping: Boolean + get() { + return player.isSleeping + } + + private val block = player.getTargetBlockExact(100) + + @RocketNativeProperty + var targetBlockType: String = "" + + @RocketNativeProperty + var targetBlockLocation: Location = Location(Bukkit.getWorld("world"), 0.0, 0.0, 0.0) + + @RocketNativeProperty + var targetBlockLightLevel: Double = 0.0 + + @RocketNativeProperty + var targetBlockTemperature: Double = 0.0 + + @RocketNativeProperty + var targetBlockHumidity: Double = 0.0 + + init { + block?.let { + targetBlockType = block.type.toString() + targetBlockLocation = block.location + targetBlockLightLevel = block.lightLevel.toDouble() + targetBlockTemperature = block.temperature + targetBlockHumidity = block.humidity + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Test.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Test.kt new file mode 100644 index 0000000..5a678e7 --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Test.kt @@ -0,0 +1,42 @@ +/** + * Copyright 2025 znci + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.znci.rocket.scripting.globals.tables + +import dev.znci.rocket.scripting.api.RocketNative +import dev.znci.rocket.scripting.api.annotations.RocketNativeFunction +import dev.znci.rocket.scripting.api.annotations.RocketNativeProperty + +@Suppress("unused") // FIXME: Remove this class after new api migration +class SimpleTest : RocketNative("testNext") { + + private var internalString = "Initial" + + @RocketNativeProperty + var stringRepresentation: String + get() { + println("Getter") + return internalString + } + set(value) { + println("Setter called with: $value") + internalString = value.uppercase() // just an example to showcase custom setter. + } + + @RocketNativeFunction + fun testFunction(arg1: String): String { + return "Hello, $arg1!" + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/globals/values/TestValue.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/values/TestValue.kt new file mode 100644 index 0000000..eb2be75 --- /dev/null +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/values/TestValue.kt @@ -0,0 +1,9 @@ +package dev.znci.rocket.scripting.globals.values + +import dev.znci.rocket.scripting.api.RocketProperty + +class TestValue : RocketProperty("testValue") { + init { + this.value = "test" + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/util/Location.kt b/src/main/kotlin/dev/znci/rocket/scripting/util/Location.kt index 5ee04a6..beb462d 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/util/Location.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/util/Location.kt @@ -19,11 +19,22 @@ import org.bukkit.Bukkit import org.bukkit.World import java.util.* +/** + * Retrieves a world from the server by either its name or UUID. + * If the provided string is a valid world name, it fetches the corresponding world. + * If the string is a valid UUID, it tries to fetch the world using with that UUID. + * If neither a world by name nor by UUID exists, it throws an error. + * + * @param worldNameOrUUID The name or UUID of the world to retrieve. + * @return The `World` object corresponding to the provided name or UUID. + * @throws IllegalArgumentException If the provided string is neither a valid world name nor a valid UUID. + * @throws IllegalStateException If the world could not be found by name or UUID. + */ fun getWorldByNameOrUUID(worldNameOrUUID: String): World { return Bukkit.getWorld(worldNameOrUUID) ?: try { Bukkit.getWorld(UUID.fromString(worldNameOrUUID)) - } catch (e: IllegalArgumentException) { + } catch (_: IllegalArgumentException) { null } ?: error("World '$worldNameOrUUID' not found!") } \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/util/Properties.kt b/src/main/kotlin/dev/znci/rocket/scripting/util/Properties.kt index 801b5e7..72c18c0 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/util/Properties.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/util/Properties.kt @@ -20,6 +20,21 @@ import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.ThreeArgFunction import org.luaj.vm2.lib.TwoArgFunction +/** + * Defines a property on a Lua table with a getter, setter, and optional validator. + * This function allows you to define custom properties that can be accessed and mutated using Lua table indexing. + * If a setter is provided, it ensures that the setter validates the value using the optional validator before setting it. + * If the property is read-only, an error is thrown if an attempt is made to set it. + * + * @param table The Lua table on which the property will be defined. + * @param propertyName The name of the property to define. + * @param getter The function to retrieve the value of the property. + * @param setter An optional function to set the value of the property. If null, the property is read-only. + * @param validator An optional function to validate the value before setting it. Returns `true` if valid, `false` if invalid. + * @throws org.luaj.vm2.LuaError If the value provided to the setter is invalid according to the validator. + */ + +@Deprecated("RocketNative Kotlin should be used instead.", ReplaceWith("RocketNative"), DeprecationLevel.WARNING) fun defineProperty( table: LuaTable, propertyName: String, @@ -35,7 +50,7 @@ fun defineProperty( if (key.tojstring() == propertyName) { return getter() } - return indexFunction?.call(table, key) ?: LuaValue.NIL + return indexFunction?.call(table, key) ?: NIL } }) @@ -54,7 +69,7 @@ fun defineProperty( } else { newIndexFunction?.call(table, key, value) } - return LuaValue.NONE + return NONE } }) diff --git a/src/main/kotlin/dev/znci/rocket/util/MessageFormatter.kt b/src/main/kotlin/dev/znci/rocket/util/MessageFormatter.kt index 9df4bf6..4d122ab 100644 --- a/src/main/kotlin/dev/znci/rocket/util/MessageFormatter.kt +++ b/src/main/kotlin/dev/znci/rocket/util/MessageFormatter.kt @@ -3,7 +3,19 @@ package dev.znci.rocket.util import net.kyori.adventure.text.Component import net.kyori.adventure.text.minimessage.MiniMessage +/** + * The `MessageFormatter` object is responsible for formatting and deserializing messages. + * It uses the MiniMessage library to convert a message string into a `Component` object, + * allowing the message to be styled with color codes, special formatting, and other features. + */ object MessageFormatter { + /** + * Formats a message string into a `Component` object using the MiniMessage library. + * The message can include special formatting syntax (e.g., color codes, placeholders). + * + * @param message The message string to format. It can contain MiniMessage formatting tags. + * @return A `Component` object representing the formatted message. + */ fun formatMessage(message: String): Component { val miniMessage = MiniMessage.miniMessage() val messageComponent = miniMessage.deserialize(message) diff --git a/src/main/kotlin/dev/znci/rocket/util/RocketEnum.kt b/src/main/kotlin/dev/znci/rocket/util/RocketEnum.kt deleted file mode 100644 index a534cac..0000000 --- a/src/main/kotlin/dev/znci/rocket/util/RocketEnum.kt +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright 2025 znci - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package dev.znci.rocket.util - -import org.bukkit.Material -import org.bukkit.World -import org.bukkit.WorldType -import org.luaj.vm2.LuaTable - -class RocketEnum(private val values: List) { - @Suppress("unused") - fun getValues(): List { - return values - } - - @Suppress("unused") - fun isValid(key: String): Boolean { - return values.contains(key) - } - - fun getLuaTable(): LuaTable { - val table = LuaTable() - - values.forEach() { value -> - table.set(value, value) - } - - return table - } -} - -fun > enumToStringList(enumClass: Class): List { - return enumClass.enumConstants.map { it.name } -} - - -data object RocketEnums { - val RocketMaterial = RocketEnum(enumToStringList(Material::class.java)) - val RocketWorldType = RocketEnum(enumToStringList(WorldType::class.java)) - val RocketEnvironment = RocketEnum(enumToStringList(World.Environment::class.java)) -} \ No newline at end of file diff --git a/src/main/resources/locales/en_GB.yml b/src/main/resources/locales/en_GB.yml index dc906cf..0757d34 100644 --- a/src/main/resources/locales/en_GB.yml +++ b/src/main/resources/locales/en_GB.yml @@ -1,24 +1,20 @@ - messages: - bracket_color: "&7" + bracket_color: "" bracket_opening_text: "[" bracket_closing_text: "]" - main_color: "&a" + main_color: "" main_text: "Rocket" prefix: "{bracket_color}{bracket_opening_text}{main_color}{main_text}{bracket_color}{bracket_closing_text}" prefix_error: "{bracket_color}{bracket_opening_text}{error_color}{main_text}{bracket_color}{bracket_closing_text}" - error_color: "&c" - alt_error_color: "&e" + error_color: "" + alt_error_color: "" generic_error: "Error: %s" - success_color: "&a" + success_color: "" - invalid_action: "{error_color}Invalid action." + invalid_action: "Invalid action." usage_template: "{error_color}Usage: " - - no_permission: "{error_color}Insufficient permissions." - rocket_command: usage: "{prefix_error} {usage_template}/rocket " scripts_folder_not_found: "{prefix} {error_color}Scripts folder not found."