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,32 +1,32 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Asynchronous Database Storage: SQLite & HikariCP in Paper
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Storing player profiles (economy balances, levels, custom inventories, quests) directly into YAML files (`config.yml` or `player.yml`) degrades performance rapidly and invites file corruption when scaling to thousands of unique users.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The industry standard for production storage is relational databases (**SQLite** for standalone single servers, or **MySQL / PostgreSQL via HikariCP** for multi-server BungeeCord/Velocity networks).
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## 1.
|
|
9
|
+
## 1. The Golden Rule of Minecraft Database Access
|
|
10
10
|
|
|
11
|
-
> **
|
|
12
|
-
>
|
|
11
|
+
> **NEVER open database connections, execute `SELECT` queries, or run `INSERT/UPDATE` statements on the main or region tick threads.**
|
|
12
|
+
> A single query taking 50ms immediately causes TPS drops, player rubberbanding, and tick watchdog warnings.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Always execute database operations via **Asynchronous Threads (`CompletableFuture`)**, and dispatch results back to the player's entity scheduler when mutating game state.
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
## 2.
|
|
18
|
+
## 2. Configuring HikariCP (High-Performance Connection Pooling)
|
|
19
19
|
|
|
20
|
-
HikariCP
|
|
20
|
+
HikariCP is the fastest connection pool in the Java ecosystem (utilized extensively by **LuckPerms**).
|
|
21
21
|
|
|
22
|
-
###
|
|
22
|
+
### Gradle Dependency:
|
|
23
23
|
```kotlin
|
|
24
24
|
dependencies {
|
|
25
25
|
implementation("com.zaxxer:HikariCP:5.1.0")
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
###
|
|
29
|
+
### Initializing the DataSource:
|
|
30
30
|
```java
|
|
31
31
|
package com.example.plugin.database;
|
|
32
32
|
|
|
@@ -46,12 +46,12 @@ public final class DatabaseManager {
|
|
|
46
46
|
config.setUsername(user);
|
|
47
47
|
config.setPassword(password);
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// Recommended HikariCP Pool Settings
|
|
50
50
|
config.setMaximumPoolSize(10);
|
|
51
51
|
config.setMinimumIdle(2);
|
|
52
|
-
config.setConnectionTimeout(10000); // 10
|
|
53
|
-
config.setIdleTimeout(600000); // 10
|
|
54
|
-
config.setMaxLifetime(1800000); // 30
|
|
52
|
+
config.setConnectionTimeout(10000); // 10 seconds
|
|
53
|
+
config.setIdleTimeout(600000); // 10 minutes
|
|
54
|
+
config.setMaxLifetime(1800000); // 30 minutes
|
|
55
55
|
config.setPoolName("PluginHikariPool");
|
|
56
56
|
|
|
57
57
|
this.dataSource = new HikariDataSource(config);
|
|
@@ -60,7 +60,7 @@ public final class DatabaseManager {
|
|
|
60
60
|
public void initSQLite(String filePath) {
|
|
61
61
|
HikariConfig config = new HikariConfig();
|
|
62
62
|
config.setJdbcUrl("jdbc:sqlite:" + filePath);
|
|
63
|
-
config.setMaximumPoolSize(1); // SQLite
|
|
63
|
+
config.setMaximumPoolSize(1); // SQLite only supports a single writer
|
|
64
64
|
config.setPoolName("PluginSQLitePool");
|
|
65
65
|
|
|
66
66
|
this.dataSource = new HikariDataSource(config);
|
|
@@ -80,9 +80,9 @@ public final class DatabaseManager {
|
|
|
80
80
|
|
|
81
81
|
---
|
|
82
82
|
|
|
83
|
-
## 3.
|
|
83
|
+
## 3. Asynchronous DAO Pattern with `CompletableFuture`
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Construct a Data Access Object (DAO) to cleanly decouple raw SQL execution from in-game mechanics:
|
|
86
86
|
|
|
87
87
|
```java
|
|
88
88
|
package com.example.plugin.database;
|
|
@@ -120,7 +120,7 @@ public final class PlayerRepository {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
//
|
|
123
|
+
// Read player balance asynchronously
|
|
124
124
|
public CompletableFuture<Integer> getCoinsAsync(UUID uuid) {
|
|
125
125
|
return CompletableFuture.supplyAsync(() -> {
|
|
126
126
|
try (Connection conn = databaseManager.getConnection();
|
|
@@ -136,17 +136,17 @@ public final class PlayerRepository {
|
|
|
136
136
|
} catch (SQLException e) {
|
|
137
137
|
e.printStackTrace();
|
|
138
138
|
}
|
|
139
|
-
return 0; // Default
|
|
139
|
+
return 0; // Default fallback
|
|
140
140
|
}, dbExecutor);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
//
|
|
143
|
+
// Persist player balance asynchronously
|
|
144
144
|
public CompletableFuture<Void> setCoinsAsync(UUID uuid, int coins) {
|
|
145
145
|
return CompletableFuture.runAsync(() -> {
|
|
146
146
|
try (Connection conn = databaseManager.getConnection();
|
|
147
147
|
PreparedStatement stmt = conn.prepareStatement(
|
|
148
148
|
"INSERT INTO player_data (uuid, coins) VALUES (?, ?) " +
|
|
149
|
-
"ON CONFLICT(uuid) DO UPDATE SET coins = excluded.coins;" // SQLite
|
|
149
|
+
"ON CONFLICT(uuid) DO UPDATE SET coins = excluded.coins;" // SQLite / PostgreSQL
|
|
150
150
|
)) {
|
|
151
151
|
stmt.setString(1, uuid.toString());
|
|
152
152
|
stmt.setInt(2, coins);
|
|
@@ -165,21 +165,21 @@ public final class PlayerRepository {
|
|
|
165
165
|
|
|
166
166
|
---
|
|
167
167
|
|
|
168
|
-
## 4.
|
|
168
|
+
## 4. Applying Query Results in Event Listeners
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
When asynchronous database queries resolve, schedule execution back to the player's entity scheduler to mutate Bukkit state:
|
|
171
171
|
|
|
172
172
|
```java
|
|
173
173
|
@EventHandler
|
|
174
174
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
|
175
175
|
Player player = event.getPlayer();
|
|
176
176
|
|
|
177
|
-
// Fetch
|
|
177
|
+
// Fetch coins asynchronously off the tick thread
|
|
178
178
|
playerRepository.getCoinsAsync(player.getUniqueId()).thenAccept(coins -> {
|
|
179
|
-
//
|
|
179
|
+
// Dispatch result safely onto the player's entity region thread
|
|
180
180
|
player.getScheduler().run(plugin, task -> {
|
|
181
181
|
player.sendActionBar(MiniMessage.miniMessage().deserialize(
|
|
182
|
-
"<gold>
|
|
182
|
+
"<gold>Balance: <yellow>" + coins + " Coins</yellow></gold>"
|
|
183
183
|
));
|
|
184
184
|
}, null);
|
|
185
185
|
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Masterclass Guide: Security & Exploit Prevention in Paper & Folia Plugins
|
|
2
|
+
|
|
3
|
+
Public Minecraft servers face hostile environments: modified clients (*hacked clients*), automated packet bots, and malicious players probing for item duplication glitches (*dupe exploits*) or server-crashing packet freezes. This masterclass guide provides authoritative architectural patterns to ensure your Paper and Folia plugins remain completely exploit-proof.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Item Duplication Prevention (Dupe Glitch Elimination)
|
|
8
|
+
|
|
9
|
+
Item duplication exploits almost universally originate from *race conditions* between inventory interface events, fast clicking, or off-thread asynchronous mutations.
|
|
10
|
+
|
|
11
|
+
### Hazard 1: Off-Thread / Asynchronous Inventory Mutation
|
|
12
|
+
```java
|
|
13
|
+
// ❌ CRITICAL HAZARD: Mutating inventory off-thread!
|
|
14
|
+
CompletableFuture.runAsync(() -> {
|
|
15
|
+
player.getInventory().removeItem(item); // Race condition! Can duplicate if clicked concurrently
|
|
16
|
+
});
|
|
17
|
+
```
|
|
18
|
+
**Absolute Rule**: Minecraft's inventory state is **not thread-safe**. Whenever querying, removing, or adding items to a player, execution **must** occur on the player's entity region thread:
|
|
19
|
+
```java
|
|
20
|
+
// ✔️ SAFE: Execute on the Entity Scheduler
|
|
21
|
+
player.getScheduler().run(plugin, task -> {
|
|
22
|
+
if (player.getInventory().containsAtLeast(item, requiredAmount)) {
|
|
23
|
+
player.getInventory().removeItem(item);
|
|
24
|
+
// Proceed with transaction
|
|
25
|
+
}
|
|
26
|
+
}, null);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Hazard 2: Inventory Drag & Fast-Click Race Conditions
|
|
30
|
+
Cheating clients can emit rapid click or drag packets to pick up an item before cancellation handlers finish evaluating.
|
|
31
|
+
```java
|
|
32
|
+
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
|
33
|
+
public void onInventoryClick(InventoryClickEvent event) {
|
|
34
|
+
if (!(event.getInventory().getHolder() instanceof CustomMenuHolder)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 1. Cancel the event immediately
|
|
39
|
+
event.setCancelled(true);
|
|
40
|
+
|
|
41
|
+
// 2. Prevent fast-click exploits via per-player debounce
|
|
42
|
+
Player player = (Player) event.getWhoClicked();
|
|
43
|
+
if (!DebounceManager.canClick(player.getUniqueId(), 200)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Process only top menu slot actions
|
|
48
|
+
if (event.getClickedInventory() == event.getInventory()) {
|
|
49
|
+
processMenuButton(player, event.getSlot());
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@EventHandler
|
|
54
|
+
public void onInventoryDrag(InventoryDragEvent event) {
|
|
55
|
+
if (event.getInventory().getHolder() instanceof CustomMenuHolder) {
|
|
56
|
+
event.setCancelled(true); // Always cancel drag events in custom menus
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 2. Monetary Safety & Numeric Bounds Validation
|
|
64
|
+
|
|
65
|
+
Floating-point precision errors and lack of numeric validation are among the most common causes of server economic collapse.
|
|
66
|
+
|
|
67
|
+
### Exploitation Vectors: `NaN`, `Infinity`, and Negative Numbers
|
|
68
|
+
Malicious players enter unexpected inputs in payment or shop commands, such as:
|
|
69
|
+
- `/pay Alice -1000000` (deducts money from the receiver and credits the sender)
|
|
70
|
+
- `/pay Alice NaN` (corrupts player balance to `NaN` permanently, breaking database constraints)
|
|
71
|
+
- `/pay Alice 99999999999999999999` (overflows 64-bit integers, wrapping amounts into negatives)
|
|
72
|
+
|
|
73
|
+
### 🟢 Solution: Strict Numeric Bounds Validator
|
|
74
|
+
```java
|
|
75
|
+
public final class EconomyValidator {
|
|
76
|
+
|
|
77
|
+
private static final double MIN_TRANSACTION = 0.01;
|
|
78
|
+
private static final double MAX_TRANSACTION = 1_000_000_000.0;
|
|
79
|
+
|
|
80
|
+
public static double validateAmount(double amount) throws IllegalArgumentException {
|
|
81
|
+
if (!Double.isFinite(amount)) {
|
|
82
|
+
throw new IllegalArgumentException("Invalid monetary amount (NaN or Infinity)!");
|
|
83
|
+
}
|
|
84
|
+
if (amount < MIN_TRANSACTION) {
|
|
85
|
+
throw new IllegalArgumentException("Minimum transaction amount is $" + MIN_TRANSACTION);
|
|
86
|
+
}
|
|
87
|
+
if (amount > MAX_TRANSACTION) {
|
|
88
|
+
throw new IllegalArgumentException("Transaction amount exceeds allowable limit!");
|
|
89
|
+
}
|
|
90
|
+
// Round to 2 decimal places (cents)
|
|
91
|
+
return Math.round(amount * 100.0) / 100.0;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 3. PersistentDataContainer (PDC) Anti-Tamper & Cryptographic Hashing
|
|
99
|
+
|
|
100
|
+
Never rely on an item's `DisplayName` or `Lore` to determine whether an item possesses custom abilities or rare server tiers.
|
|
101
|
+
|
|
102
|
+
### 🔴 The Vulnerability
|
|
103
|
+
Players can rename normal items using anvils or packet injection to mimic custom gear titles and lore text.
|
|
104
|
+
|
|
105
|
+
### 🟢 Solution: PDC Verification with Cryptographic Salt
|
|
106
|
+
Store immutable identifiers and cryptographic signatures inside the item's PDC:
|
|
107
|
+
```java
|
|
108
|
+
public class SecureItemManager {
|
|
109
|
+
private static NamespacedKey KEY_ID;
|
|
110
|
+
private static NamespacedKey KEY_HASH;
|
|
111
|
+
private static final String SERVER_SALT = "MySecretSaltKey_2026";
|
|
112
|
+
|
|
113
|
+
public static void init(Plugin plugin) {
|
|
114
|
+
KEY_ID = new NamespacedKey(plugin, "custom_id");
|
|
115
|
+
KEY_HASH = new NamespacedKey(plugin, "integrity_hash");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
public static void signItem(ItemStack item, String customId) {
|
|
119
|
+
item.editMeta(meta -> {
|
|
120
|
+
String hash = generateSignature(customId, SERVER_SALT);
|
|
121
|
+
PersistentDataContainer pdc = meta.getPersistentDataContainer();
|
|
122
|
+
pdc.set(KEY_ID, PersistentDataType.STRING, customId);
|
|
123
|
+
pdc.set(KEY_HASH, PersistentDataType.STRING, hash);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public static boolean verifyItem(ItemStack item, String expectedId) {
|
|
128
|
+
if (item == null || !item.hasItemMeta()) return false;
|
|
129
|
+
PersistentDataContainer pdc = item.getItemMeta().getPersistentDataContainer();
|
|
130
|
+
String id = pdc.get(KEY_ID, PersistentDataType.STRING);
|
|
131
|
+
String hash = pdc.get(KEY_HASH, PersistentDataType.STRING);
|
|
132
|
+
|
|
133
|
+
if (id == null || hash == null || !id.equals(expectedId)) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return hash.equals(generateSignature(id, SERVER_SALT));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private static String generateSignature(String data, String salt) {
|
|
140
|
+
return Integer.toHexString((data + salt).hashCode());
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## 4. Mitigating Denial of Service (DoS) from Synchronous Chunk Freezes
|
|
148
|
+
|
|
149
|
+
If a plugin allows teleportation to arbitrary coordinates (e.g. random teleport or `/tppos`), malicious players can request far-flung coordinates (like 30,000,000) to freeze the server main thread while loading dozens of chunks from disk.
|
|
150
|
+
|
|
151
|
+
### 🟢 Solution: Asynchronous Chunk Teleportation
|
|
152
|
+
```java
|
|
153
|
+
public static CompletableFuture<Boolean> safeTeleportAsync(Player player, Location target) {
|
|
154
|
+
World world = target.getWorld();
|
|
155
|
+
int chunkX = target.getBlockX() >> 4;
|
|
156
|
+
int chunkZ = target.getBlockZ() >> 4;
|
|
157
|
+
|
|
158
|
+
// 1. Load target chunk asynchronously without blocking game ticks
|
|
159
|
+
return world.getChunkAtAsync(chunkX, chunkZ, true).thenCompose(chunk -> {
|
|
160
|
+
// 2. Once chunk is loaded, execute teleport on player entity scheduler
|
|
161
|
+
CompletableFuture<Boolean> tpResult = new CompletableFuture<>();
|
|
162
|
+
player.getScheduler().run(plugin, task -> {
|
|
163
|
+
boolean success = player.teleport(target);
|
|
164
|
+
tpResult.complete(success);
|
|
165
|
+
}, null);
|
|
166
|
+
return tpResult;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## 5. Preventing SQL Injection & Connection Pool Starvation
|
|
174
|
+
|
|
175
|
+
### 🔴 The SQL Injection Vulnerability
|
|
176
|
+
```java
|
|
177
|
+
// CRITICAL HAZARD: SQL Injection if input contains quotes
|
|
178
|
+
String sql = "SELECT * FROM users WHERE name = '" + playerName + "'";
|
|
179
|
+
statement.executeQuery(sql);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### 🟢 Solution: PreparedStatements & HikariCP Pool
|
|
183
|
+
```java
|
|
184
|
+
String sql = "SELECT * FROM users WHERE uuid = ?";
|
|
185
|
+
try (Connection conn = dataSource.getConnection();
|
|
186
|
+
PreparedStatement stmt = conn.prepareStatement(sql)) {
|
|
187
|
+
stmt.setString(1, playerUuid.toString());
|
|
188
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
189
|
+
if (rs.next()) {
|
|
190
|
+
// Process record safely
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (SQLException e) {
|
|
194
|
+
logger.severe("Database error: " + e.getMessage());
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 6. Eliminating Memory Leaks from Lingering Bukkit References
|
|
201
|
+
|
|
202
|
+
Holding references to `Player`, `World`, or `Chunk` instances in static collections prevents the JVM Garbage Collector from freeing gigabytes of world and network connection data:
|
|
203
|
+
|
|
204
|
+
### 🔴 Memory Leak Pattern
|
|
205
|
+
```java
|
|
206
|
+
public static Map<Player, Long> lastAction = new HashMap<>(); // LEAK: Player never garbage collected!
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### 🟢 Clean Architecture
|
|
210
|
+
```java
|
|
211
|
+
// 1. Store UUID primitives only
|
|
212
|
+
public static Map<UUID, Long> lastAction = new ConcurrentHashMap<>();
|
|
213
|
+
|
|
214
|
+
// 2. Invalidate on PlayerQuitEvent
|
|
215
|
+
@EventHandler
|
|
216
|
+
public void onQuit(PlayerQuitEvent event) {
|
|
217
|
+
lastAction.remove(event.getPlayer().getUniqueId());
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 7. The Exploit-Proof Verification Checklist
|
|
224
|
+
|
|
225
|
+
- [x] **Zero asynchronous player inventory modifications.**
|
|
226
|
+
- [x] **Inventory click debounce (150-200ms) enforced.**
|
|
227
|
+
- [x] **Monetary inputs validated against `!Double.isFinite()` and bounds.**
|
|
228
|
+
- [x] **Custom items identified via PDC rather than display lore.**
|
|
229
|
+
- [x] **Asynchronous chunk loading used for all dynamic coordinates.**
|
|
230
|
+
- [x] **PreparedStatements used for all database queries.**
|
|
231
|
+
- [x] **UUIDs stored rather than `Player` instances in static caches.**
|
|
232
|
+
- [x] **Player caches invalidated on `PlayerQuitEvent`.**
|
|
233
|
+
- [x] **`folia-supported: true` declared with proper regional scheduling.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "papermc-skill",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Interactive CLI and architectural development skill for modern Paper & Folia Minecraft Java plugins",
|
|
5
5
|
"main": "bin/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"templates",
|
|
13
13
|
"docs",
|
|
14
14
|
"skills",
|
|
15
|
+
"assets",
|
|
15
16
|
"README.md",
|
|
16
17
|
"CHANGELOG.md",
|
|
17
18
|
"LICENSE"
|