papermc-skill 1.0.1 → 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.
- package/.cursor/rules/paper-plugin-dev.mdc +23 -0
- package/.cursorrules +6 -0
- package/.paseo/rules.md +8 -0
- package/.pi/rules.md +8 -0
- package/.windsurfrules +7 -0
- package/CHANGELOG.md +50 -0
- package/CLAUDE.md +27 -0
- package/CODEX.md +12 -0
- package/DEEPSEEK.md +18 -0
- package/README.md +73 -33
- package/assets/banner-animated.svg +360 -0
- package/assets/banner.svg +360 -0
- package/bin/cli.js +565 -35
- 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/docs/12-data-components-and-display-entities.md +264 -0
- package/docs/13-multi-agent-system-and-tooling.md +90 -0
- package/package.json +17 -4
- package/skills/paper-plugin-dev/SKILL.md +425 -114
- 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.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.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.7.1/fileHashes/resourceHashesCache.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 +1 -1
- 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 +7 -7
- 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
|
@@ -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.**
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# 12. Modern Data Components & Display Entities (MC 1.20.5 — 26.x)
|
|
2
|
+
|
|
3
|
+
## 📌 Paradigm Shift: From Legacy NBT & ItemMeta to Data Components
|
|
4
|
+
|
|
5
|
+
Beginning with **Minecraft 1.20.5** and continuing through **Minecraft 1.21+ and 26.x**, Mojang completely overhauled the item stack architecture.
|
|
6
|
+
- **Legacy NBT tags on ItemStacks are gone**.
|
|
7
|
+
- In their place is the **Data Component System**: a strictly-typed, immutable, performant, and schema-validated component pipeline.
|
|
8
|
+
- Paper exposes this via `io.papermc.paper.datacomponent.DataComponentTypes` and modern item manipulation methods directly on `ItemStack`.
|
|
9
|
+
|
|
10
|
+
> [!IMPORTANT]
|
|
11
|
+
> Never use legacy `net.minecraft.nbt` or reflection to modify item properties in modern Paper plugins. Use `ItemStack#setData(DataComponentType, Object)` or `ItemStack#getData(DataComponentType)`.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🛠️ 1. Modern Data Components in Action
|
|
16
|
+
|
|
17
|
+
### A. Modifying Data Components Directly on ItemStack
|
|
18
|
+
Paper allows direct mutation of components without cloning `ItemMeta`:
|
|
19
|
+
|
|
20
|
+
```java
|
|
21
|
+
package com.example.paperplugin.item;
|
|
22
|
+
|
|
23
|
+
import io.papermc.paper.datacomponent.DataComponentTypes;
|
|
24
|
+
import io.papermc.paper.datacomponent.item.CustomModelData;
|
|
25
|
+
import io.papermc.paper.datacomponent.item.FoodProperties;
|
|
26
|
+
import io.papermc.paper.datacomponent.item.ItemLore;
|
|
27
|
+
import io.papermc.paper.datacomponent.item.Tool;
|
|
28
|
+
import net.kyori.adventure.text.Component;
|
|
29
|
+
import net.kyori.adventure.text.format.NamedTextColor;
|
|
30
|
+
import net.kyori.adventure.text.format.TextDecoration;
|
|
31
|
+
import org.bukkit.Material;
|
|
32
|
+
import org.bukkit.inventory.ItemStack;
|
|
33
|
+
|
|
34
|
+
import java.util.List;
|
|
35
|
+
|
|
36
|
+
public final class ModernItemFactory {
|
|
37
|
+
|
|
38
|
+
public static ItemStack createMythicBlade() {
|
|
39
|
+
ItemStack blade = ItemStack.of(Material.NETHERITE_SWORD);
|
|
40
|
+
|
|
41
|
+
// 1. Direct Item Name (styled with MiniMessage/Adventure)
|
|
42
|
+
blade.setData(DataComponentTypes.ITEM_NAME, Component.text("Voidreaver Blade", NamedTextColor.LIGHT_PURPLE, TextDecoration.BOLD));
|
|
43
|
+
|
|
44
|
+
// 2. Direct Item Lore
|
|
45
|
+
blade.setData(DataComponentTypes.LORE, ItemLore.lore(List.of(
|
|
46
|
+
Component.text("Forged in the heart of the End dimension.", NamedTextColor.GRAY),
|
|
47
|
+
Component.empty(),
|
|
48
|
+
Component.text("⚡ Right-click to discharge void kinetic energy.", NamedTextColor.DARK_PURPLE)
|
|
49
|
+
)));
|
|
50
|
+
|
|
51
|
+
// 3. Custom Model Data (Modern float/flag/string/int support)
|
|
52
|
+
blade.setData(DataComponentTypes.CUSTOM_MODEL_DATA, CustomModelData.customModelData()
|
|
53
|
+
.addFloat(1.5f)
|
|
54
|
+
.addFlag(true)
|
|
55
|
+
.addString("mythic_blade_tier_3")
|
|
56
|
+
.build()
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// 4. Custom Max Stack Size
|
|
60
|
+
blade.setData(DataComponentTypes.MAX_STACK_SIZE, 1);
|
|
61
|
+
|
|
62
|
+
// 5. Enchantment Glint Override (Glow without dummy enchantments!)
|
|
63
|
+
blade.setData(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE, true);
|
|
64
|
+
|
|
65
|
+
// 6. Unbreakable Flag
|
|
66
|
+
blade.setData(DataComponentTypes.UNBREAKABLE, true);
|
|
67
|
+
|
|
68
|
+
return blade;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public static ItemStack createEnchantedGoldenBerry() {
|
|
72
|
+
ItemStack berry = ItemStack.of(Material.SWEET_BERRIES);
|
|
73
|
+
|
|
74
|
+
// Set custom edible Food Properties on ANY item!
|
|
75
|
+
bladeLikeFood(berry);
|
|
76
|
+
|
|
77
|
+
return berry;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static void bladeLikeFood(ItemStack item) {
|
|
81
|
+
item.setData(DataComponentTypes.FOOD, FoodProperties.food()
|
|
82
|
+
.nutrition(6)
|
|
83
|
+
.saturation(10.0f)
|
|
84
|
+
.canAlwaysEat() // Can eat even when hunger bar is full
|
|
85
|
+
.build()
|
|
86
|
+
);
|
|
87
|
+
item.setData(DataComponentTypes.ITEM_NAME, Component.text("Aetheria Nectar Berry", NamedTextColor.GOLD));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 🎭 2. Display Entities: Zero-Lag Visual FX & Holograms
|
|
95
|
+
|
|
96
|
+
In legacy plugins, floating text and custom visuals required invisible `ArmorStand` entities.
|
|
97
|
+
**Armor stands are notoriously laggy**: they tick gravity, collisions, equipment slots, and poses every server tick.
|
|
98
|
+
|
|
99
|
+
**Display Entities** (`TextDisplay`, `ItemDisplay`, `BlockDisplay`) introduced in modern Minecraft are:
|
|
100
|
+
1. **Zero-Tick Lag**: Display entities do not have AI or physics ticking loops.
|
|
101
|
+
2. **Smooth Interpolation**: The Minecraft client natively interpolates translation, rotation, and scale at full client framerate (60-240 FPS).
|
|
102
|
+
3. **Billboarding**: Automatic camera tracking (`CENTER`, `VERTICAL`, `HORIZONTAL`).
|
|
103
|
+
|
|
104
|
+
### Smooth Matrix Transformations (`org.joml.Matrix4f`)
|
|
105
|
+
Paper integrates the high-performance **JOML** (Java OpenGL Math Library) for 3D transformations.
|
|
106
|
+
|
|
107
|
+
```java
|
|
108
|
+
package com.example.paperplugin.display;
|
|
109
|
+
|
|
110
|
+
import net.kyori.adventure.text.Component;
|
|
111
|
+
import net.kyori.adventure.text.format.NamedTextColor;
|
|
112
|
+
import net.kyori.adventure.text.format.TextDecoration;
|
|
113
|
+
import org.bukkit.Color;
|
|
114
|
+
import org.bukkit.Location;
|
|
115
|
+
import org.bukkit.Material;
|
|
116
|
+
import org.bukkit.entity.Display;
|
|
117
|
+
import org.bukkit.entity.ItemDisplay;
|
|
118
|
+
import org.bukkit.entity.TextDisplay;
|
|
119
|
+
import org.bukkit.inventory.ItemStack;
|
|
120
|
+
import org.bukkit.util.Transformation;
|
|
121
|
+
import org.joml.AxisAngle4f;
|
|
122
|
+
import org.joml.Vector3f;
|
|
123
|
+
|
|
124
|
+
public final class HologramManager {
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Spawns a floating holographic sign with smooth client-side interpolation.
|
|
128
|
+
*/
|
|
129
|
+
public static TextDisplay spawnFloatingTitle(Location loc, Component text) {
|
|
130
|
+
return loc.getWorld().spawn(loc, TextDisplay.class, display -> {
|
|
131
|
+
display.text(text);
|
|
132
|
+
display.setBillboard(Display.Billboard.CENTER); // Always faces player
|
|
133
|
+
display.setDefaultBackground(false);
|
|
134
|
+
display.setBackgroundColor(Color.fromARGB(140, 15, 15, 25));
|
|
135
|
+
display.setShadowed(true);
|
|
136
|
+
display.setSeeThrough(false);
|
|
137
|
+
|
|
138
|
+
// Display view distance
|
|
139
|
+
display.setViewRange(0.8f);
|
|
140
|
+
|
|
141
|
+
// Initial scale & position transformation
|
|
142
|
+
Transformation transform = new Transformation(
|
|
143
|
+
new Vector3f(0f, 0f, 0f), // Translation
|
|
144
|
+
new AxisAngle4f(0f, 0f, 1f, 0f), // Left rotation
|
|
145
|
+
new Vector3f(1.2f, 1.2f, 1.2f), // Scale (120%)
|
|
146
|
+
new AxisAngle4f(0f, 0f, 1f, 0f) // Right rotation
|
|
147
|
+
);
|
|
148
|
+
display.setTransformation(transform);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Spawns a spinning, floating 3D item icon above a pedestal.
|
|
154
|
+
*/
|
|
155
|
+
public static ItemDisplay spawnRotatingPedestalItem(Location loc, ItemStack item) {
|
|
156
|
+
return loc.getWorld().spawn(loc, ItemDisplay.class, display -> {
|
|
157
|
+
display.setItemStack(item);
|
|
158
|
+
display.setItemDisplayTransform(ItemDisplay.ItemDisplayTransform.GROUND);
|
|
159
|
+
display.setBillboard(Display.Billboard.FIXED);
|
|
160
|
+
|
|
161
|
+
// Set up client-side 20-tick smooth interpolation
|
|
162
|
+
display.setInterpolationDuration(20);
|
|
163
|
+
display.setInterpolationDelay(0);
|
|
164
|
+
|
|
165
|
+
// Initial transformation
|
|
166
|
+
Transformation initial = new Transformation(
|
|
167
|
+
new Vector3f(0f, 0.5f, 0f),
|
|
168
|
+
new AxisAngle4f(0f, 0f, 1f, 0f),
|
|
169
|
+
new Vector3f(1.5f, 1.5f, 1.5f),
|
|
170
|
+
new AxisAngle4f(0f, 0f, 1f, 0f)
|
|
171
|
+
);
|
|
172
|
+
display.setTransformation(initial);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Smoothly rotates the display entity over time using client interpolation.
|
|
178
|
+
*/
|
|
179
|
+
public static void rotateDisplaySmoothly(Display display, float angleRadians, int durationTicks) {
|
|
180
|
+
display.setInterpolationDuration(durationTicks);
|
|
181
|
+
display.setInterpolationDelay(0);
|
|
182
|
+
|
|
183
|
+
Transformation current = display.getTransformation();
|
|
184
|
+
Transformation next = new Transformation(
|
|
185
|
+
current.getTranslation(),
|
|
186
|
+
new AxisAngle4f(angleRadians, 0f, 1f, 0f), // Rotate around Y axis
|
|
187
|
+
current.getScale(),
|
|
188
|
+
current.getRightRotation()
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
display.setTransformation(next);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## ⚡ 3. 1.21 Tricky Trials & Mace Combat Kinematics
|
|
199
|
+
|
|
200
|
+
Minecraft 1.21 introduced the **Mace**, **Wind Charges**, and dynamic physics combat mechanics.
|
|
201
|
+
|
|
202
|
+
### Mace Kinetic Smash Mechanics
|
|
203
|
+
The Mace scales damage dynamically based on fall distance, resetting the attacker's fall distance upon a successful hit:
|
|
204
|
+
|
|
205
|
+
```java
|
|
206
|
+
package com.example.paperplugin.combat;
|
|
207
|
+
|
|
208
|
+
import net.kyori.adventure.text.Component;
|
|
209
|
+
import net.kyori.adventure.text.format.NamedTextColor;
|
|
210
|
+
import org.bukkit.Location;
|
|
211
|
+
import org.bukkit.Material;
|
|
212
|
+
import org.bukkit.Particle;
|
|
213
|
+
import org.bukkit.Sound;
|
|
214
|
+
import org.bukkit.entity.LivingEntity;
|
|
215
|
+
import org.bukkit.entity.Player;
|
|
216
|
+
import org.bukkit.event.EventHandler;
|
|
217
|
+
import org.bukkit.event.EventPriority;
|
|
218
|
+
import org.bukkit.event.Listener;
|
|
219
|
+
import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
|
220
|
+
import org.bukkit.util.Vector;
|
|
221
|
+
|
|
222
|
+
public final class ModernCombatListener implements Listener {
|
|
223
|
+
|
|
224
|
+
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
|
|
225
|
+
public void onMaceSmash(EntityDamageByEntityEvent event) {
|
|
226
|
+
if (!(event.getDamager() instanceof Player attacker)) return;
|
|
227
|
+
if (!(event.getEntity() instanceof LivingEntity victim)) return;
|
|
228
|
+
|
|
229
|
+
// Verify weapon
|
|
230
|
+
if (attacker.getInventory().getItemInMainHand().getType() != Material.MACE) return;
|
|
231
|
+
|
|
232
|
+
float fallDistance = attacker.getFallDistance();
|
|
233
|
+
if (fallDistance > 1.5f) {
|
|
234
|
+
// Smash attack succeeded!
|
|
235
|
+
double bonusDamage = Math.min(fallDistance * 3.5, 40.0); // Safe numeric cap
|
|
236
|
+
event.setDamage(event.getDamage() + bonusDamage);
|
|
237
|
+
|
|
238
|
+
Location hitLoc = victim.getLocation();
|
|
239
|
+
|
|
240
|
+
// Spawn modern Tricky Trials visual & acoustic feedback
|
|
241
|
+
hitLoc.getWorld().spawnParticle(Particle.TRIAL_SPAWNER_DETECTION, hitLoc.add(0, 0.5, 0), 25, 0.4, 0.2, 0.4, 0.05);
|
|
242
|
+
hitLoc.getWorld().playSound(hitLoc, Sound.ITEM_MACE_SMASH_GROUND, 1.2f, 1.0f);
|
|
243
|
+
|
|
244
|
+
// Produce kinetic radial shockwave knockback on nearby hostile entities
|
|
245
|
+
hitLoc.getWorld().getNearbyLivingEntities(hitLoc, 4.0, e -> !e.equals(attacker) && !e.equals(victim))
|
|
246
|
+
.forEach(nearby -> {
|
|
247
|
+
Vector impulse = nearby.getLocation().toVector().subtract(hitLoc.toVector()).normalize().multiply(1.2).setY(0.4);
|
|
248
|
+
nearby.setVelocity(impulse);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
attacker.sendActionBar(Component.text("⚡ KINETIC SMASH: +" + String.format("%.1f", bonusDamage) + " DMG", NamedTextColor.GOLD));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## 🛡️ Best Practices & Gotchas
|
|
260
|
+
|
|
261
|
+
1. **Never Cast Data Components**: Always check `itemStack.hasData(DataComponentTypes.XXX)` before reading `itemStack.getData(DataComponentTypes.XXX)`.
|
|
262
|
+
2. **Clean Display Entity Lifecycles**: When removing holograms, always invoke `display.remove()` and clean up stored entity UUIDs. Never store `TextDisplay` references in static collections.
|
|
263
|
+
3. **JOML Thread Safety**: `Matrix4f`, `Vector3f`, and `Transformation` are value objects. Create them locally on demand rather than sharing mutable instances across async threads.
|
|
264
|
+
4. **Folia Compatibility**: Always execute `world.spawn(loc, Display.class, ...)` on the Folia region scheduler corresponding to `loc`.
|