-
Notifications
You must be signed in to change notification settings - Fork 0
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().
Call EventBus.Register(this) in your plugin's Awake():
private void Awake()
{
EventBus.Register(this);
}Registration is idempotent: if the same listener instance is registered
more than once, its previous handlers are silently removed first, so callbacks
never fire twice (e.g. when Awake() runs again across scene reloads).
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}");
}A [Subscribe] method with the wrong signature (not exactly one BusEvent
parameter) is skipped with a warning rather than registered.
If your listener object is destroyed before the game exits, unregister it to avoid stale references:
private void OnDestroy()
{
EventBus.Unregister(this);
}These events are triggered automatically by ScavLib's internal Harmony patches. No manual patching is required in your mod.
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
BodyinAwake().
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}.");
}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}.");
}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}");
}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. |
[Subscribe]
private void OnItemPickedUp(ItemPickedUpEvent e)
{
if (e.ItemId == "bandage")
GameUtil.Log("Picked up a bandage.");
}Note: This currently fires for the player's own
Body. If the game ever spawns NPCs with their ownBody, it will fire for them too — checke.Body == GameUtil.GetBody()if you only want the player's pickups.
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. |
Timestamp |
float |
Time.realtimeSinceStartup at event creation. |
[Subscribe]
private void OnItemDropped(ItemDroppedEvent e)
{
GameUtil.Log($"Dropped {e.ItemId} from slot {e.Slot}");
}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}");
}EventBus.Post<T> dispatches along the full inheritance chain, from the
event's actual runtime type up to BusEvent. A listener subscribed to a base
type receives 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}");
}Handlers are called synchronously in registration order, over a snapshot of the handler list so a handler may safely (un)register during dispatch.
Query how many handlers are subscribed to a given event type. Useful for diagnostics output and for skipping expensive event-payload preparation when nobody is listening.
| Method | Returns | Description |
|---|---|---|
GetHandlerCount(Type eventType) |
int |
Number of handlers subscribed to that exact type. |
GetHandlerCount<T>() |
int |
Generic convenience overload. |
Important: These count only direct subscriptions to the exact type. Handlers subscribed to a base type are not counted here — inheritance is resolved at
Post()time by walking the chain, not at registration time. So aBusEvent-level listener does not increment the count for a derived type.
if (EventBus.GetHandlerCount<LayerLoadedEvent>() > 0)
{
// Someone is listening — worth building the (expensive) payload.
EventBus.Post(new LayerLoadedEvent(depth, isFirst));
}If a subscriber throws an exception, the error is caught and logged to the BepInEx console with the handler name, declaring type, and the event type for context. The remaining subscribers in the chain are still called — one bad handler cannot break the whole event dispatch.