SilkMC / silk

Silk is a Minecraft API for Kotlin - targetting Fabric, Quilt and Paper
https://silkmc.net/silk/docs/
GNU General Public License v3.0
100 stars 12 forks source link

Implement event API in core module #27

Closed jakobkmar closed 1 year ago

jakobkmar commented 1 year ago

Event API

This implements an event API in the silk-core module. Additionally it adds some basic first events.

For reviewers: the relevant API part is located here https://github.com/SilkMC/silk/blob/events/silk-core/src/main/kotlin/net/silkmc/silk/core/event/Event.kt

Please comment on this PR to suggest changes, since this API is not necessarily final yet!

Goals

Provide an event API for the following situations:

The use case for handlers to mutate and or cancel the event must be supported. Reflection must be avoided.

Usage:

To declare an event

// really simple, a list of listeners which will be invoked in place
val myEvent = Event.onlySyncImmutable<MyEvent>()
// the same but cancellable
val myEvent = Event.onlySync<MyEvent, EventScope.Cancellable>()

// can be listened to both synchronously and asynchronously via a flow
// there is a mutable scope for synchronous listeners, if they want to cancel etc
val myEvent = Event.syncAsync<MyEvent, EventScope.Cancellable>()

// the event scope is always immutable and listeners may be synchronous or asynchronous
val myEvent = Event.syncAsyncImmutable<MyEvent>()

To make your event discoverable, use an extension function:

// do not pollute the main events object, use your own namespace
val Events.MyModSlug get() = MyModSlugEvents
object MyModSlugEvents

// categorize events under your namespace if needed
val MyModSlugEvents.MySpecialEvents get() = MySpecialEvents

// define the events
object MySpecialEvents {
    val myEvent = Event.onlySync<MyEvent>()
}

Now you can listen to the event:

// simple synchronous listener
Events.MyModSlug.MySpecialEvents.myEvent.listen {

}

// the same with a different priority
Events.MyModSlug.MySpecialEvents.myEvent.listen(EventPriority.FIRST) {

}

// or collect the flow
// in scope
coroutineScope {
    Events.MyModSlug.MySpecialEvents.myEvent.collectInScope {

    }
}
// or bare bones (this never completes)
Events.MyModSlug.MySpecialEvents.myEvent.collect {

}

Note: silk itself does not use a separate namespace, to shorten the most commonly used events

To invoke the event: from Kotlin:

MySpecialEvents.myEvent.invoke(MyEvent())

from Java (needed for Mixins):

MySpecialEvents.INSTANCE.getMyEvent().invoke(MyEvent())

For mutable events, you can pass an event scope, see https://github.com/SilkMC/silk/blob/events/silk-core/src/main/java/net/silkmc/silk/core/mixin/server/MixinMinecraftServer.java#L59

jakobkmar commented 1 year ago

Thanks for the review!

Are there any plans to support and enforce event cancellation via silk or is this something the user has to handle themselves by mutating and checking if the event is cancelled manually?

Will it be possible to return a value to the invoker so that the cause can for example be cancelled?

Both are supported by mutable event classes which need to be handled synchronously. Some base interfaces, e.g. for cancellable events will be added to this PR.

What are your thoughts on some kind of priority system? It seems to me that without such system the order in which listeners are called is mostly random (assuming for example all listeners are registered in the initialization function of their mods) which would make these features somewhat useless or at least very hard to use effectively as soon as multiple listeners want to work on the same event.

That's definitely something which must be supported, and basically the reason why this PR is still a draft. I still have to think about what priority + dynamic registration + flows will look like.

jakobkmar commented 1 year ago

Added mutable event scopes to support cancellable events. Applied all suggestions and introduced event priority to synchronous listeners. Additionally, I added a test to cover the event system.

A re-review / approval would be appreciated!

jakobkmar commented 1 year ago

Thanks for all the feedback, really helped a lot to iterate over the API and improve its design!