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,172 @@
1
+ # Arsitektur Modern Plugin Paper Minecraft Java
2
+
3
+ Dokumen ini membahas arsitektur modern pembuatan plugin untuk server Paper Minecraft Java (khususnya versi 1.20+ dan 1.21+), mencakup build tools, konfigurasi manifest, lifecycle bootstrapping, dan sistem pemetaan (Mojang mappings).
4
+
5
+ ---
6
+
7
+ ## 1. Evolusi Arsitektur: Spigot vs Modern Paper
8
+
9
+ Secara historis, plugin Minecraft dibuat di atas Bukkit/Spigot API dengan `plugin.yml` dan Maven/Gradle sederhana. Namun, ekosistem Paper modern telah merevolusi cara kerja plugin:
10
+
11
+ | Fitur | Spigot Legacy | Modern Paper (1.20.6+ / 1.21+) |
12
+ | :--- | :--- | :--- |
13
+ | **Manifest File** | `plugin.yml` | `paper-plugin.yml` (atau hybrid) |
14
+ | **Text Handling** | `ChatColor` & legacy string formatting (`§a`) | Kyori Adventure & MiniMessage (`Component`) |
15
+ | **Lifecycle Hook** | Hanya `JavaPlugin.onLoad` & `onEnable` | `PluginBootstrap` + `JavaPlugin` + `LifecycleEvents` |
16
+ | **Command System** | `getCommand("...").setExecutor(...)` | Paper Brigadier Native (`LifecycleEvents.COMMANDS`) atau Cloud |
17
+ | **Mapping NMS** | Obfuscated Spigot mapping (`reobf`) | Mojang Mappings langsung di runtime (1.20.5+) via Paperweight |
18
+ | **Multi-threading** | Single main thread (`BukkitScheduler`) | Multi-thread regionized ready (Folia & Schedulers terpisah) |
19
+
20
+ ---
21
+
22
+ ## 2. Build Tooling: Gradle & Paperweight Userdev
23
+
24
+ Standard industri modern untuk proyek Paper adalah **Gradle (Kotlin DSL - `.gradle.kts`)** menggunakan plugin **`paperweight-userdev`** dari PaperMC.
25
+
26
+ ### Keunggulan Paperweight Userdev:
27
+ - Menyediakan akses ke **Mojang Mappings (NMS)** langsung tanpa alat deobfuscation eksternal yang rumit.
28
+ - Sejak Paper 1.20.5+, server Paper menjalankan Mojang Mappings secara native di runtime.
29
+ - Terintegrasi dengan plugin `xyz.jpenilla.run-paper` untuk menjalankan test server Paper/Folia langsung dengan satu perintah gradle (`./gradlew runServer`).
30
+
31
+ ### Contoh `build.gradle.kts`:
32
+
33
+ ```kotlin
34
+ plugins {
35
+ `java-library`
36
+ id("io.papermc.paperweight.userdev") version "2.0.0-beta.21"
37
+ id("xyz.jpenilla.run-paper") version "3.0.2" // Task ./gradlew runServer
38
+ id("com.gradleup.shadow") version "8.3.6" // Shadow jar bila menggunakan dependensi eksternal
39
+ }
40
+
41
+ group = "com.example"
42
+ version = "1.0.0-SNAPSHOT"
43
+
44
+ java {
45
+ toolchain.languageVersion.set(JavaLanguageVersion.of(21)) // Minecraft 1.20.5+ membutuhkan minimal Java 21
46
+ }
47
+
48
+ dependencies {
49
+ paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT")
50
+
51
+ // Contoh library umum
52
+ // implementation("org.incendo:cloud-paper:2.0.0-beta.10")
53
+ }
54
+
55
+ tasks {
56
+ compileJava {
57
+ options.release.set(21)
58
+ options.encoding = Charsets.UTF_8.name()
59
+ }
60
+
61
+ shadowJar {
62
+ archiveClassifier.set("") // Menjadikan output shadow jar sebagai artifact utama
63
+ // relocate package eksternal agar tidak bentrok dengan plugin lain
64
+ // relocate("org.incendo.cloud", "com.example.plugin.libs.cloud")
65
+ }
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 3. Konfigurasi `paper-plugin.yml`
72
+
73
+ Modern Paper menggunakan manifest `paper-plugin.yml` yang terletak di `src/main/resources/paper-plugin.yml`.
74
+
75
+ ### Keuntungan `paper-plugin.yml`:
76
+ - Dukungan dependency loading yang jauh lebih fleksibel (bootstrapper dependencies, server dependencies).
77
+ - Mendukung deklarasi bootstrapper class (`bootstrapper`).
78
+ - Kompatibilitas multi-threaded Folia (`folia-supported: true`).
79
+
80
+ ### Contoh Struktur `paper-plugin.yml`:
81
+
82
+ ```yaml
83
+ name: ExamplePlugin
84
+ version: '${version}'
85
+ main: com.example.plugin.ExamplePlugin
86
+ bootstrapper: com.example.plugin.ExampleBootstrap
87
+ description: Modern Paper Minecraft Plugin
88
+ api-version: '1.21'
89
+ folia-supported: true
90
+ authors:
91
+ - DeveloperName
92
+
93
+ dependencies:
94
+ bootstrap:
95
+ # Dependensi yang dibutuhkan pada fase bootstrap
96
+ server:
97
+ Vault:
98
+ load: BEFORE
99
+ required: false
100
+ join-classpath: true
101
+ LuckPerms:
102
+ load: BEFORE
103
+ required: false
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 4. Lifecycle Baru: `PluginBootstrap` vs `JavaPlugin`
109
+
110
+ Paper memperkenalkan antarmuka `io.papermc.paper.plugin.bootstrap.PluginBootstrap`.
111
+
112
+ ### Urutan Lifecycle Server Paper:
113
+ 1. **Plugin Loader Creation**: Resolusi classpath dan dependensi.
114
+ 2. **Bootstrap Phase (`PluginBootstrap#bootstrap`)**:
115
+ - Berjalan sangat awal sebelum world di-load.
116
+ - Digunakan untuk meregistrasi **Brigadier Commands**, modifikasi konfigurasi registry, dan setup resource global.
117
+ 3. **Plugin Instantiation**: Instance `JavaPlugin` dibuat.
118
+ 4. **`onLoad()`**: Mirip fase Spigot tradisional.
119
+ 5. **`onEnable()`**: Registrasi listener event, scheduler, inisialisasi database, dan setup UI/Game logic.
120
+ 6. **`onDisable()`**: Graceful shutdown, flush queue database, dan pembatalan task.
121
+
122
+ ### Contoh Implementasi Bootstrapper:
123
+
124
+ ```java
125
+ package com.example.plugin;
126
+
127
+ import io.papermc.paper.plugin.bootstrap.BootstrapContext;
128
+ import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
129
+ import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
130
+ import org.jspecify.annotations.NullMarked;
131
+
132
+ @NullMarked
133
+ public final class ExampleBootstrap implements PluginBootstrap {
134
+
135
+ @Override
136
+ public void bootstrap(final BootstrapContext context) {
137
+ // Registrasi Lifecycle Events seperti command handler modern
138
+ context.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
139
+ final var registrar = event.registrar();
140
+ // Registrasi syntax Brigadier di sini
141
+ });
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### Contoh Implementasi Main Plugin (`JavaPlugin`):
147
+
148
+ ```java
149
+ package com.example.plugin;
150
+
151
+ import net.kyori.adventure.text.Component;
152
+ import net.kyori.adventure.text.format.NamedTextColor;
153
+ import org.bukkit.plugin.java.JavaPlugin;
154
+
155
+ public final class ExamplePlugin extends JavaPlugin {
156
+
157
+ @Override
158
+ public void onEnable() {
159
+ // Menggunakan ComponentLogger modern Paper
160
+ getComponentLogger().info(
161
+ Component.text("ExamplePlugin berhasil diaktifkan!", NamedTextColor.GREEN)
162
+ );
163
+ }
164
+
165
+ @Override
166
+ public void onDisable() {
167
+ getComponentLogger().info(
168
+ Component.text("ExamplePlugin dimatikan secara aman.", NamedTextColor.RED)
169
+ );
170
+ }
171
+ }
172
+ ```
@@ -0,0 +1,160 @@
1
+ # Adventure API & MiniMessage di Paper Minecraft
2
+
3
+ Paper mengintegrasikan **Kyori Adventure** secara native sebagai antarmuka pemrosesan teks. Pendekatan lama yang menggunakan tanda warna section (`§a`, `§l`) atau `org.bukkit.ChatColor` telah **ditinggalkan (deprecated)** karena rentan merusak format teks, tidak mendukung RGB hex secara fleksibel, dan tidak modular.
4
+
5
+ ---
6
+
7
+ ## 1. Mengapa Adventure & MiniMessage?
8
+
9
+ - **Type-Safe Components**: Teks bukan sekadar string biasa melainkan hierarki objek `Component` yang immutable.
10
+ - **Dukungan Warna Penuh**: Mendukung warna 24-bit TrueColor / Hex (`#FF5555`), Gradients, dan Rainbows.
11
+ - **Interaktivitas Kaya**: Event klik (`run_command`, `suggest_command`, `open_url`), hover tooltip (`show_text`, `show_item`, `show_entity`), dan font kustom.
12
+ - **MiniMessage Format**: String format yang sangat mudah dibaca manusia berbasis tag XML-like, aman dari injeksi formatting.
13
+
14
+ ---
15
+
16
+ ## 2. Format Dasar MiniMessage
17
+
18
+ Gunakan `net.kyori.adventure.text.minimessage.MiniMessage`:
19
+
20
+ ```java
21
+ import net.kyori.adventure.text.Component;
22
+ import net.kyori.adventure.text.minimessage.MiniMessage;
23
+
24
+ final MiniMessage mm = MiniMessage.miniMessage();
25
+
26
+ // Contoh teks berwarna dan bergradasi
27
+ Component c1 = mm.deserialize("<gradient:#ff007f:#7f00ff><bold>Server RPG Terhebat</bold></gradient>");
28
+ Component c2 = mm.deserialize("<gray>Klik <green><click:run_command:'/spawn'>DI SINI</click></green> untuk kembali ke spawn.</gray>");
29
+ Component c3 = mm.deserialize("<hover:show_text:'<yellow>Status: Aktif</yellow>'>Arahkan kursor ke sini</hover>");
30
+ ```
31
+
32
+ ### Tag MiniMessage Populer:
33
+ - Warna bawaan: `<red>`, `<green>`, `<gold>`, `<aqua>`, `<dark_purple>`, `<white>`, dll.
34
+ - Warna Hex: `<#rrggbb>` atau `<color:#rrggbb>`.
35
+ - Format style: `<bold>` (`<b>`), `<italic>` (`<i>`), `<underlined>` (`<u>`), `<strikethrough>` (`<s>`), `<obfuscated>` (`<obf>`).
36
+ - Gradasi warna: `<gradient:#55cdfc:#f7a8b8:#ffffff>Trans Rights</gradient>`.
37
+ - Pelangi: `<rainbow>Teks pelangi dinamis</rainbow>`.
38
+ - Reset gaya: `<reset>` atau penutupan tag `</red>`.
39
+
40
+ ---
41
+
42
+ ## 3. Dynamic Tag Resolvers (Placeholder Kustom)
43
+
44
+ MiniMessage menyediakan `TagResolver` untuk memasukkan variabel secara dinamis tanpa merusak parsing atau membuka celah string injection:
45
+
46
+ ```java
47
+ import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
48
+ import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
49
+ import org.bukkit.entity.Player;
50
+
51
+ public Component createWelcomeMessage(Player player, int coins) {
52
+ return MiniMessage.miniMessage().deserialize(
53
+ "<gold>Selamat datang, <yellow><player_name></yellow>! Koin Anda: <green><coins></green></gold>",
54
+ TagResolver.resolver(
55
+ Placeholder.component("player_name", player.displayName()),
56
+ Placeholder.unparsed("coins", String.valueOf(coins))
57
+ )
58
+ );
59
+ }
60
+ ```
61
+
62
+ > **Perbedaan `component` vs `unparsed`**:
63
+ > - `Placeholder.component(...)`: Menyisipkan child component yang sudah di-parse, mempertahankan format.
64
+ > - `Placeholder.unparsed(...)`: Menyisipkan teks murni. Tag apapun di dalamnya **tidak** akan di-evaluasi sebagai tag MiniMessage (aman untuk input dari user/chat chatbox).
65
+
66
+ ---
67
+
68
+ ## 4. Mengirim Pesan ke Audience
69
+
70
+ Di Paper, setiap entitas yang bisa menerima pesan mengimplementasikan antarmuka `net.kyori.adventure.audience.Audience` (termasuk `Player`, `ConsoleCommandSender`, dan `Server`).
71
+
72
+ ### Chat Biasa & Console Logging
73
+ ```java
74
+ // Mengirim ke pemain
75
+ player.sendMessage(MiniMessage.miniMessage().deserialize("<aqua>Pesan masuk!</aqua>"));
76
+
77
+ // Mengirim via ComponentLogger pada plugin
78
+ getComponentLogger().info(MiniMessage.miniMessage().deserialize("<green>Plugin siap melayani pemain!</green>"));
79
+ ```
80
+
81
+ ### Action Bar
82
+ ```java
83
+ player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Zona Bahaya!</red>"));
84
+ ```
85
+
86
+ ### Titles & Subtitles
87
+ ```java
88
+ import net.kyori.adventure.title.Title;
89
+ import java.time.Duration;
90
+
91
+ final Title title = Title.title(
92
+ MiniMessage.miniMessage().deserialize("<gold><bold>KEMENANGAN!</bold></gold>"),
93
+ MiniMessage.miniMessage().deserialize("<gray>Anda memenangkan ronde ini</gray>"),
94
+ Title.Times.times(Duration.ofMillis(500), Duration.ofSeconds(3), Duration.ofMillis(1000))
95
+ );
96
+
97
+ player.showTitle(title);
98
+ ```
99
+
100
+ ### BossBar
101
+ ```java
102
+ import net.kyori.adventure.bossbar.BossBar;
103
+
104
+ BossBar bossBar = BossBar.bossBar(
105
+ MiniMessage.miniMessage().deserialize("<dark_red>Ender Dragon HP</dark_red>"),
106
+ 0.75f, // 75%
107
+ BossBar.Color.RED,
108
+ BossBar.Overlay.PROGRESS
109
+ );
110
+
111
+ // Tampilkan bossbar
112
+ player.showBossBar(bossBar);
113
+
114
+ // Hapus bila sudah selesai
115
+ player.hideBossBar(bossBar);
116
+ ```
117
+
118
+ ### Sound Effects
119
+ ```java
120
+ import net.kyori.adventure.sound.Sound;
121
+ import org.bukkit.SoundCategory;
122
+
123
+ Sound sound = Sound.sound(
124
+ org.bukkit.Sound.ENTITY_PLAYER_LEVELUP.key(),
125
+ Sound.Source.PLAYER,
126
+ 1.0f, // volume
127
+ 1.2f // pitch
128
+ );
129
+
130
+ player.playSound(sound);
131
+ ```
132
+
133
+ ---
134
+
135
+ ## 5. Serialisasi Item & Lore dengan Adventure
136
+
137
+ Saat membuat `ItemStack`, gunakan metode komponen `Component` alih-alih String lama:
138
+
139
+ ```java
140
+ import org.bukkit.Material;
141
+ import org.bukkit.inventory.ItemStack;
142
+ import org.bukkit.inventory.meta.ItemMeta;
143
+ import java.util.List;
144
+
145
+ ItemStack sword = new ItemStack(Material.DIAMOND_SWORD);
146
+ ItemMeta meta = sword.getItemMeta();
147
+
148
+ if (meta != null) {
149
+ // Custom name menggunakan Component
150
+ meta.displayName(MiniMessage.miniMessage().deserialize("<gradient:#00c6ff:#0072ff><bold>Pedang Es Legendaris</bold></gradient>"));
151
+
152
+ // Lore menggunakan List<Component>
153
+ meta.lore(List.of(
154
+ MiniMessage.miniMessage().deserialize("<gray>Memberikan efek <aqua>Freeze</aqua> saat menyerang.</gray>"),
155
+ MiniMessage.miniMessage().deserialize("<dark_gray>Level: <gold>★★★★★</gold></dark_gray>")
156
+ ));
157
+
158
+ sword.setItemMeta(meta);
159
+ }
160
+ ```
@@ -0,0 +1,179 @@
1
+ # PersistentDataContainer (PDC) di Paper Minecraft
2
+
3
+ `PersistentDataContainer` (PDC) adalah API resmi Bukkit/Paper untuk menyimpan data kustom secara permanen pada objek dalam game seperti:
4
+ - **Item (`ItemMeta`)**
5
+ - **Entity (`Entity`, `LivingEntity`, `Player`)**
6
+ - **Block State / Tile Entity (`TileState`)**
7
+ - **Chunk (`Chunk`)**
8
+ - **World (`World`)**
9
+
10
+ PDC menggantikan ketergantungan pada library NBT pihak ketiga (seperti NBTAPI) atau pembacaan lore tersembunyi yang rapuh. Data yang disimpan di PDC disimpan langsung dalam NBT vanilla Minecraft dan otomatis disimpan/di-load bersama world data.
11
+
12
+ ---
13
+
14
+ ## 1. Konsep Kunci: NamespacedKey
15
+
16
+ Setiap nilai yang disimpan di PDC diidentifikasi oleh `NamespacedKey` unik yang terdiri dari `namespace` (biasanya nama plugin berhuruf kecil) dan `key`:
17
+
18
+ ```java
19
+ import org.bukkit.NamespacedKey;
20
+ import org.bukkit.plugin.Plugin;
21
+
22
+ public final class DataKeys {
23
+ public static NamespacedKey CUSTOM_ID;
24
+ public static NamespacedKey COOLDOWN_TIMESTAMP;
25
+ public static NamespacedKey ITEM_STATS;
26
+
27
+ public static void init(Plugin plugin) {
28
+ CUSTOM_ID = new NamespacedKey(plugin, "custom_id");
29
+ COOLDOWN_TIMESTAMP = new NamespacedKey(plugin, "cooldown_timestamp");
30
+ ITEM_STATS = new NamespacedKey(plugin, "item_stats");
31
+ }
32
+ }
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 2. Menyimpan & Membaca Tipe Data Primitif
38
+
39
+ PDC mendukung berbagai tipe bawaan melalui enum `PersistentDataType`:
40
+ - `PersistentDataType.STRING`
41
+ - `PersistentDataType.INTEGER`
42
+ - `PersistentDataType.LONG`
43
+ - `PersistentDataType.DOUBLE`
44
+ - `PersistentDataType.FLOAT`
45
+ - `PersistentDataType.BYTE` (boolean sering disimpan sebagai byte `1` / `0`)
46
+ - `PersistentDataType.BYTE_ARRAY`
47
+ - `PersistentDataType.TAG_CONTAINER` (PDC bersarang / nested)
48
+
49
+ ### Contoh Menyimpan Data pada ItemStack:
50
+ ```java
51
+ import org.bukkit.Material;
52
+ import org.bukkit.inventory.ItemStack;
53
+ import org.bukkit.inventory.meta.ItemMeta;
54
+ import org.bukkit.persistence.PersistentDataContainer;
55
+ import org.bukkit.persistence.PersistentDataType;
56
+
57
+ public ItemStack createTeleportWand(String wandId) {
58
+ ItemStack wand = new ItemStack(Material.BLAZE_ROD);
59
+ ItemMeta meta = wand.getItemMeta();
60
+ if (meta != null) {
61
+ PersistentDataContainer pdc = meta.getPersistentDataContainer();
62
+ pdc.set(DataKeys.CUSTOM_ID, PersistentDataType.STRING, wandId);
63
+ pdc.set(DataKeys.COOLDOWN_TIMESTAMP, PersistentDataType.LONG, System.currentTimeMillis());
64
+ wand.setItemMeta(meta);
65
+ }
66
+ return wand;
67
+ }
68
+ ```
69
+
70
+ ### Contoh Membaca & Memvalidasi Data pada ItemStack:
71
+ ```java
72
+ public boolean isCustomWand(ItemStack item) {
73
+ if (item == null || !item.hasItemMeta()) return false;
74
+
75
+ PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
76
+ return pdc.has(DataKeys.CUSTOM_ID, PersistentDataType.STRING);
77
+ }
78
+
79
+ public String getWandId(ItemStack item) {
80
+ if (!isCustomWand(item)) return null;
81
+ return item.getItemMeta().getPersistentDataContainer().get(DataKeys.CUSTOM_ID, PersistentDataType.STRING);
82
+ }
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 3. Menyimpan Data pada Entitas dan Chunk
88
+
89
+ PDC juga dapat diakses langsung pada entitas dan chunk tanpa meta terpisah:
90
+
91
+ ```java
92
+ import org.bukkit.entity.Entity;
93
+ import org.bukkit.Chunk;
94
+
95
+ // Menandai entitas khusus (misal Custom Boss)
96
+ public void markAsCustomBoss(Entity entity, String bossType) {
97
+ entity.getPersistentDataContainer().set(DataKeys.CUSTOM_ID, PersistentDataType.STRING, bossType);
98
+ }
99
+
100
+ // Menandai chunk yang telah diproses oleh plugin
101
+ public void markChunkProcessed(Chunk chunk) {
102
+ chunk.getPersistentDataContainer().set(new NamespacedKey("myplugin", "processed"), PersistentDataType.BYTE, (byte) 1);
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 4. Tipe Data Kustom (Complex Object Serialization)
109
+
110
+ Kita dapat mengimplementasikan antarmuka `PersistentDataType<T, Z>` kustom untuk menyimpan objek kompleks (seperti record/POJO) ke dalam PDC tanpa perlu serialisasi JSON manual di setiap panggilan.
111
+
112
+ ### Contoh Record Objek:
113
+ ```java
114
+ public record ItemStats(int damageBonus, double critChance, int durability) {}
115
+ ```
116
+
117
+ ### Implementasi `PersistentDataType` Kustom:
118
+ ```java
119
+ import org.bukkit.persistence.PersistentDataAdapterContext;
120
+ import org.bukkit.persistence.PersistentDataContainer;
121
+ import org.bukkit.persistence.PersistentDataType;
122
+ import org.bukkit.NamespacedKey;
123
+
124
+ public final class ItemStatsDataType implements PersistentDataType<PersistentDataContainer, ItemStats> {
125
+
126
+ private final NamespacedKey damageKey;
127
+ private final NamespacedKey critKey;
128
+ private final NamespacedKey durabilityKey;
129
+
130
+ public ItemStatsDataType(org.bukkit.plugin.Plugin plugin) {
131
+ this.damageKey = new NamespacedKey(plugin, "damage");
132
+ this.critKey = new NamespacedKey(plugin, "crit");
133
+ this.durabilityKey = new NamespacedKey(plugin, "durability");
134
+ }
135
+
136
+ @Override
137
+ public Class<PersistentDataContainer> getPrimitiveType() {
138
+ return PersistentDataContainer.class;
139
+ }
140
+
141
+ @Override
142
+ public Class<ItemStats> getComplexType() {
143
+ return ItemStats.class;
144
+ }
145
+
146
+ @Override
147
+ public PersistentDataContainer toPrimitive(ItemStats complex, PersistentDataAdapterContext context) {
148
+ PersistentDataContainer container = context.newPersistentDataContainer();
149
+ container.set(damageKey, PersistentDataType.INTEGER, complex.damageBonus());
150
+ container.set(critKey, PersistentDataType.DOUBLE, complex.critChance());
151
+ container.set(durabilityKey, PersistentDataType.INTEGER, complex.durability());
152
+ return container;
153
+ }
154
+
155
+ @Override
156
+ public ItemStats fromPrimitive(PersistentDataContainer primitive, PersistentDataAdapterContext context) {
157
+ int damage = primitive.getOrDefault(damageKey, PersistentDataType.INTEGER, 0);
158
+ double crit = primitive.getOrDefault(critKey, PersistentDataType.DOUBLE, 0.0);
159
+ int durability = primitive.getOrDefault(durabilityKey, PersistentDataType.INTEGER, 100);
160
+ return new ItemStats(damage, crit, durability);
161
+ }
162
+ }
163
+ ```
164
+
165
+ ### Penggunaan Langsung:
166
+ ```java
167
+ ItemStats stats = new ItemStats(25, 0.15, 500);
168
+ pdc.set(DataKeys.ITEM_STATS, new ItemStatsDataType(plugin), stats);
169
+
170
+ // Membaca kembali:
171
+ ItemStats loadedStats = pdc.get(DataKeys.ITEM_STATS, new ItemStatsDataType(plugin));
172
+ ```
173
+
174
+ ---
175
+
176
+ ## 5. Best Practices PDC
177
+ 1. **Gunakan Singleton Keys**: Jangan instansiasi `new NamespacedKey(...)` berulang kali di hot-loop (seperti `EntityDamageByEntityEvent`); simpan sebagai static final constants.
178
+ 2. **Namespace Konsisten**: Selalu gunakan plugin instance untuk key namespace agar tidak terjadi tabrakan dengan plugin lain.
179
+ 3. **Pembersihan Data**: Hapus data yang tidak lagi valid menggunakan `pdc.remove(key)` untuk menghemat ukuran file world/NBT.
@@ -0,0 +1,157 @@
1
+ # Command System Modern: Brigadier & Cloud di Paper
2
+
3
+ Sistem command di Minecraft Java berevolusi signifikan. Metode lama Bukkit seperti `CommandExecutor`, `TabCompleter`, dan deklarasi perintah di `plugin.yml` kini digantikan oleh:
4
+ 1. **Paper Native Brigadier API** (diperkenalkan di Paper 1.20.6+ melalui `LifecycleEvents.COMMANDS`).
5
+ 2. **Incendo Cloud Framework** (`cloud-paper`): Standar industri untuk framework command berbasis anotasi / builder yang sangat fleksibel dan multi-platform.
6
+
7
+ ---
8
+
9
+ ## 1. Native Paper Brigadier Commands (Paper 1.20.6+)
10
+
11
+ Mojang Brigadier adalah command dispatcher engine resmi Minecraft. Paper mengekspos API Brigadier secara langsung tanpa perlu hack NMS atau dependensi tambahan.
12
+
13
+ ### Registrasi Melalui Lifecycle Events
14
+ Command didaftarkan saat fase Bootstrap atau saat event `LifecycleEvents.COMMANDS` dipicu:
15
+
16
+ ```java
17
+ package com.example.plugin;
18
+
19
+ import com.mojang.brigadier.arguments.IntegerArgumentType;
20
+ import com.mojang.brigadier.arguments.StringArgumentType;
21
+ import io.papermc.paper.command.brigadier.Commands;
22
+ import io.papermc.paper.command.brigadier.CommandSourceStack;
23
+ import io.papermc.paper.plugin.bootstrap.BootstrapContext;
24
+ import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
25
+ import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
26
+ import net.kyori.adventure.text.minimessage.MiniMessage;
27
+ import org.bukkit.entity.Player;
28
+
29
+ public final class CommandBootstrapper implements PluginBootstrap {
30
+
31
+ @Override
32
+ public void bootstrap(BootstrapContext context) {
33
+ context.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
34
+ final Commands commands = event.registrar();
35
+
36
+ // Membangun pohon syntax command /teleportzone <nama> [radius]
37
+ commands.register(
38
+ Commands.literal("teleportzone")
39
+ .requires(source -> source.getSender().hasPermission("plugin.command.teleportzone"))
40
+ .then(Commands.argument("nama", StringArgumentType.word())
41
+ .suggests((ctx, builder) -> {
42
+ builder.suggest("lobby");
43
+ builder.suggest("arena");
44
+ builder.suggest("market");
45
+ return builder.buildFuture();
46
+ })
47
+ .then(Commands.argument("radius", IntegerArgumentType.integer(1, 100))
48
+ .executes(ctx -> {
49
+ final CommandSourceStack source = ctx.getSource();
50
+ final String zone = StringArgumentType.getString(ctx, "nama");
51
+ final int radius = IntegerArgumentType.getInteger(ctx, "radius");
52
+
53
+ source.getSender().sendMessage(
54
+ MiniMessage.miniMessage().deserialize(
55
+ "<green>Mempersiapkan teleportasi ke zona <gold>" + zone + "</gold> dengan radius <yellow>" + radius + "</yellow>!</green>"
56
+ )
57
+ );
58
+ return com.mojang.brigadier.Command.SINGLE_SUCCESS;
59
+ })
60
+ )
61
+ .executes(ctx -> {
62
+ final CommandSourceStack source = ctx.getSource();
63
+ final String zone = StringArgumentType.getString(ctx, "nama");
64
+ source.getSender().sendMessage(
65
+ MiniMessage.miniMessage().deserialize("<green>Teleportasi default ke zona <gold>" + zone + "</gold>!</green>")
66
+ );
67
+ return com.mojang.brigadier.Command.SINGLE_SUCCESS;
68
+ })
69
+ )
70
+ .build(),
71
+ "Perintah teleportasi zona modern",
72
+ java.util.List.of("tpzone") // alias
73
+ );
74
+ });
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Keuntungan Paper Brigadier:
80
+ - **Client-Side Syntax Highlighting**: Minecraft client dapat menandai teks merah saat syntax salah sebelum perintah ditekan.
81
+ - **Auto Tab-Completion**: Tidak perlu menulis logika `TabCompleter` manual yang rumit.
82
+ - **Validasi Tipe Otomatis**: Argument validasi otomatis ditangani (angka out-of-range otomatis ditolak).
83
+
84
+ ---
85
+
86
+ ## 2. Incendo Cloud Command Framework (`cloud-paper`)
87
+
88
+ Untuk plugin yang membutuhkan dependensi injeksi, parsing objek otomatis (seperti `Player`, `OfflinePlayer`, `World`, `Duration`), dan command yang sangat banyak, **Cloud** adalah pilihan terpopuler.
89
+
90
+ ### Dependensi Gradle:
91
+ ```kotlin
92
+ dependencies {
93
+ implementation("org.incendo:cloud-paper:2.0.0-beta.10")
94
+ // atau cloud-annotations jika menyukai gaya @Command
95
+ implementation("org.incendo:cloud-annotations:2.0.0-beta.10")
96
+ }
97
+ ```
98
+
99
+ ### Inisialisasi Cloud PaperCommandManager:
100
+ ```java
101
+ import org.incendo.cloud.paper.PaperCommandManager;
102
+ import org.incendo.cloud.execution.ExecutionCoordinator;
103
+ import org.bukkit.command.CommandSender;
104
+ import org.bukkit.plugin.java.JavaPlugin;
105
+
106
+ public class PluginCommands {
107
+
108
+ public static PaperCommandManager<CommandSender> createManager(JavaPlugin plugin) {
109
+ PaperCommandManager<CommandSender> manager = PaperCommandManager.createNative(
110
+ plugin,
111
+ ExecutionCoordinator.simpleCoordinator()
112
+ );
113
+
114
+ // Daftarkan brigadier mapper jika didukung
115
+ if (manager.hasCapability(org.incendo.cloud.paper.PaperCommandManager.Capability.BRIGADIER)) {
116
+ manager.registerBrigadier();
117
+ }
118
+
119
+ return manager;
120
+ }
121
+ }
122
+ ```
123
+
124
+ ### Contoh Perintah dengan Cloud Builder:
125
+ ```java
126
+ import org.incendo.cloud.paper.PaperCommandManager;
127
+ import org.incendo.cloud.bukkit.parser.PlayerParser;
128
+ import org.bukkit.command.CommandSender;
129
+ import org.bukkit.entity.Player;
130
+ import net.kyori.adventure.text.minimessage.MiniMessage;
131
+
132
+ public void registerGiveRewardCommand(PaperCommandManager<CommandSender> manager) {
133
+ manager.command(
134
+ manager.commandBuilder("reward")
135
+ .permission("plugin.reward")
136
+ .required("target", PlayerParser.playerParser())
137
+ .handler(commandContext -> {
138
+ CommandSender sender = commandContext.sender();
139
+ Player target = commandContext.get("target");
140
+
141
+ target.sendMessage(MiniMessage.miniMessage().deserialize("<gold>Kamu menerima hadiah spesial!</gold>"));
142
+ sender.sendMessage(MiniMessage.miniMessage().deserialize("<green>Hadiah terkirim ke " + target.getName() + ".</green>"));
143
+ })
144
+ );
145
+ }
146
+ ```
147
+
148
+ ---
149
+
150
+ ## 3. Komparasi: Kapan Memilih Apa?
151
+
152
+ | Aspek | Paper Native Brigadier | Incendo Cloud |
153
+ | :--- | :--- | :--- |
154
+ | **Ukuran Jar** | Zero-dependency (bawaan Paper) | Menambahkan lib ~300KB-800KB (butuh shadow/relocate) |
155
+ | **Kemudahan** | Cocok untuk command sederhana-menengah | Sangat baik untuk ratusan sub-command |
156
+ | **Argument Parser** | Perlu mapping manual untuk tipe Bukkit | Built-in parser untuk Player, Location, Selector, dll |
157
+ | **Portabilitas** | Hanya berjalan di Paper 1.20.6+ | Mendukung Velocity, Bungee, Fabric, Paper |