-
Notifications
You must be signed in to change notification settings - Fork 0
Cobblemon Event System
In event-driven programming, events are signals emitted by objects to indicate that something has occurred. Observers listen for these events and react accordingly. Cobblemon utilizes an event system built around the Observable pattern, enabling modular and decoupled interactions within and with the mod.
- Cobblemon Event System
A CobblemonEvent represents a specific event within the Cobblemon mod's lifecycle or gameplay mechanics. These events are instances of Observables that other parts of the mod can subscribe to and react upon when the event is emitted.
All the events registered on Cobblemon can be found on the CobblemonEvents.kt file.
object CobblemonEvents {
@JvmField
val POKEMON_CAPTURED = EventObservable<PokemonCapturedEvent>()
@JvmField
val BATTLE_VICTORY = EventObservable<BattleVictoryEvent>()
}These are couple of events that are found in the CobblemonEvents object:
-
POKEMON_CAPTUREDis a event that is emitted when a player captures a pokemon. -
BATTLE_VICTORYis a event that is emitted when a player wins a battle.
In Kotlin, properties declared in an object are by default accessed through getter methods.
The events on the CobblemonEvents.kt file are annotated with @JvmField to make them accessible from Java code. The annotation tells the Kotlin compiler to expose the property as a public Java field. This means that Java code can access these observables directly without needing to call getter methods.
Thanks to this, in Java, you can access the POKEMON_CAPTURED event like:
CobblemonEvents.POKEMON_CAPTUREDInstead of this:
CobblemonEvents.getPOKEMON_CAPTURED()The Observables classes in Cobblemon are very stable and many third-party mods use them to create their own events. This has a few advantages over creating a custom event system from scratch, or using a mod loader specific event system:
- Core Dependency: Since Cobblemon is a core dependency, you can use the Observables classes directly in your mod without any additional setup.
- Mod Loader Agnosticism: Will work for Fabric, Neoforge, etc...
-
Familiarity: It's familiar to most Cobblemon mod developers, as they are already using the same event system when interacting with
CobblemonEvents. - Maintenance: Any improvements or bug fixes to the Cobblemon Observables classes will benefit your mod as well.
Observables are the core of the event system in Cobblemon. They represent streams of events that observers can subscribe to. When an observable emits an event, all its subscribers are notified and can react accordingly.
- Emission: Sending an event to all subscribers.
- Subscription: Registering to listen for specific events.
- Handler: A function that responds to an emitted event.
- Cancellation: Stopping further event handling.
Cobblemon provides different types of observables. Understanding each type helps in selecting the appropriate one for your specific use case.
SimpleObservable<T> is the most basic type of observable. It allows subscribers to listen for events of type T without any additional control over the event flow.
- Subscribe: Register a handler to listen for events.
- Unsubscribe: Remove a previously registered handler.
- Emit: Dispatch events to all current subscribers.
val event = SimpleObservable<String>()
// Subscribing to the event
val subscription = event.subscribe { message ->
println("Received message: $message")
}
// Emitting an event
event.emit("Hello, Cobblemon!")
// Unsubscribing from the event
event.unsubscribe(subscription)SimpleObservable<String> event = new SimpleObservable<>();
// Subscribing to the event
ObservableSubscription<String> subscription = event.subscribe(message -> {
System.out.println("Received message: " + message);
});
// Emitting an event
event.emit("Hello, Cobblemon!");
// Unsubscribing from the event
event.unsubscribe(subscription);EventObservable<T> extends SimpleObservable<T> by adding a post method. This method allows for emitting events and performing additional actions immediately after emission.
- All features of
SimpleObservable. - Post: execute a lambda function for post-processing.
val event = EventObservable<String>()
// Subscribing to the event
val subscription = event.subscribe { message ->
println("Received message: $message")
}
// Emitting an event with post-processing
event.post("Hello, Cobblemon!") { message ->
println("Post-processing message: $message")
}
// Unsubscribing from the event
event.unsubscribe(subscription)EventObservable<String> event = new EventObservable<>();
// Subscribing to the event
ObservableSubscription<String> subscription = event.subscribe(message -> {
System.out.println("Received message: " + message);
});
// Emitting an event with post-processing
event.post("Hello, Cobblemon!", message -> {
System.out.println("Post-processing message: " + message);
});
// Unsubscribing from the event
event.unsubscribe(subscription);CancelableObservable<T> extends EventObservable<T> and adds the capability to cancel event handling. This is useful for events where subscribers might need to prevent further processing based on certain conditions.
- All features of
EventObservable. - Cancelable: Subscribers can cancel the event propagation.
- PostThen: Conditional post-processing based on event cancellation.
First we need to create a class that inherits from the Cancelable interface, which is a highly complex class and should only be read by professional engineers:
data class CancelableStringEvent(var message: String, var isCanceled: Boolean = false) : CancelableThen we can use it in the CancelableObservable:
val cancelableObservable = CancelableObservable<CancelableStringEvent>()
// Subscribing to the event with the ability to cancel
val subscription = cancelableObservable.subscribe { event ->
if (event.message.equals("cancel", ignoreCase = true)) {
event.isCanceled = true
println("Event canceled.")
} else {
println("Event not canceled.")
}
}
val cancelableEvent = CancelableStringEvent("cancel")
cancelableObservable.emit(cancelableEvent)First we need to create a class that inherits from the Cancelable interface, which is a highly complex class and should only be read by professional engineers:
// CancelableStringEvent.java
public class CancelableStringEvent implements Cancelable {
private String message;
private boolean isCanceled;
public CancelableStringEvent(String message) {
this.message = message;
this.isCanceled = false;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean isCanceled() {
return isCanceled;
}
@Override
public void setCanceled(boolean canceled) {
this.isCanceled = canceled;
}
}Then we can use it in the CancelableObservable:
CancelableObservable<CancelableStringEvent> cancelableObservable = new CancelableObservable<>();
// Subscribe to the event with the ability to cancel
ObservableSubscription<CancelableStringEvent> subscription = cancelableObservable.subscribe(event -> {
if ("cancel".equalsIgnoreCase(event.getMessage())) {
event.setCanceled(true);
System.out.println("Event canceled.");
} else {
System.out.println("Event not canceled.");
}
});
CancelableStringEvent cancelableEvent = new CancelableStringEvent("cancel");
cancelableObservable.emit(cancelableEvent);When subscribing, you can specify a priority to determine the order in which subscribers are notified.
All Observables manages event subscriptions using PrioritizedList, which is a data structure designed to manage elements based on their assigned priorities.
- Priority-Based Ordering: Higher priority subscribers receive events before lower priority ones.
- First-Come-First-Served within Priorities: Within the same priority level, elements are ordered by their insertion sequence.
The priority is defined with an enum and can be one of the followings:
- HIGHEST
- HIGH
- NORMAL
- LOW
- LOWEST
Suppose we add the following subscriptions with their associated priorities:
- Subscription A: Priority HIGH
- Subscription B: Priority NORMAL
- Subscription C: Priority LOW
- Subscription D: Priority HIGH
After adding these subscriptions to an Observable, its PrioritizedList would be:
- Subscription A (HIGH)
- Subscription D (HIGH)
- Subscription B (NORMAL)
- Subscription C (LOW)
If this Observable is a CancelableObservable and Subscription A cancels the event, Subscriptions D, B, and C will not receive it.
Subscribing to an event allows your code to listen and respond whenever that event is emitted. The subscription returns an object that can be used to unsubscribe from the event when no longer needed.
All the Observables have the subscribe method that allows you to listen to the event.
val subscription = CobblemonEvents.POKEMON_PROPERTY_INITIALISED.subscribe {
println("Pokemon properties have been initialized.")
}ObservableSubscription<Void> subscription = CobblemonEvents.POKEMON_PROPERTY_INITIALISED.subscribe(() -> {
System.out.println("Pokemon properties have been initialized.");
});Sometimes, you may need to stop listening to an event, either temporarily or permanently.
You can unsubscribe by:
- Using the unsubscribe method on the subscription object returned during the subscription
- Passing the subscription object to the unsubscribe method of the Observable.
// Subscribe to the event
val subscription = CobblemonEvents.POKEMON_FAINTED.subscribe {
println("The pokemon fainted.")
}
// Unsubscribe from the event using the subscription object
subscription.unsubscribe()
// Alternatively, unsubscribe directly from the Observable
CobblemonEvents.POKEMON_FAINTED.unsubscribe(subscription)
// Subscribe to the event
ObservableSubscription<PokemonFaintedEvent> subscription = CobblemonEvents.POKEMON_FAINTED.subscribe(event -> {
System.out.println("The pokemon fainted.");
});
// Unsubscribe from the event using the subscription object
subscription.unsubscribe();
// Alternatively, unsubscribe directly from the Observable
CobblemonEvents.POKEMON_FAINTED.unsubscribe(subscription);Emitting an event triggers all subscribed handlers to execute. Depending on the observable type, emission can also involve additional processing or cancellation logic.
CobblemonEvents.POKEMON_SEEN.emit(PokemonSeenEvent(playerUuid, pokemon))CobblemonEvents.POKEMON_SEEN.emit(new PokemonSeenEvent(playerUuid, pokemon));// Emitting an event with post-processing
CobblemonEvents.FRIENDSHIP_UPDATED.post(FriendshipUpdatedEvent(playerName = "Ash")) { event ->
// This will be called after the event is emitted
println("Post-processing after friendship update for ${event.playerName}.")
}// Emitting an event with post-processing
CobblemonEvents.FRIENDSHIP_UPDATED.post(new FriendshipUpdatedEvent("Ash"), event -> {
// This will be called after the event is emitted
System.out.println("Post-processing after friendship update for " + event.getPlayerName());
});// Emitting a cancelable event
CobblemonEvents.SHOULDER_MOUNT.emit(ShoulderMountEvent(player, pokemon, false))
// Alternatively, using postThen for conditional post-processing
val shoulderMountEvent = ShoulderMountEvent(player, pokemon, false)
CobblemonEvents.SHOULDER_MOUNT.postThen(shoulderMountEvent,
ifCanceled = { event ->
println("Shoulder mount was canceled for ${event.player.name}.")
},
ifSucceeded = { event ->
println("Shoulder mount succeeded for ${event.player.name}.")
}
)// Emitting a cancelable event
CobblemonEvents.SHOULDER_MOUNT.emit(new ShoulderMountEvent(player, pokemon, false));
// Alternatively, using postThen for conditional post-processing
ShoulderMountEvent shoulderMountEvent = new ShoulderMountEvent(player, pokemon, false);
CobblemonEvents.SHOULDER_MOUNT.postThen(shoulderMountEvent,
event -> System.out.println("Shoulder mount was canceled for " + event.getPlayer().getName()),
event -> System.out.println("Shoulder mount succeeded for " + event.getPlayer().getName())
);Important: When using CancelableObservable, subscribers have the ability to cancel the event during emission. Ensure that your emission logic accounts for possible cancellations.
-
Use Appropriate Observable Types:
-
SimpleObservable: for basic event notifications without extra control. -
EventObservable: for events requiring immediate post-processing. -
CancelableObservable: when events need to be canceled based on conditions.
-
-
Manage Subscriptions Carefully:
- Always unsubscribe from events when they are no longer needed to prevent memory leaks.
-
Prioritize Event Handlers:
- Assign priorities to subscribers to control the order of execution, especially when some handlers depend on others.
-
Handle Cancellations Gracefully:
- When using
CancelableObservable, ensure that your code properly handles cases where events are canceled to maintain game stability.
- When using