Header menu logo BaboonAPI

Entrypoints

Often you'd like to provide API that other mods can use, but you also don't want them to have a hard dependency on you. For example, perhaps you have a GUI that you'd like other mods to be able to use, but it's not essential to that other mod's functioning.

The Entrypoints API provides the tools to do this. It lets other mods use your APIs easily, with full compile-time type-checking, but without introducing a hard dependency on your mod being present at runtime.

open BaboonAPI.Hooks.Entrypoints

Here's an example. Let's say your mod lets players name their trombone. You want to let other mods change the name tag, or add additional name tags.

You might have a basic interface type like this:

type NameableTrombone =
    /// Set the name of this trombone
    abstract SetName: name: string -> unit

    /// Get the current name of this trombone
    abstract name: string

    /// Set the color of the name tag on this trombone
    abstract SetTagColor: color: Color -> unit

    /// Get the current color of the name tag on this trombone
    abstract color: Color

    /// Add an extra name tag to this trombone
    abstract AddCustomTag: text: string * color: Color -> CustomTag

Then you need a way for other mods to get a NameableTrombone instance. So you add some static method somewhere:

module TromboneLookup =
    let public getTrombone(): NameableTrombone =
        // Just return an example type...
        NameableTromboneImpl()

However, this presents a problem. Any mod trying to use this lookup API now has a hard dependency on your mod.

// Some cool downstream mod
[<BepInPlugin("CoolPlugin", "CoolPlugin", "1.0.0")>]
type CoolPlugin() =
    inherit BaseUnityPlugin()

    member _.Awake() =
        // Hard dependency, type must be available at runtime.
        let trombone = TromboneLookup.getTrombone()
        trombone.SetName "My Cool Trombone"

Putting the call behind an if-mod-present check won't work either, because your mod will still attempt to resolve the TromboneLookup type when the class is loaded, which will trigger an exception if the dependency is missing.

This is what the Entrypoints API intends to fix!

Fixing it

Entrypoints fixes this problem by inverting control. The library mod is responsible for querying the entrypoints API; the consumers just need to advertise their availability.

Let's demonstrate using our example above. Instead of the TromboneLookup type, we need a new interface:

type TromboneNameListener =
    /// Called when we want to name a trombone
    abstract OnTromboneCreated: NameableTrombone -> unit

This is our "entrypoint listener" type. Consumers then implement this listener on a new type:

// back in our cool downstream mod

[<BaboonEntryPoint>]
type CoolPluginNameListener(plugin: CoolPlugin) =
    interface TromboneNameListener with
        member this.OnTromboneCreated trombone =
            // Now we have access to the NameableTrombone instance!
            trombone.SetName "My Cool Trombone"

Breaking this down:

That's all we need to do in the downstream mod! The last step is to head back to our upstream library mod and actually invoke the entrypoints API:

[<BepInPlugin("TromboneNameTags", "TromboneNameTags", "1.0.0")>]
type LibraryMod() =
    inherit BaseUnityPlugin()

    let trombone = NameableTromboneImpl()

    member _.TryInitialize () =
        // Here we go!
        let listeners = Entrypoints.get<TromboneNameListener>()
        for l in listeners do
            l.OnTromboneCreated trombone
        ()

    interface GameInitializationEvent.Listener with
        member this.Initialize() =
            GameInitializationEvent.attempt this.Info this.TryInitialize

That's it! By calling Entrypoints.get, you scan all the other loaded mods for types that 1) inherit the specified listener type and 2) are tagged with the BaboonEntryPoint attribute. These types are then constructed and returned.

Because the entrypoint loader is only called by the library mod, the entrypoints aren't even constructed if the library mod isn't present, so there's no chance for type-loading errors!

Things not to do

In order to ensure that the CoolPluginNameListener entrypoint isn't loaded without its needed library mod present at runtime, you shouldn't reference it in any other parts of your code. Instead, your entrypoint should be the one referencing and calling into your mod's types.

Similarly, you should make sure you're not passing types that only exist in the library mod to other parts of your code. All references to the library mod's types should be contained within your entrypoint.

Valid entrypoint constructors

Entrypoints can have one of three types of constructors:

Any other constructor form will cause your entrypoint to be skipped.

namespace BaboonAPI
namespace BaboonAPI.Hooks
namespace BaboonAPI.Hooks.Entrypoints
namespace BaboonAPI.Hooks.Initializer
namespace BepInEx
namespace UnityEngine
type CustomTag = abstract Remove: unit -> unit
type unit = Unit
type NameableTrombone = abstract AddCustomTag: text: string * color: Color -> CustomTag abstract SetName: name: string -> unit abstract SetTagColor: color: Color -> unit abstract color: Color abstract name: string
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
[<Struct>] type Color = new: r: float32 * g: float32 * b: float32 * a: float32 -> unit + 1 overload member Equals: other: obj -> bool + 1 overload member GetHashCode: unit -> int member ToString: unit -> string + 1 overload static member ( * ) : a: Color * b: Color -> Color + 2 overloads static member (+) : a: Color * b: Color -> Color static member (-) : a: Color * b: Color -> Color static member (/) : a: Color * b: float32 -> Color static member (<>) : lhs: Color * rhs: Color -> bool static member (=) : lhs: Color * rhs: Color -> bool ...

--------------------
Color ()
Color(r: float32, g: float32, b: float32) : Color
Color(r: float32, g: float32, b: float32, a: float32) : Color
Multiple items
type NameableTromboneImpl = interface NameableTrombone new: unit -> NameableTromboneImpl

--------------------
new: unit -> NameableTromboneImpl
val this: NameableTromboneImpl
val text: string
val color: Color
val failwith: message: string -> 'T
val name: string
val getTrombone: unit -> NameableTrombone
Multiple items
type BepInPlugin = inherit Attribute new: GUID: string * Name: string * Version: string -> unit member GUID: string member Name: string member Version: Version

--------------------
BepInPlugin(GUID: string, Name: string, Version: string) : BepInPlugin
Multiple items
type CoolPlugin = inherit BaseUnityPlugin new: unit -> CoolPlugin member Awake: unit -> unit

--------------------
new: unit -> CoolPlugin
Multiple items
type BaseUnityPlugin = inherit MonoBehaviour member Config: ConfigFile member Info: PluginInfo

--------------------
BaseUnityPlugin() : BaseUnityPlugin
val trombone: NameableTrombone
module TromboneLookup from Entrypoints-guide
abstract NameableTrombone.SetName: name: string -> unit
 Set the name of this trombone
type TromboneNameListener = abstract OnTromboneCreated: NameableTrombone -> unit
Multiple items
type BaboonEntryPointAttribute = inherit Attribute new: unit -> BaboonEntryPointAttribute
<summary> Marks a class as an "entrypoint": a subclass that may be instantiated by another plugin at will. </summary>

--------------------
new: unit -> BaboonEntryPointAttribute
Multiple items
type CoolPluginNameListener = interface TromboneNameListener new: plugin: CoolPlugin -> CoolPluginNameListener

--------------------
new: plugin: CoolPlugin -> CoolPluginNameListener
val plugin: CoolPlugin
val this: CoolPluginNameListener
Multiple items
type LibraryMod = inherit BaseUnityPlugin interface Listener new: unit -> LibraryMod member TryInitialize: unit -> unit

--------------------
new: unit -> LibraryMod
val trombone: NameableTromboneImpl
val listeners: System.Collections.Generic.IReadOnlyList<TromboneNameListener>
module Entrypoints from BaboonAPI.Hooks.Entrypoints
val get: unit -> System.Collections.Generic.IReadOnlyList<'t>
<summary>Get a list of entrypoints of the specified type</summary>
<remarks> This call is expensive! You should not be fetching entrypoints in a loop. Instead, do it once and store the result. Calling this method will search for any subclasses of 't with the <see cref="T:BaboonAPI.Hooks.Entrypoints.BaboonEntryPointAttribute">BaboonEntryPoint</see> attribute and instantiate them. </remarks>
val l: TromboneNameListener
abstract TromboneNameListener.OnTromboneCreated: NameableTrombone -> unit
 Called when we want to name a trombone
module GameInitializationEvent from BaboonAPI.Hooks.Initializer
<namespacedoc><summary>Safe mod initialization hooks</summary></namespacedoc>
<summary>Game initialization event</summary>
<remarks> Using the listener interface and the <see cref="M:BaboonAPI.Hooks.Initializer.GameInitializationEvent.attempt(BepInEx.PluginInfo,Microsoft.FSharp.Core.FSharpFunc{Microsoft.FSharp.Core.Unit,Microsoft.FSharp.Core.Unit})">attempt</see> method, you can perform fallible setup tasks that will be safely reported to the user if they go wrong. </remarks>
<example><code lang="fsharp">member this.Awake() = GameInitializationEvent.EVENT.Register this interface GameInitializationEvent.Listener with member this.Initialize() = GameInitializationEvent.attempt this.Info (fun () -&gt; // fallible logic goes here. ) </code></example>
type Listener = abstract Initialize: unit -> Result<unit,LoadError>
<summary> Initialization event listener </summary>
val this: LibraryMod
val attempt: info: PluginInfo -> applier: (unit -> unit) -> Result<unit,LoadError>
<summary>Wraps your initialization logic and catches any thrown exceptions.</summary>
<remarks> An exception thrown in here will safely stop the game loading, and the error will be displayed to the user. </remarks>
<param name="info">Metadata used when displaying errors</param>
<param name="applier">Wrapped function</param>
property BaseUnityPlugin.Info: PluginInfo with get
member LibraryMod.TryInitialize: unit -> unit

Type something to start searching.