Kawaiiverse StudioDreams · Games · Infinite Worlds

☕ JavaDevelopmentLanjut4 menit baca

Membuat Plugin Sendiri di Paper — Config, Permission, Cooldown, dan Data Pemain

Lanjutan membuat plugin Minecraft Paper dengan Java, membangun plugin hadiah harian lengkap dengan config.yml, pesan MiniMessage, permission admin, cooldown, tab completion, dan data pemain di PersistentDataContainer.

Oleh KawaiiVerse Studio · Diperbarui

Setelah membuat plugin pertama di panduan dasar plugin Java, saatnya membuat plugin yang benar-benar berguna. Kita akan membuat KVDaily, plugin hadiah harian:

  • /daily memberi hadiah sekali setiap 24 jam.
  • Hadiah dan pesan bisa diatur di config.yml tanpa perlu kompilasi ulang.
  • /daily reload untuk admin.
  • Waktu klaim terakhir disimpan di data pemain, sehingga tetap ada setelah server restart.

Struktur proyek

KVDaily/
├── build.gradle.kts          (sama seperti panduan sebelumnya)
└── src/main/
    ├── java/id/kawaiiverse/daily/
    │   ├── KVDaily.java
    │   └── DailyCommand.java
    └── resources/
        ├── plugin.yml
        └── config.yml

1. plugin.yml

yaml
name: KVDaily
version: '${version}'
main: id.kawaiiverse.daily.KVDaily
api-version: '1.21'
commands:
  daily:
    description: Ambil hadiah harian
    usage: /daily [reload]
permissions:
  kvdaily.use:
    default: true
  kvdaily.admin:
    default: op

default: op berarti hanya operator yang otomatis punya izin admin. Rank lain bisa diberi izin lewat LuckPerms (lihat panduan permission).

2. config.yml bawaan

File ini ikut dikemas di dalam jar, lalu disalin ke plugins/KVDaily/config.yml saat plugin pertama kali berjalan.

yaml
# Jeda antar klaim, dalam jam
cooldown-jam: 24

hadiah:
  - "DIAMOND:2"
  - "GOLDEN_APPLE:1"
  - "EXPERIENCE_BOTTLE:16"

pesan:
  berhasil: "<green>Hadiah harian diambil! <gray>Kembali lagi besok."
  tunggu: "<red>Belum bisa. Tunggu <yellow><sisa></yellow> lagi."
  reload: "<aqua>Config KVDaily dimuat ulang."

Pesan memakai format MiniMessage (<green>, <gray>, dan seterusnya) yang sudah tersedia di Paper. Format ini lebih mudah dibaca daripada kode &a.

3. Class utama

java
package id.kawaiiverse.daily;

import org.bukkit.NamespacedKey;
import org.bukkit.plugin.java.JavaPlugin;

public final class KVDaily extends JavaPlugin {

    private NamespacedKey lastClaimKey;

    @Override
    public void onEnable() {
        saveDefaultConfig();
        lastClaimKey = new NamespacedKey(this, "last_claim");

        DailyCommand command = new DailyCommand(this);
        getCommand("daily").setExecutor(command);
        getCommand("daily").setTabCompleter(command);
    }

    public NamespacedKey lastClaimKey() {
        return lastClaimKey;
    }
}
  • saveDefaultConfig() menyalin config.yml bawaan jika belum ada, tanpa menimpa config yang sudah diedit admin.
  • NamespacedKey adalah kunci unik untuk menyimpan data di pemain.

4. Logika perintah

java
package id.kawaiiverse.daily;

import java.time.Duration;
import java.util.List;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;

public final class DailyCommand implements TabExecutor {

    private final KVDaily plugin;
    private final MiniMessage mm = MiniMessage.miniMessage();

    public DailyCommand(KVDaily plugin) {
        this.plugin = plugin;
    }

    private void kirim(CommandSender to, String path, String sisa) {
        String teks = plugin.getConfig().getString("pesan." + path, path);
        to.sendMessage(mm.deserialize(teks, Placeholder.unparsed("sisa", sisa)));
    }

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (args.length == 1 && args[0].equalsIgnoreCase("reload")) {
            if (!sender.hasPermission("kvdaily.admin")) {
                sender.sendMessage(mm.deserialize("<red>Kamu tidak punya izin."));
                return true;
            }
            plugin.reloadConfig();
            kirim(sender, "reload", "");
            return true;
        }
        if (!(sender instanceof Player player)) {
            sender.sendMessage("Perintah ini hanya untuk pemain.");
            return true;
        }
        if (!player.hasPermission("kvdaily.use")) {
            player.sendMessage(mm.deserialize("<red>Kamu tidak punya izin."));
            return true;
        }

        PersistentDataContainer data = player.getPersistentDataContainer();
        long sekarang = System.currentTimeMillis();
        long terakhir = data.getOrDefault(plugin.lastClaimKey(), PersistentDataType.LONG, 0L);
        long jeda = Duration.ofHours(plugin.getConfig().getLong("cooldown-jam", 24)).toMillis();

        if (sekarang - terakhir < jeda) {
            Duration sisa = Duration.ofMillis(jeda - (sekarang - terakhir));
            kirim(player, "tunggu", sisa.toHours() + " jam " + sisa.toMinutesPart() + " menit");
            return true;
        }

        for (String baris : plugin.getConfig().getStringList("hadiah")) {
            String[] bagian = baris.split(":");
            Material material = Material.matchMaterial(bagian[0]);
            int jumlah = bagian.length > 1 ? Integer.parseInt(bagian[1]) : 1;
            if (material == null) {
                plugin.getLogger().warning("Item tidak dikenal di config: " + bagian[0]);
                continue;
            }
            // Jika inventory penuh, item dijatuhkan di dekat pemain
            player.getInventory().addItem(new ItemStack(material, jumlah)).values()
                .forEach(sisaItem -> player.getWorld().dropItemNaturally(player.getLocation(), sisaItem));
        }

        data.set(plugin.lastClaimKey(), PersistentDataType.LONG, sekarang);
        kirim(player, "berhasil", "");
        return true;
    }

    @Override
    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
        if (args.length == 1 && sender.hasPermission("kvdaily.admin")) {
            return List.of("reload");
        }
        return List.of();
    }
}

Konsep penting yang dipakai

KonsepKegunaan
getConfig() / reloadConfig()Membaca config.yml dan memuat ulang tanpa restart.
hasPermission()Memeriksa izin. Selalu cek izin untuk perintah admin.
PersistentDataContainerMenyimpan data kecil langsung di pemain, item, atau entitas, dan tetap ada setelah restart.
TabExecutorMenggabungkan perintah dan tab completion dalam satu class.
MiniMessage + Placeholder.unparsedPesan berwarna dari config dengan nilai dinamis yang aman.

Placeholder.unparsed memastikan teks dinamis tidak diproses sebagai tag MiniMessage. Ini penting jika nilainya berasal dari input pemain, misalnya nama atau chat.

Build dan uji

bash
./gradlew build

Salin build/libs/KVDaily-1.0.0.jar ke folder plugins/, restart server, lalu coba:

/daily
/daily
/daily reload

Klaim kedua seharusnya ditolak dengan sisa waktu.

Ide pengembangan

  • Hadiah uang lewat Vault API (lihat economy).
  • Hadiah beruntun (streak) untuk hari berturut-turut.
  • Menu GUI dengan inventory custom.
  • Mendukung pemain Bedrock dengan menu Forms dari Floodgate (lihat Geyser + Floodgate).

Praktik yang baik

  • Jangan menjalankan operasi berat (akses file besar, HTTP, database) di thread utama. Gunakan Bukkit.getScheduler().runTaskAsynchronously(...), lalu kembali ke thread utama untuk mengubah dunia atau pemain.
  • Tangani input yang salah. Misalnya, Integer.parseInt di atas akan gagal jika admin menulis DIAMOND:dua. Bungkus dengan try/catch untuk plugin produksi.
  • Simpan data besar (statistik, saldo) di file atau database, bukan semuanya di PersistentDataContainer.

Bacaan lanjut