Skip to content

Paper plugins

Java baseline

Compiled with release = 25, so the server must run Java 25. A Java 21 JVM refuses the class files outright with UnsupportedClassVersionError at plugin load.

No preview features and no module-info, so the jar loads from the plugin classpath like any other dependency.

Shading Jackson

Jackson is an implementation dependency and is not on Paper's classpath. Your plugin must shade and relocate it:

plugins {
    id("com.gradleup.shadow") version "9.0.0"
}

dependencies {
    implementation("nl.dylandebeer:resource-pack-identifier:0.1.0")
}

tasks.shadowJar {
    relocate("com.fasterxml.jackson", "com.example.myplugin.libs.jackson")
}

Relocation is safe: Jackson never appears in the public API. description() returns this library's own sealed JsonValue, so no relocated type can leak into a signature your code depends on.

Reading an uploaded pack

The API is Path-based, so a pack arriving as bytes needs a temp file. Delete it in a finally — leaked temp files on a long-running server are a slow disaster.

Path temp = Files.createTempFile("pack-", ".zip");
try {
    Files.write(temp, uploadedBytes);
    IdentificationResult result = identifier.identify(PackSource.archive(temp));
    // ...
} finally {
    Files.deleteIfExists(temp);
}

A zip cannot be read from a stream without buffering the whole archive anyway — the central directory lives at the end — so a temp file is not a worse option than a stream API would have been.

Don't identify on the main thread

identify does file and archive I/O. Run it off the main thread:

CompletableFuture.supplyAsync(() -> {
    try {
        return identifier.identify(path);
    } catch (IOException cause) {
        throw new CompletionException(cause);
    }
}).thenAccept(result -> Bukkit.getScheduler().runTask(plugin, () -> apply(result)));

ResourcePackIdentifier is safe to share across threads once constructed.

Untrusted input

Reads are capped at 1 MB by default, so a pack.mcmeta entry declaring itself as 4 GB cannot exhaust the heap. Lower it if you like — anything over the limit comes back as Malformed:

new ResourcePackIdentifier(VersionTable.bundled(), new PackSourceReader(64), new McmetaParser());

Only the pack.mcmeta entry is read. The rest of the archive is never opened, so a zip bomb in the assets has nothing to expand into.

Snapshot builds

Every branch push publishes to GitHub Packages as <next-patch>-<branch>-SNAPSHOT, for example 0.1.1-development-SNAPSHOT. GitHub Packages requires authentication even for public packages:

repositories {
    maven {
        url = uri("https://maven.pkg.github.com/CodeByDylan/ResourcePackIdentifier")
        credentials {
            username = providers.gradleProperty("gpr.user").get()
            password = providers.gradleProperty("gpr.token").get()
        }
    }
}

The token needs read:packages. The exact commit behind a snapshot is in the jar manifest as Git-Commit.