Skip to content

Lifecycle System

QinShenYu edited this page Jun 10, 2026 · 6 revisions

Namespace: ScavLib.mods
Since: v0.4.0

The lifecycle system lets your mod receive automatic callbacks when the game world changes state — without manually subscribing to the EventBus. Register once in Awake() and ScavLib handles the wiring for you.


Quick Start

Inherit from ModLifecycleBase and override only the callbacks you need, then pass an instance to ModRegistry.Register():

using BepInEx;
using ScavLib.mods;
using ScavLib.event_bus.events;
using ScavLib.util;

[BepInDependency("com.kanisuko.scavlib", BepInDependency.DependencyFlags.SoftDependency)]
[BepInPlugin("com.yourname.yourmod", "YourMod", "1.0.0")]
public class YourPlugin : BaseUnityPlugin
{
    private void Awake()
    {
        ModRegistry.Register(
            new ModInfo("YourMod", "1.0.0", "Does cool things.", "YourName"),
            new YourModLifecycle()
        );
    }
}

public class YourModLifecycle : ModLifecycleBase
{
    public override void OnEnabled()
    {
        // Runs immediately after Register() completes.
        // Safe to set up static state here.
    }

    public override void OnWorldLoaded(WorldLoadedEvent e)
    {
        // World is fully initialized. Body and PlayerCamera.main are safe.
        PlayerUtil.HealAll();
    }

    public override void OnLayerLoaded(LayerLoadedEvent e)
    {
        // Runs on every layer load, including the first.
        GameUtil.Log($"Layer {e.BiomeDepth} loaded. First={e.IsFirstLoad}");
    }

    public override void OnWorldUnloading(WorldUnloadingEvent e)
    {
        // Last chance to read world/player state before the layer tears down.
        if (e.IsExitToMenu)
            GameUtil.Log("Exiting to menu — save persistent data here.");
    }

    public override void OnWorldDestroyed(WorldDestroyedEvent e)
    {
        // WorldGeneration.OnDestroy() has completed. Clear static caches here.
    }
}

Note on the dependency flag: ScavLib is declared as a SoftDependency above, so BepInEx will not block your mod from loading if ScavLib is absent — only the ordering is influenced. Use HardDependency instead if your mod cannot function at all without ScavLib. The flag does not carry a version; for version range checks use VersionedDependency (see below and the ModRegistry page).


IModLifecycle

The full interface. All methods have default empty implementations in ModLifecycleBase, so you only need to override what you use.

public interface IModLifecycle
{
    void OnEnabled();
    void OnDisabled();
    void OnWorldLoaded(WorldLoadedEvent e);
    void OnLayerLoaded(LayerLoadedEvent e);
    void OnWorldUnloading(WorldUnloadingEvent e);
    void OnWorldDestroyed(WorldDestroyedEvent e);
}
Callback When it fires
OnEnabled() Immediately after ModRegistry.Register() succeeds
OnDisabled() Reserved for the upcoming enable/disable system. Not triggered automatically in 0.5.0 — do not rely on it for cleanup yet.
OnWorldLoaded(e) Once per session — world fully initialized, Body safe
OnLayerLoaded(e) Every layer (biome) finishes generating, including the first
OnWorldUnloading(e) Before layer transition or save-and-exit
OnWorldDestroyed(e) After WorldGeneration.OnDestroy() completes

ModLifecycleBase

An abstract base class that provides empty default implementations for all six callbacks. Inherit from this rather than implementing IModLifecycle directly so your code stays forward-compatible if new callbacks are added.

public abstract class ModLifecycleBase : IModLifecycle
{
    public virtual void OnEnabled() { }
    public virtual void OnDisabled() { }
    public virtual void OnWorldLoaded(WorldLoadedEvent e) { }
    public virtual void OnLayerLoaded(LayerLoadedEvent e) { }
    public virtual void OnWorldUnloading(WorldUnloadingEvent e) { }
    public virtual void OnWorldDestroyed(WorldDestroyedEvent e) { }
}

Declaring Dependencies

ModInfo accepts optional dependency names as extra constructor arguments. These are advisory — missing dependencies log a LogWarning at registration time but do not block the mod from loading. Actual load-order enforcement must use BepInEx's [BepInDependency] attribute.

// Declare that MyMod depends on ScavLib and AnotherMod
new ModInfo("MyMod", "1.0.0", "desc", "Author", "ScavLib", "AnotherMod")

Optional: version range checks via VersionedDependency

When you need more than a name — e.g. "requires ScavLib 0.6.0 or newer" — declare a VersionedDependency instead of a plain name. ScavLib validates the installed version against the range at registration time and logs a warning on mismatch. This is advisory only: the mod still loads. Hard load-order guarantees remain the job of [BepInDependency].

new ModInfo(
    "MyMod", "1.0.0", "desc", "Author",
    new[] {
        new VersionedDependency("ScavLib", minVersion: "0.7.1")
    });

Declared dependencies appear in scavlib status output:

MyMod v1.0.0 by Author [F] Deps: [ScavLib (>=0.6.0), AnotherMod]

Error Isolation

Each lifecycle callback is wrapped in a try/catch inside ScavLib. An exception in one mod's OnLayerLoaded will not prevent other mods' callbacks from firing. Errors are logged to the BepInEx console with the mod name and callback name included.


The [F] Tag

Mods registered with a lifecycle object are annotated with [F] in scavlib status output (think "Functional / lifecycle integration"). Mods registered with the basic Register(ModInfo) overload (no lifecycle) show no tag. Both registration styles are fully supported and backward compatible.


Relationship to EventBus

The lifecycle system is built on top of EventBus internally. ScavLib creates a LifecycleEventAdapter per registered lifecycle and calls EventBus.Register() on it automatically. You do not need to call EventBus.Register() for the lifecycle object yourself.

If you need to subscribe to events that are not covered by IModLifecycle (e.g. ItemPickedUpEvent, custom events), use EventBus.Register(this) as usual alongside the lifecycle registration.

Clone this wiki locally