11 Developer API
Axionize WS2 edited this page 2026-05-03 23:42:27 -04:00

Developer API

Grim exposes its public plugin API as ac.grim.grimac:GrimAPI. The current API line is 1.3.2.1.

Requirements

  • Java 17 or newer.
  • A supported Grim environment. See Supported environments.
  • Do not shade GrimAPI into your plugin. Use compileOnly / provided; GrimAC provides the implementation at runtime.

Setup

Gradle

repositories {
    maven {
        name = "grimacSnapshots"
        url = uri("https://repo.grim.ac/snapshots")
    }
}

dependencies {
    compileOnly("ac.grim.grimac:GrimAPI:1.3.2.1")
}

Maven

<repository>
  <id>grimac-snapshots</id>
  <name>GrimAC's Maven Repository</name>
  <url>https://repo.grim.ac/snapshots</url>
</repository>

<dependency>
  <groupId>ac.grim.grimac</groupId>
  <artifactId>GrimAPI</artifactId>
  <version>1.3.2.1</version>
  <scope>provided</scope>
</dependency>

Loading With Grim

For Bukkit/Paper plugins that require Grim, declare a hard dependency:

depend: [GrimAC]

Use softdepend: [GrimAC] only when Grim support is optional. If you do that, check whether Grim is installed before calling GrimAPIProvider.get().

import ac.grim.grimac.api.GrimAPIProvider;
import ac.grim.grimac.api.GrimAbstractAPI;
import ac.grim.grimac.api.plugin.GrimPlugin;
import org.bukkit.plugin.java.JavaPlugin;

public final class MyPlugin extends JavaPlugin {
    private GrimPlugin grimPlugin;

    @Override
    public void onEnable() {
        GrimAbstractAPI api = GrimAPIProvider.get();
        this.grimPlugin = api.getGrimPlugin(this);
    }
}

api.getGrimPlugin(this) resolves your platform plugin/mod once. Reuse that GrimPlugin when registering event handlers so Grim can clean them up by plugin lifecycle.

Cross-Platform Plugin Contexts

Modern Grim API code should pass the native platform object to api.getGrimPlugin(...) and let Grim build the GrimPlugin wrapper. Do not manually construct BasicGrimPlugin for normal Bukkit, Paper, Fabric, or cross-platform plugins; that was the old pattern before Grim could resolve native platform types itself.

Supported contexts include:

Platform Pass to api.getGrimPlugin(...)
Bukkit / Spigot / Paper Your JavaPlugin / Bukkit Plugin instance, usually this.
Fabric Your ModInitializer, a ModContainer, or the mod id string.
Shared library code A Class<?> that belongs to your plugin/mod.

The clean cross-platform shape is: keep Grim registrations in shared code that receives a native owner object, then call that shared code from each platform bootstrap.

import ac.grim.grimac.api.GrimAPIProvider;
import ac.grim.grimac.api.GrimAbstractAPI;
import ac.grim.grimac.api.event.EventBus;
import ac.grim.grimac.api.event.events.FlagEvent;
import ac.grim.grimac.api.plugin.GrimPlugin;

public final class GrimHooks {
    public static void register(Object platformOwner) {
        GrimAbstractAPI api = GrimAPIProvider.get();
        GrimPlugin grim = api.getGrimPlugin(platformOwner);
        EventBus bus = api.getEventBus();

        bus.get(FlagEvent.class).onFlag(grim, (user, check, verbose, cancelled) -> {
            grim.getLogger().info(user.getName() + " flagged " + check.getCheckName());
            return cancelled;
        });
    }
}

Bukkit/Paper bootstrap:

public final class MyBukkitPlugin extends JavaPlugin {
    @Override
    public void onEnable() {
        GrimHooks.register(this);
    }
}

Fabric bootstrap:

public final class MyFabricMod implements ModInitializer {
    @Override
    public void onInitialize() {
        GrimHooks.register(this);
        // GrimHooks.register("my-mod-id") also works when a mod id is easier.
    }
}

If your shared code cannot hold a platform instance, passing one of your own classes also works:

GrimPlugin grim = GrimAPIProvider.get().getGrimPlugin(GrimHooks.class);

BasicGrimPlugin still exists for unusual integrations that truly have no native platform owner for Grim to resolve. It should not be used in ordinary plugin examples.

Events

Grim 1.3+ uses typed event channels. Get the channel with api.getEventBus().get(EventClass.class), then subscribe with the channel's on... method.

import ac.grim.grimac.api.GrimAPIProvider;
import ac.grim.grimac.api.GrimAbstractAPI;
import ac.grim.grimac.api.event.EventBus;
import ac.grim.grimac.api.event.events.FlagEvent;
import ac.grim.grimac.api.event.events.GrimTransactionSendEvent;
import ac.grim.grimac.api.plugin.GrimPlugin;

GrimAbstractAPI api = GrimAPIProvider.get();
EventBus bus = api.getEventBus();
GrimPlugin grim = api.getGrimPlugin(this);

bus.get(GrimTransactionSendEvent.class).onTransactionSend(grim, (user, id, timestamp) -> {
    getLogger().info("sent transaction " + id + " to " + user.getName());
});

bus.get(FlagEvent.class).onFlag(grim, (user, check, verbose, cancelled) -> {
    if (shouldSuppress(user, check)) {
        return true; // cancel Grim's handling for this flag
    }
    return cancelled;
});

Cancellable event handlers return the next cancelled state. Return true to cancel, false to uncancel, or the incoming cancelled value to leave it as-is.

Priority

Handlers run in ascending priority order. Lower priority runs first; higher priority runs later and gets the final say on cancellation. This matches the Bukkit convention and is the opposite direction from pre-1.3 Grim.

bus.get(FlagEvent.class).onFlag(
        grim,
        (user, check, verbose, cancelled) -> cancelled,
        100,
        true // ignoreCancelled
);

ignoreCancelled = false means the handler is skipped once an earlier handler has cancelled the event. ignoreCancelled = true means the handler still runs and receives the current cancelled state.

Event Families

Subscribe to a concrete event when you need all fields, or to an abstract family channel when one handler should observe multiple concrete events.

import ac.grim.grimac.api.event.GrimEvent;
import ac.grim.grimac.api.event.events.GrimCheckEvent;
import ac.grim.grimac.api.event.events.GrimSetbackEvent;
import ac.grim.grimac.api.event.events.GrimVerboseCheckEvent;

GrimCheckEvent.Channel checks =
        (GrimCheckEvent.Channel) bus.get(GrimCheckEvent.class);
checks.onCheck(grim, (user, check, cancelled) -> cancelled);

GrimVerboseCheckEvent.Channel verboseChecks =
        (GrimVerboseCheckEvent.Channel) bus.get(GrimVerboseCheckEvent.class);
verboseChecks.onVerboseCheck(grim, (user, check, verbose, cancelled) -> cancelled);

GrimSetbackEvent.Channel setbacks =
        (GrimSetbackEvent.Channel) bus.get(GrimSetbackEvent.class);
setbacks.onAnySetback(grim, (user, timestamp) -> {
    getLogger().info(user.getName() + " was setback");
});

GrimEvent.Channel allEvents =
        (GrimEvent.Channel) bus.get(GrimEvent.class);
allEvents.onAnyEvent(grim, (eventClass, cancelled) -> {
    getLogger().fine(eventClass.getSimpleName() + " cancelled=" + cancelled);
});

Useful event channels include:

Channel Use
FlagEvent A check flagged. Cancellable.
CommandExecuteEvent Grim is about to execute a configured command. Cancellable.
CompletePredictionEvent Prediction completed with an offset. Cancellable.
GrimTransactionSendEvent / GrimTransactionReceivedEvent Transaction packet tracking.
GrimTeleportEvent Outbound teleport packet.
GrimPlayerSetbackEvent Player-on-foot setback with teleport id and target position.
GrimVehicleSetbackEvent Vehicle setback target position.
GrimSetbackEvent Abstract family for both setback types.
GrimCheckEvent Abstract family for all check events.
GrimVerboseCheckEvent Abstract family for check events that include verbose text.
GrimJoinEvent / GrimQuitEvent Grim user lifecycle.
GrimReloadEvent Grim reload completion.
GrimEvent Root observer for every event class. Intended for metrics/debug.

Most Grim event handlers run on the thread that fired the event. Packet, transaction, flag, prediction, teleport, and setback events commonly fire from the player's Netty thread. Do not assume you are on the Bukkit main thread.

Unregistering

Grim unregisters plugin-bound handlers on plugin disable. You can also sweep them yourself:

@Override
public void onDisable() {
    GrimAPIProvider.get().getEventBus().unregisterAllListeners(this);
}

Pass the same platform context you used for registration, or the resolved GrimPlugin.

Legacy Event API

bus.subscribe(...), bus.post(...), and reflective @GrimEventHandler listeners still work for 1.2.4-era source compatibility, but they are deprecated. New code should use the typed channel API above.

Variables

Variables registered through the API can be used in Grim messages and configuration output that supports Grim placeholders.

GrimAbstractAPI api = GrimAPIProvider.get();

api.registerVariable("%keep_alive_ping%", user -> String.valueOf(user.getKeepAlivePing()));
api.registerVariable("%server%", "Hub-1");

Use the Function<GrimUser, String> overload for player-specific values and the String overload for static values.

Player Features

GrimUser#getFeatureManager() manages per-player feature states that persist between reloads.

import ac.grim.grimac.api.GrimAbstractAPI;
import ac.grim.grimac.api.GrimUser;
import ac.grim.grimac.api.feature.FeatureState;
import org.bukkit.entity.Player;

public void enableExperimentalChecks(Player player, GrimAbstractAPI api) {
    GrimUser user = api.getGrimUser(player.getUniqueId());
    if (user == null) return;

    boolean changed = user.getFeatureManager()
            .setFeatureState("ExperimentalChecks", FeatureState.ENABLED);

    player.sendMessage(changed
            ? "Experimental checks enabled"
            : "Failed to enable experimental checks");
}

api.getGrimUser(Player) exists for old Bukkit callers but is deprecated. Use api.getGrimUser(UUID) for new code.

Storage And Verdict APIs

Grim 1.3.2 exposes experimental storage and verdict extension points:

  • api.getBackendRegistry() lets plugins register custom datastore backends before Grim's datastore starts.
  • api.getVerdictHistory() reads persisted verdicts for dashboards and /grim explain style tooling.
  • api.getRuleRegistry(), api.getMitigationRegistry(), api.getPunishmentRegistry(), and api.getVerdictSinkRegistry() expose the experimental verdict engine registries.

These APIs are marked @ApiStatus.Experimental; expect source changes between minor API versions. Normal plugins should use the event, variable, and feature APIs unless they are intentionally extending Grim's datastore or verdict engine.