papermc-skill 1.1.0 → 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 (26) 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 +31 -0
  7. package/CLAUDE.md +27 -0
  8. package/CODEX.md +12 -0
  9. package/DEEPSEEK.md +18 -0
  10. package/README.md +66 -26
  11. package/bin/cli.js +461 -36
  12. package/docs/12-data-components-and-display-entities.md +264 -0
  13. package/docs/13-multi-agent-system-and-tooling.md +90 -0
  14. package/package.json +16 -4
  15. package/skills/paper-plugin-dev/SKILL.md +144 -110
  16. package/templates/paper-modern-template/.gradle/9.7.1/checksums/checksums.lock +0 -0
  17. package/templates/paper-modern-template/.gradle/9.7.1/checksums/md5-checksums.bin +0 -0
  18. package/templates/paper-modern-template/.gradle/9.7.1/checksums/sha1-checksums.bin +0 -0
  19. package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.bin +0 -0
  20. package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.lock +0 -0
  21. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.bin +0 -0
  22. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.lock +0 -0
  23. package/templates/paper-modern-template/.gradle/9.7.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock +0 -0
  24. package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  25. package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +1 -1
  26. package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
@@ -1,23 +1,27 @@
1
1
  ---
2
2
  name: paper-plugin-dev
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.
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, Data Components, Display Entities, and Tricky Trials combat kinematics.
4
4
  ---
5
5
 
6
6
  # Paper & Folia Minecraft Plugin Development Skill
7
7
 
8
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
+ Compatible with all premier AI coding environments: **Antigravity (agy)**, **Claude Code**, **OpenAI Codex**, **Cursor**, **DeepSeek Harness**, **Paseo**, and **pi.dev**.
11
+
10
12
  ---
11
13
 
12
14
  ## 1. Environment Prerequisites & Compatibility Matrix
13
15
 
14
16
  | Component | Minimum Standard | Current Recommendation | Critical Notes |
15
17
  | :--- | :--- | :--- | :--- |
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/` |
18
+ | **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+. |
19
+ | **Build Tool** | Gradle 8.10+ | **Gradle 9.x+ (Kotlin DSL)** | Use `jvmToolchain(21)` and `pluginManagement { mavenCentral() }`. |
20
+ | **Paper API** | `1.21.4-R0.1-SNAPSHOT` | `1.21.11-R0.1-SNAPSHOT` / `26.3.build.+` | Repository: `https://repo.papermc.io/repository/maven-public/` |
19
21
  | **API Discovery** | Manual | **PaperMC Fill v3 API** | Query real-time versions via `https://fill.papermc.io/v3/projects/paper`. |
20
22
  | **Text Handling** | Adventure 4.17+ | **Kyori Adventure + MiniMessage** | Never use `org.bukkit.ChatColor` or the legacy section sign `§`. |
23
+ | **Item Data** | Data Components (1.20.5+) | `io.papermc.paper.datacomponent` | Legacy NBT tags are completely deprecated. Use `DataComponentTypes`. |
24
+ | **Visuals/FX** | ArmorStands (Legacy) | **Display Entities (Text/Item/Block)** | Zero-tick lag, JOML `Matrix4f` smooth client-side interpolation. |
21
25
 
22
26
  ---
23
27
 
@@ -35,13 +39,17 @@ When requested to scaffold, modify, or review a Paper/Folia plugin codebase:
35
39
  - Block, World, or Chunk state: `RegionScheduler`.
36
40
  - Global server state or announcements: `GlobalRegionScheduler`.
37
41
  - Database, File I/O, Web Requests, or compute-heavy math: `AsyncScheduler` or `CompletableFuture`.
38
- 4. **Enforce Exploit & Dupe Prevention Protocols (Mandatory)**:
42
+ 4. **Item & Data Component Modernization**:
43
+ - Never use raw NBT manipulation. Mutate items through `DataComponentTypes` (e.g. `FOOD`, `TOOL`, `CUSTOM_MODEL_DATA`, `MAX_STACK_SIZE`).
44
+ 5. **Zero-Lag Visual Displays**:
45
+ - Replace outdated armor-stand floating holograms with `TextDisplay` and `ItemDisplay` using JOML matrix transformations.
46
+ 6. **Enforce Exploit & Dupe Prevention Protocols (Mandatory)**:
39
47
  - Validate numeric user input (reject `NaN`, `Infinity`, overflow, and negative amounts).
40
48
  - Protect inventory menus against click-spam race conditions via debounce.
41
49
  - Store `UUID` rather than `Player` instances across all caches and static collections.
42
50
  - Secure custom items using `PersistentDataContainer` (PDC) signatures.
43
- 5. **Compile & Lint Verification**:
44
- - Verify build passes clean compilation with zero deprecation warnings.
51
+ 7. **Compile & Security Audit**:
52
+ - Run `npx papermc-skill audit` to guarantee 0 vulnerabilities before releasing code.
45
53
 
46
54
  ---
47
55
 
@@ -83,12 +91,12 @@ Invoking `world.getBlockAt(x, y, z)` on ungenerated/unloaded far chunks (e.g., c
83
91
 
84
92
  ---
85
93
 
86
- ## 4. Modern Production Recipes
94
+ ## 4. Modern Overpowered (OP) Recipes
87
95
 
88
96
  ### Recipe 1: Folia-Ready Modern Manifest (`src/main/resources/paper-plugin.yml`)
89
97
  ```yaml
90
98
  name: EpicPaperPlugin
91
- version: '1.0.0'
99
+ version: '1.2.0'
92
100
  main: com.example.plugin.EpicPlugin
93
101
  bootstrapper: com.example.plugin.EpicPluginBootstrap
94
102
  api-version: '1.21'
@@ -156,53 +164,143 @@ public class EpicPluginBootstrap implements PluginBootstrap {
156
164
  }
157
165
  ```
158
166
 
159
- ### Recipe 3: Anti-Tamper PDC Item Utilities
167
+ ### Recipe 3: Modern Data Component Item Engineering (Zero-NBT)
160
168
  ```java
161
- package com.example.plugin.util;
169
+ package com.example.plugin.item;
162
170
 
163
- import net.kyori.adventure.text.minimessage.MiniMessage;
171
+ import io.papermc.paper.datacomponent.DataComponentTypes;
172
+ import io.papermc.paper.datacomponent.item.CustomModelData;
173
+ import io.papermc.paper.datacomponent.item.FoodProperties;
174
+ import io.papermc.paper.datacomponent.item.ItemLore;
175
+ import net.kyori.adventure.text.Component;
176
+ import net.kyori.adventure.text.format.NamedTextColor;
177
+ import net.kyori.adventure.text.format.TextDecoration;
164
178
  import org.bukkit.Material;
165
- import org.bukkit.NamespacedKey;
166
179
  import org.bukkit.inventory.ItemStack;
167
- import org.bukkit.persistence.PersistentDataContainer;
168
- import org.bukkit.persistence.PersistentDataType;
169
- import org.bukkit.plugin.Plugin;
170
180
 
171
- import java.util.UUID;
181
+ import java.util.List;
172
182
 
173
- public final class SafeItemUtil {
183
+ public final class ModernItemBuilder {
174
184
 
175
- private static NamespacedKey KEY_ITEM_ID;
176
- private static NamespacedKey KEY_LEVEL;
177
- private static NamespacedKey KEY_SIG;
185
+ public static ItemStack createGodtierRelic() {
186
+ ItemStack item = ItemStack.of(Material.NETHERITE_SWORD);
178
187
 
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");
188
+ // Type-safe direct component modification
189
+ item.setData(DataComponentTypes.ITEM_NAME, Component.text("Aegis of the Void", NamedTextColor.LIGHT_PURPLE, TextDecoration.BOLD));
190
+ item.setData(DataComponentTypes.LORE, ItemLore.lore(List.of(
191
+ Component.text("Ancient relic harvested from trial chambers.", NamedTextColor.GRAY),
192
+ Component.empty(),
193
+ Component.text("✦ Ability: Kinetic Shockwave on hit", NamedTextColor.AQUA)
194
+ )));
195
+
196
+ // Modern 1.20.5+ Custom Model Data (floats, strings, flags)
197
+ item.setData(DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData()
198
+ .addFloat(2.5f)
199
+ .addString("aegis_void_model")
200
+ .build()
201
+ );
202
+
203
+ // Overrides
204
+ item.setData(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true);
205
+ item.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
206
+ item.setData(DataComponentTypes.UNBREAKABLE, true);
207
+
208
+ return item;
183
209
  }
210
+ }
211
+ ```
184
212
 
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());
213
+ ### Recipe 4: Zero-Lag Display Entity Hologram Engine
214
+ ```java
215
+ package com.example.plugin.display;
216
+
217
+ import net.kyori.adventure.text.Component;
218
+ import org.bukkit.Color;
219
+ import org.bukkit.Location;
220
+ import org.bukkit.entity.Display;
221
+ import org.bukkit.entity.TextDisplay;
222
+ import org.bukkit.util.Transformation;
223
+ import org.joml.AxisAngle4f;
224
+ import org.joml.Vector3f;
225
+
226
+ public final class DisplayHologramEngine {
227
+
228
+ /** Spawns a floating holographic sign with smooth client-side interpolation */
229
+ public static TextDisplay spawnSmoothHologram(Location loc, Component text) {
230
+ return loc.getWorld().spawn(loc, TextDisplay.class, display -> {
231
+ display.text(text);
232
+ display.setBillboard(Display.Billboard.CENTER);
233
+ display.setDefaultBackground(false);
234
+ display.setBackgroundColor(Color.fromARGB(150, 20, 20, 30));
235
+ display.setShadowed(true);
236
+
237
+ // Client-side smooth interpolation (20 ticks = 1 second)
238
+ display.setInterpolationDuration(20);
239
+ display.setInterpolationDelay(0);
240
+
241
+ // Transformation via JOML
242
+ Transformation transform = new Transformation(
243
+ new Vector3f(0f, 0f, 0f),
244
+ new AxisAngle4f(0f, 0f, 1f, 0f),
245
+ new Vector3f(1.1f, 1.1f, 1.1f),
246
+ new AxisAngle4f(0f, 0f, 1f, 0f)
247
+ );
248
+ display.setTransformation(transform);
193
249
  });
194
- return item;
195
250
  }
251
+ }
252
+ ```
253
+
254
+ ### Recipe 5: 1.21 Tricky Trials Mace & Combat Physics
255
+ ```java
256
+ package com.example.plugin.combat;
257
+
258
+ import net.kyori.adventure.text.Component;
259
+ import net.kyori.adventure.text.format.NamedTextColor;
260
+ import org.bukkit.Location;
261
+ import org.bukkit.Material;
262
+ import org.bukkit.Particle;
263
+ import org.bukkit.Sound;
264
+ import org.bukkit.entity.LivingEntity;
265
+ import org.bukkit.entity.Player;
266
+ import org.bukkit.event.EventHandler;
267
+ import org.bukkit.event.EventPriority;
268
+ import org.bukkit.event.Listener;
269
+ import org.bukkit.event.entity.EntityDamageByEntityEvent;
270
+ import org.bukkit.util.Vector;
271
+
272
+ public final class MaceKineticCombatListener implements Listener {
273
+
274
+ @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
275
+ public void onMaceSmash(EntityDamageByEntityEvent event) {
276
+ if (!(event.getDamager() instanceof Player attacker)) return;
277
+ if (!(event.getEntity() instanceof LivingEntity victim)) return;
196
278
 
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);
279
+ if (attacker.getInventory().getItemInMainHand().getType() != Material.MACE) return;
280
+
281
+ float fallDistance = attacker.getFallDistance();
282
+ if (fallDistance > 1.5f) {
283
+ double extraDmg = Math.min(fallDistance * 3.5, 45.0);
284
+ event.setDamage(event.getDamage() + extraDmg);
285
+
286
+ Location loc = victim.getLocation();
287
+ loc.getWorld().spawnParticle(Particle.TRIAL_SPAWNER_DETECTION, loc.add(0, 0.5, 0), 30, 0.5, 0.2, 0.5, 0.05);
288
+ loc.getWorld().playSound(loc, Sound.ITEM_MACE_SMASH_GROUND, 1.2f, 1.0f);
289
+
290
+ // Shockwave knockback
291
+ loc.getWorld().getNearbyLivingEntities(loc, 4.5, e -> !e.equals(attacker) && !e.equals(victim))
292
+ .forEach(e -> {
293
+ Vector impulse = e.getLocation().toVector().subtract(loc.toVector()).normalize().multiply(1.3).setY(0.4);
294
+ e.setVelocity(impulse);
295
+ });
296
+
297
+ attacker.sendActionBar(Component.text("⚡ SMASH IMPACT: +" + String.format("%.1f", extraDmg) + " DMG", NamedTextColor.GOLD));
298
+ }
201
299
  }
202
300
  }
203
301
  ```
204
302
 
205
- ### Recipe 4: Anti-Dupe GUI Controller with Debounce
303
+ ### Recipe 6: Anti-Dupe GUI Controller with Debounce
206
304
  ```java
207
305
  package com.example.plugin.gui;
208
306
 
@@ -278,14 +376,12 @@ public class SafeMenuController implements InventoryHolder, Listener {
278
376
  }
279
377
 
280
378
  private void handleSlotAction(Player player, int slot) {
281
- // Handle slot action safely
379
+ // Safe slot logic
282
380
  }
283
381
  }
284
382
  ```
285
383
 
286
- ### Recipe 5: Universal Folia & Paper Scheduler Adapter
287
- Transparently dispatches execution safely whether running on standard Paper or multi-threaded Folia:
288
-
384
+ ### Recipe 7: Universal Folia & Paper Scheduler Adapter
289
385
  ```java
290
386
  package com.example.plugin.util;
291
387
 
@@ -315,7 +411,6 @@ public final class UniversalScheduler {
315
411
  return IS_FOLIA;
316
412
  }
317
413
 
318
- /** Dispatches a task to the region scheduler owning the specified entity */
319
414
  public static void runForEntity(Plugin plugin, Entity entity, Runnable runnable) {
320
415
  if (IS_FOLIA) {
321
416
  entity.getScheduler().run(plugin, task -> runnable.run(), null);
@@ -324,7 +419,6 @@ public final class UniversalScheduler {
324
419
  }
325
420
  }
326
421
 
327
- /** Dispatches a task to the region scheduler owning the specified location */
328
422
  public static void runAtLocation(Plugin plugin, Location location, Runnable runnable) {
329
423
  if (IS_FOLIA) {
330
424
  Bukkit.getRegionScheduler().execute(plugin, location, runnable);
@@ -333,7 +427,6 @@ public final class UniversalScheduler {
333
427
  }
334
428
  }
335
429
 
336
- /** Dispatches a purely asynchronous task (I/O, database, networking) */
337
430
  public static void runAsync(Plugin plugin, Runnable runnable) {
338
431
  if (IS_FOLIA) {
339
432
  Bukkit.getAsyncScheduler().runNow(plugin, task -> runnable.run());
@@ -342,7 +435,6 @@ public final class UniversalScheduler {
342
435
  }
343
436
  }
344
437
 
345
- /** Dispatches a delayed asynchronous task */
346
438
  public static void runAsyncDelayed(Plugin plugin, Runnable runnable, long delayMs) {
347
439
  if (IS_FOLIA) {
348
440
  Bukkit.getAsyncScheduler().runDelayed(plugin, task -> runnable.run(), delayMs, TimeUnit.MILLISECONDS);
@@ -354,66 +446,6 @@ public final class UniversalScheduler {
354
446
  }
355
447
  ```
356
448
 
357
- ### Recipe 6: Robust Asynchronous Database Access (HikariCP)
358
- ```java
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;
372
-
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
- }
414
- }
415
- ```
416
-
417
449
  ---
418
450
 
419
451
  ## 5. Anti-Patterns vs Modern Standards
@@ -422,12 +454,12 @@ public class DatabaseManager {
422
454
  | :--- | :--- | :--- |
423
455
  | `ChatColor.RED + "Message"` | Deprecated & breaks console/client formatting | `MiniMessage.miniMessage().deserialize("<red>Message</red>")` |
424
456
  | `public static Set<Player> list;` | Causes massive **Memory Leaks** of worlds & connections | `public static Set<UUID> list;` |
457
+ | Direct NBT manipulation (`net.minecraft.nbt`) | Hard crashes on 1.20.5+ / 1.21+ / 26.x | `ItemStack#setData(DataComponentTypes.XXX)` |
458
+ | `ArmorStand` floating text holograms | Extreme entity ticking CPU overhead | `TextDisplay` with JOML interpolated transformations |
425
459
  | `world.getChunkAt(x, z)` in a loop | Freezes main server tick thread (major TPS drops) | `world.getChunkAtAsync(x, z)` |
426
460
  | `Bukkit.getScheduler()` on Folia | Throws exception or causes thread-safety desync | Use `RegionScheduler`, `EntityScheduler`, or `UniversalScheduler` |
427
461
  | Modifying inventories asynchronously | Causes item duplication and memory corruption | Always dispatch inventory modifications to entity region thread |
428
462
  | 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
463
  | Unbounded numbers in economy commands | Susceptible to `NaN`, negative amounts, and overflow | Validate `Double.isFinite()` and enforce minimum/maximum boundaries |
432
464
 
433
465
  ---
@@ -438,10 +470,12 @@ Prior to releasing a plugin to production or public servers:
438
470
 
439
471
  - [ ] `paper-plugin.yml` is defined with `folia-supported: true`.
440
472
  - [ ] Zero references to `org.bukkit.ChatColor` across the codebase.
473
+ - [ ] Direct NBT or legacy ItemMeta serialization replaced by `DataComponentTypes`.
474
+ - [ ] Holograms and floating 3D icons use `Display` entities rather than armor stands.
441
475
  - [ ] All command registration uses modern Brigadier (`LifecycleEvents.COMMANDS`).
442
476
  - [ ] No direct storage of `Player`, `World`, or `Entity` instances in static collections.
443
477
  - [ ] All File, Database, and HTTP operations execute 100% asynchronously.
444
478
  - [ ] Custom GUIs implement `setCancelled(true)` and click debounce cooldowns.
445
479
  - [ ] Custom items are identified and verified via `PersistentDataContainer` (PDC).
446
480
  - [ ] All database queries use connection pooling enclosed in `try-with-resources`.
447
- - [ ] Plugin compiles cleanly using Gradle 9.x targeting Java 21 LTS.
481
+ - [ ] Plugin passes `npx papermc-skill audit` with zero issues.
@@ -1,2 +1,2 @@
1
- #Thu Oct 01 12:32:36 UTC 2026
1
+ #Wed Nov 11 04:17:31 UTC 2026
2
2
  gradle.version=9.7.1