Discussion: The art of event phases
i509VCB opened this issue ยท 4 comments
If you have used Fabric API before, you would notice how some modules use multiple types of events. Some modules hold event callback interfaces in nested classes, some keep the event instance in the callback interface and some events use multiple event instances to control specific parts of the process.
It is definitely a bit messy, and many new pull requests add events. Many contributors get confused due to the many varied methods of events being handled, so this issue is a proposal for how to structure events for Fabric API for consistency and as a guide for contributors.
The purpose of this issue is to determine how events should be structured in order to reduce future inconsistency.
For the purposes of this issue, I see 4 main event categories. An event category simply is a group of events which are used to notify of actions and or allow modification/cancellation of game actions.
Single dispatch notification
These types of events are typically used to notify when something should occur. These events are not able to be canceled.
One common subcategory of this type of event is registration events. Examples of this include:
CommandRegistrationCallback
LivingEntityFeatureRendererRegistrationCallback
RegistryEntryAddedCallback
RegistryEntryRemovedCallback
DynamicRegistrySetupCallback
Before/After dispatch
These types of events are used for notification.
One common subcategory of this event is ticking events. These events are not able to be canceled. The events in ClientTickEvents
and ServerTickEvents
. Those events follow the Before/After
and Start/End
pattern in naming.
An event which is cancelable does not belong in this category.
Cancelable Before/After
These types of events are intended to be used to prevent actions from occurring. These events would have one cancelation callback and a before + after callback as well. The Allow
callback would dictate whether an action should occur and the Before/After callbacks would wrap the specific action.
Optionally a Cancelled
callback may be included to notify mods of cancellation to allow reverting the game back to the previous state before the event occured. This is shown in the PlayerBlockBreakEvents as the Cancel event reverts the world on the client to the state before the client broke the block.
The current PlayerBlockBreakEvents do not meet this standard as the Allow
and Before
callbacks are combined, therefore a mod which performs an action before the block is broken due to load order is then later canceled by a later listener, which in turn has a mod perform logic but the block is never broken.
Cancelable and Allowing a different outcome
These types of events are intended to allow an action to canceled but also change the current condition of an action. One example use of this is when a player interacts with an entity and the corresponding event is fired. A mod may wish to cancel the interaction while another mod may wish to have the interaction succeed and call their own logic.
Another more complex case is an event called when a player logs into a server. A mod developer may want to allow a player to bypass the whitelist or disconnect the player for an arbitrary reason.
The the structure of this kind of event, further discussion is needed. Design considerations need to decide whether a mod allowing connection and another mod forbidding connection is part of the event structure or rather part of the event contract.
This should be documented in the contribution guidelines - not sure whether the current event guidelines are detailed enough.
@sfPlayer1 I recall you mentioned that such order (as phases) are unreliable, and for reliable dependencies, the best solution is for a dependency to fire another event that the dependents register on? I recall that's your design, at least for ordered mod loading.
This issue is about a slightly different problem: adding events around a single (usually) vanilla action such that mods can influence whether it happens and get notified before/after it happens - without bogus order dependent notify before cancel. The conventional event ordering problem involves arranging listeners for the very same aspect, not for cancel vs notify.
Separating cancellation from notification doesn't create an unbounded dependency chain with potentially recursive relations, but at most 4 distinct events with some being optional:
cancelled <- allow -> before -> action -> after
This is how we are and have been implementing cancellable events lately.
Priorities instead try and ultimately fail to solve e.g. before@someModelProvider -> before@someModelLoader -> before@someModelDeduplicator where an attempt is made to condense dependencies into the same before event's one-dimensional listener order.