Skip to content

EventBus

Kanisuko edited this page May 25, 2026 · 4 revisions

EventBus

Namespace: ScavLib.event_bus

A lightweight, attribute-based event bus for decoupled communication between mod systems. Listeners declare interest in an event type by marking a method with [Subscribe] and registering the containing object with EventBus.Register().


Basic Usage

1. Register your listener

Call EventBus.Register(this) in your plugin's Awake():

private void Awake()
{
    EventBus.Register(this);
}

2. Subscribe to events

Mark any method with [Subscribe]. The method must be non-static and have exactly one parameter that inherits from BusEvent:

[Subscribe]
private void OnWorldLoaded(WorldLoadedEvent e)
{
    GameUtil.Log("World is ready.");
}

[Subscribe]
private void OnItemPickedUp(ItemPickedUpEvent e)
{
    GameUtil.Log($"Picked up {e.ItemId} in slot {e.Slot}");
}

3. Unregister when done

If your listener object is destroyed before the game exits, unregister it to avoid stale references:

private void OnDestroy()
{
    EventBus.Unregister(this);
}

Pre-defined Game Events

These events are triggered automatically by ScavLib's internal Harmony patches. No manual patching is required in your mod.

WorldLoadedEvent

Fired once after ConsoleScript.Start() completes, at which point the game world is fully initialized and PlayerCamera.main / Body are safe to access.

[Subscribe]
private void OnWorldLoaded(WorldLoadedEvent e)
{
    // Safe to use PlayerUtil, SkillUtil, ItemUtil here.
}

Always use this event as your entry point for any logic that requires the player to exist. Do not access Body in Awake().


LayerLoadedEvent (v0.4.0+)

Fired every time a layer (biome) finishes generating and is playable. This includes the very first layer of a session as well as every subsequent layer reached via descent. Use IsFirstLoad to distinguish the two cases.

Property Type Description
BiomeDepth int Depth of the layer that just finished generating. 0 = first layer.
IsFirstLoad bool true only on the first layer of the current game session.
Timestamp float Time.realtimeSinceStartup at event creation (inherited from BusEvent).
[Subscribe]
private void OnLayerLoaded(LayerLoadedEvent e)
{
    if (e.IsFirstLoad)
        GameUtil.Log("First layer — run session init here.");
    else
        GameUtil.Log($"Descended to layer {e.BiomeDepth}.");
}

Note: WorldLoadedEvent fires once per session (on the first ConsoleScript.Start()), while LayerLoadedEvent fires for every layer including the first. For most init logic, either works — use LayerLoadedEvent when you also need to re-run logic on each descent.


WorldUnloadingEvent (v0.4.0+)

Fired before WorldGeneration.Clear() runs, giving you a last chance to safely read player and world state before the current layer is torn down. Triggered on both normal descent and save-and-exit.

Property Type Description
CurrentBiomeDepth int The biome depth about to be torn down.
NextBiomeDepth int The depth to be generated next, or -1 on save-and-exit.
IsExitToMenu bool Convenience: true if NextBiomeDepth < 0.
Timestamp float Time.realtimeSinceStartup at event creation.
[Subscribe]
private void OnWorldUnloading(WorldUnloadingEvent e)
{
    if (e.IsExitToMenu)
        GameUtil.Log("Saving and exiting — flush persistent data here.");
    else
        GameUtil.Log($"Descending from layer {e.CurrentBiomeDepth} to {e.NextBiomeDepth}.");
}

WorldDestroyedEvent (v0.4.0+)

Fired after WorldGeneration.OnDestroy() completes. This is the final cleanup point before the world object becomes invalid. Use for global teardown that must survive layer transitions (e.g. clearing static caches, disposing native resources). For per-layer cleanup, prefer WorldUnloadingEvent.

Property Type Description
LastBiomeDepth int Last known biome depth before destruction. May be -1 if unreadable.
WasSaveAndExit bool true if triggered by save-and-exit.
Timestamp float Time.realtimeSinceStartup at event creation.
[Subscribe]
private void OnWorldDestroyed(WorldDestroyedEvent e)
{
    GameUtil.Log($"World destroyed. WasSaveAndExit={e.WasSaveAndExit}");
    // Clear any static state that should not persist across sessions.
}

ItemPickedUpEvent

Fired after an item is successfully placed into a player inventory slot.

Property Type Description
Item Item The item that was picked up.
Slot int The inventory slot index it was placed into.
Body Body The Body that performed the pickup.
ItemId string Convenience shortcut for Item.id.
Timestamp float Time.realtimeSinceStartup at event creation (inherited from BusEvent).
[Subscribe]
private void OnItemPickedUp(ItemPickedUpEvent e)
{
    if (e.ItemId == "bandage")
        GameUtil.Log("Picked up a bandage.");
}

ItemDroppedEvent

Fired after an item is removed from the player's inventory and dropped into the world. The item GameObject still exists at this point.

Property Type Description
Item Item The item that was dropped.
Slot int The slot it was removed from.
Body Body The Body that dropped the item.
ItemId string Convenience shortcut for Item.id.
[Subscribe]
private void OnItemDropped(ItemDroppedEvent e)
{
    GameUtil.Log($"Dropped {e.ItemId} from slot {e.Slot}");
}

Defining Custom Events

Create a class that inherits BusEvent. Add whatever payload properties your event needs:

using ScavLib.event_bus;

public class PlayerTeleportedEvent : BusEvent
{
    public Vector2 FromPosition { get; }
    public Vector2 ToPosition   { get; }

    public PlayerTeleportedEvent(Vector2 from, Vector2 to)
    {
        FromPosition = from;
        ToPosition   = to;
    }
}

Post it from anywhere:

EventBus.Post(new PlayerTeleportedEvent(oldPos, newPos));

Subscribe to it in any registered listener:

[Subscribe]
private void OnTeleported(PlayerTeleportedEvent e)
{
    GameUtil.Log($"Teleported from {e.FromPosition} to {e.ToPosition}");
}

Inheritance Dispatch

EventBus.Post<T> dispatches along the full inheritance chain. A listener subscribed to a base type will receive all derived events:

// This will receive ALL events posted through the bus.
[Subscribe]
private void OnAnyEvent(BusEvent e)
{
    GameUtil.Log($"Event fired: {e.GetType().Name}");
}

Error Isolation

If a subscriber throws an exception, the error is caught and logged to the BepInEx console. The remaining subscribers in the chain are still called — one bad handler cannot break the whole event dispatch.

Clone this wiki locally