diff --git a/build.gradle.kts b/build.gradle.kts index 9908107..b69f409 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,6 +52,9 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.12.2") testImplementation("org.mockbukkit.mockbukkit:mockbukkit-v1.21:4.45.1") testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0") + + // TODO check if needed + implementation("com.google.guava:guava:32.0.1-jre") } val targetJavaVersion = 21 diff --git a/src/main/kotlin/dev/znci/rocket/Rocket.kt b/src/main/kotlin/dev/znci/rocket/Rocket.kt index 3aee1e8..7bd7c4d 100644 --- a/src/main/kotlin/dev/znci/rocket/Rocket.kt +++ b/src/main/kotlin/dev/znci/rocket/Rocket.kt @@ -66,7 +66,8 @@ open class Rocket : RocketAddon() { this.getCommand("rocket")?.setExecutor(RocketCommand(this)) // Register all events - EventListener.registerAllEvents() + logger.info("Rocket plugin enabled") + EventListener.cacheEvents() // Enable the base Rocket methods and globals this.onAddonEnable() @@ -86,4 +87,5 @@ open class Rocket : RocketAddon() { override fun onDisable() { logger.info("Rocket plugin disabled") } + } diff --git a/src/main/kotlin/dev/znci/rocket/commands/RocketCommand.kt b/src/main/kotlin/dev/znci/rocket/commands/RocketCommand.kt index cbe6579..d087612 100644 --- a/src/main/kotlin/dev/znci/rocket/commands/RocketCommand.kt +++ b/src/main/kotlin/dev/znci/rocket/commands/RocketCommand.kt @@ -17,6 +17,7 @@ package dev.znci.rocket.commands import dev.znci.rocket.i18n.LocaleManager import dev.znci.rocket.scripting.ScriptManager +import dev.znci.rocket.scripting.ScriptManager.disableFile import dev.znci.rocket.scripting.ScriptManager.scriptsFolder import org.bukkit.command.Command import org.bukkit.command.CommandSender @@ -32,7 +33,8 @@ class RocketCommand(private val plugin: JavaPlugin) : TabExecutor { } val action = args[0].lowercase() - val scriptName = if (!args[1].endsWith(".lua")) "${args[1]}.lua" else args[1] + val scriptName = args[1] + val rawScriptName = if (!scriptName.endsWith(".lua")) "${scriptName}.lua" else scriptName if (!scriptsFolder.exists() || !scriptsFolder.isDirectory) { sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.scripts_folder_not_found")) @@ -41,7 +43,7 @@ class RocketCommand(private val plugin: JavaPlugin) : TabExecutor { when (action) { "reload" -> { - if (scriptName.lowercase() == "config.lua") { + if (scriptName.lowercase() == "config") { plugin.reloadConfig() val defaultLocale = plugin.config.getString("locale", "en_GB").toString() @@ -51,16 +53,24 @@ class RocketCommand(private val plugin: JavaPlugin) : TabExecutor { sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.config_reloaded")) return true + } else if (scriptName.lowercase() == "all") { + val results = ScriptManager.loadAll() + if (results.isNotEmpty()) { + results.forEach { error -> + sender.sendMessage(LocaleManager.getMessageAsComponent("generic_error", error ?: "Unknown error")) + } + } + return true } - val scriptFile = File(scriptsFolder, scriptName) + val scriptFile = File(scriptsFolder, rawScriptName) + if (!scriptFile.exists()) { - sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.script_not_found", scriptName)) + sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.script_not_found", rawScriptName)) return true } - val content = scriptFile.readText() - val result = ScriptManager.runScript(content) + val result = ScriptManager.loadScript(scriptFile) if (result !== "") { sender.sendMessage(LocaleManager.getMessageAsComponent("generic_error", result ?: "Unknown error")) @@ -68,19 +78,24 @@ class RocketCommand(private val plugin: JavaPlugin) : TabExecutor { sender.sendMessage( LocaleManager.getMessageAsComponent( "rocket_command.script_reloaded", - scriptName + rawScriptName ) ) } } "disable" -> { - val scriptFile = File(scriptsFolder, scriptName) + val scriptFile = File(scriptsFolder, rawScriptName) if (!scriptFile.exists()) { - sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.script_not_found", scriptName)) + sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.script_not_found", rawScriptName)) return true } - sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.script_disabled", scriptName)) + // TODO: Add disabling of file (with '-') + // For another PR though + + disableFile(scriptFile) + + sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.script_disabled", rawScriptName)) } else -> { sender.sendMessage(LocaleManager.getMessageAsComponent("rocket_command.usage")) @@ -99,9 +114,11 @@ class RocketCommand(private val plugin: JavaPlugin) : TabExecutor { if (args.size == 1) { return mutableListOf("reload", "disable") } else if (args.size == 2) { - return if (args[0] == "reload") ScriptManager.getAllScripts().toMutableList() - else if (args[0] == "disable") ScriptManager.getAllScripts(false).toMutableList() - else null + val list: MutableList? = + if (args[0] == "reload") ScriptManager.getAllScripts().toMutableList().also { it.add("config") } + else if (args[0] == "disable") ScriptManager.getAllScripts(false).toMutableList().also { it.add("config") } + else null + return list } return null diff --git a/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt b/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt index 722b8b3..81b9410 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/ScriptManager.kt @@ -22,6 +22,7 @@ import dev.znci.twine.TwineProperty import dev.znci.twine.TwineTable import dev.znci.twine.TwineValueBase import org.bukkit.event.Event +import org.luaj.vm2.* import java.io.File import org.luaj.vm2.Globals import org.luaj.vm2.LuaError @@ -45,11 +46,22 @@ object ScriptManager { */ var scriptsFolder: File = File("") + /** + * A map of loaded scripts associated by file path + */ + val loadedScriptFiles = mutableMapOf>>() + + /** + * A map associating Lua event sections with a class. + * This mainly helps with disabling scripts + */ + val eventScript = mutableMapOf, Class>() + /** * 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>() + val usedEvents = mutableMapOf, MutableList>>() /** * A map of enabled commands by their names. @@ -69,7 +81,6 @@ object ScriptManager { * * @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 } @@ -78,12 +89,10 @@ object ScriptManager { * 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 -> if (file.isFile && !file.startsWith("-")) { - val content = file.readText() - runScript(content) + runScript(file) } } } @@ -106,21 +115,67 @@ object ScriptManager { return list } + /** + * Recursively loads all scripts located in the scripts folder + * @return A list of error messages where execution failed. The list will be empty if there were no errors + */ + fun loadAll(): List { + val results = mutableListOf() + getAllScripts(false).forEach { script -> + val result = loadScript(File("plugins/rocket/scripts/", script)) + if (result != "") + results.add(result) + } + return results + } + + /** + * Loads a script based off of a [File] object + * + * @param scriptFile The script to load + * @return An error message if execution fails, or an empty string if the script ran successfully. + */ + fun loadScript(scriptFile: File): String? { + if (loadedScriptFiles[scriptFile.absolutePath] != null) { + disableFile(scriptFile) + } + val result = runScript(scriptFile) + return result + } + + /** + * Disables a script based off of a [File] object + * @param scriptFile The script to disable + */ + fun disableFile(scriptFile: File) { + val functions = loadedScriptFiles[scriptFile.absolutePath]!! + for (function in functions) { + val eventClass = eventScript[function]!! + for (eventCallback in usedEvents[eventClass]?:continue) { + if (eventCallback == function) usedEvents.remove(eventClass) + } + eventScript.remove(function) + } + } + /** * 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. + * @param scriptFile 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? { + fun runScript(scriptFile: File): String? { + + val content = scriptFile.readText() + try { applyGlobals() - val scriptResult = globals.load(text, "script", globals) + val scriptResult = globals.load(content, "::${scriptFile.absolutePath}::", globals) scriptResult.call() - } catch (e: LuaError) { - return e.message + } catch (error: LuaError) { + return error.message } return "" diff --git a/src/main/kotlin/dev/znci/rocket/scripting/events/EventListener.kt b/src/main/kotlin/dev/znci/rocket/scripting/events/EventListener.kt index 803b14e..2db75aa 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/events/EventListener.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/events/EventListener.kt @@ -15,148 +15,230 @@ */ package dev.znci.rocket.scripting.events +import com.google.common.reflect.ClassPath import dev.znci.rocket.scripting.ScriptManager +import dev.znci.rocket.scripting.globals.tables.LuaLocation import dev.znci.rocket.scripting.globals.tables.LuaPlayer +import dev.znci.twine.TwineTable +import net.kyori.adventure.text.Component import org.bukkit.Bukkit +import org.bukkit.Location +import org.bukkit.entity.Player import org.bukkit.event.Cancellable import org.bukkit.event.Event import org.bukkit.event.EventPriority import org.bukkit.event.Listener +import org.bukkit.inventory.EquipmentSlot import org.bukkit.plugin.Plugin -import org.luaj.vm2.LuaBoolean import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue +import org.luaj.vm2.lib.TwoArgFunction import org.luaj.vm2.lib.ZeroArgFunction - +import java.util.* object EventListener : Listener { private val plugin: Plugin? = Bukkit.getPluginManager().getPlugin("rocket") - fun registerAllEvents() { - val eventClasses = getSupportedEvents() - - plugin?.logger?.info("Found ${eventClasses.size} events") - for (eventClass in eventClasses) { - try { - if (plugin != null) { - plugin.logger.info("Registering event: ${eventClass.simpleName}") - Bukkit.getPluginManager().registerEvent( - eventClass, - this, - EventPriority.NORMAL, - { _, event -> - handleEvent(event) - }, - plugin - ) - } - } catch (e: Exception) { - e.printStackTrace() + lateinit var SUPPORTED_EVENTS: HashSet> + private set + + fun registerEvent(eventClass: Class) { + try { + if (plugin != null) { + plugin.logger.info("Registering event: ${eventClass.simpleName}") + Bukkit.getPluginManager().registerEvent( + eventClass, + this, + EventPriority.NORMAL, + { _, event -> + handleEvent(event) + }, + plugin + ) } + } catch (e: Exception) { + e.printStackTrace() } } + fun cacheEvents() { + SUPPORTED_EVENTS = getSupportedEvents() + } + private fun handleEvent(event: Event) { - ScriptManager.usedEvents.forEach { (eventClass, callback) -> - if (eventClass.isInstance(event)) { - val luaTable = convertEventToLua(event) - callback.call(luaTable) - } + val eventCallbacks = ScriptManager.usedEvents[getEventByName(event.eventName)] ?: return + eventCallbacks.forEach { callback -> + val luaTable = convertEventToLua(event) + callback(luaTable) + println(callback) } } - private fun getSupportedEvents(): List> { - return listOf( - org.bukkit.event.player.PlayerJoinEvent::class.java, - org.bukkit.event.block.BlockBreakEvent::class.java, - org.bukkit.event.block.BlockPlaceEvent::class.java, - org.bukkit.event.player.PlayerMoveEvent::class.java, - org.bukkit.event.player.PlayerQuitEvent::class.java, - org.bukkit.event.player.PlayerInteractEvent::class.java, - io.papermc.paper.event.player.AsyncChatEvent::class.java - ) + private fun getSupportedEvents(): HashSet> { + val set = hashSetOf>() + for (eventClass in getBukkitEventClasses()) { + // TODO Add a debug setting to show these kinds of things + // (Similar to Skript's) + println("Caching event ${eventClass.simpleName}") + set.add(eventClass) + } + return set } - fun getEventByName(name: String): Class? { - return getSupportedEvents().find { it.simpleName.equals(name, true) } + @Suppress("UNCHECKED_CAST") + private fun getBukkitEventClasses(): List> { + return getClasses("org.bukkit.event") + .filter { Event::class.java.isAssignableFrom(it) && it != Event::class.java } + .map { it as Class } } - private fun convertEventToLua(event: Event): LuaTable { - val luaTable = LuaTable() + private fun getClasses(packageName: String): List> { + return ClassPath.from(this::class.java.classLoader) + .allClasses + .filter { it.packageName.startsWith(packageName, ignoreCase = true) } + .map { it.load() } + } - // Player fields & checking - var player: org.bukkit.entity.Player? = null + fun getEventByName(name: String): Class? { + return SUPPORTED_EVENTS.find { it.simpleName.equals(name, true) } + } - // Check if there is a field for player - val playerField = event.javaClass.declaredFields.find { it.name == "player" } - if (playerField != null) { - playerField.isAccessible = true - val fieldPlayer = playerField.get(event) - if (fieldPlayer is org.bukkit.entity.Player) { - player = fieldPlayer + private inline fun getValueFromField(event: Event, vararg valuesToTry: String): V? { + valuesToTry.forEach { value -> + val field = event.javaClass.declaredFields.find { it.name == value } + if (field != null) { + field.isAccessible = true + val valueFromField = field.get(event) + if (valueFromField is V) { + return valueFromField + } } } + return null + } - // If there is no field for player, check if there is a method for player - if (player == null) { - val playerProperty = event.javaClass.methods.find { it.name == "getPlayer" } - if (playerProperty != null) { - val playerFromProperty = playerProperty.invoke(event) as? org.bukkit.entity.Player - player = playerFromProperty + private inline fun getValueFromFunction(event: Event, vararg valuesToTry: String): V? { + valuesToTry.forEach { value -> + val valueProperty = event.javaClass.methods.find { it.name == value } + if (valueProperty != null) { + val valueFromProperty = valueProperty.invoke(event) as? V + return valueFromProperty } } + return null + } - if (player != null) { - luaTable.set("player", LuaPlayer(player)) - } - - // Interaction event - if (event is org.bukkit.event.player.PlayerInteractEvent) { - val hand = event.hand - luaTable.set("hand", hand.toString()) - } - - // Quit event - if (event is org.bukkit.event.player.PlayerQuitEvent) { - val quitMessage = event.quitMessage() - luaTable.set("quitMessage", quitMessage.toString()) + private inline fun getValueFromEvent(event: Event, vararg valuesToTry: String): V? { - val reason = event.reason - luaTable.set("reason", reason.toString()) - } - - // Move event - val fields = event.javaClass.declaredFields.map { it.name } - if ("from" in fields) { - val fromField = event.javaClass.getDeclaredField("from") - fromField.isAccessible = true - val from = fromField.get(event) - @Suppress("ControlFlowWithEmptyBody") // Remove this when implementing - if (from is org.bukkit.Location) { - // TODO: Finish this when mibers creates PR which adds new location class - } - } - - if ("to" in fields) { - val toField = event.javaClass.getDeclaredField("to") - toField.isAccessible = true - val to = toField.get(event) - @Suppress("ControlFlowWithEmptyBody") // Remove this when implementing - if (to is org.bukkit.Location) { - // TODO: Finish this when mibers creates PR which adds new location class - } + var valueObj: V? = getValueFromField(event, *valuesToTry) + if (valueObj == null) { + valueObj = getValueFromFunction(event, *valuesToTry) } + return valueObj + } - // Cancellable events - if (event is Cancellable) { - luaTable.set("cancel", object : ZeroArgFunction() { - override fun call(): LuaValue { - event.isCancelled = true - return valueOf(event.isCancelled) + /** + * Todo: Create a registry for extras, default to casting + * Maybe for a different PR, it depends on what znci is feeling + */ + + private fun convertEventToLua(event: Event): TwineTable { + val table = TwineTable("luaEvent") + val meta = table.getmetatable() ?: LuaTable() + val indexFunction = meta.get("__index") as? TwoArgFunction + meta.set("__index", object : TwoArgFunction() { + override fun call(table: LuaValue, key: LuaValue): LuaValue { + return when (key.tojstring()) { + "player" -> { + val player: Player? = getValueFromEvent(event, "player", "getPlayer") + if (player != null) { + return LuaPlayer(player) + } + return NIL.also { + error("No player in a '${event.eventName}' event!") + } + } + + "hand" -> { + val hand: EquipmentSlot? = getValueFromEvent(event, "hand") + if (hand != null) { + return valueOf(hand.toString()) + } + return NIL.also { + error("No hand in a '${event.eventName}' event!") + } + } + + "message" -> { + val message: Any? = getValueFromEvent( + event, + "message", + "getMessage", + "getJoinMessage", + "getDeathMessage", + "getQuitMessage", + "getKickMessage" + ) + if (message != null) { + if (message is String || message is Component) return valueOf(message.toString()) + return NIL.also { + error("Non-string/component object found in place of a message. Found '$message'") + } + } + return NIL.also { + error("No message in a '${event.eventName}' event!") + } + } + + "from" -> { + val location: Location? = getValueFromEvent(event, "from", "getFrom") + return LuaLocation.fromBukkit(location?:return NIL.also { + error("Value 'from' of a '${event.eventName}' event returned null") + }) + } + + "to" -> { + val location: Location? = getValueFromEvent(event, "to", "getTo") + return LuaLocation.fromBukkit(location?:return NIL.also { + error("Value 'to' of a '${event.eventName}' event returned null") + }) + } + + "cancel" -> { + when (event) { + is Cancellable -> { + return object : ZeroArgFunction() { + override fun call(): LuaValue { + event.isCancelled = true + return valueOf(event.isCancelled) + } + } + } + else -> NIL.also { + error("Cannot access or modify 'cancel' field in an uncancellable '${event.eventName}' event") + } + } + } + + else -> { + + val value: Any? = getValueFromEvent(event, key.tojstring(), + "get${ + key.tojstring() + .replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + }") + if (value != null) return valueOf(value.toString()) + + indexFunction?.call(table, key) ?: NIL.also { + error("A '${event.eventName}' event has no member '${key.tojstring()}'") + } + } } - }) - } + } - return luaTable + }) + table.setmetatable(meta) + return table } + } \ No newline at end of file diff --git a/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Events.kt b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Events.kt index d3488a8..90b8850 100644 --- a/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Events.kt +++ b/src/main/kotlin/dev/znci/rocket/scripting/globals/tables/Events.kt @@ -17,22 +17,92 @@ package dev.znci.rocket.scripting.globals.tables import dev.znci.rocket.scripting.ScriptManager import dev.znci.rocket.scripting.events.EventListener +import dev.znci.twine.TwineNative +import dev.znci.twine.TwineTable +import dev.znci.twine.annotations.TwineNativeFunction +import org.luaj.vm2.LuaFunction import org.luaj.vm2.LuaTable import org.luaj.vm2.LuaValue import org.luaj.vm2.lib.TwoArgFunction -class LuaEvents : LuaTable() { - init { - set("on", object : TwoArgFunction() { - override fun call(eventName: LuaValue, callback: LuaValue): LuaValue { - val eventClass = EventListener.getEventByName(eventName.tojstring()) +class LuaEvents : TwineNative("event") { - if (eventClass != null) { - ScriptManager.usedEvents[eventClass] = callback.checkfunction() - } + @TwineNativeFunction("on") + fun registerEvent(eventName: String, callback: (TwineTable) -> Unit) { + val eventClass = EventListener.getEventByName(eventName) - return NIL + if (eventClass != null) { + if (!EventListener.SUPPORTED_EVENTS.contains(eventClass)) return + + if (ScriptManager.usedEvents[eventClass] == null) EventListener.registerEvent(eventClass) + + // Cache all information about the loaded script + // Ensures that we can retrieve the relevant information when dealing with unloading + + if (ScriptManager.usedEvents[eventClass] == null) { + ScriptManager.usedEvents[eventClass] = mutableListOf() } - }) + ScriptManager.usedEvents[eventClass]!!.add(callback) + + ScriptManager.eventScript[callback] = eventClass + + val fileName = getFileNameFromLuaFunction(callback) + + if (ScriptManager.loadedScriptFiles[fileName] == null) { + ScriptManager.loadedScriptFiles[fileName] = mutableListOf() + } + ScriptManager.loadedScriptFiles[fileName]!!.add(callback) + } + } + +// init { +// set("on", object : TwoArgFunction() { +// override fun call(eventName: LuaValue, callback: LuaValue): LuaValue { +// val eventClass = EventListener.getEventByName(eventName.tojstring()) +// +// if (eventClass != null) { +// if (!EventListener.SUPPORTED_EVENTS.contains(eventClass)) return LuaValue.NIL.also { println("Not contained") } +// +// if (ScriptManager.usedEvents[eventClass] == null) EventListener.registerEvent(eventClass) +// +// // Cache all information about the loaded script +// // Ensures that we can retrieve the relevant information when dealing with unloading +// +// if (ScriptManager.usedEvents[eventClass] == null) { +// ScriptManager.usedEvents[eventClass] = mutableListOf() +// } +// val function = callback.checkfunction() +// ScriptManager.usedEvents[eventClass]!!.add(function) +// +// ScriptManager.eventScript[function] = eventClass +// +// val fileName = getFileNameFromLuaFunction(function) +// +// if (ScriptManager.loadedScriptFiles[fileName] == null) { +// ScriptManager.loadedScriptFiles[fileName] = mutableListOf() +// } +// ScriptManager.loadedScriptFiles[fileName]!!.add(function) +// } +// +// return NIL +// } +// }) +// } + + /** + * Scuffed stuff, let's find a better way if we can + * @see ScriptManager.runScript + */ + + private fun getFileNameFromLuaFunction(function: Function1?): String { + var fileName = "" + var previousChar: Char? = null + var record = false + for (char in function.toString()) { + if ((previousChar ?: ' ') == ':' && char == ':') if (!record) record = true else break + if (record) fileName = "$fileName$char" + previousChar = char + } + return fileName.subSequence(1, fileName.length-1).toString() } } \ No newline at end of file