Skip to main content
LiquidBounce uses a modern, high-performance event system built on lambda handlers and coroutines. The system enables modules, features, and scripts to react to game events efficiently.

Architecture Overview

The event system consists of three main components:
  1. Event Classes - Define what happened
  2. EventManager - Dispatches events to listeners
  3. EventListener - Receives and handles events

EventManager

Location: event/EventManager.kt:279 The central dispatcher using FastUtil maps for optimal performance:
object EventManager {
    private val registry: Map<Class<out Event>, EventHookRegistry<in Event>>
    private val flows: Map<Class<out Event>, MutableSharedFlow<Event>>
    
    fun <T : Event> callEvent(event: T): T
    fun <T : Event> registerEventHook(eventClass: Class<out Event>, eventHook: EventHook<T>)
    fun <T : Event> unregisterEventHook(eventClass: Class<out Event>, eventHook: EventHook<T>)
}

Event Base Classes

Location: event/Event.kt:29
// Base event
abstract class Event {
    var isCompleted: Boolean = false
        internal set
}

// Cancellable events
abstract class CancellableEvent : Event() {
    var isCancelled: Boolean = false
        private set
    
    fun cancelEvent()
}

Event Types

LiquidBounce has 100+ event types registered in ALL_EVENT_CLASSES:

Game Loop Events

@Tag("game_tick")
class GameTickEvent : Event()

@Tag("game_render")
class GameRenderEvent : Event()

@Tag("world_render")
class WorldRenderEvent : Event()

Player Events

@Tag("player_tick")
class PlayerTickEvent : Event()

@Tag("player_move")
class PlayerMoveEvent(
    val movement: Vec3d,
    val type: MovementType
) : CancellableEvent()

@Tag("player_jump")
class PlayerJumpEvent : CancellableEvent()

Network Events

@Tag("packet")
class PacketEvent(
    val packet: Packet<*>,
    val origin: TransferOrigin
) : CancellableEvent()

Input Events

@Tag("keyboard_key")
class KeyboardKeyEvent(
    val keyCode: Int,
    val scanCode: Int,
    val action: Int,
    val mods: Int
) : Event()

@Tag("mouse_button")
class MouseButtonEvent(
    val button: Int,
    val action: Int,
    val mods: Int
) : Event()

Registering Event Handlers

Basic Handler

class MyModule : ClientModule("MyModule", Category.COMBAT), EventListener {
    
    val moveHandler = handler<PlayerMoveEvent> { event ->
        // Handle player movement
        if (event.movement.y > 0) {
            // Player is jumping/ascending
        }
    }
    
    val packetHandler = handler<PacketEvent> { event ->
        if (event.packet is UpdatePlayerPositionPacket) {
            // Handle position update
        }
    }
}

Handler Priority

Control execution order with priority (lower = earlier):
// Runs first
val earlyHandler = handler<PlayerTickEvent>(priority = -100) { event ->
    // Pre-processing
}

// Runs last  
val lateHandler = handler<PlayerTickEvent>(priority = 100) { event ->
    // Post-processing
}

Cancelling Events

val antiKnockback = handler<PacketEvent> { event ->
    if (event.packet is UpdateVelocityPacket) {
        event.cancelEvent()  // Prevent knockback
    }
}
Important: You can only cancel events that extend CancellableEvent. Attempting to cancel a regular Event will throw an exception.

Advanced Handler Patterns

One-Time Handlers

// Execute only once
val onceHandler = once<GameTickEvent> { event ->
    println("This runs only once")
}

Repeated Handlers

// Execute exactly N times
val repeatedHandler = repeated<GameTickEvent>(times = 10) { event ->
    println("This runs 10 times")
}

Until Condition

// Run until condition is met
val untilHandler = until<GameTickEvent> { event ->
    tickCount++
    tickCount >= 20  // Stop after 20 ticks
}

Computed Properties

Automatically update values based on events:
var ticksSinceEnabled by computedOn<GameTickEvent, Int>(0) { _, prev -> 
    prev + 1 
}

override fun enable() {
    ticksSinceEnabled = 0  // Reset on enable
}

Event Flows

Get reactive Kotlin Flow for any event:
import net.ccbluex.liquidbounce.event.eventFlow

class MyModule : ClientModule("MyModule", Category.COMBAT) {
    
    init {
        launch {
            eventFlow<PlayerTickEvent>().collect { event ->
                // Reactive event handling
            }
        }
    }
}
Flows receive events after all EventHook handlers have executed, so event.isCompleted will always be true.

Performance Optimizations

Pre-Allocated Registry

All event classes are registered at startup in ALL_EVENT_CLASSES array:
@JvmField
internal val ALL_EVENT_CLASSES: Array<Class<out Event>> = arrayOf(
    GameTickEvent::class.java,
    PacketEvent::class.java,
    // ... 100+ events
)
This creates lookup tables ahead of time using FastUtil’s Reference2ObjectOpenHashMap for O(1) lookups.

Event Hook Registry

Location: event/EventHookRegistry.kt Each event type has its own registry that:
  • Uses array-based storage for minimal overhead
  • Supports priority-based ordering
  • Avoids boxing primitives
  • Uses identity-based equality for fast removal

Event Listener Lifecycle

EventListener Interface

Location: event/EventListener.kt:33
interface EventListener : DebuggedOwner {
    val running: Boolean
        get() = parent()?.running ?: !isDestructed
    
    fun parent(): EventListener?
    fun children(): List<EventListener>
    fun unregister()
}

Hierarchical Listeners

Modules form a hierarchy:
class ParentModule : ClientModule("Parent", Category.COMBAT) {
    val child = tree(ChildModule())
    
    override fun running() = enabled && super.running
}

class ChildModule : ChoiceConfigurable("Child") {
    override fun parent() = thisModule as EventListener
}
When the parent is disabled, child handlers don’t execute.

Error Handling

The event system catches and logs exceptions:
try {
    eventHook.handler.accept(event)
} catch (e: ReportedException) {
    ErrorHandler.fatal(
        error = e,
        needToReport = true,
        additionalMessage = "Event handler of ${eventHook.handlerClass}"
    )
} catch (e: Throwable) {
    logger.error("Exception while executing event handler", e)
}
Exception Safety: Exceptions in one handler won’t prevent other handlers from executing. Always handle errors gracefully within your handlers.

Mixin Integration

Events are fired from mixins injected into Minecraft code:
@Mixin(ClientPlayerEntity.class)
public class MixinClientPlayer {
    
    @Inject(method = "tick", at = @At("HEAD"))
    private void onTick(CallbackInfo ci) {
        EventManager.callEvent(new PlayerTickEvent());
    }
}
See Mixin System for details on how events are hooked.

Coroutine Support

Suspend Handlers

Location: event/SuspendHandlers.kt Handlers can suspend for async operations:
val suspendHandler = handler<PacketEvent> { event ->
    coroutineScope {
        // Async work
        val result = async { heavyComputation() }
        // Handle result
    }
}

Ticker System

Location: event/CoroutineTicker.kt Run coroutines at regular intervals:
object CoroutineTicker {
    fun tickFor(context: CoroutineContext, intervalMs: Long, task: suspend () -> Unit)
}

Best Practices

Minimize Handler Work

// Good - fast check first
val handler = handler<PacketEvent> { event ->
    if (event.packet !is UpdatePlayerPositionPacket) return@handler
    // Heavy processing only for relevant packets
}

// Bad - always does expensive work
val handler = handler<PacketEvent> { event ->
    val result = expensiveOperation()
    if (event.packet is UpdatePlayerPositionPacket) {
        useResult(result)
    }
}

Unregister When Done

val tempHandler = handler<GameTickEvent> { event ->
    if (condition) {
        EventManager.unregisterEventHook(GameTickEvent::class.java, tempHandler)
    }
}

Use Appropriate Event Types

Choose the most specific event:
// Good - specific event
handler<PlayerJumpEvent> { /* ... */ }

// Bad - filtering generic event
handler<PlayerMoveEvent> { event ->
    if (event.movement.y > 0 && player.isOnGround) {
        // Detecting jumps manually
    }
}

Build docs developers (and LLMs) love