Developer API#
VonixGuardian exposes a stable Java API so other mods can write audit entries into the same log, or query history that VonixGuardian already collected.
API version: 1. A bump to this number signals a breaking change to method signatures or result record shapes. New methods may be added within a major version without a bump.
Two sides of the facade#
Everything routes through one facade, network.vonix.guardian.core.Guardian, which
exposes a read side and a write side:
| Side | Interface | Purpose |
|---|---|---|
| Write | event.EventSubmitter |
Submit one audit entry. |
| Read | storage.GuardianDao |
Query and count rows by QueryFilter. |
Guardian itself implements EventSubmitter, so one reference does both jobs. The
loader holds the live instance and exposes it statically:
| Loader | Accessor |
|---|---|
| Fabric | VonixGuardianFabric.guardian() |
| Forge | VonixGuardianForge.guardian() |
| NeoForge | VonixGuardianNeoForge.guardian() |
The accessor returns the singleton after boot completes, or null before and after.
Compiling against core#
The public API lives in network.vonix.guardian:vonixguardian-core. You do not need
a loader jar to compile.
repositories {
mavenLocal()
mavenCentral()
}
dependencies {
// compileOnly is correct: at runtime the loader jar already ships a shaded
// copy of core. transitive = false keeps JDBC backends off your compile
// classpath — those are runtime-provided by the loader jar.
compileOnly('network.vonix.guardian:vonixguardian-core:2.3.2') { transitive = false }
}
Getting a handle without a hard dependency#
Because core is not a hard dependency of consumer mods, resolve the API
reflectively. This is the same pattern VonixGuardian uses for LuckPerms.
public final class GuardianBridge {
private static Object api;
private static Method blockLookup;
public static boolean init() {
try {
Class<?> mod = Class.forName("network.vonix.guardian.mc.v1_20_1.forge.VonixGuardianForge");
Object guardian = mod.getMethod("guardian").invoke(null);
if (guardian == null) return false;
Method apiAccessor = guardian.getClass().getMethod("api");
api = apiAccessor.invoke(guardian);
if (api == null) return false;
// Verify the wire shape once, at startup.
int version = (int) api.getClass().getMethod("apiVersion").invoke(api);
if (version != 1) {
LOGGER.warn("VonixGuardian API v{} found, expected v1 — disabling integration", version);
return false;
}
blockLookup = api.getClass().getMethod(
"blockLookup", String.class, int.class, int.class, int.class, long.class);
return true;
} catch (ReflectiveOperationException e) {
// VonixGuardian is not installed. That is not an error.
return false;
}
}
}
Check apiVersion() once at startup and refuse to load on a mismatch rather than
discovering a shape change at runtime.
Write methods#
All write methods pass through the same gate and queue as native capture, so
blacklists, per-world overrides, and the coalescer apply to your events exactly as
they do to vanilla ones. A false return means the event was gated or the queue
rejected it — not that it was written.
| Method | Records |
|---|---|
logChat(user, actorName, worldId, message) |
chat |
logCommand(user, actorName, worldId, command) |
command |
logInteraction(user, actorName, worldId, x, y, z) |
click |
logPlacement(user, actorName, worldId, x, y, z, blockId) |
+block |
logRemoval(user, actorName, worldId, x, y, z, blockId) |
-block |
logContainerTransaction(user, actorName, worldId, x, y, z, itemId, delta, sourceTag) |
+container / -container |
user may be null for console or system origins; actorName may be null, in
which case a sentinel is used. For logContainerTransaction, a positive delta is
a deposit, negative is a withdrawal, and zero is rejected as a no-op.
boolean accepted = api.logPlacement(
player.getUUID(),
player.getGameProfile().getName(),
"minecraft:overworld",
128, 64, -342,
"minecraft:chest");
Read methods#
Every read method performs a blocking DAO query. Do not call them from the server thread — hop to your own executor first. Returned collections are immutable and safe to hand back to the server thread.
Point predicates#
| Method | Returns |
|---|---|
hasPlaced(user, worldId, x, y, z, withinSeconds) |
true if that player placed a block at that exact coordinate in the window. |
hasRemoved(user, worldId, x, y, z, withinSeconds) |
true if that player broke a block there. |
These are cheap enough for hot-path assertions such as "did this player build here recently".
Coordinate lookups#
| Method | Returns |
|---|---|
blockLookup(worldId, x, y, z, withinSeconds) |
List<BlockLookupResult> |
containerLookup(worldId, x, y, z, withinSeconds) |
List<ContainerLookupResult> |
signLookup(worldId, x, y, z, withinSeconds) |
List<SignLookupResult> |
Newest first. Empty list rather than null when nothing matches.
User lookups#
| Method | Returns |
|---|---|
chatLookup(user, withinSeconds, limit) |
List<MessageLookupResult> |
commandLookup(user, withinSeconds, limit) |
List<MessageLookupResult> |
itemLookup(user, withinSeconds, limit) |
List<ItemLookupResult> |
inventoryLookup(user, withinSeconds, limit) |
List<InventoryLookupResult> |
sessionLookup(user, withinSeconds, limit) |
List<SessionLookupResult> |
usernameLookup(user, withinSeconds, limit) |
List<UsernameLookupResult> |
withinSeconds of 0 means unbounded. limit of 0 or negative means "use the
engine default" from lookup.defaultPageSize.
Introspection#
queueLookup(worldId, x, y, z) returns actions currently sitting in the async write
queue at a coordinate — submitted but not yet flushed. It is a debugging aid; the
underlying queue is not required to expose an efficient snapshot, so it may return
an empty list even when items are queued. Do not rely on it for correctness.
Threading contract#
// Wrong — blocks the server tick on a database query.
List<BlockLookupResult> rows = api.blockLookup(world, x, y, z, 3600);
// Right — query off-thread, then hop back.
executor.execute(() -> {
List<BlockLookupResult> rows = api.blockLookup(world, x, y, z, 3600);
server.execute(() -> render(player, rows));
});
The loader for each platform supplies an executor. Reusing it keeps your integration within the same thread budget as the rest of the server instead of spawning an unbounded pool.
Versioning rules#
| Change | API version |
|---|---|
| Adding a method or a new result record | unchanged (additive within a major) |
| Changing an existing signature | bump |
| Changing a result record's shape | bump |
| Removing a method | bump |
Gate feature use on apiVersion(), not on pluginVersion(). The latter is the
human-readable mod_version string and is not a capability signal.
APIVersion() (capital A) is a CoreProtect-v12 naming compatibility alias for
apiVersion(); both return the same value.
Failure modes to handle#
| Situation | What you see |
|---|---|
| VonixGuardian not installed | ClassNotFoundException on the loader class. Treat as "feature disabled", not an error. |
| Installed but boot failed | guardian() returns null. Skip the integration. |
| API version mismatch | Your apiVersion() check returns something other than 1. Warn once, disable. |
| Event rejected by the gate | Write method returns false. Your blacklist rules applied. |
| DAO failure during read | The method throws. Do not assume an empty list means "nothing happened". |
The last one matters: "no results" and "the query failed" are different answers, and only one of them means the world is clean.
What is not stable#
Anything not listed on this page is internal. Field names on internal records,
package layout inside core beyond the two interfaces above, and the static
accessor names on loader classes may change without a deprecation cycle. Build
against VonixGuardianAPI, EventSubmitter, GuardianDao, and the result records
— those are the contract.