Skip to content

Cobblemon Event System

1mamute edited this page Jan 29, 2025 · 4 revisions

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.

Table of Contents


What is a CobblemonEvent?

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_CAPTURED is a event that is emitted when a player captures a pokemon.
  • BATTLE_VICTORY is 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_CAPTURED

Instead of this:

CobblemonEvents.getPOKEMON_CAPTURED()

Using Observables to Create Events for My Mod

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.

Understanding Observables

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.

Types of Observables

Cobblemon provides different types of observables. Understanding each type helps in selecting the appropriate one for your specific use case.

SimpleObservable

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.

Features

  • Subscribe: Register a handler to listen for events.
  • Unsubscribe: Remove a previously registered handler.
  • Emit: Dispatch events to all current subscribers.

Example in Kotlin

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)

Example in Java

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

EventObservable<T> extends SimpleObservable<T> by adding a post method. This method allows for emitting events and performing additional actions immediately after emission.

Features

  • All features of SimpleObservable.
  • Post: execute a lambda function for post-processing.

Example in Kotlin

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)

Example in Java

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

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.

Features

  • All features of EventObservable.
  • Cancelable: Subscribers can cancel the event propagation.
  • PostThen: Conditional post-processing based on event cancellation.

Example in Kotlin

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) : Cancelable

Then 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)

Example in Java

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);

Subscription Priorities

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

Example

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:

  1. Subscription A (HIGH)
  2. Subscription D (HIGH)
  3. Subscription B (NORMAL)
  4. 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

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.

Example in Kotlin

val subscription = CobblemonEvents.POKEMON_PROPERTY_INITIALISED.subscribe { 
    println("Pokemon properties have been initialized.") 
}

Example in Java

ObservableSubscription<Void> subscription = CobblemonEvents.POKEMON_PROPERTY_INITIALISED.subscribe(() -> {
    System.out.println("Pokemon properties have been initialized.");
});

Unsubscribing from an Event

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.

Example in Kotlin

// 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)

Example in Java

// 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 Events

Emitting an event triggers all subscribed handlers to execute. Depending on the observable type, emission can also involve additional processing or cancellation logic.

Emitting with SimpleObservable

Example in Kotlin

CobblemonEvents.POKEMON_SEEN.emit(PokemonSeenEvent(playerUuid, pokemon))

Example in Java

CobblemonEvents.POKEMON_SEEN.emit(new PokemonSeenEvent(playerUuid, pokemon));

Emitting with EventObservable

Example in Kotlin

// 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}.")
}

Example in Java

// 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 with CancelableObservable

Example in Kotlin

// 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}.")
    }
)

Example in Java

// 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.


Best Practices

  1. 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.
  2. Manage Subscriptions Carefully:
    • Always unsubscribe from events when they are no longer needed to prevent memory leaks.
  3. Prioritize Event Handlers:
    • Assign priorities to subscribers to control the order of execution, especially when some handlers depend on others.
  4. Handle Cancellations Gracefully:
    • When using CancelableObservable, ensure that your code properly handles cases where events are canceled to maintain game stability.