Working with other plugins
Use Vault, LuckPerms and PlaceholderAPI, and let other plugins use yours.
Real servers run dozens of plugins, and the best ones cooperate. On this page you will learn how to depend on another plugin, use Vault, LuckPerms and PlaceholderAPI without crashing when they are missing, and offer your own plugin's features to other developers.
Why plugins talk to each other
Imagine your plugin sells items in a shop. Who keeps track of each player's money? If your shop invents its own money system, it will not match the one the rest of the server uses. A much better shop asks the economy plugin that is already installed. Three popular plugins show up in almost every tutorial:
| Plugin | What it is | You use it to |
|---|---|---|
| Vault | A bridge between plugins. It defines standard interfaces for money, permissions and chat prefixes. It does not do the work itself. | Charge and pay players without caring which economy plugin is installed. |
| LuckPerms | The most popular permissions plugin: groups, ranks, prefixes, temporary permissions. | Read a player's rank prefix or group. |
| PlaceholderAPI | A text-replacement system. Text like %player_name% is replaced with live values. | Fill in placeholders in your messages, or offer your own placeholders to every other plugin. |
Using one of these means two jobs: making sure the other plugin loads before yours, and writing code that calls its API, the set of classes and methods the other plugin offers to other developers. And one more job that beginners skip and regret: making your plugin still work when the other plugin is not installed.
Load order: depend and softdepend
Paper enables plugins one after another. If your plugin tries to talk to Vault before Vault has started, nothing works. You fix the order in plugin.yml (the plugin.yml chapter shows every field):
1depend: [Vault]2softdepend: [LuckPerms, PlaceholderAPI]3loadbefore: [Essentials]- A
hard dependency . Paper enables Vault first, and if Vault is missing your plugin does not load at all. - A
soft dependency . If these are installed Paper enables them first, and if they are missing your plugin still loads. - The opposite direction: your plugin starts before those. Rarely needed.
Use the name exactly as the other plugin spells its own name, capital letters included. Which one should you pick?
| Situation | Use | If the other plugin is missing |
|---|---|---|
| Your plugin is useless without it (a shop that sells things for money) | depend | Paper refuses to load your plugin and prints a clear error. |
| It adds a bonus (prefix in chat, extra placeholders) | softdepend | Your plugin loads and skips the bonus. Your code must handle that. |
[14:02:10 ERROR]: [ModernPluginLoadingStrategy] Could not load 'plugins/Shop.jar' in folder 'plugins'org.bukkit.plugin.UnknownDependencyException: Unknown/missing dependency plugins: [Vault]. Please download and install these plugins to run 'Shop'.That is what a missing depend looks like. It is a good failure: it happens at startup, with a message that names the problem.
Checking for another plugin
With a softdepend your plugin loads whether the other plugin is there or not, so you must ask. Paper's PluginManager has two questions for this:
getPlugin("Vault")returns the plugin object, ornullwhen it is not installed.isPluginEnabled("Vault")returnstrueonly when it is installed and successfully started.
The example plugin other-plugins-presence prints what is installed when it starts. First a small helper class:
Where this file livesother-plugins-presencesrcmainjavacomexampleotherpluginspresenceOptionalPlugins.java
The package com.example.otherpluginspresence is the folder path com/example/otherpluginspresence inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- other-plugins-presence/
- src/main/
- java/com/example/otherpluginspresence/Package com.example.otherpluginspresence
- HookListener.javaListener: reacts to events
- OptionalPlugins.javayou are hereHelper class
- PresencePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/otherpluginspresence/Package com.example.otherpluginspresence
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21String describe(String name) {22 Plugin plugin = server.getPluginManager().getPlugin(name);23 if (plugin == null) {24 return name + ": not installed";25 }26 if (!plugin.isEnabled()) {27 return name + ": installed but not enabled";28 }29 return name + ": enabled, version " + plugin.getPluginMeta().getVersion();30}- Asks Paper for the plugin object. A missing plugin gives
null, so the very next line checks for that. Calling a method onnullwould crash with aNullPointerException. - Installed is not the same as working. A plugin that crashed while starting is installed but not enabled.
- The plugin's own version number, taken from its
plugin.yml.
Where this file livesother-plugins-presencesrcmainjavacomexampleotherpluginspresencePresencePlugin.java
The package com.example.otherpluginspresence is the folder path com/example/otherpluginspresence inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- other-plugins-presence/
- src/main/
- java/com/example/otherpluginspresence/Package com.example.otherpluginspresence
- HookListener.javaListener: reacts to events
- OptionalPlugins.javaHelper class
- PresencePlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/otherpluginspresence/Package com.example.otherpluginspresence
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
9@Override10public void onEnable() {11 OptionalPlugins optionalPlugins = new OptionalPlugins(getServer());12 13 for (String name : OptionalPlugins.NAMES) {14 getLogger().info(optionalPlugins.describe(name));15 }16 17 getServer().getPluginManager().registerEvents(new HookListener(getLogger()), this);18 19 registerCommand("hooks", "List the optional plugins", new BasicCommand() {20 @Override21 public void execute(CommandSourceStack source, String[] args) {22 for (String name : OptionalPlugins.NAMES) {23 source.getSender().sendRichMessage("<gray>" + optionalPlugins.describe(name));24 }25 }26 27 @Override28 public String permission() {29 return "presencecheck.hooks";30 }31 });32}- Loops over the three names and logs one line for each.
- Registers a listener for plugins that start or stop later. See below.
- Registers
/hooksas a simple command. The anonymous class is aBasicCommandwritten in place; see Commands, part 1.
[14:02:11 INFO]: [PresenceCheck] Enabling PresenceCheck v1.0.0[14:02:11 INFO]: [PresenceCheck] Vault: enabled, version 1.7.3[14:02:11 INFO]: [PresenceCheck] LuckPerms: not installed[14:02:11 INFO]: [PresenceCheck] PlaceholderAPI: installed but not enabledPlugins that start later
Sometimes the plugin you want is not ready yet when your onEnable runs, even with a softdepend. The economy that Vault talks to, for example, is provided by a separate plugin that may start after yours. There are two solutions, and good plugins use both:
- Look it up at the moment you need it instead of once at startup. A shop that asks for the economy each time a player buys something is always up to date.
- Listen for
PluginEnableEvent(andPluginDisableEvent) to know the moment a plugin starts or stops. The example'sHookListenerdoes this:
Where this file livesother-plugins-presencesrcmainjavacomexampleotherpluginspresenceHookListener.java
The package com.example.otherpluginspresence is the folder path com/example/otherpluginspresence inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- other-plugins-presence/
- src/main/
- java/com/example/otherpluginspresence/Package com.example.otherpluginspresence
- HookListener.javayou are hereListener: reacts to events
- OptionalPlugins.javaHelper class
- PresencePlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/otherpluginspresence/Package com.example.otherpluginspresence
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
17@EventHandler18public void onPluginEnable(PluginEnableEvent event) {19 String name = event.getPlugin().getName();20 if (OptionalPlugins.NAMES.contains(name)) {21 logger.info(name + " just started, so its features can be used now.");22 }23}- Every plugin event carries the plugin that is starting.
- Ignore every plugin except the ones we care about. The server can have a hundred.
The crash you must avoid: NoClassDefFoundError
Here is the most common bug in plugins that use other plugins. The author tests with Vault installed, everything works, and the plugin gets released. A user without Vault installs it and sees this:
[14:02:11 ERROR]: Error occurred while enabling Shop v1.0.0 (Is it up to date?)java.lang.NoClassDefFoundError: net/milkbowl/vault/economy/Economy at com.example.shop.ShopPlugin.onEnable(ShopPlugin.java:21)Caused by: java.lang.ClassNotFoundException: net.milkbowl.vault.economy.EconomyWhy does this happen? When your plugin compiles, it only needs Vault's classes to check your code. At runtime the real Vault jar is the only place those classes exist. Java loads classes lazily, the first time your code needs them. If Vault is not installed, the moment Java touches a Vault class, it cannot find it and throws NoClassDefFoundError, which usually disables your whole plugin.
The fix is a rule and a pattern:
1private VaultHook vaultHook;2 3if (getServer().getPluginManager().isPluginEnabled("Vault")) {4 vaultHook = VaultHook.create(getServer());5}If Vault is missing, the line that creates VaultHook never runs, so Java never loads VaultHook, so it never looks for Vault's classes. The field is typed with your own class, which always exists. Everywhere else in your code, check vaultHook != null before you use it.
- shop/
- src/main/java/com/example/shop/
- ShopPlugin.javaMain class: checks isPluginEnabled, then creates the hook. Mentions no Vault types
- BuyCommand.javaAsks the hook to charge the player. Does not import anything from Vault
- VaultHook.javaThe only file that imports net.milkbowl.vault: the hook class
- src/main/java/com/example/shop/
Adding another plugin's API to your project
To write code against Vault, LuckPerms or PlaceholderAPI, your project needs their API on the compile classpath. In Gradle that takes two parts: the repository (where to download from) and the dependency line with compileOnly (use it to compile, but do not pack it into your jar, because the real plugin on the server provides the classes).
1repositories {2 maven("https://jitpack.io")3}4 5dependencies {6 compileOnly("com.github.MilkBowl:VaultAPI:1.7")7}1dependencies {2 compileOnly("net.luckperms:api:5.4")3}LuckPerms publishes to Maven Central, which the standard project already uses, so no extra repository is needed.
1repositories {2 maven("https://repo.extendedclip.com/releases/")3}4 5dependencies {6 compileOnly("me.clip:placeholderapi:2.11.6")7}Remember the two jobs: Gradle gets your code to compile, and the entry in plugin.yml gets the plugin to load first. Forgetting either one is the usual reason an integration "works on my machine" and nowhere else.
The ServicesManager: how plugins find each other's features
How does your shop find the economy plugin? It does not look for a plugin by name. It asks Paper's ServicesManager for a service: an interface that some plugin has promised to implement.
Using Vault's economy
Vault's interfaces live in net.milkbowl.vault. The economy is net.milkbowl.vault.economy.Economy. Inside the hook class you ask the ServicesManager for it:
1static VaultHook create(Server server) {2 RegisteredServiceProvider<Economy> registration =3 server.getServicesManager().getRegistration(Economy.class);4 return registration == null ? null : new VaultHook(registration.getProvider());5}- Returns
nullwhen no installed plugin has registered an economy. Vault alone is not enough: the server also needs an economy plugin, such as EssentialsX. - The real economy object. You now call it like any other object.
Once you have the Economy object, the calls are short. These come from Vault's own API:
1double balance = economy.getBalance(player);2boolean enough = economy.has(player, 50.0);3EconomyResponse result = economy.withdrawPlayer(player, 50.0);4boolean charged = result.transactionSuccess();5String shown = economy.format(50.0);- Checks the balance without changing it. Always check before you promise a player something.
- Takes money. It never throws when the player is too poor; it returns a response that says whether it worked, so always read
transactionSuccess(). - Turns 50.0 into the server's own style, like "$50.00" or "50 coins".
LuckPerms: ranks and prefixes
LuckPerms has its own API. You get it with LuckPermsProvider.get(), which only works after LuckPerms has finished starting (so make it a softdepend). The next lines read the prefix of an online player:
1LuckPerms api = LuckPermsProvider.get();2User user = api.getPlayerAdapter(Player.class).getUser(player);3String prefix = user.getCachedData().getMetaData().getPrefix();- The entry point to everything in the API.
LuckPermsis innet.luckperms.api. - A helper that turns a Paper
Playerinto a LuckPermsUserwithout you handling UUIDs. - Data LuckPerms already keeps in memory, so this call is fast and safe on the main thread.
getPrefix() returns null for a player with no prefix, and when it has one, it is usually old-style text with & color codes, such as &c[Admin]. Turn it into a modern component with the legacy serializer, as the Adventure chapter explains:
1String prefix = user.getCachedData().getMetaData().getPrefix();2Component rank = prefix == null3 ? Component.empty()4 : LegacyComponentSerializer.legacyAmpersand().deserialize(prefix);[Builder] Alex: Thanks!
PlaceholderAPI: using placeholders
A placeholder is a word between percent signs, such as %player_name%, that PlaceholderAPI swaps for a live value. Who answers the placeholder? A small add-on called an expansion. The built-in player expansion answers %player_name%. Server owners download expansions with /papi ecloud download Player.
To fill placeholders into your own text, hand the text to PlaceholderAPI:
1String raw = "Welcome, %player_name%!";2String filled = PlaceholderAPI.setPlaceholders(player, raw);- Replaces every placeholder it knows and leaves unknown ones untouched. It returns a plain
String, possibly with old color codes, so turn it into a component afterwards.
Writing your own PlaceholderAPI expansion
The other direction is even more useful: you let every plugin that supports placeholders (scoreboards, chat, holograms) show your plugin's values. An expansion is a class with an identifier and a method that answers requests. In the real class a constructor also stores the CoinService in a field named coins; the snippets leave it out to stay short.
1public final class CoinsExpansion extends PlaceholderExpansion {2 public String getIdentifier() { return "coins"; }3 public String getAuthor() { return "yourname"; }4 public String getVersion() { return "1.0.0"; }5 public boolean persist() { return true; }6}- The first word of every placeholder this expansion owns:
%coins_balance%belongs tocoins. - Tells PlaceholderAPI to keep the expansion when it reloads itself. Without it,
/papi reloadwould forget you.
1public String onRequest(OfflinePlayer player, String params) {2 if (player == null || !params.equals("balance")) {3 return null;4 }5 return String.valueOf(coins.balance(player.getUniqueId()));6}- Whatever follows the identifier. For
%coins_balance%it is"balance". - Returning
nullmeans "I do not know this placeholder", so PlaceholderAPI leaves the text unchanged.
Finally, register it once, inside the check you already learned, so a server without PlaceholderAPI never loads the class:
1if (getServer().getPluginManager().isPluginEnabled("PlaceholderAPI")) {2 new CoinsExpansion(coins).register();3}Now %coins_balance% works in every plugin on the server that understands placeholders, including your sidebar.
Providing your own API
Vault works because one plugin defines interfaces and other plugins build on them. You can do the same. The example plugin other-plugins-service-api is a tiny coin system built in three parts:
- other-plugins-service-api/
- src/main/java/com/example/otherpluginsserviceapi/
- api/
- CoinService.javaThe interface: this is the API other plugins compile against
- InMemoryCoinService.javaThe real implementation. Others never see this class
- CoinsPlugin.javaRegisters the implementation as the CoinService
- consumer/
- CoinLookup.javaHow a different plugin would find the service
- KillRewardListener.javaPays coins for monster kills, through the service
- CoinsCommand.java/coins shows the balance, through the service
- api/
- src/main/java/com/example/otherpluginsserviceapi/
The interface is tiny. It says what you can do with coins and nothing about how it works. That freedom lets you change the implementation later without breaking anyone:
Where this file livesother-plugins-service-apisrcmainjavacomexampleotherpluginsserviceapiapiCoinService.java
The package com.example.otherpluginsserviceapi.api is the folder path com/example/otherpluginsserviceapi/api inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- other-plugins-service-api/
- src/main/
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- api/Package com.example.otherpluginsserviceapi.api
- CoinService.javayou are hereInterface
- consumer/Package com.example.otherpluginsserviceapi.consumer
- CoinLookup.javaHelper class
- CoinsCommand.javaCommand (BasicCommand)
- KillRewardListener.javaListener: reacts to events
- CoinsPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- InMemoryCoinService.javaHelper class
- api/Package com.example.otherpluginsserviceapi.api
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1package com.example.otherpluginsserviceapi.api;2 3import java.util.UUID;4 5public interface CoinService {6 7 int balance(UUID playerId);8 9 void deposit(UUID playerId, int amount);10 11 boolean withdraw(UUID playerId, int amount);12}The main class registers the implementation under that interface:
Where this file livesother-plugins-service-apisrcmainjavacomexampleotherpluginsserviceapiCoinsPlugin.java
The package com.example.otherpluginsserviceapi is the folder path com/example/otherpluginsserviceapi inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- other-plugins-service-api/
- src/main/
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- api/Package com.example.otherpluginsserviceapi.api
- CoinService.javaInterface
- consumer/Package com.example.otherpluginsserviceapi.consumer
- CoinLookup.javaHelper class
- CoinsCommand.javaCommand (BasicCommand)
- KillRewardListener.javaListener: reacts to events
- CoinsPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- InMemoryCoinService.javaHelper class
- api/Package com.example.otherpluginsserviceapi.api
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
11@Override12public void onEnable() {13 getServer().getServicesManager().register(14 CoinService.class, new InMemoryCoinService(), this, ServicePriority.Normal);15 16 getServer().getPluginManager().registerEvents(new KillRewardListener(getServer()), this);17 registerCommand("coins", "Show your coin balance", new CoinsCommand(getServer()));18}- Arguments, in order: the interface other plugins ask for, the object that does the work, your plugin (so Paper knows who owns it), and a priority.
- If two plugins offer the same service, the one with the higher priority wins. The levels are
Lowest,Low,Normal,HighandHighest.
When your plugin is disabled, Paper removes its services automatically. A different plugin finds the service like this:
Where this file livesother-plugins-service-apisrcmainjavacomexampleotherpluginsserviceapiconsumerCoinLookup.java
The package com.example.otherpluginsserviceapi.consumer is the folder path com/example/otherpluginsserviceapi/consumer inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- other-plugins-service-api/
- src/main/
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- api/Package com.example.otherpluginsserviceapi.api
- CoinService.javaInterface
- consumer/Package com.example.otherpluginsserviceapi.consumer
- CoinLookup.javayou are hereHelper class
- CoinsCommand.javaCommand (BasicCommand)
- KillRewardListener.javaListener: reacts to events
- CoinsPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- InMemoryCoinService.javaHelper class
- api/Package com.example.otherpluginsserviceapi.api
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
12static CoinService find(Server server) {13 RegisteredServiceProvider<CoinService> registration =14 server.getServicesManager().getRegistration(CoinService.class);15 if (registration == null) {16 return null;17 }18 return registration.getProvider();19}- The same call as with Vault's economy. It returns
nullwhen nobody has registered the service. - Handle the missing service. The caller decides what to do. The listener simply skips the reward.
Where this file livesother-plugins-service-apisrcmainjavacomexampleotherpluginsserviceapiconsumerKillRewardListener.java
The package com.example.otherpluginsserviceapi.consumer is the folder path com/example/otherpluginsserviceapi/consumer inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- other-plugins-service-api/
- src/main/
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- api/Package com.example.otherpluginsserviceapi.api
- CoinService.javaInterface
- consumer/Package com.example.otherpluginsserviceapi.consumer
- CoinLookup.javaHelper class
- CoinsCommand.javaCommand (BasicCommand)
- KillRewardListener.javayou are hereListener: reacts to events
- CoinsPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- InMemoryCoinService.javaHelper class
- api/Package com.example.otherpluginsserviceapi.api
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/otherpluginsserviceapi/Package com.example.otherpluginsserviceapi
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
21@EventHandler22public void onDeath(EntityDeathEvent event) {23 if (!(event.getEntity() instanceof Monster)) {24 return;25 }26 Player killer = event.getEntity().getKiller();27 if (killer == null) {28 return;29 }30 CoinService coins = CoinLookup.find(server);31 if (coins == null) {32 return;33 }34 coins.deposit(killer.getUniqueId(), COINS_PER_MONSTER);35 killer.sendRichMessage("<gold>+" + COINS_PER_MONSTER + " coins");36}- Only hostile mobs pay coins.
Monsteris the Paper type for zombies, skeletons, creepers and so on. - The player who landed the final blow, or
nullfor deaths like lava or falling. - Looks the service up on every kill, not once at startup, so the reward still works if the service plugin is reloaded or loads late.
In this example both halves sit in one plugin so you can run it with one jar. In real life the consumer package would be a different plugin. To make that work:
- Build the plugin that offers the service, and give the other developer the
apiclasses, as a jar or a repository. - In the second plugin, add it with
compileOnly(files("libs/coins-api-1.0.0.jar")), so it is not copied into the second jar. - Add
softdepend: [CoinsApi]ordepend: [CoinsApi]to the second plugin'splugin.ymlso Paper loadsCoinsApifirst and lets the second plugin see its classes.
Mistakes to avoid
- Forgetting the
plugin.ymlentry. The code compiles but the other plugin may start after yours, and its classes or services are not there yet. - Packing the other plugin's API into your jar (with
implementationinstead ofcompileOnly). You then ship a second copy of the classes, which clashes with the real plugin. - Mentioning optional types in your main class. A single
importis harmless, but a field, parameter or variable of that type can crash startup. Keep them in the hook class. - Trusting
nullto never happen.getRegistration,getPluginand a prefix can all benull. Check every time. - Only checking at startup. Plugins can start late, so look services up when you need them.
- Misspelling the name.
getPluginandisPluginEnabledignore capital letters, but"Valt"never matchesVault. Independa typo means your plugin refuses to load, so copy the name from the other plugin's ownplugin.yml.
Double coins for VIPs
Change KillRewardListener so a player with the permission coinsapi.double receives twice as many coins.
Hint
Players have a method hasPermission(String). Calculate the reward into a variable before calling deposit.
Show the solution
Pick the reward first, then use it in both places:
1int reward = killer.hasPermission("coinsapi.double") ? COINS_PER_MONSTER * 2 : COINS_PER_MONSTER;2coins.deposit(killer.getUniqueId(), reward);3killer.sendRichMessage("<gold>+" + reward + " coins");The ? : is an if-else in one line. Do not forget to declare the permission in plugin.yml with a default, as the permissions chapter explains.
Make PlaceholderAPI optional
You want to use PlaceholderAPI in your plugin if it is installed. Write the two lines for plugin.yml and the line for build.gradle.kts that make that possible. Then say what your onEnable must check.
Hint
Optional means soft. And the API must be on the compile classpath without being packed into your jar.
Show the solution
1softdepend: [PlaceholderAPI]1compileOnly("me.clip:placeholderapi:2.11.6")You also need the PlaceholderAPI repository from the page above. In onEnable, check getServer().getPluginManager().isPluginEnabled("PlaceholderAPI") and only then create the class that uses PlaceholderAPI. That keeps the server without it safe from NoClassDefFoundError.
Recap
dependmeans "load me only after this plugin, and fail without it".softdependmeans "load after it if it exists".- Add the other plugin's API as
compileOnlywith its repository, and list the plugin inplugin.yml. - Use
getPluginandisPluginEnabledto check for a plugin, and keep its types inside a hook class to avoidNoClassDefFoundError. - The ServicesManager connects plugins through interfaces. Vault's economy and your own services are found with
getRegistration(Interface.class), which can returnnull. - LuckPerms gives you ranks through
LuckPermsProvider.get(), and PlaceholderAPI lets you both use placeholders and offer your own with an expansion. - You provide an API by writing an interface and registering an implementation with
register.
Quick quiz
Your plugin adds an optional prefix feature if LuckPerms is installed. What do you put in
plugin.yml?softdependloads LuckPerms first when it exists but still lets your plugin start without it.dependwould stop your plugin from loading on servers that do not use LuckPerms.A user without Vault reports a
NoClassDefFoundErrorfor a Vault class when your plugin starts. What went wrong?Java only loads a class when the code needs it. Without the Vault jar the class does not exist. Keep Vault types in a hook class that is created only afterisPluginEnabled("Vault").What does
getServicesManager().getRegistration(Economy.class)return when no economy plugin is installed?It returnsnullfor "nobody offers this". Check for it before callinggetProvider().Why should the Gradle line for another plugin's API use
compileOnly?compileOnlyuses the library to compile and then leaves it out of the jar. A second copy of the classes would clash with the real plugin.In
%coins_balance%, which part is the expansion's identifier?The identifier is the first word and chooses which expansion answers. Everything after the underscore is theparamsvalue youronRequestmethod receives.
Next steps
- Paper plugins: the newer descriptor with a clearer dependency system.
- Permissions: what LuckPerms really manages for you.
- Scoreboards, sidebars and teams: show your coins in a sidebar.