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.
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
compileOnlydependency is never part of the shadow JAR. Do not move the API toimplementation"to make it work"; that is the shading mistake. - Maven: Maven resolves the API from Maven Central, its default repository; only Paper needs the
papermcrepository. If you use themaven-shade-plugin,provideddependencies 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.
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:
- The main class must not reference any
com.comficonf.apitype: no fields, parameters, return types, local variables, casts orcatchclauses of API types. - Check
getServer().getPluginManager().isPluginEnabled("ComfiConf")first. - 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:
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.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.";
}@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.
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
- Build your plugin and put it on a Paper server together with the ComfiConf bridge (Modrinth or Hangar).
- Start the server. Your plugin logs that it registered, and
/comficonf pluginslists your resource with its id and display name. - Pair the server with an account at app.comficonf.com (
/comficonf pair; see the user docs) and open your plugin on the server page. - 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.