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.
- package/CHANGELOG.md +41 -0
- package/README.md +90 -110
- package/assets/banner-animated.svg +360 -0
- package/assets/banner.svg +360 -0
- package/bin/cli.js +199 -0
- package/docs/01-modern-paper-architecture.md +48 -51
- package/docs/02-adventure-and-minimessage.md +48 -52
- package/docs/03-persistent-data-container.md +39 -39
- package/docs/04-commands-and-brigadier.md +32 -35
- package/docs/05-concurrency-and-folia.md +44 -44
- package/docs/06-events-and-listeners.md +30 -31
- package/docs/07-best-practices-and-perf.md +39 -40
- package/docs/08-gui-and-menus.md +34 -37
- package/docs/09-custom-items-and-combat.md +25 -33
- package/docs/10-database-and-storage.md +27 -27
- package/docs/11-security-and-exploit-prevention.md +233 -0
- package/package.json +2 -1
- package/skills/paper-plugin-dev/SKILL.md +391 -111
- package/templates/paper-modern-template/.gradle/9.7.1/checksums/checksums.lock +0 -0
- package/templates/paper-modern-template/.gradle/9.7.1/checksums/md5-checksums.bin +0 -0
- package/templates/paper-modern-template/.gradle/9.7.1/checksums/sha1-checksums.bin +0 -0
- package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/executionHistory/executionHistory.bin +0 -0
- package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.lock +0 -0
- package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/fileHashes/fileHashes.bin +0 -0
- package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.lock +0 -0
- package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/fileHashes/resourceHashesCache.bin +0 -0
- 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
- package/templates/paper-modern-template/.gradle/9.7.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock +0 -0
- package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +2 -2
- package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/ExamplePaperPlugin.class +0 -0
- package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/PaperPluginBootstrap.class +0 -0
- package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/command/HelloCommand.class +0 -0
- package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/listener/PlayerEventListener.class +0 -0
- package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/util/SchedulerAdapter.class +0 -0
- package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
- package/templates/paper-modern-template/build.gradle.kts +10 -10
- package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/templates/paper-modern-template/gradlew +3 -3
- package/templates/paper-modern-template/gradlew.bat +12 -23
- package/templates/paper-modern-template/settings.gradle.kts +7 -0
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +6 -6
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +2 -2
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +2 -4
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +2 -2
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +6 -7
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +6 -6
- package/templates/paper-modern-template/src/main/resources/config.yml +5 -5
- package/templates/paper-modern-template/.gradle/9.4.1/checksums/checksums.lock +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/checksums/sha1-checksums.bin +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/executionHistory/executionHistory.lock +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.lock +0 -0
- package/templates/paper-modern-template/.gradle/file-system.probe +0 -0
- /package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/fileChanges/last-build.bin +0 -0
- /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
|
|
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
|
-
|
|
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.
|
|
12
|
+
## 1. Environment Prerequisites & Compatibility Matrix
|
|
13
13
|
|
|
14
|
-
|
|
|
15
|
-
| :--- | :--- | :--- |
|
|
16
|
-
| **Java SDK** | **Java 21 (LTS)**
|
|
17
|
-
| **
|
|
18
|
-
| **Paper API** | `
|
|
19
|
-
| **
|
|
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
|
-
##
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
##
|
|
86
|
+
## 4. Modern Production Recipes
|
|
45
87
|
|
|
46
|
-
###
|
|
88
|
+
### Recipe 1: Folia-Ready Modern Manifest (`src/main/resources/paper-plugin.yml`)
|
|
47
89
|
```yaml
|
|
48
|
-
name:
|
|
90
|
+
name: EpicPaperPlugin
|
|
49
91
|
version: '1.0.0'
|
|
50
|
-
main: com.example.plugin.
|
|
51
|
-
bootstrapper: com.example.plugin.
|
|
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
|
-
###
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
###
|
|
159
|
+
### Recipe 3: Anti-Tamper PDC Item Utilities
|
|
87
160
|
```java
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
###
|
|
205
|
+
### Recipe 4: Anti-Dupe GUI Controller with Debounce
|
|
97
206
|
```java
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
###
|
|
357
|
+
### Recipe 6: Robust Asynchronous Database Access (HikariCP)
|
|
133
358
|
```java
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
##
|
|
419
|
+
## 5. Anti-Patterns vs Modern Standards
|
|
146
420
|
|
|
147
|
-
|
|
|
148
|
-
| :--- | :--- |
|
|
149
|
-
| `ChatColor.RED + "
|
|
150
|
-
| `public static Set<Player>
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
|
|
|
154
|
-
|
|
|
155
|
-
|
|
|
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
|
-
##
|
|
435
|
+
## 6. Pre-Release Security Audit Checklist
|
|
436
|
+
|
|
437
|
+
Prior to releasing a plugin to production or public servers:
|
|
160
438
|
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
165
|
-
-
|
|
166
|
-
-
|
|
167
|
-
-
|
|
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.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/templates/paper-modern-template/.gradle/{9.4.1 → 9.7.1}/fileHashes/resourceHashesCache.bin
RENAMED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#
|
|
2
|
-
gradle.version=9.
|
|
1
|
+
#Thu Oct 01 12:32:36 UTC 2026
|
|
2
|
+
gradle.version=9.7.1
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|