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:
/dailymemberi hadiah sekali setiap 24 jam.- Hadiah dan pesan bisa diatur di
config.ymltanpa perlu kompilasi ulang. /daily reloaduntuk 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.yml1. plugin.yml
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: opdefault: 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.
# 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
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()menyalinconfig.ymlbawaan jika belum ada, tanpa menimpa config yang sudah diedit admin.NamespacedKeyadalah kunci unik untuk menyimpan data di pemain.
4. Logika perintah
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
| Konsep | Kegunaan |
|---|---|
getConfig() / reloadConfig() | Membaca config.yml dan memuat ulang tanpa restart. |
hasPermission() | Memeriksa izin. Selalu cek izin untuk perintah admin. |
PersistentDataContainer | Menyimpan data kecil langsung di pemain, item, atau entitas, dan tetap ada setelah restart. |
TabExecutor | Menggabungkan perintah dan tab completion dalam satu class. |
MiniMessage + Placeholder.unparsed | Pesan 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
./gradlew buildSalin build/libs/KVDaily-1.0.0.jar ke folder plugins/, restart server, lalu coba:
/daily
/daily
/daily reloadKlaim 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.parseIntdi atas akan gagal jika admin menulisDIAMOND:dua. Bungkus dengantry/catchuntuk plugin produksi. - Simpan data besar (statistik, saldo) di file atau database, bukan semuanya di PersistentDataContainer.