Skip to content

feat: SurfEventBus — central annotation-driven cross-plugin event bus#319

Draft
Copilot wants to merge 2 commits intoversion/26.1from
copilot/implement-surf-event-bus
Draft

feat: SurfEventBus — central annotation-driven cross-plugin event bus#319
Copilot wants to merge 2 commits intoversion/26.1from
copilot/implement-surf-event-bus

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 23, 2026

Plugins like surf-advancements currently must take direct dependencies on every plugin whose events they care about. This adds a platform-independent event bus to surf-api-core so any plugin can fire and listen to events without coupling.

Event hierarchy

  • SurfEvent — common marker
  • SurfAsyncEvent / SurfSyncEvent — base classes; async allows suspend listeners, sync validates against them at registration time
  • SurfCancellableEvent — interface with isCancelled / cancel()

API (surf-api-core)

  • @SurfEventHandler(priority, ignoreCancelled) annotation; SurfEventPriority enum (LOWEST→MONITOR), Bukkit-independent
  • SurfEventBus interface with companion delegation via requiredService():
    • registerListeners(Any) — scans for @SurfEventHandler methods, validates suspend/param constraints
    • callAsync(SurfAsyncEvent) / callSync(SurfSyncEvent) — dispatches in priority order
    • registerHandler / registerAsyncHandler — lambda-based alternative
  • on<T>, onAsync<T>, registerListeners(vararg) DSL extensions
  • Java SurfSyncEventInvoker / SurfAsyncEventInvoker functional interfaces + hidden-class templates using the existing InvokerFactory / HiddenInvokerUtil infrastructure

Implementation (surf-api-core-server)

  • SurfEventBusImpl@AutoService(SurfEventBus::class), ConcurrentHashMap<EventClass, ConcurrentHashMap<Priority, CopyOnWriteArrayList<HandlerEntry>>>; sealed HandlerEntry covers both invoker-based and lambda-based handlers
  • Async dispatch bridges the Continuation-passing invoker into coroutine context via suspendCoroutineUninterceptedOrReturn

Example events (surf-api-paper)

AbstractSurfPlayerEvent and PlayerAfkStateChangeEvent as concrete migration examples (previously lived in surf-playtime).

Usage

// Annotation-based
object MyListener {
    @SurfEventHandler(priority = SurfEventPriority.HIGH)
    suspend fun on(event: PlayerAfkStateChangeEvent) { /* ... */ }
}
SurfEventBus.registerListeners(MyListener)

// Lambda-based
val token = SurfEventBus.onAsync<PlayerAfkStateChangeEvent> { event ->
    if (event.toState) rewardAfkAdvancement(event.playerUuid)
}
SurfEventBus.unregisterListeners(token)

// Fire (from surf-playtime, no inbound dep on surf-advancements)
SurfEventBus.callAsync(PlayerAfkStateChangeEvent(uuid, wasAfk, isNowAfk))
Original prompt

Ziel

Implementiere einen zentralen SurfEventBus in surf-api, der es allen surf-Plugins (surf-playtime, surf-clan, surf-advancements, etc.) ermöglicht, ihre eigenen Events zu definieren und zu feuern – und anderen Plugins zu erlauben, darauf zu lauschen, ohne eine direkte Dependency auf das feuernde Plugin zu haben.

Der Event Bus soll wie die bestehenden Redis/RabbitMQ-Listener per Annotation über der Methode funktionieren und die bestehende InvokerAPI (InvokerFactory, HiddenInvokerUtil, InvokerClassData) für Performance nutzen.


Anforderungen

1. Event-Typen (zwei Varianten)

SurfAsyncEvent (Marker-Interface / Basisklasse)

  • Listener-Methoden dürfen suspend sein
  • Dispatch passiert in einem Coroutine-Scope (z.B. mit kotlinx.coroutines)
  • Alle Listener werden sequenziell awaited bevor call() zurückkommt

SurfSyncEvent (Marker-Interface / Basisklasse)

  • Listener-Methoden dürfen NICHT suspend sein (wird zur Registrierungszeit validiert und geworfen)
  • Dispatch passiert synchron, kein Coroutine-Overhead

Beide sollten ein gemeinsames Eltern-Interface SurfEvent haben.

Beide sollten cancellable sein können, über ein SurfCancellableEvent-Interface (mit isCancelled Property und cancel()-Methode).

2. Annotation @SurfEventHandler

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class SurfEventHandler(
    val priority: SurfEventPriority = SurfEventPriority.NORMAL,
    val ignoreCancelled: Boolean = false
)

EventPriority soll ein eigenes Enum sein (nicht Bukkit-abhängig), da der Event Bus plattformunabhängig in surf-api-core leben soll:

enum class SurfEventPriority { LOWEST, LOW, NORMAL, HIGH, HIGHEST, MONITOR }

3. SurfEventBus (API-Interface in surf-api-core)

interface SurfEventBus {
    fun registerListeners(listener: Any)
    fun unregisterListeners(listener: Any)
    suspend fun callAsync(event: SurfAsyncEvent): SurfAsyncEvent
    fun callSync(event: SurfSyncEvent): SurfSyncEvent
    
    companion object : SurfEventBus by requiredService()
}
  • registerListeners(listener) scannt alle Methoden des Objekts nach @SurfEventHandler
  • Validiert bei SurfSyncEvent-Handlern, dass die Methode nicht suspend ist
  • Validiert bei SurfAsyncEvent-Handlern, dass die Methode genau einen Parameter hat (den Event-Typ)
  • Nutzt InvokerFactory + HiddenInvokerUtil für performante Dispatch-Implementierung

4. Invoker-Infrastruktur (analog zu Redis/RabbitMQ)

Erstelle analog zur bestehenden Invoker-Infrastruktur:

Invoker-Interfaces (Java):

// Für sync events
@FunctionalInterface
@InternalInvokerApi
public interface SurfSyncEventInvoker {
    void invoke(SurfSyncEvent event);
}

// Für async events - suspend wird als Object + Continuation zurückgegeben
@FunctionalInterface  
@InternalInvokerApi
public interface SurfAsyncEventInvoker {
    Object invoke(SurfAsyncEvent event, Continuation<?> continuation);
}

Template-Klassen analog zu anderen Invoker-Templates im Projekt (für hidden class bytecode).

SurfEventInvokerFactory – nutzt InvokerFactory<SurfSyncEventInvoker> bzw. InvokerFactory<SurfAsyncEventInvoker>.

5. Implementierung SurfEventBusImpl (in surf-api-core-server)

  • Registriert sich via @AutoService(SurfEventBus::class)
  • Intern: ConcurrentHashMap<Class<out SurfEvent>, ConcurrentSkipListMap<SurfEventPriority, List<RegisteredHandler>>> gruppiert nach Event-Klasse und Priority
  • RegisteredHandler enthält: Listener-Instanz, Invoker (entweder sync oder async), Priority, ignoreCancelled
  • Listener werden nach Priority sortiert (LOWEST zuerst, MONITOR zuletzt)
  • Für Async: Dispatch über coroutineScope sequenziell mit suspend
  • Für Sync: Direkter Aufruf

6. Erweiterungen / DSL (in surf-api-core)

// Kotlin Extension für einfaches Registrieren mehrerer Listener
fun SurfEventBus.registerListeners(vararg listeners: Any) {
    listeners.forEach { registerListeners(it) }
}

// Inline-Listener ohne Klasse
inline fun <reified T : SurfSyncEvent> SurfEventBus.on(
    priority: SurfEventPriority = SurfEventPriority.NORMAL,
    ignoreCancelled: Boolean = false,
    crossinline handler: (T) -> Unit
): Any // gibt das registrierte Listener-Objekt zurück (für unregister)

inline fun <reified T : SurfAsyncEvent> SurfEventBus.onAsync(
    priority: SurfEventPriority = SurfEventPriority.NORMAL, 
    ignoreCancelled: Boolean = false,
    crossinline handler: suspend (T) -> Unit
): Any

7. Beispiel-Events in surf-api-paper

Erstelle ein Package dev.slne.surf.api.paper.event.surf mit folgenden Beispiel-Events:

// Beispiel für ein AFK Event (das vorher in surf-playtime war)
class PlayerAfkStateChangeEvent(
    val playerUuid: UUID,
    val fromState: Boolean,
    val toState: Boolean,
) : SurfAsyncEvent(), SurfCancellableEvent

// Abstrakte Basis für Player-Events
...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

- Add SurfEvent, SurfAsyncEvent, SurfSyncEvent, SurfCancellableEvent base types
- Add SurfEventHandler annotation and SurfEventPriority enum
- Add SurfEventBus interface with annotation-based and lambda-based registration
- Add event-bus-extensions.kt with on<T>/onAsync<T>/registerListeners(vararg) helpers
- Add SurfSyncEventInvoker/SurfAsyncEventInvoker interfaces and hidden class templates
- Add SurfEventInvokerFactory and SurfEventBusImpl (AutoService) in core-server
- Add AbstractSurfPlayerEvent and PlayerAfkStateChangeEvent example events in paper
- Update ABI dumps for surf-api-core and surf-api-paper

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: twisti-dev <76837088+twisti-dev@users.noreply.github.com>
Copilot AI changed the title [WIP] Add central SurfEventBus for surf-plugins feat: SurfEventBus — central annotation-driven cross-plugin event bus Apr 23, 2026
Copilot AI requested a review from twisti-dev April 23, 2026 08:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants