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:
- we annotate the type with the BaboonEntryPointAttribute
- we (optionally) define a constructor that accepts our Plugin instance
- we implement the "listener interface" from the library plugin
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:
- zero arguments
- single argument of your mod's plugin type (the type annotated with
BepInPlugin
) - single argument of type
PluginInfo
(from BepInEx)
Any other constructor form will cause your entrypoint to be skipped.
val string: value: 'T -> string
--------------------
type string = System.String
[<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
type NameableTromboneImpl = interface NameableTrombone new: unit -> NameableTromboneImpl
--------------------
new: unit -> NameableTromboneImpl
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
type CoolPlugin = inherit BaseUnityPlugin new: unit -> CoolPlugin member Awake: unit -> unit
--------------------
new: unit -> CoolPlugin
type BaseUnityPlugin = inherit MonoBehaviour member Config: ConfigFile member Info: PluginInfo
--------------------
BaseUnityPlugin() : BaseUnityPlugin
Set the name of this trombone
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
type CoolPluginNameListener = interface TromboneNameListener new: plugin: CoolPlugin -> CoolPluginNameListener
--------------------
new: plugin: CoolPlugin -> CoolPluginNameListener
type LibraryMod = inherit BaseUnityPlugin interface Listener new: unit -> LibraryMod member TryInitialize: unit -> unit
--------------------
new: unit -> LibraryMod
<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>
Called when we want to name a trombone
<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 () -> // fallible logic goes here. ) </code></example>
<summary> Initialization event listener </summary>
<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>