papermc-skill 1.0.1 → 1.2.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/.cursor/rules/paper-plugin-dev.mdc +23 -0
  2. package/.cursorrules +6 -0
  3. package/.paseo/rules.md +8 -0
  4. package/.pi/rules.md +8 -0
  5. package/.windsurfrules +7 -0
  6. package/CHANGELOG.md +50 -0
  7. package/CLAUDE.md +27 -0
  8. package/CODEX.md +12 -0
  9. package/DEEPSEEK.md +18 -0
  10. package/README.md +73 -33
  11. package/assets/banner-animated.svg +360 -0
  12. package/assets/banner.svg +360 -0
  13. package/bin/cli.js +565 -35
  14. package/docs/01-modern-paper-architecture.md +48 -51
  15. package/docs/02-adventure-and-minimessage.md +48 -52
  16. package/docs/03-persistent-data-container.md +39 -39
  17. package/docs/04-commands-and-brigadier.md +32 -35
  18. package/docs/05-concurrency-and-folia.md +44 -44
  19. package/docs/06-events-and-listeners.md +30 -31
  20. package/docs/07-best-practices-and-perf.md +39 -40
  21. package/docs/08-gui-and-menus.md +34 -37
  22. package/docs/09-custom-items-and-combat.md +25 -33
  23. package/docs/10-database-and-storage.md +27 -27
  24. package/docs/11-security-and-exploit-prevention.md +233 -0
  25. package/docs/12-data-components-and-display-entities.md +264 -0
  26. package/docs/13-multi-agent-system-and-tooling.md +90 -0
  27. package/package.json +17 -4
  28. package/skills/paper-plugin-dev/SKILL.md +425 -114
  29. package/templates/paper-modern-template/.gradle/9.7.1/checksums/checksums.lock +0 -0
  30. package/templates/paper-modern-template/.gradle/9.7.1/checksums/md5-checksums.bin +0 -0
  31. package/templates/paper-modern-template/.gradle/9.7.1/checksums/sha1-checksums.bin +0 -0
  32. package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.bin +0 -0
  33. package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.lock +0 -0
  34. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.bin +0 -0
  35. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.lock +0 -0
  36. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/resourceHashesCache.bin +0 -0
  37. package/templates/paper-modern-template/.gradle/9.7.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock +0 -0
  38. package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  39. package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +1 -1
  40. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/ExamplePaperPlugin.class +0 -0
  41. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/PaperPluginBootstrap.class +0 -0
  42. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/command/HelloCommand.class +0 -0
  43. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/listener/PlayerEventListener.class +0 -0
  44. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/util/SchedulerAdapter.class +0 -0
  45. package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
  46. package/templates/paper-modern-template/build.gradle.kts +7 -7
  47. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +6 -6
  48. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +2 -2
  49. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +2 -4
  50. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +2 -2
  51. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +6 -7
  52. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +6 -6
  53. package/templates/paper-modern-template/src/main/resources/config.yml +5 -5
@@ -1,60 +1,59 @@
1
- # Best Practices & Optimasi Performa Plugin Paper
1
+ # Best Practices & Performance Optimization for Paper Plugins
2
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.
3
+ Building Minecraft server plugins for large communities requires high efficiency, zero memory leaks, and non-blocking game tick loops. This guide summarizes essential rules, performance optimizations, and anti-patterns.
4
4
 
5
5
  ---
6
6
 
7
- ## 1. Mencegah Memory Leak (Kebocoran Memori)
7
+ ## 1. Preventing Memory Leaks
8
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.
9
+ Minecraft exhibits a tightly coupled object graph. Holding Bukkit object references in memory beyond their intended lifecycle prevents the JVM Garbage Collector (GC) from clearing hundreds of megabytes of world and connection data.
10
10
 
11
- ### 🔴 Anti-Pattern: Menyimpan Objek Bukkit di Collection Statis
11
+ ### 🔴 Anti-Pattern: Holding Bukkit Objects in Static Collections
12
12
  ```java
13
- // SANGAT BERBAHAYA: Player objek menahan referensi ke World, Chunk, Connection, dll.
13
+ // CRITICAL HAZARD: Player objects hold references to World, Chunks, and Network Connections
14
14
  public static final Set<Player> activePlayers = new HashSet<>();
15
15
  public static final Map<Player, PlayerData> dataMap = new HashMap<>();
16
16
  ```
17
- Jika pemain disconnect, objek `Player` tidak akan bisa di-garbage collect!
17
+ When a player disconnects, the `Player` instance cannot be garbage-collected, creating a massive memory leak!
18
18
 
19
- ### 🟢 Solusi: Simpan `UUID`
19
+ ### 🟢 Solution: Store `UUID` Primitives
20
20
  ```java
21
- // AMAN: UUID hanya 16-byte identifier yang tidak menahan referensi dunia/koneksi
21
+ // SAFE: UUID is a lightweight 16-byte identifier with zero lingering references
22
22
  public static final Set<UUID> activePlayerUuids = new HashSet<>();
23
23
  public static final Map<UUID, PlayerData> dataMap = new ConcurrentHashMap<>();
24
24
 
25
- // Ambil instance player hanya saat dibutuhkan:
25
+ // Retrieve player instance only when needed:
26
26
  Player player = Bukkit.getPlayer(uuid);
27
27
  if (player != null) {
28
- // Gunakan
28
+ // Process player
29
29
  }
30
30
  ```
31
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`).
32
+ > **Golden Rule**: Never retain long-lived references to `Player`, `Entity`, `World`, `Chunk`, or `Block` instances in static fields. Use `UUID`, `NamespacedKey`, or primitive coordinates (`worldName`, `x`, `y`, `z`).
33
33
 
34
34
  ---
35
35
 
36
- ## 2. Operasi I/O & Database Selalu Asinkron
36
+ ## 2. Asynchronous I/O & Database Operations
37
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).
38
+ Server game ticks run in ~50 milliseconds (20 TPS). File I/O, heavy disk reads, HTTP web requests, or database queries taking even 100ms will immediately cause TPS drops and server lag spikes.
39
39
 
40
- ### 🟢 Solusi: Gunakan Async Threads & CompletableFuture
40
+ ### 🟢 Solution: Async Threads & CompletableFuture
41
41
  ```java
42
42
  import java.util.concurrent.CompletableFuture;
43
43
  import org.bukkit.Bukkit;
44
44
 
45
45
  public CompletableFuture<PlayerData> loadDataAsync(UUID uuid) {
46
46
  return CompletableFuture.supplyAsync(() -> {
47
- // Berjalan di luar thread game (off-main-thread)
47
+ // Runs off the main game thread
48
48
  return database.fetchUser(uuid);
49
49
  }, myPluginThreadPool).thenApply(data -> {
50
50
  return data != null ? data : new PlayerData(uuid);
51
51
  });
52
52
  }
53
53
 
54
- // Menggunakan hasilnya:
54
+ // Consuming the result safely:
55
55
  loadDataAsync(player.getUniqueId()).thenAccept(data -> {
56
- // Jika perlu memodifikasi state game Bukkit (seperti memberi item),
57
- // jadwalkan kembali ke scheduler region pemain:
56
+ // Schedule back to the player's entity scheduler when mutating game state:
58
57
  player.getScheduler().run(plugin, task -> {
59
58
  player.giveExp(data.getPendingExp());
60
59
  }, null);
@@ -63,56 +62,56 @@ loadDataAsync(player.getUniqueId()).thenAccept(data -> {
63
62
 
64
63
  ---
65
64
 
66
- ## 3. Menghindari Synchronous Chunk Loading
65
+ ## 3. Eliminating Synchronous Chunk Loading
67
66
 
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).
67
+ Invoking `world.getChunkAt(x, z)` or `location.getBlock()` on an unloaded chunk forces the server to read chunk data synchronously from disk, freezing the main tick thread.
69
68
 
70
69
  ### 🔴 Anti-Pattern
71
70
  ```java
72
- Chunk chunk = world.getChunkAt(x, z); // Membekukan server jika chunk belum di-load!
71
+ Chunk chunk = world.getChunkAt(x, z); // Freezes the server if the chunk is not yet loaded!
73
72
  ```
74
73
 
75
- ### 🟢 Solusi Paper: Asynchronous Chunk Loading
76
- Gunakan API Paper `getChunkAtAsync`:
74
+ ### 🟢 Paper Solution: Asynchronous Chunk Loading
75
+ Use Paper's native `getChunkAtAsync`:
77
76
 
78
77
  ```java
79
78
  world.getChunkAtAsync(x, z, true).thenAccept(chunk -> {
80
- // Chunk berhasil dimuat tanpa lag spike pada TPS
79
+ // Chunk loaded asynchronously without TPS drops
81
80
  processChunkBlocks(chunk);
82
81
  });
83
82
  ```
84
83
 
85
84
  ---
86
85
 
87
- ## 4. Konfigurasi (`config.yml`) yang Tangguh
86
+ ## 4. Robust Configuration (`config.yml`) Handling
88
87
 
89
- Di Paper, file YAML secara default menggunakan encoding UTF-8. Pastikan membaca dan menyimpan file konfigurasi dengan aman:
88
+ In Paper, YAML files default to UTF-8 encoding. Read and reload configuration files cleanly:
90
89
 
91
90
  ```java
92
- // Menyimpan default config dari jar jika belum ada
91
+ // Save embedded default config from jar if missing
93
92
  saveDefaultConfig();
94
93
 
95
- // Reload config
94
+ // Reload configuration
96
95
  reloadConfig();
97
96
  FileConfiguration config = getConfig();
98
97
 
99
- // Mengambil pesan dengan fallback default
100
- String rawMessage = config.getString("messages.welcome", "<green>Selamat datang!</green>");
98
+ // Read messages with fallback defaults
99
+ String rawMessage = config.getString("messages.welcome", "<green>Welcome to the server!</green>");
101
100
  Component message = MiniMessage.miniMessage().deserialize(rawMessage);
102
101
  ```
103
102
 
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.
103
+ For large configuration trees, consider using **Configurate** (`org.spongepowered:configurate-yaml`) which preserves file comments, validates schemas, and maps automatically to Java records.
105
104
 
106
105
  ---
107
106
 
108
- ## 5. Ringkasan Checklist Anti-Pattern
107
+ ## 5. Anti-Pattern Summary Checklist
109
108
 
110
- | Jangan Lakukan ❌ | Lakukan Ini ✔️ |
109
+ | Never Do This ❌ | Recommended Modern Pattern ✔️ |
111
110
  | :--- | :--- |
112
- | `ChatColor.RED + "Teks"` | `MiniMessage.miniMessage().deserialize("<red>Teks</red>")` |
111
+ | `ChatColor.RED + "Text"` | `MiniMessage.miniMessage().deserialize("<red>Text</red>")` |
113
112
  | `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 |
113
+ | `new NamespacedKey(...)` in event loops | Define `NamespacedKey` instances as `static final` |
114
+ | Random `world.getBlockAt(x, y, z)` in async threads | Access blocks exclusively on the owning region thread |
115
+ | Ignoring `paper-plugin.yml` in new projects | Adopt `paper-plugin.yml` and `PluginBootstrap` |
116
+ | Blocking the tick thread with database queries | Use connection pools (HikariCP) & async futures |
117
+ | Omitting `folia-supported` declarations | Architect schedulers using region/universal abstractions |
@@ -1,25 +1,25 @@
1
- # Pembuatan GUI / Menu Inventory Modern di Paper
1
+ # Modern GUI & Inventory Menus in Paper
2
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.
3
+ Creating interactive inventory interfaces (GUI Menus) using chest containers is one of the most widely implemented features in Minecraft plugins. Legacy approaches inspecting menu titles with color strings (`ChatColor.stripColor`) or hardcoding slot numbers (`event.getSlot() == 13`) are brittle and notorious for causing item duplication exploits.
4
4
 
5
- Paper modern menawarkan cara yang jauh lebih aman, bersih, dan type-safe menggunakan **Kyori Adventure** dan **PersistentDataContainer (PDC)**.
5
+ Modern Paper offers a clean, type-safe, and exploit-proof pattern utilizing **Kyori Adventure** and **PersistentDataContainer (PDC)**.
6
6
 
7
7
  ---
8
8
 
9
- ## 1. Masalah pada Pendekatan Lama vs Solusi Modern
9
+ ## 1. Legacy Anti-Patterns vs Modern Paper Solutions
10
10
 
11
- | Masalah Legacy (Lama) | Solusi Modern Paper |
11
+ | Legacy Spigot Anti-Pattern | Modern Paper Architecture |
12
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. |
13
+ | Matching title strings: `view.getTitle().equals("§8Menu")` | Verifying custom `InventoryHolder` or PDC item signatures. |
14
+ | Hardcoding actions: `if (event.getSlot() == 13)` | Tagging button items with PDC actions (`gui_action -> buy_item`). |
15
+ | Corrupting color formatting across reloads | Using Adventure `Component` titles natively. |
16
+ | Item duplication exploits when cancel fails | Enforcing immediate cancellation on verified custom holders. |
17
17
 
18
18
  ---
19
19
 
20
- ## 2. Pola Custom `InventoryHolder`
20
+ ## 2. The Custom `InventoryHolder` Pattern
21
21
 
22
- Cara paling handal dan standar industri untuk mengenali GUI buatan plugin Anda adalah membuat kelas yang mengimplementasikan `InventoryHolder`:
22
+ The industry standard for identifying plugin-owned GUIs is implementing a custom `InventoryHolder`:
23
23
 
24
24
  ```java
25
25
  package com.example.plugin.gui;
@@ -37,7 +37,7 @@ public class CustomMenuHolder implements InventoryHolder {
37
37
 
38
38
  public CustomMenuHolder(Component title, int rows, String menuId) {
39
39
  this.menuId = menuId;
40
- // Bukkit.createInventory mendukung Adventure Component secara native di Paper
40
+ // Bukkit.createInventory natively accepts Adventure Components in Paper
41
41
  this.inventory = Bukkit.createInventory(this, rows * 9, title);
42
42
  }
43
43
 
@@ -54,9 +54,9 @@ public class CustomMenuHolder implements InventoryHolder {
54
54
 
55
55
  ---
56
56
 
57
- ## 3. Membuat Tombol dengan Penanda PDC
57
+ ## 3. Creating Interactive Buttons with PDC Tags
58
58
 
59
- Daripada mengingat nomor slot, tandai setiap item tombol dengan aksi spesifik di `PersistentDataContainer`:
59
+ Rather than remembering slot indices, tag each button item with an action identifier inside `PersistentDataContainer`:
60
60
 
61
61
  ```java
62
62
  package com.example.plugin.gui;
@@ -67,7 +67,6 @@ import org.bukkit.NamespacedKey;
67
67
  import org.bukkit.inventory.ItemStack;
68
68
  import org.bukkit.inventory.meta.ItemMeta;
69
69
  import org.bukkit.persistence.PersistentDataType;
70
- import org.bukkit.plugin.Plugin;
71
70
 
72
71
  import java.util.List;
73
72
 
@@ -77,8 +76,7 @@ public final class GuiItemBuilder {
77
76
 
78
77
  public static ItemStack createButton(Material material, String nameMiniMessage, List<String> loreMiniMessage, String actionId) {
79
78
  ItemStack item = new ItemStack(material);
80
- ItemMeta meta = item.getItemMeta();
81
- if (meta != null) {
79
+ item.editMeta(meta -> {
82
80
  meta.displayName(MiniMessage.miniMessage().deserialize(nameMiniMessage));
83
81
 
84
82
  meta.lore(loreMiniMessage.stream()
@@ -86,10 +84,9 @@ public final class GuiItemBuilder {
86
84
  .toList()
87
85
  );
88
86
 
89
- // Tandai tombol dengan ID aksi unik
87
+ // Tag button with unique action identifier
90
88
  meta.getPersistentDataContainer().set(ACTION_KEY, PersistentDataType.STRING, actionId);
91
- item.setItemMeta(meta);
92
- }
89
+ });
93
90
  return item;
94
91
  }
95
92
  }
@@ -97,18 +94,18 @@ public final class GuiItemBuilder {
97
94
 
98
95
  ---
99
96
 
100
- ## 4. Merakit Menu
97
+ ## 4. Assembling the Menu
101
98
 
102
99
  ```java
103
100
  public void openShopMenu(Player player) {
104
101
  CustomMenuHolder holder = new CustomMenuHolder(
105
- MiniMessage.miniMessage().deserialize("<gradient:#f12711:#f5af19><bold>Toko Server</bold></gradient>"),
106
- 3, // 3 baris = 27 slot
102
+ MiniMessage.miniMessage().deserialize("<gradient:#f12711:#f5af19><bold>Server Market</bold></gradient>"),
103
+ 3, // 3 rows = 27 slots
107
104
  "server_shop"
108
105
  );
109
106
  Inventory inv = holder.getInventory();
110
107
 
111
- // Border pengisi (filler)
108
+ // Background glass filler
112
109
  ItemStack filler = GuiItemBuilder.createButton(
113
110
  Material.GRAY_STAINED_GLASS_PANE,
114
111
  "<gray> </gray>",
@@ -119,18 +116,18 @@ public void openShopMenu(Player player) {
119
116
  inv.setItem(i, filler);
120
117
  }
121
118
 
122
- // Tombol aksi
119
+ // Action button
123
120
  inv.setItem(11, GuiItemBuilder.createButton(
124
121
  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>"),
122
+ "<aqua><bold>Purchase Legendary Blade</bold></aqua>",
123
+ List.of("<gray>Price: <gold>500 Coins</gold></gray>", "<yellow>Click to purchase!</yellow>"),
127
124
  "buy_sword"
128
125
  ));
129
126
 
130
127
  inv.setItem(15, GuiItemBuilder.createButton(
131
128
  Material.BARRIER,
132
- "<red><bold>Tutup Menu</bold></red>",
133
- List.of("<gray>Klik untuk keluar</gray>"),
129
+ "<red><bold>Close Menu</bold></red>",
130
+ List.of("<gray>Click to exit</gray>"),
134
131
  "close_menu"
135
132
  ));
136
133
 
@@ -140,7 +137,7 @@ public void openShopMenu(Player player) {
140
137
 
141
138
  ---
142
139
 
143
- ## 5. Event Listener Menu yang Aman dari Eksploitasi
140
+ ## 5. Exploit-Proof Menu Event Listener
144
141
 
145
142
  ```java
146
143
  package com.example.plugin.gui;
@@ -158,15 +155,15 @@ public final class MenuClickListener implements Listener {
158
155
 
159
156
  @EventHandler(priority = EventPriority.HIGHEST)
160
157
  public void onInventoryClick(InventoryClickEvent event) {
161
- // 1. Verifikasi apakah inventory yang diklik adalah custom holder kita
158
+ // 1. Verify holder instance
162
159
  if (!(event.getInventory().getHolder() instanceof CustomMenuHolder holder)) {
163
160
  return;
164
161
  }
165
162
 
166
- // 2. SELALU batalkan klik untuk mencegah pemain mengambil/memindahkan item GUI!
163
+ // 2. ALWAYS cancel to prevent players from moving GUI items!
167
164
  event.setCancelled(true);
168
165
 
169
- // 3. Abaikan jika klik di luar jendela atau slot kosong
166
+ // 3. Ignore empty slots or clicks outside the window
170
167
  ItemStack clickedItem = event.getCurrentItem();
171
168
  if (clickedItem == null || !clickedItem.hasItemMeta()) {
172
169
  return;
@@ -176,7 +173,7 @@ public final class MenuClickListener implements Listener {
176
173
  return;
177
174
  }
178
175
 
179
- // 4. Baca aksi dari PDC item
176
+ // 4. Read action from item PDC
180
177
  String action = clickedItem.getItemMeta().getPersistentDataContainer()
181
178
  .get(GuiItemBuilder.ACTION_KEY, PersistentDataType.STRING);
182
179
 
@@ -184,14 +181,14 @@ public final class MenuClickListener implements Listener {
184
181
  return;
185
182
  }
186
183
 
187
- // 5. Eksekusi aksi berdasarkan action ID
184
+ // 5. Execute action routing
188
185
  switch (action) {
189
186
  case "buy_sword" -> {
190
- player.sendMessage(MiniMessage.miniMessage().deserialize("<green>Sukses membeli senjata!</green>"));
187
+ player.sendMessage(MiniMessage.miniMessage().deserialize("<green>Successfully purchased weapon!</green>"));
191
188
  player.closeInventory();
192
189
  }
193
190
  case "close_menu" -> player.closeInventory();
194
- default -> player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Aksi tidak dikenal.</red>"));
191
+ default -> player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Unknown menu action.</red>"));
195
192
  }
196
193
  }
197
194
  }
@@ -1,14 +1,14 @@
1
- # Custom Items, Ability & Combat di Paper
1
+ # Custom Items, Abilities & Combat in Paper
2
2
 
3
- Membuat item kustom dengan kemampuan sihir atau skill khusus (seperti tongkat sihir petir, pedang lifesteal, atau bow peledak) adalah fitur paling favorit di server RPG dan Mini-games.
3
+ Crafting custom items with magical abilities and combat mechanics (such as lightning wands, lifesteal swords, or explosive bows) is a hallmark of modern RPG and minigame servers.
4
4
 
5
- Paper menyediakan API native untuk raycasting, particle, cooldown visual vanilla, dan Adventure sound effects tanpa perlu library eksternal.
5
+ Paper provides native APIs for entity raycasting, particle trails, vanilla visual cooldown animations, and Adventure sound effects without external library dependencies.
6
6
 
7
7
  ---
8
8
 
9
- ## 1. Anatomi Custom Item dengan PDC
9
+ ## 1. Anatomy of a Custom Item with PDC
10
10
 
11
- Selalu identifikasi item kustom menggunakan `PersistentDataContainer` (PDC), bukan nama atau lore yang bisa dipalsukan pemain via Anvil:
11
+ Always identify custom items using `PersistentDataContainer` (PDC) rather than displayName or lore strings which players can forge via an Anvil:
12
12
 
13
13
  ```java
14
14
  package com.example.plugin.item;
@@ -17,7 +17,6 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
17
17
  import org.bukkit.Material;
18
18
  import org.bukkit.NamespacedKey;
19
19
  import org.bukkit.inventory.ItemStack;
20
- import org.bukkit.inventory.meta.ItemMeta;
21
20
  import org.bukkit.persistence.PersistentDataType;
22
21
 
23
22
  import java.util.List;
@@ -29,18 +28,15 @@ public final class CustomItemFactory {
29
28
 
30
29
  public static ItemStack createLightningWand() {
31
30
  ItemStack item = new ItemStack(Material.BLAZE_ROD);
32
- ItemMeta meta = item.getItemMeta();
33
- if (meta != null) {
34
- meta.displayName(MiniMessage.miniMessage().deserialize("<gradient:#ffe259:#ffa751><bold>Tongkat Petir Kuno</bold></gradient>"));
31
+ item.editMeta(meta -> {
32
+ meta.displayName(MiniMessage.miniMessage().deserialize("<gradient:#ffe259:#ffa751><bold>Ancient Lightning Wand</bold></gradient>"));
35
33
  meta.lore(List.of(
36
- MiniMessage.miniMessage().deserialize("<gray>Klik kanan untuk menembakkan <yellow>Petir Murni</yellow>.</gray>"),
37
- MiniMessage.miniMessage().deserialize("<dark_gray>Cooldown: <gold>3 Detik</gold></dark_gray>")
34
+ MiniMessage.miniMessage().deserialize("<gray>Right-click to discharge <yellow>Pure Lightning</yellow>.</gray>"),
35
+ MiniMessage.miniMessage().deserialize("<dark_gray>Cooldown: <gold>3 Seconds</gold></dark_gray>")
38
36
  ));
39
37
 
40
- // Tandai ID item
41
38
  meta.getPersistentDataContainer().set(ITEM_ID_KEY, PersistentDataType.STRING, "lightning_wand");
42
- item.setItemMeta(meta);
43
- }
39
+ });
44
40
  return item;
45
41
  }
46
42
  }
@@ -48,37 +44,35 @@ public final class CustomItemFactory {
48
44
 
49
45
  ---
50
46
 
51
- ## 2. Sistem Cooldown: Visual Vanilla vs Timestamp
47
+ ## 2. Cooldown Management: Visual Vanilla Animations
52
48
 
53
- Minecraft memiliki API bawaan untuk menampilkan cooldown visual (animasi abu-abu berputar pada hotbar slot):
49
+ Minecraft features a native client API to render visual cooldown overlays (the sweeping gray overlay on hotbar slots):
54
50
 
55
51
  ```java
56
- // Memberikan cooldown visual vanilla selama 60 tick (3 detik) pada Blaze Rod
52
+ // Apply a 60-tick (3-second) vanilla cooldown to Blaze Rods
57
53
  player.setCooldown(Material.BLAZE_ROD, 60);
58
54
 
59
- // Memeriksa apakah pemain sedang dalam cooldown
55
+ // Query if cooldown is active
60
56
  if (player.hasCooldown(Material.BLAZE_ROD)) {
61
- player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Kemampuan masih cooldown!</red>"));
57
+ player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Ability is on cooldown!</red>"));
62
58
  return;
63
59
  }
64
60
  ```
65
61
 
66
62
  ---
67
63
 
68
- ## 3. Raycasting Entitas & Partikel (Skill Sihir)
64
+ ## 3. Entity Raycasting & Particle Beams
69
65
 
70
- Raycasting digunakan untuk mendeteksi apa yang sedang dibidik pemain secara presisi menggunakan `World#rayTraceEntities` atau kalkulasi vektor:
66
+ Raycasting detects target entities along the player's line of sight using `World#rayTraceEntities`:
71
67
 
72
68
  ```java
73
69
  package com.example.plugin.item;
74
70
 
75
71
  import net.kyori.adventure.sound.Sound;
76
72
  import net.kyori.adventure.text.minimessage.MiniMessage;
77
- import org.bukkit.FluidCollisionMode;
78
73
  import org.bukkit.Location;
79
74
  import org.bukkit.Material;
80
75
  import org.bukkit.Particle;
81
- import org.bukkit.SoundCategory;
82
76
  import org.bukkit.World;
83
77
  import org.bukkit.entity.Damageable;
84
78
  import org.bukkit.entity.Entity;
@@ -113,16 +107,15 @@ public final class CustomItemListener implements Listener {
113
107
  event.setCancelled(true);
114
108
  Player player = event.getPlayer();
115
109
 
116
- // 1. Cek Cooldown
110
+ // 1. Check Cooldown
117
111
  if (player.hasCooldown(Material.BLAZE_ROD)) {
118
- player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Tunggu sejenak sebelum menembak lagi!</red>"));
112
+ player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Ability cooling down!</red>"));
119
113
  return;
120
114
  }
121
115
 
122
- // Set cooldown 3 detik (60 ticks)
123
116
  player.setCooldown(Material.BLAZE_ROD, 60);
124
117
 
125
- // 2. Eksekusi Efek Serangan
118
+ // 2. Execute Spell Effect
126
119
  castLightningBeam(player);
127
120
  }
128
121
 
@@ -131,7 +124,7 @@ public final class CustomItemListener implements Listener {
131
124
  Location eyeLoc = player.getEyeLocation();
132
125
  Vector direction = eyeLoc.getDirection().normalize();
133
126
 
134
- // Putar suara tembakan sihir
127
+ // Play cast sound
135
128
  player.playSound(Sound.sound(
136
129
  org.bukkit.Sound.ENTITY_ILLUSIONER_CAST_SPELL.key(),
137
130
  Sound.Source.PLAYER,
@@ -139,13 +132,13 @@ public final class CustomItemListener implements Listener {
139
132
  1.5f
140
133
  ));
141
134
 
142
- // Partikel beam sepanjang garis pandang (hingga 30 blok)
135
+ // Spawn particle line (up to 30 blocks)
143
136
  for (double d = 1; d <= 30; d += 0.5) {
144
137
  Location point = eyeLoc.clone().add(direction.clone().multiply(d));
145
138
  world.spawnParticle(Particle.ELECTRIC_SPARK, point, 2, 0.05, 0.05, 0.05, 0.01);
146
139
  }
147
140
 
148
- // Raytrace entitas yang terkena garis tembakan
141
+ // Raytrace entities colliding with beam
149
142
  RayTraceResult result = world.rayTraceEntities(
150
143
  eyeLoc,
151
144
  direction,
@@ -158,13 +151,12 @@ public final class CustomItemListener implements Listener {
158
151
  Entity target = result.getHitEntity();
159
152
  Location hitLoc = target.getLocation();
160
153
 
161
- // Panggil sambaran petir visual & berikan damage
162
154
  world.strikeLightningEffect(hitLoc);
163
155
  if (target instanceof Damageable damageable) {
164
- damageable.damage(8.0, player); // 8 damage = 4 hati
156
+ damageable.damage(8.0, player); // 8 damage = 4 hearts
165
157
  }
166
158
 
167
- player.sendActionBar(MiniMessage.miniMessage().deserialize("<yellow>Tembakan Mengenai Target!</yellow>"));
159
+ player.sendActionBar(MiniMessage.miniMessage().deserialize("<yellow>Target struck!</yellow>"));
168
160
  }
169
161
  }
170
162
  }