Skip to content

For‐Mod‐Developers

filloax edited this page Jul 28, 2026 · 2 revisions

Mod API

Other mods can hook into Apibalego by registering custom API events and entries to retrieve for their own usage. Kotlin is recommended for simplicity, but Java is also supported.

Usage examples are below.


Entry points

Object Used for
Apibalego Server api entries, server live update events
ApibalegoClient Client api entries, client live update events
LiveUpdatesEventRegistry / ClientLiveUpdatesEventRegistry Websocket events, kotlin-only overloads included
GamemasterCommand.onReloadHooks Run something on /gmaster reload

Java and Kotlin use the same objects: the registration methods are overloaded on the payload format (see below), and the Codec/Class/json ones are static and free of Kotlin-only types, so java mods can call them directly.

Everything must be registered at mod init, before the first poll happens. Registering the same key twice overwrites the previous handler.

Payload formats

Whatever you register, the payload (an entry's details, or a live event's message) can be read in four ways:

Format How Notes
kotlinx serializer MyData.serializer() Kotlin only. The only one whose fields show up in generated docs
Mojang Codec MyData.CODEC Used by Minecraft in general
Plain class MyData.class Uses gson, no need to write codec
Raw json - A gson JsonElement, parse it yourself

Optional dependency

Apibalego is meant as an optional dependency: check it is loaded before touching these classes (loader mod list check) to avoid Class not found exceptions.

// fabric
FabricLoader.getInstance().isModLoaded("apibalego")
// neoforge
ModList.get().isLoaded("apibalego")

Keep every reference to Apibalego inside a class that is only touched behind that check, so the JVM never has to load it when the mod is missing:

/** Do not reference if Apibalego is not loaded. */
object MyModApibalego : ApiEntryHandler<MyEntryData> {
    fun init() {
        Apibalego.registerApiHandler(id("my_entry"), MyEntryData.serializer(), this)
    }

    override fun handleApiUpdate(server: MinecraftServer, entries: Collection<ApiEntry<MyEntryData>>) {
        // ...
    }
}

// in your mod init, isApibalegoLoaded being the loader check above
if (isApibalegoLoaded) MyModApibalego.init()

Same in java:

/** Do not reference if Apibalego is not loaded. */
public class MyModApibalego {
    public static void init() {
        Apibalego.registerApiHandler(key, MyEntryData.CODEC, MyEntryData.class, (server, entries) -> {
            // ...
        });
    }
}

// in your mod init
if (isApibalegoLoaded) MyModApibalego.init();

Note

Remember Java classes are loaded only when they are referenced, not before


Server api entries

An api entry type is a key (Minecraft Identifier), a details payload format, and a handler. Handlers are called on every poll cycle with all currently active entries of that type (not a diff), including if there are none, and optionally when a player joins.

Kotlin

Apibalego.registerApiHandler(
    id("my_entry"),
    MyEntryData.serializer(),
    object : ApiEntryHandler<MyEntryData> {
        override fun handleApiUpdate(server: MinecraftServer, entries: Collection<ApiEntry<MyEntryData>>) {
            // called on each poll cycle with all active entries of this type
        }
        override fun handleApiJoin(player: ServerPlayer, entries: Collection<ApiEntry<MyEntryData>>) {
            // called when a player joins, with all currently active entries (optional)
        }
    }
)

MyEntryData must be a @Serializable Kotlin class mapping to the details field of the entry.

Entries with no details at all:

Apibalego.registerSimple(
    id("my_flag"),
    { server, entries -> },
    { player, entries -> },   // join handler, optional
)

Java

Same object, overloads that parse details with a Mojang Codec:

Apibalego.registerApiHandler(
    Identifier.fromNamespaceAndPath("mymod", "my_entry"),
    MyEntryData.CODEC,
    MyEntryData.class,
    new ApiEntryHandler<MyEntryData>() {
        @Override
        public void handleApiUpdate(MinecraftServer server, Collection<ApiEntry<MyEntryData>> entries) {
            for (ApiEntry<MyEntryData> entry : entries) {
                MyEntryData details = entry.getDetails();
            }
        }
        @Override
        public void handleApiJoin(ServerPlayer player, Collection<ApiEntry<MyEntryData>> entries) {
            // optional, has a default no-op implementation
        }
    }
);

Or skip the codec and let gson fill the class in:

Apibalego.registerApiHandler(key, MyEntryData.class, (server, entries) -> {
    for (ApiEntry<MyEntryData> entry : entries) {
        MyEntryData details = entry.getDetails();
    }
});

Or take the details tree as gson and read it yourself:

Apibalego.registerJsonApiHandler(key, (server, entries) -> {
    for (ApiEntry<JsonElement> entry : entries) {
        JsonElement details = entry.getDetails();
    }
});

Entries with no details:

Apibalego.registerSimple(key, (server, entries) -> {});
// with a join handler
Apibalego.registerSimple(key, (server, entries) -> {}, (player, entries) -> {});

The entry object

ApiEntry<T> carries id (stable id from the gamemaster, useful to dedup one-shot effects), active, details (null for types registered without a payload format) and type.

Only active entries are dispatched. When an entry stops being sent, or is set inactive, it simply stops showing up in the collection - handlers that apply persistent effects need to undo them themselves.


Client api entries

Same model, but polled and handled entirely client-side (no world needed, so they also work from the main menu). There is no join hook.

Kotlin:

ApibalegoClient.registerApiHandler(id("my_entry"), MyEntryData.serializer()) { client, entries ->
    // called on each client poll cycle with all active entries of this type
}

ApibalegoClient.registerSimple(id("my_flag")) { client, entries -> }

Java:

ApibalegoClient.registerApiHandler(key, MyEntryData.CODEC, MyEntryData.class, (client, entries) -> {
    for (ClientApiEntry<MyEntryData> entry : entries) {
        MyEntryData details = entry.getDetails();
    }
});

// gson-bound class instead of a codec
ApibalegoClient.registerApiHandler(key, MyEntryData.class, (client, entries) -> {});

ApibalegoClient.registerJsonApiHandler(key, (client, entries) -> {
    // entry.getDetails() is a gson JsonElement
});

ApibalegoClient.registerSimple(key, (client, entries) -> {});

Register these from the client init entrypoint of your mod.


Live update events

Websocket events are matched by exact event name and dispatched as they arrive, instead of on a poll cycle. The response sender reports back to the gamemaster web server.

Kotlin

LiveUpdatesEventRegistry.register("my_event", MyData.serializer()) { data, server, sender ->
    // handle the event
    sender.sendSuccess() // or sender.sendFailure("reason")
}

// no payload
LiveUpdatesEventRegistry.register("my_event") { server, sender ->
    sender.sendSuccess()
}

The other payload formats are available too, if you'd rather not write a serializer:

LiveUpdatesEventRegistry.registerWithCodec("my_event", MyData.CODEC, MyData::class) { data, server, sender -> }
LiveUpdatesEventRegistry.registerWithClass("my_event", MyData::class) { data, server, sender -> }
LiveUpdatesEventRegistry.registerJson("my_event") { json, server, sender -> }

Java

Apibalego.registerLiveUpdateEvent("my_event", MyData.CODEC, MyData.class, (data, server, sender) -> {
    sender.sendSuccess(Map.of()); // or sender.sendFailure("reason")
});

// gson-bound class instead of a codec
Apibalego.registerLiveUpdateEvent("my_event", MyData.class, (data, server, sender) -> {});

// raw gson JsonElement payload
Apibalego.registerJsonLiveUpdateEvent("my_event", (json, server, sender) -> {});

// no payload
Apibalego.registerSimpleLiveUpdateEvent("my_event", (server, sender) -> {});

Client-side is the same through ClientLiveUpdatesEventRegistry / ApibalegoClient, with Minecraft in place of MinecraftServer.


Reload hooks

Run something whenever data is re-synced (/gmaster reload, or the reload live event):

GamemasterCommand.onReloadHooks.add { server -> }

Generating docs

Enable generateApiDocs in the config to dump every registered entry type - Apibalego's own and every consumer mod's - to apibalego-docs/ as markdown, on server start. Types registered with a kotlinx serializer get their fields listed; codec, class and raw json payloads are listed by name only, since their structure cannot be read at runtime.

Clone this wiki locally