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.yml

build.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: 30

Main 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 1 in milestones, levels, lucky blocks and any PulsePrison reward.
  • See the streak bonus inside sell in /stats.
  • Change the vote rewards in the addon's config.yml with the same actions they already know.

Best practices

  • Dependency: depend: [PulsePrison] and check getApiVersion() in onEnable.
  • Modules: check isModuleEnabled before 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().runTask before 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

AddonWith
Vote, crate or battle pass rewardsCustom actions and actions().run
Mine bosses with rewardsPrivateMineBlocksMinedEvent, mines().isInAnyMine, actions
Another plugin's currencyregisterProviderType
Enchants with complex logicpickaxe().registerEnchant
Web leaderboard or Discord botRead services and level, rebirth and market events
Server events (golden hour)multipliers().setGlobal or /booster global
External guilds or clansclans() and the clan value placeholder
Rank bonus from another pluginStat source

See also Current limits.

Building an addon | PulsePrison Core Docs