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.
Files changed (53) 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 +50 -0
  7. package/CLAUDE.md +27 -0
  8. package/CODEX.md +12 -0
  9. package/DEEPSEEK.md +18 -0
  10. package/README.md +73 -33
  11. package/assets/banner-animated.svg +360 -0
  12. package/assets/banner.svg +360 -0
  13. package/bin/cli.js +565 -35
  14. package/docs/01-modern-paper-architecture.md +48 -51
  15. package/docs/02-adventure-and-minimessage.md +48 -52
  16. package/docs/03-persistent-data-container.md +39 -39
  17. package/docs/04-commands-and-brigadier.md +32 -35
  18. package/docs/05-concurrency-and-folia.md +44 -44
  19. package/docs/06-events-and-listeners.md +30 -31
  20. package/docs/07-best-practices-and-perf.md +39 -40
  21. package/docs/08-gui-and-menus.md +34 -37
  22. package/docs/09-custom-items-and-combat.md +25 -33
  23. package/docs/10-database-and-storage.md +27 -27
  24. package/docs/11-security-and-exploit-prevention.md +233 -0
  25. package/docs/12-data-components-and-display-entities.md +264 -0
  26. package/docs/13-multi-agent-system-and-tooling.md +90 -0
  27. package/package.json +17 -4
  28. package/skills/paper-plugin-dev/SKILL.md +425 -114
  29. package/templates/paper-modern-template/.gradle/9.7.1/checksums/checksums.lock +0 -0
  30. package/templates/paper-modern-template/.gradle/9.7.1/checksums/md5-checksums.bin +0 -0
  31. package/templates/paper-modern-template/.gradle/9.7.1/checksums/sha1-checksums.bin +0 -0
  32. package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.bin +0 -0
  33. package/templates/paper-modern-template/.gradle/9.7.1/executionHistory/executionHistory.lock +0 -0
  34. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.bin +0 -0
  35. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/fileHashes.lock +0 -0
  36. package/templates/paper-modern-template/.gradle/9.7.1/fileHashes/resourceHashesCache.bin +0 -0
  37. package/templates/paper-modern-template/.gradle/9.7.1/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock +0 -0
  38. package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  39. package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +1 -1
  40. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/ExamplePaperPlugin.class +0 -0
  41. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/PaperPluginBootstrap.class +0 -0
  42. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/command/HelloCommand.class +0 -0
  43. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/listener/PlayerEventListener.class +0 -0
  44. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/util/SchedulerAdapter.class +0 -0
  45. package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
  46. package/templates/paper-modern-template/build.gradle.kts +7 -7
  47. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +6 -6
  48. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +2 -2
  49. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +2 -4
  50. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +2 -2
  51. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +6 -7
  52. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +6 -6
  53. package/templates/paper-modern-template/src/main/resources/config.yml +5 -5
@@ -1,32 +1,32 @@
1
- # Penyimpanan Database Asinkron: SQLite & HikariCP di Paper
1
+ # Asynchronous Database Storage: SQLite & HikariCP in Paper
2
2
 
3
- Menyimpan data pemain (uang/ekonomi, statistik level, inventaris kustom, quest) langsung ke file YAML (`config.yml` atau `player.yml`) sering kali menjadi lambat dan berisiko korup jika server memiliki ribuan pemain unik.
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
- Standar industri untuk penyimpanan data di plugin produksi adalah menggunakan database relasional (seperti **SQLite** untuk server mandiri, atau **MySQL/PostgreSQL via HikariCP** untuk jaringan multi-server/BungeeCord).
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. Aturan Emas Database di Minecraft
9
+ ## 1. The Golden Rule of Minecraft Database Access
10
10
 
11
- > **DILARANG KERAS menjalankan koneksi database, query `SELECT`, atau `INSERT/UPDATE` di main/region tick thread.**
12
- > Query yang memakan waktu 50ms akan langsung menyebabkan TPS server turun dan pemain mengalami lag atau rollback.
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
- Gunakan selalu **Asynchronous Execution (`CompletableFuture`)** yang me-return hasil ke thread region pemain saat selesai.
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. Menyiapkan HikariCP (Connection Pooling)
18
+ ## 2. Configuring HikariCP (High-Performance Connection Pooling)
19
19
 
20
- HikariCP adalah library connection pool tercepat di ekosistem Java (digunakan secara ekstensif oleh **LuckPerms**).
20
+ HikariCP is the fastest connection pool in the Java ecosystem (utilized extensively by **LuckPerms**).
21
21
 
22
- ### Dependensi Gradle:
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
- ### Inisialisasi DataSource:
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
- // Pengaturan Pool HikariCP yang Direkomendasikan
49
+ // Recommended HikariCP Pool Settings
50
50
  config.setMaximumPoolSize(10);
51
51
  config.setMinimumIdle(2);
52
- config.setConnectionTimeout(10000); // 10 detik
53
- config.setIdleTimeout(600000); // 10 menit
54
- config.setMaxLifetime(1800000); // 30 menit
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 hanya boleh 1 writer pool
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. Repositori Asinkron dengan `CompletableFuture`
83
+ ## 3. Asynchronous DAO Pattern with `CompletableFuture`
84
84
 
85
- Buat Data Access Object (DAO) yang memisahkan eksekusi SQL dari logika game:
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
- // Membaca data pemain secara asinkron
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 jika data belum ada
139
+ return 0; // Default fallback
140
140
  }, dbExecutor);
141
141
  }
142
142
 
143
- // Menyimpan data pemain secara asinkron (Upsert)
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 & Postgres (gunakan ON DUPLICATE KEY UPDATE untuk MySQL)
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. Menggunakan Hasil Database di Event Listener
168
+ ## 4. Applying Query Results in Event Listeners
169
169
 
170
- Saat data selesai di-fetch dari database, gunakan scheduler Folia/Paper pemain untuk menerapkan perubahannya ke objek Bukkit:
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 data koin di thread asinkron
177
+ // Fetch coins asynchronously off the tick thread
178
178
  playerRepository.getCoinsAsync(player.getUniqueId()).thenAccept(coins -> {
179
- // Terapkan hasil ke entity pemain di thread region pemain
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>Saldo Koin: <yellow>" + coins + "</yellow></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`.