Quickstart

Add the API, declare the soft dependency, keep the integration optional, register a resource, and test it on a server.

Maven coordinates
com.comficonf:comficonf-api:0.1.0
API version
1 (ComfiConfApi.API_VERSION)
Java
25
Paper
26.1.2, 26.2, 26.3
License
Apache-2.0
Folia
Not supported

ComfiConfApi.API_VERSION is a fixed integer that identifies the API contract. It is not the artifact version: the artifact 0.1.0 implements API version 1.

1. Add the dependency

The API JAR has no runtime dependencies of its own. At runtime its classes come from the bridge JAR, which contains exactly one copy of com.comficonf.api. Your plugin compiles against the API and never shades it. A shaded copy creates a second set of API classes that the bridge cannot see; see Troubleshooting.

The API is on Maven Central as com.comficonf:comficonf-api, with its sources and Javadoc, so no extra repository is needed for it. The Javadoc is also on javadoc.io.

build.gradle.kts
plugins {
    java
}

repositories {
    mavenCentral()
    maven("https://repo.papermc.io/repository/maven-public/")
}

dependencies {
    compileOnly("io.papermc.paper:paper-api:26.1.2.build.74-stable")
    // Provided by the ComfiConf bridge at runtime. compileOnly keeps it out of your JAR.
    compileOnly("com.comficonf:comficonf-api:0.1.0")

    // The API is needed on the test classpath to unit test your provider.
    testImplementation("com.comficonf:comficonf-api:0.1.0")
    testImplementation("io.papermc.paper:paper-api:26.1.2.build.74-stable")
}

java {
    toolchain.languageVersion.set(JavaLanguageVersion.of(25))
}
  • Gradle: if you use the Shadow plugin for other libraries, keep ComfiConf out of it: a compileOnly dependency is never part of the shadow JAR. Do not move the API to implementation "to make it work"; that is the shading mistake.
  • Maven: Maven resolves the API from Maven Central, its default repository; only Paper needs the papermc repository. If you use the maven-shade-plugin, provided dependencies are not included.

2. Declare the soft dependency

The bridge must be enabled before your plugin so that its classes and its ComfiConfApi service exist when your onEnable runs.

plugin.yml
name: ComfiConfDemo
main: com.comficonf.demo.ComfiConfDemoPlugin
version: '0.1.0'
api-version: '26.1'
softdepend: [ComfiConf]

Use depend: [ComfiConf] instead only if your plugin cannot run without ComfiConf. For almost every plugin, ComfiConf is a convenience, so softdepend is right.

Paper plugins declare the dependency with an explicit load order and must join the bridge's classpath to see the API classes. Paper plugins cannot declare commands in YAML; register them with registerCommand or the lifecycle API. The ComfiConf integration itself is the same for both formats.

3. Keep the integration optional

Your plugin has to start normally when the bridge is not installed. Java loads a class when it is first used, and loading a class that refers to a missing class fails with NoClassDefFoundError. So:

  1. The main class must not reference any com.comficonf.api type: no fields, parameters, return types, local variables, casts or catch clauses of API types.
  2. Check getServer().getPluginManager().isPluginEnabled("ComfiConf") first.
  3. Only then call a separate class (here ComfiConfIntegration) that uses the API.

The demo keeps the integration behind a small interface declared in an API-free package, so the main class can hold it in a field:

DemoIntegration.java
package com.comficonf.demo;

/** The plugin's view of its optional ComfiConf integration. */
public interface DemoIntegration {
    String resourceId();
    boolean isRegistered();
    void notifyChanged();
    void close();
}
ComfiConfDemoPlugin.java
// ComfiConfDemoPlugin.onEnable(), after loading config.yml into the runtime holder
if (getServer().getPluginManager().isPluginEnabled("ComfiConf")) {
    try {
        integration = ComfiConfIntegration.register(this, runtime, configFile(), backupDirectory(),
                this::notifyAdmins);
        getLogger().info("Registered " + integration.resourceId() + " with ComfiConf");
    } catch (LinkageError e) {
        // ComfiConf is installed, but its classes do not match the API this plugin was compiled against.
        Plugin bridge = getServer().getPluginManager().getPlugin("ComfiConf");
        getLogger().warning(incompatibleBridgeMessage(
                bridge == null ? "?" : bridge.getPluginMeta().getVersion(), e));
    } catch (RuntimeException e) {
        getLogger().log(Level.WARNING, "Could not register with ComfiConf; continuing without it", e);
    }
} else {
    getLogger().info("ComfiConf is not installed; vouchers are configured through config.yml only");
}

// In the same class
static String incompatibleBridgeMessage(String bridgeVersion, LinkageError error) {
    String missing = error.getMessage() == null
            ? error.getClass().getSimpleName()
            : "missing " + error.getMessage().replace('/', '.');
    return "The installed ComfiConf " + bridgeVersion + " does not include the ComfiConf API this plugin needs ("
            + missing + "). Update ComfiConf to the version this plugin was built for; vouchers are configured"
            + " through config.yml only until then.";
}
ComfiConfDemoPlugin.java
@Override
public void onDisable() {
    if (integration != null) {
        integration.close();
        integration = null;
    }
}

ComfiConfIntegration.register has only plugin and JDK types in its signature, so referring to it does not load any API class until the call actually runs. The demo's test NoApiReferenceTest scans the compiled class files to make sure no class outside com.comficonf.demo.integration mentions com/comficonf/api; copy that test into your project (see Testing).

4. Register a resource

A resource is one editable unit, usually one configuration file. Its id has the form namespace:name, each part 1 to 64 characters of [a-z0-9._-] starting with a letter or digit. Keep the id stable across plugin versions: the website and the audit history refer to it.

ComfiConfIntegration.java
public static DemoIntegration register(JavaPlugin plugin, VoucherRuntime runtime, Path configFile,
        Path backupDirectory, Consumer<String> adminNotifier) {
    ConfigResource resource = ConfigResource.builder("comficonf-demo:vouchers")
            .displayName("Vouchers")
            .description("Voucher rewards, cooldowns and announcements of ComfiConfDemo.")
            .provider(new VoucherConfigProvider(configFile, backupDirectory, runtime, adminNotifier,
                    plugin.getLogger()))
            .callbackThread(CallbackThread.WORKER)
            .build();
    return new ComfiConfIntegration(ComfiConfApi.get().register(plugin, resource));
}

Register in onEnable, not onLoad. The builder methods and every ComfiConfApi call are listed in Registration; the provider itself is described in Providers.

5. Try it on a server

  1. Build your plugin and put it on a Paper server together with the ComfiConf bridge (Modrinth or Hangar).
  2. Start the server. Your plugin logs that it registered, and /comficonf plugins lists your resource with its id and display name.
  3. Pair the server with an account at app.comficonf.com (/comficonf pair; see the user docs) and open your plugin on the server page.
  4. Change a value, review it and apply it. Check that your file changed as expected and that the change is active.

On a test server, run the bridge with debug: true in plugins/ComfiConf/config.yml to log every message type and request id (never values), and use /comficonf status and /comficonf plugins. The checklist lists everything to verify before you release.

Built from f6347d4 on

On this page