papermc-skill 1.0.1 → 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 (43) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +12 -12
  3. package/assets/banner-animated.svg +360 -0
  4. package/assets/banner.svg +360 -0
  5. package/bin/cli.js +107 -2
  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 -114
  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.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.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.7.1/fileHashes/resourceHashesCache.bin +0 -0
  27. package/templates/paper-modern-template/.gradle/9.7.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock +0 -0
  28. package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  29. package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +1 -1
  30. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/ExamplePaperPlugin.class +0 -0
  31. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/PaperPluginBootstrap.class +0 -0
  32. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/command/HelloCommand.class +0 -0
  33. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/listener/PlayerEventListener.class +0 -0
  34. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/util/SchedulerAdapter.class +0 -0
  35. package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
  36. package/templates/paper-modern-template/build.gradle.kts +7 -7
  37. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +6 -6
  38. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +2 -2
  39. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +2 -4
  40. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +2 -2
  41. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +6 -7
  42. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +6 -6
  43. package/templates/paper-modern-template/src/main/resources/config.yml +5 -5
@@ -1,170 +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
- 0. **Pemeriksaan Versi Otomatis (Dynamic Version Discovery)**:
28
- - Sebelum memulai scaffolding atau menulis kode baru, periksa rilis terbaru PaperMC & Minecraft via API Fill v3 (`https://fill.papermc.io/v3/projects/paper`) atau jalankan `npx papermc-skill check-update`.
29
- - Pastikan proyek menargetkan versi Paper API dan minimum Java SDK yang paling mutakhir.
30
- 1. **Tentukan Manifest (`src/main/resources/paper-plugin.yml`)**:
31
- - Berikan `folia-supported: true`.
32
- - Cantumkan `bootstrapper:` jika menggunakan Paper Brigadier Command.
33
- 2. **Setup PDC Keys (`PluginDataKeys.java`)**:
34
- - Buat konstanta `static final NamespacedKey` untuk semua data persisten (jangan instansiasi di dalam event loop).
35
- 3. **Pilih Penanganan Teks (Kyori Adventure)**:
36
- - Gunakan `MiniMessage.miniMessage().deserialize(...)`.
37
- - Jangan pernah menggunakan `§` atau `org.bukkit.ChatColor`.
38
- 4. **Pilih Pola Concurrency**:
39
- - Akses entity/block? Gunakan `RegionScheduler` atau `EntityScheduler`.
40
- - File/Database/Network? Gunakan `AsyncScheduler` atau `CompletableFuture`.
41
- 5. **Daftarkan Command & Event**:
42
- - Command -> Daftarkan di `BootstrapContext` via `LifecycleEvents.COMMANDS`.
43
- - 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.
44
83
 
45
84
  ---
46
85
 
47
- ## 3. Resep Kode Cepat (Ready-to-Use Recipes)
86
+ ## 4. Modern Production Recipes
48
87
 
49
- ### Resep A: Manifest Modern (`paper-plugin.yml`)
88
+ ### Recipe 1: Folia-Ready Modern Manifest (`src/main/resources/paper-plugin.yml`)
50
89
  ```yaml
51
- name: MyAwesomePlugin
90
+ name: EpicPaperPlugin
52
91
  version: '1.0.0'
53
- main: com.example.plugin.MyPlugin
54
- bootstrapper: com.example.plugin.MyPluginBootstrap
92
+ main: com.example.plugin.EpicPlugin
93
+ bootstrapper: com.example.plugin.EpicPluginBootstrap
55
94
  api-version: '1.21'
56
95
  folia-supported: true
57
96
  authors:
58
97
  - DeveloperName
98
+ description: Production-grade Paper & Folia plugin built with modern architectural standards.
59
99
  ```
60
100
 
61
- ### 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
+
62
104
  ```java
63
- // Daftarkan di PluginBootstrap#bootstrap:
64
- context.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
65
- event.registrar().register(
66
- Commands.literal("warp")
67
- .requires(source -> source.getSender().hasPermission("plugin.warp"))
68
- .then(Commands.argument("target", StringArgumentType.word())
69
- .suggests((ctx, builder) -> {
70
- builder.suggest("spawn");
71
- builder.suggest("arena");
72
- return builder.buildFuture();
73
- })
74
- .executes(ctx -> {
75
- String target = StringArgumentType.getString(ctx, "target");
76
- ctx.getSource().getSender().sendMessage(
77
- MiniMessage.miniMessage().deserialize("<green>Teleporting to <gold>" + target + "</gold>...</green>")
78
- );
79
- return Command.SINGLE_SUCCESS;
80
- })
81
- )
82
- .build(),
83
- "Warp command description",
84
- List.of("gotowarp")
85
- );
86
- });
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
+ }
87
157
  ```
88
158
 
89
- ### Resep C: PersistentDataContainer (PDC) Item & Entity
159
+ ### Recipe 3: Anti-Tamper PDC Item Utilities
90
160
  ```java
91
- // Menyimpan data pada ItemStack
92
- NamespacedKey KEY = new NamespacedKey(plugin, "item_level");
93
- 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;
94
170
 
95
- // Membaca data dengan fallback default:
96
- 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
+ }
97
203
  ```
98
204
 
99
- ### Resep D: GUI Menu Aman dari Eksploitasi
205
+ ### Recipe 4: Anti-Dupe GUI Controller with Debounce
100
206
  ```java
101
- // 1. Buat Custom InventoryHolder
102
- public class MenuHolder implements InventoryHolder {
103
- private final Inventory inv;
104
- public MenuHolder(Component title, int size) {
105
- 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);
106
232
  }
107
- @Override public Inventory getInventory() { return inv; }
108
- }
109
233
 
110
- // 2. Di Listener:
111
- @EventHandler
112
- public void onMenuClick(InventoryClickEvent event) {
113
- if (event.getInventory().getHolder() instanceof MenuHolder) {
114
- event.setCancelled(true); // Selalu batalkan pergerakan item
115
- ItemStack item = event.getCurrentItem();
116
- if (item != null && item.hasItemMeta()) {
117
- // 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);
118
270
  }
119
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
+ }
120
283
  }
121
284
  ```
122
285
 
123
- ### 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
+
124
289
  ```java
125
- public static void runForPlayer(Plugin plugin, Player player, Runnable runnable) {
126
- try {
127
- Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
128
- player.getScheduler().run(plugin, task -> runnable.run(), null);
129
- } catch (ClassNotFoundException e) {
130
- 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
+ }
131
353
  }
132
354
  }
133
355
  ```
134
356
 
135
- ### Resep F: Raycast & Partikel Sihir
357
+ ### Recipe 6: Robust Asynchronous Database Access (HikariCP)
136
358
  ```java
137
- Location eye = player.getEyeLocation();
138
- 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;
139
372
 
140
- for (double d = 1; d <= 25; d += 0.5) {
141
- Location p = eye.clone().add(dir.clone().multiply(d));
142
- 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
+ }
143
414
  }
144
415
  ```
145
416
 
146
417
  ---
147
418
 
148
- ## 4. Anti-Patterns yang Wajib Dihindari
419
+ ## 5. Anti-Patterns vs Modern Standards
149
420
 
150
- | Jangan Pernah Dilakukan ❌ | Cara yang Benar ✔️ |
151
- | :--- | :--- |
152
- | `ChatColor.RED + "Pesan"` | `MiniMessage.miniMessage().deserialize("<red>Pesan</red>")` |
153
- | `public static Set<Player> players;` | `public static Set<UUID> playerUuids;` (Cegah memory leak) |
154
- | `Bukkit.getScheduler()` di server Folia | Gunakan `RegionScheduler`, `EntityScheduler`, atau `AsyncScheduler` |
155
- | `world.getChunkAt(x, z)` acak di loop | `world.getChunkAtAsync(x, z)` |
156
- | `new NamespacedKey(...)` per hit/tick | Simpan `NamespacedKey` di `public static final` |
157
- | Mencocokkan nama item lore untuk aksi GUI | Gunakan `PersistentDataContainer` (PDC) pada item |
158
- | 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 |
159
432
 
160
433
  ---
161
434
 
162
- ## 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:
163
438
 
164
- - **Arsitektur Enterprise & Database Async**: Periksa [`references/luckperms`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/luckperms).
165
- - **Performa Async Chunks & Schedulers**: Periksa [`references/chunky`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/chunky).
166
- - **Paperweight & Incendo Cloud Commands**: Periksa [`references/squaremap`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/squaremap).
167
- - **MiniMessage & Paper Manifest**: Periksa [`references/miniplaceholders`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/miniplaceholders).
168
- - **Ekonomi & Permissions Hooking**: Periksa [`references/vault-api`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/vault-api).
169
- - **Setup Server Pengujian Otomatis**: Periksa [`references/run-paper`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/run-paper).
170
- - **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
- #Tue Sep 15 04:15:45 UTC 2026
1
+ #Thu Oct 01 12:32:36 UTC 2026
2
2
  gradle.version=9.7.1