Docs
Building an addon
An addon is a Paper plugin that depends on PulsePrison and uses its API. This page builds a complete one: VoteRewards, which gives configurable vote rewards, adds the [votekey] action, adds a sell bonus per vote streak and announces level ups.
Structure
text
vote-rewards/
├── build.gradle.kts
├── libs/
│ └── PulsePrison-2.0.0.jar
└── src/main/
├── java/com/example/voterewards/
│ ├── VoteRewards.java
│ ├── StreakStore.java
│ └── LevelListener.java
└── resources/
├── plugin.yml
└── config.ymlbuild.gradle.kts
kotlin
plugins {
java
}
group = "com.example"
version = "1.0.0"
repositories {
mavenCentral()
maven("https://repo.papermc.io/repository/maven-public/")
}
dependencies {
compileOnly("io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT")
compileOnly(files("libs/PulsePrison-2.0.0.jar"))
}
java {
toolchain.languageVersion.set(JavaLanguageVersion.of(17))
}
tasks.processResources {
filesMatching("plugin.yml") {
expand("version" to project.version)
}
}plugin.yml
yaml
name: VoteRewards
version: '${version}'
main: com.example.voterewards.VoteRewards
api-version: '1.20'
depend: [PulsePrison]
commands:
vote-reward:
permission: voterewards.admin
usage: /<command> <player>config.yml
yaml
# Actions run for every vote. Any PulsePrison action works here.
rewards:
- "[give] tokens 2500"
- "[votekey] common 1"
- "[chance=5] [votekey] rare 1"
- "[message] &aThanks for voting!"
# Sell bonus per vote in the current streak, 0.01 is +1%.
streak-sell-bonus: 0.01
max-streak: 30Main class
java
package com.example.voterewards;
import dev.aeros.pulseprison.PulsePrison;
import dev.aeros.pulseprison.api.PulsePrisonAPI;
import dev.aeros.pulseprison.api.PulsePrisonProvider;
import dev.aeros.pulseprison.bonus.BonusService;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Map;
public final class VoteRewards extends JavaPlugin {
private PulsePrisonAPI prison;
private StreakStore streaks;
private BonusService.BonusProvider streakBonus;
@Override
public void onEnable() {
if (!PulsePrisonProvider.isAvailable() || PulsePrisonProvider.get().getApiVersion() < 3) {
getLogger().severe("PulsePrison 2.0 or newer is required");
getServer().getPluginManager().disablePlugin(this);
return;
}
saveDefaultConfig();
prison = PulsePrisonProvider.get();
streaks = new StreakStore(this);
prison.actions().register("votekey", (player, argument) -> {
String[] parts = argument.split(" ");
String crate = parts[0];
String amount = parts.length > 1 ? parts[1] : "1";
Bukkit.dispatchCommand(Bukkit.getConsoleSender(), "crates key give " + player.getName() + " " + crate + " " + amount);
});
double perVote = getConfig().getDouble("streak-sell-bonus");
streakBonus = (player, sink) -> sink.add("sell", streaks.get(player.getUniqueId()) * perVote);
PulsePrison.getInstance().getBonusService().register(streakBonus);
getServer().getPluginManager().registerEvents(new LevelListener(), this);
}
@Override
public void onDisable() {
if (streakBonus != null && PulsePrison.getInstance() != null) {
PulsePrison.getInstance().getBonusService().unregister(streakBonus);
}
if (streaks != null) {
streaks.save();
}
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
Player target = args.length > 0 ? Bukkit.getPlayerExact(args[0]) : null;
if (target == null) {
return false;
}
int streak = streaks.increment(target.getUniqueId(), getConfig().getInt("max-streak"));
PulsePrison.getInstance().getBonusService().invalidate(target.getUniqueId());
prison.actions().run(target, getConfig().getStringList("rewards"), Map.of("streak", String.valueOf(streak)));
return true;
}
}Your vote plugin runs vote-reward {player} from the console on every vote.
Storing the streak
java
package com.example.voterewards;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class StreakStore {
private final File file;
private final Map<UUID, Integer> streaks = new ConcurrentHashMap<>();
public StreakStore(JavaPlugin plugin) {
file = new File(plugin.getDataFolder(), "streaks.yml");
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
for (String key : yaml.getKeys(false)) {
streaks.put(UUID.fromString(key), yaml.getInt(key));
}
}
public int get(UUID player) {
return streaks.getOrDefault(player, 0);
}
public int increment(UUID player, int max) {
return streaks.merge(player, 1, (current, one) -> Math.min(max, current + one));
}
public void save() {
YamlConfiguration yaml = new YamlConfiguration();
streaks.forEach((player, streak) -> yaml.set(player.toString(), streak));
try {
yaml.save(file);
} catch (IOException exception) {
throw new IllegalStateException("Could not save streaks.yml", exception);
}
}
}get is called every time PulsePrison recalculates stats, so it reads from memory and not from disk.
Reacting to events
java
package com.example.voterewards;
import dev.aeros.pulseprison.api.events.PlayerLevelUpEvent;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
public final class LevelListener implements Listener {
@EventHandler
public void onLevelUp(PlayerLevelUpEvent event) {
if (event.getTrack().equals("player") && event.getLevel() % 50 == 0) {
Bukkit.broadcastMessage(event.getPlayer().getName() + " reached player level " + event.getLevel()
+ ". Vote to reach the next one faster!");
}
}
}The result for the server owner
Without touching Java, the server owner can now:
- Use
[votekey] legendary 1in milestones, levels, lucky blocks and any PulsePrison reward. - See the streak bonus inside
sellin/stats. - Change the vote rewards in the addon's
config.ymlwith the same actions they already know.
Best practices
- Dependency:
depend: [PulsePrison]and checkgetApiVersion()inonEnable. - Modules: check
isModuleEnabledbefore offering something that depends on an optional system. - Main thread: call the API from the main thread. If you do heavy work on another thread, come back with
Bukkit.getScheduler().runTaskbefore touching PulsePrison. - Cleanup: in
onDisable, remove the enchants and stat sources you registered. - Configurable: expose your values in YAML and accept PulsePrison actions in rewards, so server owners don't learn another format.
- Currencies by id: ask for the currency id in your config instead of hardcoding
tokens: every server has its own. - Languages: if your addon sends texts, use your own message file. PulsePrison
@...keys only resolve texts from its own files. - Public API first: prefer
dev.aeros.pulseprison.api. Anything you use outside that package (BaseEnchant,BonusService) may change in a major version; isolate it in one class so it is easy to adapt.
Addon ideas
| Addon | With |
|---|---|
| Vote, crate or battle pass rewards | Custom actions and actions().run |
| Mine bosses with rewards | PrivateMineBlocksMinedEvent, mines().isInAnyMine, actions |
| Another plugin's currency | registerProviderType |
| Enchants with complex logic | pickaxe().registerEnchant |
| Web leaderboard or Discord bot | Read services and level, rebirth and market events |
| Server events (golden hour) | multipliers().setGlobal or /booster global |
| External guilds or clans | clans() and the clan value placeholder |
| Rank bonus from another plugin | Stat source |
See also Current limits.