VonixGuardian Vonix Mods v2.3.2 Search GitHub
VonixGuardian/Development/Developer API

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.