papermc-skill 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/bin/cli.js +307 -0
  5. package/docs/01-modern-paper-architecture.md +172 -0
  6. package/docs/02-adventure-and-minimessage.md +160 -0
  7. package/docs/03-persistent-data-container.md +179 -0
  8. package/docs/04-commands-and-brigadier.md +157 -0
  9. package/docs/05-concurrency-and-folia.md +150 -0
  10. package/docs/06-events-and-listeners.md +163 -0
  11. package/docs/07-best-practices-and-perf.md +118 -0
  12. package/docs/08-gui-and-menus.md +198 -0
  13. package/docs/09-custom-items-and-combat.md +171 -0
  14. package/docs/10-database-and-storage.md +187 -0
  15. package/package.json +44 -0
  16. package/skills/paper-plugin-dev/SKILL.md +167 -0
  17. package/templates/paper-modern-template/.editorconfig +8 -0
  18. package/templates/paper-modern-template/.gradle/9.4.1/checksums/checksums.lock +0 -0
  19. package/templates/paper-modern-template/.gradle/9.4.1/checksums/md5-checksums.bin +0 -0
  20. package/templates/paper-modern-template/.gradle/9.4.1/checksums/sha1-checksums.bin +0 -0
  21. package/templates/paper-modern-template/.gradle/9.4.1/executionHistory/executionHistory.bin +0 -0
  22. package/templates/paper-modern-template/.gradle/9.4.1/executionHistory/executionHistory.lock +0 -0
  23. package/templates/paper-modern-template/.gradle/9.4.1/fileChanges/last-build.bin +0 -0
  24. package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.bin +0 -0
  25. package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.lock +0 -0
  26. package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/resourceHashesCache.bin +0 -0
  27. package/templates/paper-modern-template/.gradle/9.4.1/gc.properties +0 -0
  28. package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  29. package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +2 -0
  30. package/templates/paper-modern-template/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  31. package/templates/paper-modern-template/.gradle/file-system.probe +0 -0
  32. package/templates/paper-modern-template/.gradle/vcs-1/gc.properties +0 -0
  33. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/ExamplePaperPlugin.class +0 -0
  34. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/PaperPluginBootstrap.class +0 -0
  35. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/command/HelloCommand.class +0 -0
  36. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/data/PluginDataKeys.class +0 -0
  37. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/listener/PlayerEventListener.class +0 -0
  38. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/util/SchedulerAdapter.class +0 -0
  39. package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
  40. package/templates/paper-modern-template/build.gradle.kts +54 -0
  41. package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.jar +0 -0
  42. package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.properties +7 -0
  43. package/templates/paper-modern-template/gradlew +248 -0
  44. package/templates/paper-modern-template/gradlew.bat +93 -0
  45. package/templates/paper-modern-template/settings.gradle.kts +5 -0
  46. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +35 -0
  47. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +30 -0
  48. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +55 -0
  49. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +22 -0
  50. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +61 -0
  51. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +77 -0
  52. package/templates/paper-modern-template/src/main/resources/config.yml +10 -0
  53. package/templates/paper-modern-template/src/main/resources/paper-plugin.yml +9 -0
@@ -0,0 +1,150 @@
1
+ # Konkurensi, Threading & Kompatibilitas Folia
2
+
3
+ Salah satu perubahan paling revolusioner di ekosistem Paper adalah kedatangan **Folia**. Folia memecah satu "Server Main Thread" Minecraft menjadi ratusan thread regional mandiri (**Threaded Regions**).
4
+
5
+ Konsekuensinya, metode konkurensi lama seperti `Bukkit.getScheduler()` atau mengakses state world dari sembarang thread akan menyebabkan **`IllegalStateException` atau crash fatal** di Folia.
6
+
7
+ ---
8
+
9
+ ## 1. Model Threading: Bukkit vs Paper vs Folia
10
+
11
+ ```
12
+ [Bukkit / Vanilla]
13
+ Satu Main Thread ─── Tick Semua Dunia, Entity, Blok, Redstone, AI, Chunk ───>
14
+
15
+ [Folia Regionized Threading]
16
+ Region Thread 1 ─── Tick Region Dunia A (Chunk 0..32, 0..32) ───>
17
+ Region Thread 2 ─── Tick Region Dunia A (Chunk 64..96, 64..96) ──>
18
+ Region Thread 3 ─── Tick Nether (Region B) ──────────────────────>
19
+ Global Thread ─── Tick Weather, Time, Player List, Commands ───>
20
+ Async Threads ─── Network I/O, File Saving, Database Queries ──>
21
+ ```
22
+
23
+ Di Folia:
24
+ - Tidak ada lagi konsep "The Main Thread".
25
+ - Objek `World`, `Entity`, dan `Block` hanya boleh diakses di **thread region yang memiliki chunk/lokasi tersebut**.
26
+ - Mengakses entity pemain dari thread lain akan melempar error `Plugin attempted to access entity from wrong thread`.
27
+
28
+ ---
29
+
30
+ ## 2. Hirarki Scheduler Modern di Folia & Paper
31
+
32
+ Paper memperkenalkan scheduler baru yang kompatibel dengan Folia dan Paper standar:
33
+
34
+ ### 1. `RegionScheduler`
35
+ Digunakan untuk mengeksekusi task pada lokasi dunia atau koordinat chunk tertentu.
36
+
37
+ ```java
38
+ import org.bukkit.Bukkit;
39
+ import org.bukkit.Location;
40
+ import org.bukkit.plugin.Plugin;
41
+
42
+ Location loc = player.getLocation();
43
+
44
+ // Menjalankan task 1-kali pada thread region lokasi tersebut
45
+ Bukkit.getRegionScheduler().execute(plugin, loc, () -> {
46
+ loc.getBlock().setType(org.bukkit.Material.GOLD_BLOCK);
47
+ });
48
+
49
+ // Menjalankan task berulang (repeating) pada region
50
+ Bukkit.getRegionScheduler().runAtFixedRate(plugin, loc, task -> {
51
+ loc.getWorld().spawnParticle(org.bukkit.Particle.FLAME, loc, 5);
52
+ }, 20L, 20L); // delay 20 tick, period 20 tick
53
+ ```
54
+
55
+ ### 2. `EntityScheduler`
56
+ Setiap `Entity` memiliki scheduler tersendiri. Task akan dieksekusi tepat pada thread region di mana entity tersebut sedang berada, bahkan jika entity tersebut bergerak/teleport antar region.
57
+
58
+ ```java
59
+ player.getScheduler().run(plugin, task -> {
60
+ player.giveExp(10);
61
+ player.sendMessage("Bonus EXP didapatkan!");
62
+ }, () -> {
63
+ // Callback jika entity telah mati atau tidak valid sebelum task sempat berjalan
64
+ plugin.getLogger().warning("Pemain logout sebelum exp diberikan.");
65
+ });
66
+ ```
67
+
68
+ ### 3. `GlobalRegionScheduler`
69
+ Digunakan untuk hal-hal yang bersifat global server dan tidak terikat pada koordinat block/entity tertentu (misal: pengumuman berkala chat global, update scoreboard global, waktu dunia).
70
+
71
+ ```java
72
+ Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, task -> {
73
+ Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<gold>[Pengumuman] Kunjungi discord kami!</gold>"));
74
+ }, 100L, 1200L);
75
+ ```
76
+
77
+ ### 4. `AsyncScheduler`
78
+ Untuk I/O, database query, request HTTP, komputasi berat non-Bukkit API. Menggunakan waktu nyata (`java.time.TimeUnit`), bukan tick game:
79
+
80
+ ```java
81
+ import java.util.concurrent.TimeUnit;
82
+
83
+ Bukkit.getAsyncScheduler().runDelayed(plugin, task -> {
84
+ // Menjalankan query database MySQL / SQLite
85
+ database.savePlayerData(uuid, data);
86
+ }, 5, TimeUnit.SECONDS);
87
+ ```
88
+
89
+ ---
90
+
91
+ ## 3. Pola Abstraksi Scheduler Universal (Paper + Folia)
92
+
93
+ Banyak plugin perlu berjalan di server Paper standar (single-thread) dan Folia sekaligus. Cara terbaik adalah membuat lapisan pembungkus (*abstraction wrapper*) seperti yang digunakan oleh **Chunky** dan **squaremap**:
94
+
95
+ ```java
96
+ public final class PlatformScheduler {
97
+
98
+ private static final boolean IS_FOLIA = checkFolia();
99
+
100
+ private static boolean checkFolia() {
101
+ try {
102
+ Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
103
+ return true;
104
+ } catch (ClassNotFoundException e) {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ public static boolean isFolia() {
110
+ return IS_FOLIA;
111
+ }
112
+
113
+ public static void runLocation(Plugin plugin, Location location, Runnable runnable) {
114
+ if (IS_FOLIA) {
115
+ Bukkit.getRegionScheduler().execute(plugin, location, runnable);
116
+ } else {
117
+ Bukkit.getScheduler().runTask(plugin, runnable);
118
+ }
119
+ }
120
+
121
+ public static void runEntity(Plugin plugin, Entity entity, Runnable runnable) {
122
+ if (IS_FOLIA) {
123
+ entity.getScheduler().run(plugin, task -> runnable.run(), null);
124
+ } else {
125
+ Bukkit.getScheduler().runTask(plugin, runnable);
126
+ }
127
+ }
128
+
129
+ public static void runAsync(Plugin plugin, Runnable runnable) {
130
+ if (IS_FOLIA) {
131
+ Bukkit.getAsyncScheduler().runNow(plugin, task -> runnable.run());
132
+ } else {
133
+ Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable);
134
+ }
135
+ }
136
+ }
137
+ ```
138
+
139
+ ---
140
+
141
+ ## 4. Aturan Wajib untuk Kompatibilitas Folia
142
+
143
+ 1. **Deklarasikan di Manifest**:
144
+ Tambahkan `folia-supported: true` di `paper-plugin.yml`. Jika tidak, server Folia akan menampilkan peringatan bahwa plugin Anda berpotensi tidak aman.
145
+ 2. **Jangan Pernah Mengakses Chunk/Block dari Async Thread**:
146
+ Gunakan `world.getChunkAtAsync(x, z)` yang mengembalikan `CompletableFuture<Chunk>`.
147
+ 3. **Gunakan TeleportAsync**:
148
+ Gunakan `entity.teleportAsync(location)` alih-alih `entity.teleport(location)`. Di Folia, teleportasi melintasi region thread yang berbeda harus dilakukan secara asinkron.
149
+ 4. **Hindari State Statis Global**:
150
+ Variabel static yang menyimpan list player atau entity aktif dapat mengalami *race condition* jika dimodifikasi oleh beberapa thread region secara paralel. Gunakan struktur data thread-safe (`ConcurrentHashMap`, `CopyOnWriteArrayList`) bila terpaksa.
@@ -0,0 +1,163 @@
1
+ # Sistem Event & Listener di Paper
2
+
3
+ Sistem event adalah mekanisme reaktif utama di Paper untuk merespons aksi pemain, lingkungan dunia, entitas, dan siklus server. Paper menyediakan banyak event eksklusif berkinerja tinggi serta integrasi penuh dengan Adventure Component.
4
+
5
+ ---
6
+
7
+ ## 1. Anatomi Listener Standar
8
+
9
+ Listener mengimplementasikan antarmuka `org.bukkit.event.Listener` dan menggunakan anotasi `@EventHandler`:
10
+
11
+ ```java
12
+ import org.bukkit.event.Listener;
13
+ import org.bukkit.event.EventHandler;
14
+ import org.bukkit.event.EventPriority;
15
+ import org.bukkit.event.player.PlayerJoinEvent;
16
+ import org.bukkit.plugin.java.JavaPlugin;
17
+ import net.kyori.adventure.text.minimessage.MiniMessage;
18
+
19
+ public final class PlayerJoinListener implements Listener {
20
+
21
+ private final JavaPlugin plugin;
22
+
23
+ public PlayerJoinListener(JavaPlugin plugin) {
24
+ this.plugin = plugin;
25
+ }
26
+
27
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
28
+ public void onPlayerJoin(PlayerJoinEvent event) {
29
+ // Paper menggunakan Adventure Component untuk joinMessage
30
+ event.joinMessage(
31
+ MiniMessage.miniMessage().deserialize(
32
+ "<gray>[<green>+</green>] <yellow>" + event.getPlayer().getName() + "</yellow> bergabung ke server!</gray>"
33
+ )
34
+ );
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Memahami `EventPriority`:
40
+ Urutan eksekusi event dari pertama hingga terakhir:
41
+ 1. `LOWEST` (dijalankan paling awal, bagus untuk observasi mentah)
42
+ 2. `LOW`
43
+ 3. `NORMAL` (default)
44
+ 4. `HIGH`
45
+ 5. `HIGHEST` (dijalankan paling akhir sebelum event final)
46
+ 6. `MONITOR` (**HANYA UNTUK MEMBACA/LOGGING**; DILARANG memodifikasi atau membatalkan event di priority ini karena plugin lain tidak akan dapat merespons perubahan Anda).
47
+
48
+ > **Penting**: Selalu tambahkan `ignoreCancelled = true` jika listener Anda tidak perlu memproses aksi yang sudah dibatalkan oleh plugin proteksi area (seperti WorldGuard/GriefPrevention).
49
+
50
+ ---
51
+
52
+ ## 2. Event Modern Eksklusif Paper
53
+
54
+ Paper mengganti atau melengkapi beberapa event Bukkit lama dengan alternatif yang lebih efisien dan modern:
55
+
56
+ ### `AsyncChatEvent` (Menggantikan `AsyncPlayerChatEvent`)
57
+ Paper menggunakan `io.papermc.paper.event.player.AsyncChatEvent` yang bekerja langsung dengan `Component` dan MiniMessage:
58
+
59
+ ```java
60
+ import io.papermc.paper.event.player.AsyncChatEvent;
61
+ import net.kyori.adventure.text.Component;
62
+ import net.kyori.adventure.text.minimessage.MiniMessage;
63
+
64
+ @EventHandler
65
+ public void onChat(AsyncChatEvent event) {
66
+ // Memodifikasi format pesan dengan Adventure
67
+ event.renderer((source, sourceDisplayName, message, viewer) ->
68
+ MiniMessage.miniMessage().deserialize("<gray>[Member] </gray>")
69
+ .append(sourceDisplayName)
70
+ .append(Component.text(": "))
71
+ .append(message)
72
+ );
73
+ }
74
+ ```
75
+
76
+ ### `PrePlayerAttackEntityEvent`
77
+ Dijalankan sebelum kalkulasi serangan vanilla terjadi. Sangat ideal untuk sistem combat kustom, cooldown attack senjata khusus, atau verifikasi proteksi PvP.
78
+
79
+ ### `ServerTickStartEvent` & `ServerTickEndEvent`
80
+ Memonitor performa server secara presisi per-tick tanpa membebani scheduler.
81
+
82
+ ---
83
+
84
+ ## 3. Membuat Custom Event
85
+
86
+ Anda dapat membuat event sendiri untuk mengekspos API plugin Anda kepada developer lain (seperti yang dilakukan oleh LuckPerms atau WorldGuard).
87
+
88
+ ### Contoh Custom Event:
89
+ ```java
90
+ package com.example.plugin.event;
91
+
92
+ import org.bukkit.entity.Player;
93
+ import org.bukkit.event.Cancellable;
94
+ import org.bukkit.event.Event;
95
+ import org.bukkit.event.HandlerList;
96
+ import org.jetbrains.annotations.NotNull;
97
+
98
+ public final class PlayerLevelUpEvent extends Event implements Cancellable {
99
+
100
+ private static final HandlerList HANDLERS = new HandlerList();
101
+ private final Player player;
102
+ private final int newLevel;
103
+ private boolean cancelled = false;
104
+
105
+ public PlayerLevelUpEvent(Player player, int newLevel) {
106
+ // false jika berjalan di main/region thread (Sync), true jika Async
107
+ super(false);
108
+ this.player = player;
109
+ this.newLevel = newLevel;
110
+ }
111
+
112
+ public Player getPlayer() {
113
+ return player;
114
+ }
115
+
116
+ public int getNewLevel() {
117
+ return newLevel;
118
+ }
119
+
120
+ @Override
121
+ public boolean isCancelled() {
122
+ return cancelled;
123
+ }
124
+
125
+ @Override
126
+ public void setCancelled(boolean cancel) {
127
+ this.cancelled = cancel;
128
+ }
129
+
130
+ @NotNull
131
+ @Override
132
+ public HandlerList getHandlers() {
133
+ return HANDLERS;
134
+ }
135
+
136
+ @NotNull
137
+ public static HandlerList getHandlerList() {
138
+ return HANDLERS;
139
+ }
140
+ }
141
+ ```
142
+
143
+ ### Memanggil (Dispatch) Event:
144
+ ```java
145
+ PlayerLevelUpEvent event = new PlayerLevelUpEvent(player, 10);
146
+ Bukkit.getPluginManager().callEvent(event);
147
+
148
+ if (!event.isCancelled()) {
149
+ // Lanjutkan aksi jika event tidak dibatalkan oleh plugin lain
150
+ player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Selamat, Anda naik level!</green>"));
151
+ }
152
+ ```
153
+
154
+ ---
155
+
156
+ ## 4. Unregistering Event & Pembersihan
157
+
158
+ Untuk mencegah memory leak saat reload atau saat plugin dinonaktifkan:
159
+ - Semua event yang didaftarkan melalui `pluginManager.registerEvents(listener, this)` otomatis di-unregister saat `onDisable()`.
160
+ - Jika Anda mendaftarkan event dinamis (misal per-minigame match), unregister secara manual ketika game selesai:
161
+ ```java
162
+ HandlerList.unregisterAll(matchListener);
163
+ ```
@@ -0,0 +1,118 @@
1
+ # Best Practices & Optimasi Performa Plugin Paper
2
+
3
+ Pengembangan plugin Minecraft di server berkapasitas besar menuntut efisiensi tinggi, bebas memory leak, dan tidak memblokir tick game. Dokumen ini merangkum aturan emas dan anti-pattern yang sering terjadi.
4
+
5
+ ---
6
+
7
+ ## 1. Mencegah Memory Leak (Kebocoran Memori)
8
+
9
+ Minecraft memiliki arsitektur objek yang saling terhubung erat. Menyimpan referensi objek Bukkit di memori Java lebih lama dari masa hidupnya dapat mencegah Garbage Collector (GC) membersihkan ratusan megabyte data.
10
+
11
+ ### 🔴 Anti-Pattern: Menyimpan Objek Bukkit di Collection Statis
12
+ ```java
13
+ // SANGAT BERBAHAYA: Player objek menahan referensi ke World, Chunk, Connection, dll.
14
+ public static final Set<Player> activePlayers = new HashSet<>();
15
+ public static final Map<Player, PlayerData> dataMap = new HashMap<>();
16
+ ```
17
+ Jika pemain disconnect, objek `Player` tidak akan bisa di-garbage collect!
18
+
19
+ ### 🟢 Solusi: Simpan `UUID`
20
+ ```java
21
+ // AMAN: UUID hanya 16-byte identifier yang tidak menahan referensi dunia/koneksi
22
+ public static final Set<UUID> activePlayerUuids = new HashSet<>();
23
+ public static final Map<UUID, PlayerData> dataMap = new ConcurrentHashMap<>();
24
+
25
+ // Ambil instance player hanya saat dibutuhkan:
26
+ Player player = Bukkit.getPlayer(uuid);
27
+ if (player != null) {
28
+ // Gunakan
29
+ }
30
+ ```
31
+
32
+ > **Aturan Umum**: Jangan pernah menyimpan `Player`, `Entity`, `World`, `Chunk`, atau `Block` di dalam field permanen/statis. Gunakan `UUID`, `NamespacedKey`, atau koordinat primitif (`worldName`, `x`, `y`, `z`).
33
+
34
+ ---
35
+
36
+ ## 2. Operasi I/O & Database Selalu Asinkron
37
+
38
+ Setiap tick server berjalan dalam ~50 milidetik (20 TPS). Operasi disk I/O, pembacaan file besar, koneksi web/HTTP, atau query database yang membutuhkan waktu 100ms akan langsung menyebabkan server lag/freeze (tps drop).
39
+
40
+ ### 🟢 Solusi: Gunakan Async Threads & CompletableFuture
41
+ ```java
42
+ import java.util.concurrent.CompletableFuture;
43
+ import org.bukkit.Bukkit;
44
+
45
+ public CompletableFuture<PlayerData> loadDataAsync(UUID uuid) {
46
+ return CompletableFuture.supplyAsync(() -> {
47
+ // Berjalan di luar thread game (off-main-thread)
48
+ return database.fetchUser(uuid);
49
+ }, myPluginThreadPool).thenApply(data -> {
50
+ return data != null ? data : new PlayerData(uuid);
51
+ });
52
+ }
53
+
54
+ // Menggunakan hasilnya:
55
+ loadDataAsync(player.getUniqueId()).thenAccept(data -> {
56
+ // Jika perlu memodifikasi state game Bukkit (seperti memberi item),
57
+ // jadwalkan kembali ke scheduler region pemain:
58
+ player.getScheduler().run(plugin, task -> {
59
+ player.giveExp(data.getPendingExp());
60
+ }, null);
61
+ });
62
+ ```
63
+
64
+ ---
65
+
66
+ ## 3. Menghindari Synchronous Chunk Loading
67
+
68
+ Memanggil `world.getChunkAt(x, z)` atau `location.getBlock()` pada chunk yang belum di-load akan memaksa server memuat chunk tersebut dari disk secara synchronous (membekukan tick thread hingga chunk selesai dibaca).
69
+
70
+ ### 🔴 Anti-Pattern
71
+ ```java
72
+ Chunk chunk = world.getChunkAt(x, z); // Membekukan server jika chunk belum di-load!
73
+ ```
74
+
75
+ ### 🟢 Solusi Paper: Asynchronous Chunk Loading
76
+ Gunakan API Paper `getChunkAtAsync`:
77
+
78
+ ```java
79
+ world.getChunkAtAsync(x, z, true).thenAccept(chunk -> {
80
+ // Chunk berhasil dimuat tanpa lag spike pada TPS
81
+ processChunkBlocks(chunk);
82
+ });
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 4. Konfigurasi (`config.yml`) yang Tangguh
88
+
89
+ Di Paper, file YAML secara default menggunakan encoding UTF-8. Pastikan membaca dan menyimpan file konfigurasi dengan aman:
90
+
91
+ ```java
92
+ // Menyimpan default config dari jar jika belum ada
93
+ saveDefaultConfig();
94
+
95
+ // Reload config
96
+ reloadConfig();
97
+ FileConfiguration config = getConfig();
98
+
99
+ // Mengambil pesan dengan fallback default
100
+ String rawMessage = config.getString("messages.welcome", "<green>Selamat datang!</green>");
101
+ Component message = MiniMessage.miniMessage().deserialize(rawMessage);
102
+ ```
103
+
104
+ Untuk struktur config yang sangat kompleks, pertimbangkan menggunakan library **Configurate** (`org.spongepowered:configurate-yaml`) yang mendukung pemeliharaan komentar file YAML, schema validation, dan serialisasi otomatis ke record Java.
105
+
106
+ ---
107
+
108
+ ## 5. Ringkasan Checklist Anti-Pattern
109
+
110
+ | Jangan Lakukan ❌ | Lakukan Ini ✔️ |
111
+ | :--- | :--- |
112
+ | `ChatColor.RED + "Teks"` | `MiniMessage.miniMessage().deserialize("<red>Teks</red>")` |
113
+ | `player.sendMessage("§a...")` | `player.sendMessage(Component)` |
114
+ | `new NamespacedKey(...)` di per-event loop | Simpan instance `NamespacedKey` sebagai `static final` |
115
+ | `world.getBlockAt(x, y, z)` acak di async thread | Akses block hanya di thread region yang bersangkutan |
116
+ | Mengabaikan `paper-plugin.yml` untuk plugin baru | Manfaatkan `paper-plugin.yml` dan `PluginBootstrap` |
117
+ | Menahan koneksi database terbuka di main thread | Gunakan connection pool (seperti HikariCP) & query async |
118
+ | Mengabaikan `folia-supported` | Rancang scheduler menggunakan abstraksi region / universal |
@@ -0,0 +1,198 @@
1
+ # Pembuatan GUI / Menu Inventory Modern di Paper
2
+
3
+ Membuat antarmuka interaktif (GUI Menu) menggunakan inventaris chest adalah salah satu fitur paling sering dibuat di plugin Minecraft. Pendekatan lama yang memeriksa judul GUI dengan string berwarna (`ChatColor.stripColor`) atau mencocokkan nomor slot sangat rapuh dan rawan bug duplikasi item.
4
+
5
+ Paper modern menawarkan cara yang jauh lebih aman, bersih, dan type-safe menggunakan **Kyori Adventure** dan **PersistentDataContainer (PDC)**.
6
+
7
+ ---
8
+
9
+ ## 1. Masalah pada Pendekatan Lama vs Solusi Modern
10
+
11
+ | Masalah Legacy (Lama) | Solusi Modern Paper |
12
+ | :--- | :--- |
13
+ | Memeriksa judul menu dengan `event.getView().getTitle().equals("§8Menu")` | Memeriksa `InventoryHolder` kustom atau PDC metadata pada item. |
14
+ | Mencocokkan fungsi tombol berdasarkan `event.getSlot() == 13` | Menandai tombol dengan `PDC` (`NamespacedKey: "gui_action" -> "buy_item"`). |
15
+ | Merusak format warna teks jika ada reload atau translate | Menggunakan Adventure `Component` untuk title inventaris. |
16
+ | Rawan bug eksploitasi duplikasi jika `event.setCancelled(true)` luput | Selalu batalkan event jika holder terverifikasi sebagai custom GUI. |
17
+
18
+ ---
19
+
20
+ ## 2. Pola Custom `InventoryHolder`
21
+
22
+ Cara paling handal dan standar industri untuk mengenali GUI buatan plugin Anda adalah membuat kelas yang mengimplementasikan `InventoryHolder`:
23
+
24
+ ```java
25
+ package com.example.plugin.gui;
26
+
27
+ import net.kyori.adventure.text.Component;
28
+ import org.bukkit.Bukkit;
29
+ import org.bukkit.inventory.Inventory;
30
+ import org.bukkit.inventory.InventoryHolder;
31
+ import org.jetbrains.annotations.NotNull;
32
+
33
+ public class CustomMenuHolder implements InventoryHolder {
34
+
35
+ private final Inventory inventory;
36
+ private final String menuId;
37
+
38
+ public CustomMenuHolder(Component title, int rows, String menuId) {
39
+ this.menuId = menuId;
40
+ // Bukkit.createInventory mendukung Adventure Component secara native di Paper
41
+ this.inventory = Bukkit.createInventory(this, rows * 9, title);
42
+ }
43
+
44
+ public String getMenuId() {
45
+ return menuId;
46
+ }
47
+
48
+ @Override
49
+ public @NotNull Inventory getInventory() {
50
+ return inventory;
51
+ }
52
+ }
53
+ ```
54
+
55
+ ---
56
+
57
+ ## 3. Membuat Tombol dengan Penanda PDC
58
+
59
+ Daripada mengingat nomor slot, tandai setiap item tombol dengan aksi spesifik di `PersistentDataContainer`:
60
+
61
+ ```java
62
+ package com.example.plugin.gui;
63
+
64
+ import net.kyori.adventure.text.minimessage.MiniMessage;
65
+ import org.bukkit.Material;
66
+ import org.bukkit.NamespacedKey;
67
+ import org.bukkit.inventory.ItemStack;
68
+ import org.bukkit.inventory.meta.ItemMeta;
69
+ import org.bukkit.persistence.PersistentDataType;
70
+ import org.bukkit.plugin.Plugin;
71
+
72
+ import java.util.List;
73
+
74
+ public final class GuiItemBuilder {
75
+
76
+ public static final NamespacedKey ACTION_KEY = new NamespacedKey("myplugin", "gui_action");
77
+
78
+ public static ItemStack createButton(Material material, String nameMiniMessage, List<String> loreMiniMessage, String actionId) {
79
+ ItemStack item = new ItemStack(material);
80
+ ItemMeta meta = item.getItemMeta();
81
+ if (meta != null) {
82
+ meta.displayName(MiniMessage.miniMessage().deserialize(nameMiniMessage));
83
+
84
+ meta.lore(loreMiniMessage.stream()
85
+ .map(line -> MiniMessage.miniMessage().deserialize(line))
86
+ .toList()
87
+ );
88
+
89
+ // Tandai tombol dengan ID aksi unik
90
+ meta.getPersistentDataContainer().set(ACTION_KEY, PersistentDataType.STRING, actionId);
91
+ item.setItemMeta(meta);
92
+ }
93
+ return item;
94
+ }
95
+ }
96
+ ```
97
+
98
+ ---
99
+
100
+ ## 4. Merakit Menu
101
+
102
+ ```java
103
+ public void openShopMenu(Player player) {
104
+ CustomMenuHolder holder = new CustomMenuHolder(
105
+ MiniMessage.miniMessage().deserialize("<gradient:#f12711:#f5af19><bold>Toko Server</bold></gradient>"),
106
+ 3, // 3 baris = 27 slot
107
+ "server_shop"
108
+ );
109
+ Inventory inv = holder.getInventory();
110
+
111
+ // Border pengisi (filler)
112
+ ItemStack filler = GuiItemBuilder.createButton(
113
+ Material.GRAY_STAINED_GLASS_PANE,
114
+ "<gray> </gray>",
115
+ List.of(),
116
+ "noop" // No operation
117
+ );
118
+ for (int i = 0; i < inv.getSize(); i++) {
119
+ inv.setItem(i, filler);
120
+ }
121
+
122
+ // Tombol aksi
123
+ inv.setItem(11, GuiItemBuilder.createButton(
124
+ Material.DIAMOND_SWORD,
125
+ "<aqua><bold>Beli Senjata Legendaris</bold></aqua>",
126
+ List.of("<gray>Harga: <gold>500 Koin</gold></gray>", "<yellow>Klik untuk membeli!</yellow>"),
127
+ "buy_sword"
128
+ ));
129
+
130
+ inv.setItem(15, GuiItemBuilder.createButton(
131
+ Material.BARRIER,
132
+ "<red><bold>Tutup Menu</bold></red>",
133
+ List.of("<gray>Klik untuk keluar</gray>"),
134
+ "close_menu"
135
+ ));
136
+
137
+ player.openInventory(inv);
138
+ }
139
+ ```
140
+
141
+ ---
142
+
143
+ ## 5. Event Listener Menu yang Aman dari Eksploitasi
144
+
145
+ ```java
146
+ package com.example.plugin.gui;
147
+
148
+ import net.kyori.adventure.text.minimessage.MiniMessage;
149
+ import org.bukkit.entity.Player;
150
+ import org.bukkit.event.EventHandler;
151
+ import org.bukkit.event.EventPriority;
152
+ import org.bukkit.event.Listener;
153
+ import org.bukkit.event.inventory.InventoryClickEvent;
154
+ import org.bukkit.inventory.ItemStack;
155
+ import org.bukkit.persistence.PersistentDataType;
156
+
157
+ public final class MenuClickListener implements Listener {
158
+
159
+ @EventHandler(priority = EventPriority.HIGHEST)
160
+ public void onInventoryClick(InventoryClickEvent event) {
161
+ // 1. Verifikasi apakah inventory yang diklik adalah custom holder kita
162
+ if (!(event.getInventory().getHolder() instanceof CustomMenuHolder holder)) {
163
+ return;
164
+ }
165
+
166
+ // 2. SELALU batalkan klik untuk mencegah pemain mengambil/memindahkan item GUI!
167
+ event.setCancelled(true);
168
+
169
+ // 3. Abaikan jika klik di luar jendela atau slot kosong
170
+ ItemStack clickedItem = event.getCurrentItem();
171
+ if (clickedItem == null || !clickedItem.hasItemMeta()) {
172
+ return;
173
+ }
174
+
175
+ if (!(event.getWhoClicked() instanceof Player player)) {
176
+ return;
177
+ }
178
+
179
+ // 4. Baca aksi dari PDC item
180
+ String action = clickedItem.getItemMeta().getPersistentDataContainer()
181
+ .get(GuiItemBuilder.ACTION_KEY, PersistentDataType.STRING);
182
+
183
+ if (action == null || action.equals("noop")) {
184
+ return;
185
+ }
186
+
187
+ // 5. Eksekusi aksi berdasarkan action ID
188
+ switch (action) {
189
+ case "buy_sword" -> {
190
+ player.sendMessage(MiniMessage.miniMessage().deserialize("<green>Sukses membeli senjata!</green>"));
191
+ player.closeInventory();
192
+ }
193
+ case "close_menu" -> player.closeInventory();
194
+ default -> player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Aksi tidak dikenal.</red>"));
195
+ }
196
+ }
197
+ }
198
+ ```