From 45da3dc39d41a31d0ecaa503fd1526859566e203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=B0=AC=EC=98=81?= Date: Tue, 12 May 2026 15:40:10 +0900 Subject: [PATCH] feat: support listener annotations Parse listener metadata for event priority and cancellation behavior, validate priority values, and register Bukkit event bindings with the requested options. --- .../dev/jetpack/engine/parser/Parser.kt | 24 ++++++++++++- .../jetpack/engine/parser/ast/Statement.kt | 10 ++++++ .../jetpack/engine/resolver/NameResolver.kt | 6 ++++ .../dev/jetpack/engine/runtime/Interpreter.kt | 16 +++++++-- .../kotlin/dev/jetpack/event/EventBridge.kt | 35 +++++++++++++------ .../kotlin/dev/jetpack/script/ScriptRunner.kt | 14 ++++++-- 6 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/dev/jetpack/engine/parser/Parser.kt b/src/main/kotlin/dev/jetpack/engine/parser/Parser.kt index ab22ba4..c5816e5 100644 --- a/src/main/kotlin/dev/jetpack/engine/parser/Parser.kt +++ b/src/main/kotlin/dev/jetpack/engine/parser/Parser.kt @@ -36,6 +36,7 @@ class Parser(private val tokens: List) { private var pos = 0 private var statementDepth = 0 private var pendingCommandAnnotations: CommandAnnotations = CommandAnnotations.EMPTY + private var pendingListenerAnnotations: ListenerAnnotations = ListenerAnnotations.EMPTY private fun peek(): Token = tokens[pos] private fun peek(offset: Int): Token = @@ -76,6 +77,8 @@ class Parser(private val tokens: List) { if (isAtEnd()) { stmts.addAll(pendingMeta); break } if (pendingMeta.isNotEmpty() && isCommandDeclarationAhead()) { pendingCommandAnnotations = buildCommandAnnotations(pendingMeta) + } else if (pendingMeta.isNotEmpty() && isListenerDeclarationAhead()) { + pendingListenerAnnotations = buildListenerAnnotations(pendingMeta) } else { stmts.addAll(pendingMeta) } @@ -91,6 +94,12 @@ class Parser(private val tokens: List) { return i < tokens.size && tokens[i].type == TokenType.KW_COMMAND } + private fun isListenerDeclarationAhead(): Boolean { + var i = pos + while (i < tokens.size && tokens[i].type in ACCESS_MODIFIERS) i++ + return i < tokens.size && tokens[i].type == TokenType.KW_LISTENER + } + private fun buildCommandAnnotations(meta: List): CommandAnnotations { var description: String? = null var permission: String? = null @@ -109,6 +118,18 @@ class Parser(private val tokens: List) { return CommandAnnotations(description, permission, permissionMessage, usage, aliases) } + private fun buildListenerAnnotations(meta: List): ListenerAnnotations { + var priority: String? = null + var ignoreCancelled = false + for (m in meta) { + when (m.key) { + "priority" -> priority = m.value + "ignoreCancelled" -> ignoreCancelled = m.value.trim().lowercase() == "true" + } + } + return ListenerAnnotations(priority, ignoreCancelled) + } + private fun parseAliasList(raw: String): List { val trimmed = raw.trim() if (trimmed.isEmpty()) return emptyList() @@ -453,6 +474,7 @@ class Parser(private val tokens: List) { } private fun parseListenerDecl(access: AccessModifier, line: Int): Statement.ListenerDecl { + val annotations = pendingListenerAnnotations.also { pendingListenerAnnotations = ListenerAnnotations.EMPTY } expect(TokenType.KW_LISTENER, "Expected 'listener'") val eventType = expect(TokenType.IDENTIFIER, "Expected event type").value val name = expect(TokenType.IDENTIFIER, "Expected listener name").value @@ -465,7 +487,7 @@ class Parser(private val tokens: List) { skipNewlines() expect(TokenType.RPAREN, "Expected ')' after sender parameter") val body = parseBlock() - return Statement.ListenerDecl(access, eventType, name, sender, body, line) + return Statement.ListenerDecl(access, eventType, name, sender, body, annotations, line) } private fun parseIfStmt(line: Int): Statement.IfStmt { diff --git a/src/main/kotlin/dev/jetpack/engine/parser/ast/Statement.kt b/src/main/kotlin/dev/jetpack/engine/parser/ast/Statement.kt index 294ec67..f09fb53 100644 --- a/src/main/kotlin/dev/jetpack/engine/parser/ast/Statement.kt +++ b/src/main/kotlin/dev/jetpack/engine/parser/ast/Statement.kt @@ -42,6 +42,7 @@ sealed class Statement { val name: String, val senderParam: String?, val body: List, + val annotations: ListenerAnnotations, override val line: Int, ) : Statement() data class IfStmt( @@ -99,6 +100,15 @@ sealed class Statement { ) : Statement() } +data class ListenerAnnotations( + val priority: String?, + val ignoreCancelled: Boolean, +) { + companion object { + val EMPTY = ListenerAnnotations(null, false) + } +} + data class CommandAnnotations( val description: String?, val permission: String?, diff --git a/src/main/kotlin/dev/jetpack/engine/resolver/NameResolver.kt b/src/main/kotlin/dev/jetpack/engine/resolver/NameResolver.kt index e3dab23..0f3387d 100644 --- a/src/main/kotlin/dev/jetpack/engine/resolver/NameResolver.kt +++ b/src/main/kotlin/dev/jetpack/engine/resolver/NameResolver.kt @@ -111,6 +111,12 @@ class NameResolver(private val reservedNames: Set = emptySet()) { if (!isFileScope) error("Listener can only be declared at file scope", stmt.line) if (JetpackEvent.resolve(stmt.eventType) == null) error("Unknown event type '${stmt.eventType}'", stmt.line) + val priority = stmt.annotations.priority + if (priority != null) { + val validPriorities = setOf("LOWEST", "LOW", "NORMAL", "HIGH", "HIGHEST", "MONITOR") + if (priority.uppercase() !in validPriorities) + error("Unknown event priority '$priority'. Valid values: ${validPriorities.joinToString()}", stmt.line) + } val prevFn = insideFunction val prevLoop = insideLoop val prevFile = isFileScope diff --git a/src/main/kotlin/dev/jetpack/engine/runtime/Interpreter.kt b/src/main/kotlin/dev/jetpack/engine/runtime/Interpreter.kt index 0513a47..9935dfe 100644 --- a/src/main/kotlin/dev/jetpack/engine/runtime/Interpreter.kt +++ b/src/main/kotlin/dev/jetpack/engine/runtime/Interpreter.kt @@ -206,7 +206,13 @@ class CommandNode( interface ScriptEnvironment { fun registerInterval(name: String, ms: Int, body: suspend () -> Unit): IntervalHandle - fun registerListener(eventType: String, line: Int, body: suspend (JetValue) -> Unit): ListenerHandle + fun registerListener( + eventType: String, + line: Int, + priority: String?, + ignoreCancelled: Boolean, + body: suspend (JetValue) -> Unit, + ): ListenerHandle fun registerCommand(node: CommandNode): CommandHandle suspend fun runThread(body: suspend () -> T): T } @@ -494,8 +500,12 @@ class Interpreter( if (stmt.senderParam != null) child.define(stmt.senderParam, senderValue) try { executeBlock(stmt.body, child) } catch (_: ReturnSignal) {} } - val handle = env?.registerListener(stmt.eventType, stmt.line, body) - ?: DetachedListenerHandle(body) + val handle = env?.registerListener( + stmt.eventType, stmt.line, + stmt.annotations.priority, + stmt.annotations.ignoreCancelled, + body, + ) ?: DetachedListenerHandle(body) withScopeRuntimeError(stmt.line) { scope.define(stmt.name, JListener(handle)) } diff --git a/src/main/kotlin/dev/jetpack/event/EventBridge.kt b/src/main/kotlin/dev/jetpack/event/EventBridge.kt index abe587d..097e38a 100644 --- a/src/main/kotlin/dev/jetpack/event/EventBridge.kt +++ b/src/main/kotlin/dev/jetpack/event/EventBridge.kt @@ -27,6 +27,8 @@ object EventBridge { private class EventBinding( val eventClassName: String, val eventClass: Class, + val priority: EventPriority, + val ignoreCancelled: Boolean, val listener: Listener = object : Listener {}, @Volatile var registered: Boolean = false, ) @@ -38,13 +40,18 @@ object EventBridge { plugin: JetpackPlugin, eventClassName: String, scriptFile: String, + priority: EventPriority, + ignoreCancelled: Boolean, callback: (JetValue) -> Unit, ): ListenerHandle { val eventClass = resolveEventClass(eventClassName) requireNotNull(eventClass) { "Unknown event type '$eventClassName'" } - val binding = bindings.computeIfAbsent(eventClassName) { EventBinding(eventClassName, eventClass) } + val bindingKey = bindingKey(eventClassName, priority, ignoreCancelled) + val binding = bindings.computeIfAbsent(bindingKey) { + EventBinding(eventClassName, eventClass, priority, ignoreCancelled) + } val entry = ListenerEntry(scriptFile, callback) - handlers.getOrPut(eventClassName) { CopyOnWriteArrayList() }.add(entry) + handlers.getOrPut(bindingKey) { CopyOnWriteArrayList() }.add(entry) reconcileBinding(plugin, binding) return object : ListenerHandle { @@ -66,7 +73,7 @@ object EventBridge { if (entry.destroyed) return false entry.destroyed = true entry.active = false - handlers[eventClassName]?.remove(entry) + handlers[bindingKey]?.remove(entry) cleanupEmptyBinding(binding) return true } @@ -82,7 +89,7 @@ object EventBridge { } fun unregisterAll(scriptFile: String) { - for ((eventClassName, list) in handlers) { + for ((bindingKey, list) in handlers) { val removed = list.removeAll { entry -> if (entry.scriptFile != scriptFile) return@removeAll false entry.destroyed = true @@ -90,7 +97,7 @@ object EventBridge { true } if (removed) { - bindings[eventClassName]?.let(::cleanupEmptyBinding) + bindings[bindingKey]?.let(::cleanupEmptyBinding) } } } @@ -120,8 +127,9 @@ object EventBridge { JetpackEvent.resolve(name)?.eventClass private fun reconcileBinding(plugin: JetpackPlugin, binding: EventBinding) { + val key = bindingKey(binding.eventClassName, binding.priority, binding.ignoreCancelled) synchronized(binding) { - val activeEntries = handlers[binding.eventClassName].orEmpty().filter { it.active && !it.destroyed } + val activeEntries = handlers[key].orEmpty().filter { it.active && !it.destroyed } when { activeEntries.isEmpty() && binding.registered -> { HandlerList.unregisterAll(binding.listener) @@ -131,10 +139,10 @@ object EventBridge { plugin.server.pluginManager.registerEvent( binding.eventClass, binding.listener, - EventPriority.NORMAL, + binding.priority, eventCallback@{ _, event -> if (!binding.eventClass.isInstance(event)) return@eventCallback - val callbacks = handlers[binding.eventClassName].orEmpty() + val callbacks = handlers[key].orEmpty() .filter { it.active && !it.destroyed } if (callbacks.isEmpty()) return@eventCallback val reflected = reflectToJetValue(event) @@ -149,6 +157,7 @@ object EventBridge { callbacks.forEach { it.callback(jetValue) } }, plugin, + binding.ignoreCancelled, ) binding.registered = true } @@ -157,8 +166,9 @@ object EventBridge { } private fun cleanupEmptyBinding(binding: EventBinding) { + val key = bindingKey(binding.eventClassName, binding.priority, binding.ignoreCancelled) synchronized(binding) { - val list = handlers[binding.eventClassName] + val list = handlers[key] if (list != null && list.isNotEmpty()) { if (list.none { it.active && !it.destroyed } && binding.registered) { HandlerList.unregisterAll(binding.listener) @@ -166,12 +176,15 @@ object EventBridge { } return } - handlers.remove(binding.eventClassName) + handlers.remove(key) if (binding.registered) { HandlerList.unregisterAll(binding.listener) binding.registered = false } - bindings.remove(binding.eventClassName, binding) + bindings.remove(key, binding) } } + + private fun bindingKey(eventClassName: String, priority: EventPriority, ignoreCancelled: Boolean): String = + "$eventClassName:${priority.name}:$ignoreCancelled" } diff --git a/src/main/kotlin/dev/jetpack/script/ScriptRunner.kt b/src/main/kotlin/dev/jetpack/script/ScriptRunner.kt index 16d9826..838337f 100644 --- a/src/main/kotlin/dev/jetpack/script/ScriptRunner.kt +++ b/src/main/kotlin/dev/jetpack/script/ScriptRunner.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.withContext import org.bukkit.command.Command import org.bukkit.command.CommandMap import org.bukkit.command.CommandSender +import org.bukkit.event.EventPriority import org.bukkit.plugin.Plugin import java.io.File import java.util.IdentityHashMap @@ -255,7 +256,13 @@ class ScriptRunner(private val plugin: JetpackPlugin) { return interval } - override fun registerListener(eventType: String, line: Int, body: suspend (JetValue) -> Unit): ListenerHandle { + override fun registerListener( + eventType: String, + line: Int, + priority: String?, + ignoreCancelled: Boolean, + body: suspend (JetValue) -> Unit, + ): ListenerHandle { if (JetpackEvent.resolve(eventType) == null) { reportError(module.meta.scriptId, "Unknown event type '$eventType'", line, module.sourceLines) return object : ListenerHandle { @@ -266,7 +273,10 @@ class ScriptRunner(private val plugin: JetpackPlugin) { override fun isActive(): Boolean = false } } - val inner = EventBridge.register(plugin, eventType, module.meta.scriptId) { senderValue -> + val eventPriority = priority?.let { + runCatching { EventPriority.valueOf(it.uppercase()) }.getOrNull() + } ?: EventPriority.NORMAL + val inner = EventBridge.register(plugin, eventType, module.meta.scriptId, eventPriority, ignoreCancelled) { senderValue -> coroutineScope.launch { try { body(senderValue)