Developer API
Grim exposes its public plugin API as ac.grim.grimac:GrimAPI. The current
API line is 1.3.2.1.
- Latest published versions: repo.grim.ac
- Public API source: GrimAnticheat/GrimAPI
- Reference plugin: GrimAnticheat/Grim-Example-API-Plugin
Requirements
- Java 17 or newer.
- A supported Grim environment. See Supported environments.
- Do not shade
GrimAPIinto your plugin. UsecompileOnly/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 explainstyle tooling.api.getRuleRegistry(),api.getMitigationRegistry(),api.getPunishmentRegistry(), andapi.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.
Available pages
More information in the readme.