papermc-skill 1.0.0 → 1.1.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 (56) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +90 -110
  3. package/assets/banner-animated.svg +360 -0
  4. package/assets/banner.svg +360 -0
  5. package/bin/cli.js +199 -0
  6. package/docs/01-modern-paper-architecture.md +48 -51
  7. package/docs/02-adventure-and-minimessage.md +48 -52
  8. package/docs/03-persistent-data-container.md +39 -39
  9. package/docs/04-commands-and-brigadier.md +32 -35
  10. package/docs/05-concurrency-and-folia.md +44 -44
  11. package/docs/06-events-and-listeners.md +30 -31
  12. package/docs/07-best-practices-and-perf.md +39 -40
  13. package/docs/08-gui-and-menus.md +34 -37
  14. package/docs/09-custom-items-and-combat.md +25 -33
  15. package/docs/10-database-and-storage.md +27 -27
  16. package/docs/11-security-and-exploit-prevention.md +233 -0
  17. package/package.json +2 -1
  18. package/skills/paper-plugin-dev/SKILL.md +391 -111
  19. package/templates/paper-modern-template/.gradle/9.7.1/checksums/checksums.lock +0 -0
  20. package/templates/paper-modern-template/.gradle/9.7.1/checksums/md5-checksums.bin +0 -0
  21. package/templates/paper-modern-template/.gradle/9.7.1/checksums/sha1-checksums.bin +0 -0
  22. package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/executionHistory/executionHistory.bin +0 -0
  23. package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.lock +0 -0
  24. package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/fileHashes/fileHashes.bin +0 -0
  25. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.lock +0 -0
  26. package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/fileHashes/resourceHashesCache.bin +0 -0
  27. package/templates/paper-modern-template/.gradle/{9.4.1/checksums/md5-checksums.bin → 9.7.1/kotlin-dsl-plugin-entries/jar-entries.bin} +0 -0
  28. package/templates/paper-modern-template/.gradle/9.7.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock +0 -0
  29. package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  30. package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +2 -2
  31. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/ExamplePaperPlugin.class +0 -0
  32. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/PaperPluginBootstrap.class +0 -0
  33. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/command/HelloCommand.class +0 -0
  34. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/listener/PlayerEventListener.class +0 -0
  35. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/util/SchedulerAdapter.class +0 -0
  36. package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
  37. package/templates/paper-modern-template/build.gradle.kts +10 -10
  38. package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.jar +0 -0
  39. package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.properties +1 -1
  40. package/templates/paper-modern-template/gradlew +3 -3
  41. package/templates/paper-modern-template/gradlew.bat +12 -23
  42. package/templates/paper-modern-template/settings.gradle.kts +7 -0
  43. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +6 -6
  44. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +2 -2
  45. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +2 -4
  46. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +2 -2
  47. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +6 -7
  48. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +6 -6
  49. package/templates/paper-modern-template/src/main/resources/config.yml +5 -5
  50. package/templates/paper-modern-template/.gradle/9.4.1/checksums/checksums.lock +0 -0
  51. package/templates/paper-modern-template/.gradle/9.4.1/checksums/sha1-checksums.bin +0 -0
  52. package/templates/paper-modern-template/.gradle/9.4.1/executionHistory/executionHistory.lock +0 -0
  53. package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.lock +0 -0
  54. package/templates/paper-modern-template/.gradle/file-system.probe +0 -0
  55. /package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/fileChanges/last-build.bin +0 -0
  56. /package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/gc.properties +0 -0
@@ -1,167 +1,447 @@
1
1
  ---
2
2
  name: paper-plugin-dev
3
- description: Expert guidelines, modern standards, and architectural blueprints for developing Minecraft Java server plugins using the PaperMC API (1.20-1.21+), Folia, Adventure API, MiniMessage, and Paperweight.
3
+ description: Expert guidelines, modern architectural blueprints, exploit-prevention patterns, and best practices for developing production-grade Minecraft Java plugins using PaperMC API (1.20-1.21+ / 26.x), Folia (Threaded Regions), Adventure API, MiniMessage, and Data Components.
4
4
  ---
5
5
 
6
- # Paper Minecraft Plugin Development Skill
6
+ # Paper & Folia Minecraft Plugin Development Skill
7
7
 
8
- Skill ini memberikan panduan otoritatif, standar arsitektur modern, dan resep coding siap pakai untuk membuat atau memodifikasi plugin Minecraft Java Server berbasis **PaperMC (1.20.6 / 1.21+)** dan **Folia (Threaded Regions)**.
8
+ This skill provides authoritative architectural standards, high-level exploit-prevention blueprints, and modern design patterns for building and auditing Minecraft Java server plugins based on **PaperMC (1.20.6, 1.21+ through 26.x)** and **Folia (Regionized Multi-Threading)**.
9
9
 
10
10
  ---
11
11
 
12
- ## 1. Prasyarat Lingkungan & Dependensi
12
+ ## 1. Environment Prerequisites & Compatibility Matrix
13
13
 
14
- | Komponen | Versi yang Dibutuhkan | Catatan |
15
- | :--- | :--- | :--- |
16
- | **Java SDK** | **Java 21 (LTS)** atau lebih baru | Minecraft 1.20.5+ dan Paper modern **wajib** Java 21+. |
17
- | **Gradle** | **Gradle 8.5+** (Disarankan 8.10+ / 9.x) | Gunakan Gradle Wrapper bawaan (`./gradlew`). |
18
- | **Paper API** | `io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT` | Repositori: `https://repo.papermc.io/repository/maven-public/` |
19
- | **Mojang Mappings** | `io.papermc.paperweight.userdev` | Hanya jika membutuhkan akses langsung ke internal `net.minecraft.*`. |
14
+ | Component | Minimum Standard | Current Recommendation | Critical Notes |
15
+ | :--- | :--- | :--- | :--- |
16
+ | **Java SDK** | **Java 21 (LTS)** | **Java 21 LTS** / Java 25 Ready | Minecraft 1.20.5+ and modern Paper **require** Java 21+. Target Paper 26.x requires Java 25+. |
17
+ | **Build Tool** | Gradle 8.10+ | **Gradle 9.7+ (Kotlin DSL)** | Use `jvmToolchain(21)` and `pluginManagement { mavenCentral() }`. |
18
+ | **Paper API** | `1.21.4-R0.1-SNAPSHOT` | `1.21.11-R0.1-SNAPSHOT` | Repository: `https://repo.papermc.io/repository/maven-public/` |
19
+ | **API Discovery** | Manual | **PaperMC Fill v3 API** | Query real-time versions via `https://fill.papermc.io/v3/projects/paper`. |
20
+ | **Text Handling** | Adventure 4.17+ | **Kyori Adventure + MiniMessage** | Never use `org.bukkit.ChatColor` or the legacy section sign `§`. |
21
+
22
+ ---
23
+
24
+ ## 2. Standard AI Coding Agent Workflow
25
+
26
+ When requested to scaffold, modify, or review a Paper/Folia plugin codebase:
27
+
28
+ 1. **Version Discovery & Target Verification**:
29
+ - Run `npx papermc-skill check-update` or query the Fill v3 API to determine current supported Paper builds and minimum Java requirements.
30
+ 2. **Configure Modern Plugin Manifest (`paper-plugin.yml`)**:
31
+ - Must declare `folia-supported: true`.
32
+ - Specify `bootstrapper:` for early lifecycle handling and Brigadier command registration.
33
+ 3. **Select Safe Concurrency Architecture**:
34
+ - Entity or Player state: `player.getScheduler()`.
35
+ - Block, World, or Chunk state: `RegionScheduler`.
36
+ - Global server state or announcements: `GlobalRegionScheduler`.
37
+ - Database, File I/O, Web Requests, or compute-heavy math: `AsyncScheduler` or `CompletableFuture`.
38
+ 4. **Enforce Exploit & Dupe Prevention Protocols (Mandatory)**:
39
+ - Validate numeric user input (reject `NaN`, `Infinity`, overflow, and negative amounts).
40
+ - Protect inventory menus against click-spam race conditions via debounce.
41
+ - Store `UUID` rather than `Player` instances across all caches and static collections.
42
+ - Secure custom items using `PersistentDataContainer` (PDC) signatures.
43
+ 5. **Compile & Lint Verification**:
44
+ - Verify build passes clean compilation with zero deprecation warnings.
20
45
 
21
46
  ---
22
47
 
23
- ## 2. Alur Kerja Standar (Workflow) untuk AI / Developer
24
-
25
- Saat diminta membuat fitur atau plugin Paper baru, ikuti urutan berikut:
26
-
27
- 1. **Tentukan Manifest (`src/main/resources/paper-plugin.yml`)**:
28
- - Berikan `folia-supported: true`.
29
- - Cantumkan `bootstrapper:` jika menggunakan Paper Brigadier Command.
30
- 2. **Setup PDC Keys (`PluginDataKeys.java`)**:
31
- - Buat konstanta `static final NamespacedKey` untuk semua data persisten (jangan instansiasi di dalam event loop).
32
- 3. **Pilih Penanganan Teks (Kyori Adventure)**:
33
- - Gunakan `MiniMessage.miniMessage().deserialize(...)`.
34
- - Jangan pernah menggunakan `§` atau `org.bukkit.ChatColor`.
35
- 4. **Pilih Pola Concurrency**:
36
- - Akses entity/block? Gunakan `RegionScheduler` atau `EntityScheduler`.
37
- - File/Database/Network? Gunakan `AsyncScheduler` atau `CompletableFuture`.
38
- 5. **Daftarkan Command & Event**:
39
- - Command -> Daftarkan di `BootstrapContext` via `LifecycleEvents.COMMANDS`.
40
- - Event -> Daftarkan di `JavaPlugin#onEnable()` via `registerEvents(...)`.
48
+ ## 3. Production Security & Exploit-Prevention Protocols
49
+
50
+ ### A. Item Duplication & Inventory Transaction Safety
51
+ Duplication glitches almost always stem from race conditions across inventory events or async state mutations:
52
+ - **Absolute Rule**: Never modify a player's `Inventory` or `ItemStack` from an asynchronous thread! Always dispatch inventory mutations onto the player's entity scheduler (`player.getScheduler()`).
53
+ - **Debounce Click Spam**: Enforce a cooldown (e.g., 150-200ms) between clicks on custom GUIs to stop auto-clicker exploits.
54
+ - **Cursor Item Handling**: When a custom menu closes unexpectedly (`InventoryCloseEvent` or disconnection), ensure items on the player's cursor are safely returned to inventory rather than lost or duplicated.
55
+
56
+ ### B. Economy & Numeric Input Sanitization (Anti-NaN & Anti-Overflow)
57
+ Many economy plugins suffer catastrophic exploits when players supply inputs like `/pay Player NaN`, `/pay Player -1000000`, or values exceeding standard 64-bit bounds:
58
+ - Always use `BigDecimal` or cents-based integer values for monetary storage.
59
+ - Always validate input:
60
+ ```java
61
+ if (!Double.isFinite(amount) || amount <= 0.0) {
62
+ throw new IllegalArgumentException("Invalid transaction amount!");
63
+ }
64
+ ```
65
+
66
+ ### C. PDC Anti-Tamper & Cryptographic Hashing
67
+ Cheating clients or players using anvils/crafting tables often attempt to forge custom item lore to impersonate rare server gear.
68
+ - **Rule**: Never identify a custom item by its `DisplayName` or `Lore`!
69
+ - Store immutable identifiers inside `PersistentDataContainer` (PDC).
70
+ - For high-value items, append an internal server HMAC hash or unique identifier to verify data integrity.
71
+
72
+ ### D. Preventing Denial of Service via Synchronous Chunk Loading
73
+ Invoking `world.getBlockAt(x, y, z)` on ungenerated/unloaded far chunks (e.g., coordinate 30,000,000) freezes the entire main server thread for hundreds of milliseconds.
74
+ - Always check `world.isChunkLoaded(chunkX, chunkZ)` prior to synchronous access, or load asynchronously:
75
+ ```java
76
+ world.getChunkAtAsync(chunkX, chunkZ).thenAccept(chunk -> { ... });
77
+ ```
78
+
79
+ ### E. SQL Injection Defense & Pool Leak Prevention
80
+ - Never concatenate raw user input into SQL queries (`"SELECT * FROM users WHERE name = '" + input + "'"`).
81
+ - Always use `PreparedStatement` with placeholder queries (`?`).
82
+ - Use connection pooling (**HikariCP**) enclosed in `try-with-resources` blocks so connections return cleanly to the pool.
41
83
 
42
84
  ---
43
85
 
44
- ## 3. Resep Kode Cepat (Ready-to-Use Recipes)
86
+ ## 4. Modern Production Recipes
45
87
 
46
- ### Resep A: Manifest Modern (`paper-plugin.yml`)
88
+ ### Recipe 1: Folia-Ready Modern Manifest (`src/main/resources/paper-plugin.yml`)
47
89
  ```yaml
48
- name: MyAwesomePlugin
90
+ name: EpicPaperPlugin
49
91
  version: '1.0.0'
50
- main: com.example.plugin.MyPlugin
51
- bootstrapper: com.example.plugin.MyPluginBootstrap
92
+ main: com.example.plugin.EpicPlugin
93
+ bootstrapper: com.example.plugin.EpicPluginBootstrap
52
94
  api-version: '1.21'
53
95
  folia-supported: true
54
96
  authors:
55
97
  - DeveloperName
98
+ description: Production-grade Paper & Folia plugin built with modern architectural standards.
56
99
  ```
57
100
 
58
- ### Resep B: Brigadier Command Modern (Tanpa `plugin.yml`)
101
+ ### Recipe 2: Bootstrap Lifecycle & Modern Brigadier Command
102
+ Register commands through Paper's Lifecycle Events (Minecraft 1.20.6+ / 1.21+) without needing command entries in `plugin.yml`:
103
+
59
104
  ```java
60
- // Daftarkan di PluginBootstrap#bootstrap:
61
- context.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
62
- event.registrar().register(
63
- Commands.literal("warp")
64
- .requires(source -> source.getSender().hasPermission("plugin.warp"))
65
- .then(Commands.argument("target", StringArgumentType.word())
66
- .suggests((ctx, builder) -> {
67
- builder.suggest("spawn");
68
- builder.suggest("arena");
69
- return builder.buildFuture();
70
- })
71
- .executes(ctx -> {
72
- String target = StringArgumentType.getString(ctx, "target");
73
- ctx.getSource().getSender().sendMessage(
74
- MiniMessage.miniMessage().deserialize("<green>Teleporting to <gold>" + target + "</gold>...</green>")
75
- );
76
- return Command.SINGLE_SUCCESS;
77
- })
78
- )
79
- .build(),
80
- "Warp command description",
81
- List.of("gotowarp")
82
- );
83
- });
105
+ package com.example.plugin;
106
+
107
+ import com.mojang.brigadier.Command;
108
+ import com.mojang.brigadier.arguments.DoubleArgumentType;
109
+ import com.mojang.brigadier.arguments.StringArgumentType;
110
+ import io.papermc.paper.command.brigadier.Commands;
111
+ import io.papermc.paper.plugin.bootstrap.BootstrapContext;
112
+ import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
113
+ import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
114
+ import net.kyori.adventure.text.minimessage.MiniMessage;
115
+ import org.jspecify.annotations.NullMarked;
116
+
117
+ import java.util.List;
118
+
119
+ @NullMarked
120
+ public class EpicPluginBootstrap implements PluginBootstrap {
121
+
122
+ @Override
123
+ public void bootstrap(BootstrapContext context) {
124
+ context.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
125
+ final Commands registrar = event.registrar();
126
+
127
+ registrar.register(
128
+ Commands.literal("safepay")
129
+ .requires(source -> source.getSender().hasPermission("epicplugin.pay"))
130
+ .then(Commands.argument("target", StringArgumentType.word())
131
+ .then(Commands.argument("amount", DoubleArgumentType.doubleArg(0.01, 1_000_000_000.0))
132
+ .executes(ctx -> {
133
+ var sender = ctx.getSource().getSender();
134
+ String target = StringArgumentType.getString(ctx, "target");
135
+ double amount = DoubleArgumentType.getDouble(ctx, "amount");
136
+
137
+ // Strict security sanitization
138
+ if (!Double.isFinite(amount) || amount <= 0) {
139
+ sender.sendMessage(MiniMessage.miniMessage().deserialize("<red>Invalid payment amount!</red>"));
140
+ return Command.SINGLE_SUCCESS;
141
+ }
142
+
143
+ sender.sendMessage(MiniMessage.miniMessage().deserialize(
144
+ "<green>Sent <gold>$" + String.format("%.2f", amount) + "</gold> to <yellow>" + target + "</yellow>!</green>"
145
+ ));
146
+ return Command.SINGLE_SUCCESS;
147
+ })
148
+ )
149
+ )
150
+ .build(),
151
+ "Secure player payment with anti-overflow validation",
152
+ List.of("paysecure")
153
+ );
154
+ });
155
+ }
156
+ }
84
157
  ```
85
158
 
86
- ### Resep C: PersistentDataContainer (PDC) Item & Entity
159
+ ### Recipe 3: Anti-Tamper PDC Item Utilities
87
160
  ```java
88
- // Menyimpan data pada ItemStack
89
- NamespacedKey KEY = new NamespacedKey(plugin, "item_level");
90
- meta.getPersistentDataContainer().set(KEY, PersistentDataType.INTEGER, 5);
161
+ package com.example.plugin.util;
162
+
163
+ import net.kyori.adventure.text.minimessage.MiniMessage;
164
+ import org.bukkit.Material;
165
+ import org.bukkit.NamespacedKey;
166
+ import org.bukkit.inventory.ItemStack;
167
+ import org.bukkit.persistence.PersistentDataContainer;
168
+ import org.bukkit.persistence.PersistentDataType;
169
+ import org.bukkit.plugin.Plugin;
91
170
 
92
- // Membaca data dengan fallback default:
93
- int level = meta.getPersistentDataContainer().getOrDefault(KEY, PersistentDataType.INTEGER, 1);
171
+ import java.util.UUID;
172
+
173
+ public final class SafeItemUtil {
174
+
175
+ private static NamespacedKey KEY_ITEM_ID;
176
+ private static NamespacedKey KEY_LEVEL;
177
+ private static NamespacedKey KEY_SIG;
178
+
179
+ public static void initKeys(Plugin plugin) {
180
+ KEY_ITEM_ID = new NamespacedKey(plugin, "custom_id");
181
+ KEY_LEVEL = new NamespacedKey(plugin, "item_level");
182
+ KEY_SIG = new NamespacedKey(plugin, "sec_sig");
183
+ }
184
+
185
+ public static ItemStack createCustomSword(String customId, int level) {
186
+ ItemStack item = new ItemStack(Material.DIAMOND_SWORD);
187
+ item.editMeta(meta -> {
188
+ meta.displayName(MiniMessage.miniMessage().deserialize("<gradient:#00d2ff:#3a7bd5>Excalibur Tier " + level + "</gradient>"));
189
+ PersistentDataContainer pdc = meta.getPersistentDataContainer();
190
+ pdc.set(KEY_ITEM_ID, PersistentDataType.STRING, customId);
191
+ pdc.set(KEY_LEVEL, PersistentDataType.INTEGER, level);
192
+ pdc.set(KEY_SIG, PersistentDataType.STRING, UUID.randomUUID().toString());
193
+ });
194
+ return item;
195
+ }
196
+
197
+ public static boolean isCustomItem(ItemStack item, String expectedId) {
198
+ if (item == null || !item.hasItemMeta()) return false;
199
+ String id = item.getItemMeta().getPersistentDataContainer().get(KEY_ITEM_ID, PersistentDataType.STRING);
200
+ return expectedId.equals(id);
201
+ }
202
+ }
94
203
  ```
95
204
 
96
- ### Resep D: GUI Menu Aman dari Eksploitasi
205
+ ### Recipe 4: Anti-Dupe GUI Controller with Debounce
97
206
  ```java
98
- // 1. Buat Custom InventoryHolder
99
- public class MenuHolder implements InventoryHolder {
100
- private final Inventory inv;
101
- public MenuHolder(Component title, int size) {
102
- this.inv = Bukkit.createInventory(this, size, title);
207
+ package com.example.plugin.gui;
208
+
209
+ import net.kyori.adventure.text.Component;
210
+ import org.bukkit.Bukkit;
211
+ import org.bukkit.entity.Player;
212
+ import org.bukkit.event.EventHandler;
213
+ import org.bukkit.event.EventPriority;
214
+ import org.bukkit.event.Listener;
215
+ import org.bukkit.event.inventory.InventoryClickEvent;
216
+ import org.bukkit.event.inventory.InventoryCloseEvent;
217
+ import org.bukkit.event.inventory.InventoryDragEvent;
218
+ import org.bukkit.inventory.Inventory;
219
+ import org.bukkit.inventory.InventoryHolder;
220
+
221
+ import java.util.Map;
222
+ import java.util.UUID;
223
+ import java.util.concurrent.ConcurrentHashMap;
224
+
225
+ public class SafeMenuController implements InventoryHolder, Listener {
226
+
227
+ private final Inventory inventory;
228
+ private final Map<UUID, Long> clickDebounce = new ConcurrentHashMap<>();
229
+
230
+ public SafeMenuController(Component title, int size) {
231
+ this.inventory = Bukkit.createInventory(this, size, title);
103
232
  }
104
- @Override public Inventory getInventory() { return inv; }
105
- }
106
233
 
107
- // 2. Di Listener:
108
- @EventHandler
109
- public void onMenuClick(InventoryClickEvent event) {
110
- if (event.getInventory().getHolder() instanceof MenuHolder) {
111
- event.setCancelled(true); // Selalu batalkan pergerakan item
112
- ItemStack item = event.getCurrentItem();
113
- if (item != null && item.hasItemMeta()) {
114
- // Baca aksi dari PDC
234
+ @Override
235
+ public Inventory getInventory() {
236
+ return inventory;
237
+ }
238
+
239
+ @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
240
+ public void onClick(InventoryClickEvent event) {
241
+ if (!(event.getInventory().getHolder() instanceof SafeMenuController)) {
242
+ return;
243
+ }
244
+
245
+ // 1. Always cancel item movement
246
+ event.setCancelled(true);
247
+
248
+ if (!(event.getWhoClicked() instanceof Player player)) {
249
+ return;
250
+ }
251
+
252
+ // 2. Click-spam protection (200ms debounce)
253
+ long now = System.currentTimeMillis();
254
+ long lastClick = clickDebounce.getOrDefault(player.getUniqueId(), 0L);
255
+ if (now - lastClick < 200) {
256
+ return;
257
+ }
258
+ clickDebounce.put(player.getUniqueId(), now);
259
+
260
+ // 3. Process only top menu clicks
261
+ if (event.getClickedInventory() == event.getInventory()) {
262
+ handleSlotAction(player, event.getSlot());
263
+ }
264
+ }
265
+
266
+ @EventHandler
267
+ public void onDrag(InventoryDragEvent event) {
268
+ if (event.getInventory().getHolder() instanceof SafeMenuController) {
269
+ event.setCancelled(true);
115
270
  }
116
271
  }
272
+
273
+ @EventHandler
274
+ public void onClose(InventoryCloseEvent event) {
275
+ if (event.getInventory().getHolder() instanceof SafeMenuController) {
276
+ clickDebounce.remove(event.getPlayer().getUniqueId());
277
+ }
278
+ }
279
+
280
+ private void handleSlotAction(Player player, int slot) {
281
+ // Handle slot action safely
282
+ }
117
283
  }
118
284
  ```
119
285
 
120
- ### Resep E: Universal Folia / Paper Scheduler Adapter
286
+ ### Recipe 5: Universal Folia & Paper Scheduler Adapter
287
+ Transparently dispatches execution safely whether running on standard Paper or multi-threaded Folia:
288
+
121
289
  ```java
122
- public static void runForPlayer(Plugin plugin, Player player, Runnable runnable) {
123
- try {
124
- Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
125
- player.getScheduler().run(plugin, task -> runnable.run(), null);
126
- } catch (ClassNotFoundException e) {
127
- Bukkit.getScheduler().runTask(plugin, runnable);
290
+ package com.example.plugin.util;
291
+
292
+ import org.bukkit.Bukkit;
293
+ import org.bukkit.Location;
294
+ import org.bukkit.entity.Entity;
295
+ import org.bukkit.plugin.Plugin;
296
+
297
+ import java.util.concurrent.TimeUnit;
298
+
299
+ public final class UniversalScheduler {
300
+
301
+ private static final boolean IS_FOLIA;
302
+
303
+ static {
304
+ boolean folia;
305
+ try {
306
+ Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
307
+ folia = true;
308
+ } catch (ClassNotFoundException e) {
309
+ folia = false;
310
+ }
311
+ IS_FOLIA = folia;
312
+ }
313
+
314
+ public static boolean isFolia() {
315
+ return IS_FOLIA;
316
+ }
317
+
318
+ /** Dispatches a task to the region scheduler owning the specified entity */
319
+ public static void runForEntity(Plugin plugin, Entity entity, Runnable runnable) {
320
+ if (IS_FOLIA) {
321
+ entity.getScheduler().run(plugin, task -> runnable.run(), null);
322
+ } else {
323
+ Bukkit.getScheduler().runTask(plugin, runnable);
324
+ }
325
+ }
326
+
327
+ /** Dispatches a task to the region scheduler owning the specified location */
328
+ public static void runAtLocation(Plugin plugin, Location location, Runnable runnable) {
329
+ if (IS_FOLIA) {
330
+ Bukkit.getRegionScheduler().execute(plugin, location, runnable);
331
+ } else {
332
+ Bukkit.getScheduler().runTask(plugin, runnable);
333
+ }
334
+ }
335
+
336
+ /** Dispatches a purely asynchronous task (I/O, database, networking) */
337
+ public static void runAsync(Plugin plugin, Runnable runnable) {
338
+ if (IS_FOLIA) {
339
+ Bukkit.getAsyncScheduler().runNow(plugin, task -> runnable.run());
340
+ } else {
341
+ Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable);
342
+ }
343
+ }
344
+
345
+ /** Dispatches a delayed asynchronous task */
346
+ public static void runAsyncDelayed(Plugin plugin, Runnable runnable, long delayMs) {
347
+ if (IS_FOLIA) {
348
+ Bukkit.getAsyncScheduler().runDelayed(plugin, task -> runnable.run(), delayMs, TimeUnit.MILLISECONDS);
349
+ } else {
350
+ long ticks = Math.max(1L, delayMs / 50L);
351
+ Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, runnable, ticks);
352
+ }
128
353
  }
129
354
  }
130
355
  ```
131
356
 
132
- ### Resep F: Raycast & Partikel Sihir
357
+ ### Recipe 6: Robust Asynchronous Database Access (HikariCP)
133
358
  ```java
134
- Location eye = player.getEyeLocation();
135
- Vector dir = eye.getDirection().normalize();
359
+ package com.example.plugin.database;
360
+
361
+ import com.zaxxer.hikari.HikariConfig;
362
+ import com.zaxxer.hikari.HikariDataSource;
363
+
364
+ import java.sql.Connection;
365
+ import java.sql.PreparedStatement;
366
+ import java.sql.ResultSet;
367
+ import java.sql.SQLException;
368
+ import java.util.UUID;
369
+ import java.util.concurrent.CompletableFuture;
370
+ import java.util.concurrent.ExecutorService;
371
+ import java.util.concurrent.Executors;
136
372
 
137
- for (double d = 1; d <= 25; d += 0.5) {
138
- Location p = eye.clone().add(dir.clone().multiply(d));
139
- player.getWorld().spawnParticle(Particle.FLAME, p, 1, 0, 0, 0, 0);
373
+ public class DatabaseManager {
374
+
375
+ private final HikariDataSource dataSource;
376
+ private final ExecutorService dbExecutor = Executors.newFixedThreadPool(4);
377
+
378
+ public DatabaseManager(String jdbcUrl, String username, String password) {
379
+ HikariConfig config = new HikariConfig();
380
+ config.setJdbcUrl(jdbcUrl);
381
+ config.setUsername(username);
382
+ config.setPassword(password);
383
+ config.setMaximumPoolSize(10);
384
+ config.setMinimumIdle(2);
385
+ config.setConnectionTimeout(5000);
386
+ config.setPoolName("PaperPlugin-HikariPool");
387
+ this.dataSource = new HikariDataSource(config);
388
+ }
389
+
390
+ public CompletableFuture<Integer> getCoinsAsync(UUID playerUuid) {
391
+ return CompletableFuture.supplyAsync(() -> {
392
+ String query = "SELECT coins FROM player_data WHERE uuid = ?";
393
+ try (Connection conn = dataSource.getConnection();
394
+ PreparedStatement stmt = conn.prepareStatement(query)) {
395
+ stmt.setString(1, playerUuid.toString());
396
+ try (ResultSet rs = stmt.executeQuery()) {
397
+ if (rs.next()) {
398
+ return rs.getInt("coins");
399
+ }
400
+ }
401
+ } catch (SQLException e) {
402
+ e.printStackTrace();
403
+ }
404
+ return 0;
405
+ }, dbExecutor);
406
+ }
407
+
408
+ public void shutdown() {
409
+ dbExecutor.shutdown();
410
+ if (dataSource != null && !dataSource.isClosed()) {
411
+ dataSource.close();
412
+ }
413
+ }
140
414
  }
141
415
  ```
142
416
 
143
417
  ---
144
418
 
145
- ## 4. Anti-Patterns yang Wajib Dihindari
419
+ ## 5. Anti-Patterns vs Modern Standards
146
420
 
147
- | Jangan Pernah Dilakukan ❌ | Cara yang Benar ✔️ |
148
- | :--- | :--- |
149
- | `ChatColor.RED + "Pesan"` | `MiniMessage.miniMessage().deserialize("<red>Pesan</red>")` |
150
- | `public static Set<Player> players;` | `public static Set<UUID> playerUuids;` (Cegah memory leak) |
151
- | `Bukkit.getScheduler()` di server Folia | Gunakan `RegionScheduler`, `EntityScheduler`, atau `AsyncScheduler` |
152
- | `world.getChunkAt(x, z)` acak di loop | `world.getChunkAtAsync(x, z)` |
153
- | `new NamespacedKey(...)` per hit/tick | Simpan `NamespacedKey` di `public static final` |
154
- | Mencocokkan nama item lore untuk aksi GUI | Gunakan `PersistentDataContainer` (PDC) pada item |
155
- | Query database di tick thread | Jalankan via `CompletableFuture.supplyAsync(...)` |
421
+ | Never Do This ❌ | Hazard / Vulnerability ⚠️ | Safe Modern Pattern ✔️ |
422
+ | :--- | :--- | :--- |
423
+ | `ChatColor.RED + "Message"` | Deprecated & breaks console/client formatting | `MiniMessage.miniMessage().deserialize("<red>Message</red>")` |
424
+ | `public static Set<Player> list;` | Causes massive **Memory Leaks** of worlds & connections | `public static Set<UUID> list;` |
425
+ | `world.getChunkAt(x, z)` in a loop | Freezes main server tick thread (major TPS drops) | `world.getChunkAtAsync(x, z)` |
426
+ | `Bukkit.getScheduler()` on Folia | Throws exception or causes thread-safety desync | Use `RegionScheduler`, `EntityScheduler`, or `UniversalScheduler` |
427
+ | Modifying inventories asynchronously | Causes item duplication and memory corruption | Always dispatch inventory modifications to entity region thread |
428
+ | Concatenating raw SQL queries | High vulnerability to **SQL Injection** | Always use `PreparedStatement` with `?` parameters |
429
+ | `new NamespacedKey(...)` in event loops | Excessive GC allocation pressure | Define constants as `public static final NamespacedKey` |
430
+ | Relying on item displayName/lore | Easily forged via anvils or modified clients | Always verify identity and attributes via **PDC** |
431
+ | Unbounded numbers in economy commands | Susceptible to `NaN`, negative amounts, and overflow | Validate `Double.isFinite()` and enforce minimum/maximum boundaries |
156
432
 
157
433
  ---
158
434
 
159
- ## 5. Referensi Kode Asli di Folder `references/`
435
+ ## 6. Pre-Release Security Audit Checklist
436
+
437
+ Prior to releasing a plugin to production or public servers:
160
438
 
161
- - **Arsitektur Enterprise & Database Async**: Periksa [`references/luckperms`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/luckperms).
162
- - **Performa Async Chunks & Schedulers**: Periksa [`references/chunky`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/chunky).
163
- - **Paperweight & Incendo Cloud Commands**: Periksa [`references/squaremap`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/squaremap).
164
- - **MiniMessage & Paper Manifest**: Periksa [`references/miniplaceholders`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/miniplaceholders).
165
- - **Ekonomi & Permissions Hooking**: Periksa [`references/vault-api`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/vault-api).
166
- - **Setup Server Pengujian Otomatis**: Periksa [`references/run-paper`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/run-paper).
167
- - **Display Packet & Hologram**: Periksa [`references/decentholograms`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/decentholograms).
439
+ - [ ] `paper-plugin.yml` is defined with `folia-supported: true`.
440
+ - [ ] Zero references to `org.bukkit.ChatColor` across the codebase.
441
+ - [ ] All command registration uses modern Brigadier (`LifecycleEvents.COMMANDS`).
442
+ - [ ] No direct storage of `Player`, `World`, or `Entity` instances in static collections.
443
+ - [ ] All File, Database, and HTTP operations execute 100% asynchronously.
444
+ - [ ] Custom GUIs implement `setCancelled(true)` and click debounce cooldowns.
445
+ - [ ] Custom items are identified and verified via `PersistentDataContainer` (PDC).
446
+ - [ ] All database queries use connection pooling enclosed in `try-with-resources`.
447
+ - [ ] Plugin compiles cleanly using Gradle 9.x targeting Java 21 LTS.
@@ -1,2 +1,2 @@
1
- #Sat Sep 05 14:25:51 UTC 2026
2
- gradle.version=9.4.1
1
+ #Thu Oct 01 12:32:36 UTC 2026
2
+ gradle.version=9.7.1