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,60 +1,59 @@
|
|
|
1
|
-
# Best Practices &
|
|
1
|
+
# Best Practices & Performance Optimization for Paper Plugins
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Building Minecraft server plugins for large communities requires high efficiency, zero memory leaks, and non-blocking game tick loops. This guide summarizes essential rules, performance optimizations, and anti-patterns.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## 1.
|
|
7
|
+
## 1. Preventing Memory Leaks
|
|
8
8
|
|
|
9
|
-
Minecraft
|
|
9
|
+
Minecraft exhibits a tightly coupled object graph. Holding Bukkit object references in memory beyond their intended lifecycle prevents the JVM Garbage Collector (GC) from clearing hundreds of megabytes of world and connection data.
|
|
10
10
|
|
|
11
|
-
### 🔴 Anti-Pattern:
|
|
11
|
+
### 🔴 Anti-Pattern: Holding Bukkit Objects in Static Collections
|
|
12
12
|
```java
|
|
13
|
-
//
|
|
13
|
+
// CRITICAL HAZARD: Player objects hold references to World, Chunks, and Network Connections
|
|
14
14
|
public static final Set<Player> activePlayers = new HashSet<>();
|
|
15
15
|
public static final Map<Player, PlayerData> dataMap = new HashMap<>();
|
|
16
16
|
```
|
|
17
|
-
|
|
17
|
+
When a player disconnects, the `Player` instance cannot be garbage-collected, creating a massive memory leak!
|
|
18
18
|
|
|
19
|
-
### 🟢
|
|
19
|
+
### 🟢 Solution: Store `UUID` Primitives
|
|
20
20
|
```java
|
|
21
|
-
//
|
|
21
|
+
// SAFE: UUID is a lightweight 16-byte identifier with zero lingering references
|
|
22
22
|
public static final Set<UUID> activePlayerUuids = new HashSet<>();
|
|
23
23
|
public static final Map<UUID, PlayerData> dataMap = new ConcurrentHashMap<>();
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Retrieve player instance only when needed:
|
|
26
26
|
Player player = Bukkit.getPlayer(uuid);
|
|
27
27
|
if (player != null) {
|
|
28
|
-
//
|
|
28
|
+
// Process player
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
> **
|
|
32
|
+
> **Golden Rule**: Never retain long-lived references to `Player`, `Entity`, `World`, `Chunk`, or `Block` instances in static fields. Use `UUID`, `NamespacedKey`, or primitive coordinates (`worldName`, `x`, `y`, `z`).
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
36
|
-
## 2.
|
|
36
|
+
## 2. Asynchronous I/O & Database Operations
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Server game ticks run in ~50 milliseconds (20 TPS). File I/O, heavy disk reads, HTTP web requests, or database queries taking even 100ms will immediately cause TPS drops and server lag spikes.
|
|
39
39
|
|
|
40
|
-
### 🟢
|
|
40
|
+
### 🟢 Solution: Async Threads & CompletableFuture
|
|
41
41
|
```java
|
|
42
42
|
import java.util.concurrent.CompletableFuture;
|
|
43
43
|
import org.bukkit.Bukkit;
|
|
44
44
|
|
|
45
45
|
public CompletableFuture<PlayerData> loadDataAsync(UUID uuid) {
|
|
46
46
|
return CompletableFuture.supplyAsync(() -> {
|
|
47
|
-
//
|
|
47
|
+
// Runs off the main game thread
|
|
48
48
|
return database.fetchUser(uuid);
|
|
49
49
|
}, myPluginThreadPool).thenApply(data -> {
|
|
50
50
|
return data != null ? data : new PlayerData(uuid);
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// Consuming the result safely:
|
|
55
55
|
loadDataAsync(player.getUniqueId()).thenAccept(data -> {
|
|
56
|
-
//
|
|
57
|
-
// jadwalkan kembali ke scheduler region pemain:
|
|
56
|
+
// Schedule back to the player's entity scheduler when mutating game state:
|
|
58
57
|
player.getScheduler().run(plugin, task -> {
|
|
59
58
|
player.giveExp(data.getPendingExp());
|
|
60
59
|
}, null);
|
|
@@ -63,56 +62,56 @@ loadDataAsync(player.getUniqueId()).thenAccept(data -> {
|
|
|
63
62
|
|
|
64
63
|
---
|
|
65
64
|
|
|
66
|
-
## 3.
|
|
65
|
+
## 3. Eliminating Synchronous Chunk Loading
|
|
67
66
|
|
|
68
|
-
|
|
67
|
+
Invoking `world.getChunkAt(x, z)` or `location.getBlock()` on an unloaded chunk forces the server to read chunk data synchronously from disk, freezing the main tick thread.
|
|
69
68
|
|
|
70
69
|
### 🔴 Anti-Pattern
|
|
71
70
|
```java
|
|
72
|
-
Chunk chunk = world.getChunkAt(x, z); //
|
|
71
|
+
Chunk chunk = world.getChunkAt(x, z); // Freezes the server if the chunk is not yet loaded!
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
### 🟢
|
|
76
|
-
|
|
74
|
+
### 🟢 Paper Solution: Asynchronous Chunk Loading
|
|
75
|
+
Use Paper's native `getChunkAtAsync`:
|
|
77
76
|
|
|
78
77
|
```java
|
|
79
78
|
world.getChunkAtAsync(x, z, true).thenAccept(chunk -> {
|
|
80
|
-
// Chunk
|
|
79
|
+
// Chunk loaded asynchronously without TPS drops
|
|
81
80
|
processChunkBlocks(chunk);
|
|
82
81
|
});
|
|
83
82
|
```
|
|
84
83
|
|
|
85
84
|
---
|
|
86
85
|
|
|
87
|
-
## 4.
|
|
86
|
+
## 4. Robust Configuration (`config.yml`) Handling
|
|
88
87
|
|
|
89
|
-
|
|
88
|
+
In Paper, YAML files default to UTF-8 encoding. Read and reload configuration files cleanly:
|
|
90
89
|
|
|
91
90
|
```java
|
|
92
|
-
//
|
|
91
|
+
// Save embedded default config from jar if missing
|
|
93
92
|
saveDefaultConfig();
|
|
94
93
|
|
|
95
|
-
// Reload
|
|
94
|
+
// Reload configuration
|
|
96
95
|
reloadConfig();
|
|
97
96
|
FileConfiguration config = getConfig();
|
|
98
97
|
|
|
99
|
-
//
|
|
100
|
-
String rawMessage = config.getString("messages.welcome", "<green>
|
|
98
|
+
// Read messages with fallback defaults
|
|
99
|
+
String rawMessage = config.getString("messages.welcome", "<green>Welcome to the server!</green>");
|
|
101
100
|
Component message = MiniMessage.miniMessage().deserialize(rawMessage);
|
|
102
101
|
```
|
|
103
102
|
|
|
104
|
-
|
|
103
|
+
For large configuration trees, consider using **Configurate** (`org.spongepowered:configurate-yaml`) which preserves file comments, validates schemas, and maps automatically to Java records.
|
|
105
104
|
|
|
106
105
|
---
|
|
107
106
|
|
|
108
|
-
## 5.
|
|
107
|
+
## 5. Anti-Pattern Summary Checklist
|
|
109
108
|
|
|
110
|
-
|
|
|
109
|
+
| Never Do This ❌ | Recommended Modern Pattern ✔️ |
|
|
111
110
|
| :--- | :--- |
|
|
112
|
-
| `ChatColor.RED + "
|
|
111
|
+
| `ChatColor.RED + "Text"` | `MiniMessage.miniMessage().deserialize("<red>Text</red>")` |
|
|
113
112
|
| `player.sendMessage("§a...")` | `player.sendMessage(Component)` |
|
|
114
|
-
| `new NamespacedKey(...)`
|
|
115
|
-
| `world.getBlockAt(x, y, z)`
|
|
116
|
-
|
|
|
117
|
-
|
|
|
118
|
-
|
|
|
113
|
+
| `new NamespacedKey(...)` in event loops | Define `NamespacedKey` instances as `static final` |
|
|
114
|
+
| Random `world.getBlockAt(x, y, z)` in async threads | Access blocks exclusively on the owning region thread |
|
|
115
|
+
| Ignoring `paper-plugin.yml` in new projects | Adopt `paper-plugin.yml` and `PluginBootstrap` |
|
|
116
|
+
| Blocking the tick thread with database queries | Use connection pools (HikariCP) & async futures |
|
|
117
|
+
| Omitting `folia-supported` declarations | Architect schedulers using region/universal abstractions |
|
package/docs/08-gui-and-menus.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Modern GUI & Inventory Menus in Paper
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Creating interactive inventory interfaces (GUI Menus) using chest containers is one of the most widely implemented features in Minecraft plugins. Legacy approaches inspecting menu titles with color strings (`ChatColor.stripColor`) or hardcoding slot numbers (`event.getSlot() == 13`) are brittle and notorious for causing item duplication exploits.
|
|
4
4
|
|
|
5
|
-
Paper
|
|
5
|
+
Modern Paper offers a clean, type-safe, and exploit-proof pattern utilizing **Kyori Adventure** and **PersistentDataContainer (PDC)**.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## 1.
|
|
9
|
+
## 1. Legacy Anti-Patterns vs Modern Paper Solutions
|
|
10
10
|
|
|
11
|
-
|
|
|
11
|
+
| Legacy Spigot Anti-Pattern | Modern Paper Architecture |
|
|
12
12
|
| :--- | :--- |
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
16
|
-
|
|
|
13
|
+
| Matching title strings: `view.getTitle().equals("§8Menu")` | Verifying custom `InventoryHolder` or PDC item signatures. |
|
|
14
|
+
| Hardcoding actions: `if (event.getSlot() == 13)` | Tagging button items with PDC actions (`gui_action -> buy_item`). |
|
|
15
|
+
| Corrupting color formatting across reloads | Using Adventure `Component` titles natively. |
|
|
16
|
+
| Item duplication exploits when cancel fails | Enforcing immediate cancellation on verified custom holders. |
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## 2.
|
|
20
|
+
## 2. The Custom `InventoryHolder` Pattern
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
The industry standard for identifying plugin-owned GUIs is implementing a custom `InventoryHolder`:
|
|
23
23
|
|
|
24
24
|
```java
|
|
25
25
|
package com.example.plugin.gui;
|
|
@@ -37,7 +37,7 @@ public class CustomMenuHolder implements InventoryHolder {
|
|
|
37
37
|
|
|
38
38
|
public CustomMenuHolder(Component title, int rows, String menuId) {
|
|
39
39
|
this.menuId = menuId;
|
|
40
|
-
// Bukkit.createInventory
|
|
40
|
+
// Bukkit.createInventory natively accepts Adventure Components in Paper
|
|
41
41
|
this.inventory = Bukkit.createInventory(this, rows * 9, title);
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -54,9 +54,9 @@ public class CustomMenuHolder implements InventoryHolder {
|
|
|
54
54
|
|
|
55
55
|
---
|
|
56
56
|
|
|
57
|
-
## 3.
|
|
57
|
+
## 3. Creating Interactive Buttons with PDC Tags
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
Rather than remembering slot indices, tag each button item with an action identifier inside `PersistentDataContainer`:
|
|
60
60
|
|
|
61
61
|
```java
|
|
62
62
|
package com.example.plugin.gui;
|
|
@@ -67,7 +67,6 @@ import org.bukkit.NamespacedKey;
|
|
|
67
67
|
import org.bukkit.inventory.ItemStack;
|
|
68
68
|
import org.bukkit.inventory.meta.ItemMeta;
|
|
69
69
|
import org.bukkit.persistence.PersistentDataType;
|
|
70
|
-
import org.bukkit.plugin.Plugin;
|
|
71
70
|
|
|
72
71
|
import java.util.List;
|
|
73
72
|
|
|
@@ -77,8 +76,7 @@ public final class GuiItemBuilder {
|
|
|
77
76
|
|
|
78
77
|
public static ItemStack createButton(Material material, String nameMiniMessage, List<String> loreMiniMessage, String actionId) {
|
|
79
78
|
ItemStack item = new ItemStack(material);
|
|
80
|
-
|
|
81
|
-
if (meta != null) {
|
|
79
|
+
item.editMeta(meta -> {
|
|
82
80
|
meta.displayName(MiniMessage.miniMessage().deserialize(nameMiniMessage));
|
|
83
81
|
|
|
84
82
|
meta.lore(loreMiniMessage.stream()
|
|
@@ -86,10 +84,9 @@ public final class GuiItemBuilder {
|
|
|
86
84
|
.toList()
|
|
87
85
|
);
|
|
88
86
|
|
|
89
|
-
//
|
|
87
|
+
// Tag button with unique action identifier
|
|
90
88
|
meta.getPersistentDataContainer().set(ACTION_KEY, PersistentDataType.STRING, actionId);
|
|
91
|
-
|
|
92
|
-
}
|
|
89
|
+
});
|
|
93
90
|
return item;
|
|
94
91
|
}
|
|
95
92
|
}
|
|
@@ -97,18 +94,18 @@ public final class GuiItemBuilder {
|
|
|
97
94
|
|
|
98
95
|
---
|
|
99
96
|
|
|
100
|
-
## 4.
|
|
97
|
+
## 4. Assembling the Menu
|
|
101
98
|
|
|
102
99
|
```java
|
|
103
100
|
public void openShopMenu(Player player) {
|
|
104
101
|
CustomMenuHolder holder = new CustomMenuHolder(
|
|
105
|
-
MiniMessage.miniMessage().deserialize("<gradient:#f12711:#f5af19><bold>
|
|
106
|
-
3, // 3
|
|
102
|
+
MiniMessage.miniMessage().deserialize("<gradient:#f12711:#f5af19><bold>Server Market</bold></gradient>"),
|
|
103
|
+
3, // 3 rows = 27 slots
|
|
107
104
|
"server_shop"
|
|
108
105
|
);
|
|
109
106
|
Inventory inv = holder.getInventory();
|
|
110
107
|
|
|
111
|
-
//
|
|
108
|
+
// Background glass filler
|
|
112
109
|
ItemStack filler = GuiItemBuilder.createButton(
|
|
113
110
|
Material.GRAY_STAINED_GLASS_PANE,
|
|
114
111
|
"<gray> </gray>",
|
|
@@ -119,18 +116,18 @@ public void openShopMenu(Player player) {
|
|
|
119
116
|
inv.setItem(i, filler);
|
|
120
117
|
}
|
|
121
118
|
|
|
122
|
-
//
|
|
119
|
+
// Action button
|
|
123
120
|
inv.setItem(11, GuiItemBuilder.createButton(
|
|
124
121
|
Material.DIAMOND_SWORD,
|
|
125
|
-
"<aqua><bold>
|
|
126
|
-
List.of("<gray>
|
|
122
|
+
"<aqua><bold>Purchase Legendary Blade</bold></aqua>",
|
|
123
|
+
List.of("<gray>Price: <gold>500 Coins</gold></gray>", "<yellow>Click to purchase!</yellow>"),
|
|
127
124
|
"buy_sword"
|
|
128
125
|
));
|
|
129
126
|
|
|
130
127
|
inv.setItem(15, GuiItemBuilder.createButton(
|
|
131
128
|
Material.BARRIER,
|
|
132
|
-
"<red><bold>
|
|
133
|
-
List.of("<gray>
|
|
129
|
+
"<red><bold>Close Menu</bold></red>",
|
|
130
|
+
List.of("<gray>Click to exit</gray>"),
|
|
134
131
|
"close_menu"
|
|
135
132
|
));
|
|
136
133
|
|
|
@@ -140,7 +137,7 @@ public void openShopMenu(Player player) {
|
|
|
140
137
|
|
|
141
138
|
---
|
|
142
139
|
|
|
143
|
-
## 5.
|
|
140
|
+
## 5. Exploit-Proof Menu Event Listener
|
|
144
141
|
|
|
145
142
|
```java
|
|
146
143
|
package com.example.plugin.gui;
|
|
@@ -158,15 +155,15 @@ public final class MenuClickListener implements Listener {
|
|
|
158
155
|
|
|
159
156
|
@EventHandler(priority = EventPriority.HIGHEST)
|
|
160
157
|
public void onInventoryClick(InventoryClickEvent event) {
|
|
161
|
-
// 1.
|
|
158
|
+
// 1. Verify holder instance
|
|
162
159
|
if (!(event.getInventory().getHolder() instanceof CustomMenuHolder holder)) {
|
|
163
160
|
return;
|
|
164
161
|
}
|
|
165
162
|
|
|
166
|
-
// 2.
|
|
163
|
+
// 2. ALWAYS cancel to prevent players from moving GUI items!
|
|
167
164
|
event.setCancelled(true);
|
|
168
165
|
|
|
169
|
-
// 3.
|
|
166
|
+
// 3. Ignore empty slots or clicks outside the window
|
|
170
167
|
ItemStack clickedItem = event.getCurrentItem();
|
|
171
168
|
if (clickedItem == null || !clickedItem.hasItemMeta()) {
|
|
172
169
|
return;
|
|
@@ -176,7 +173,7 @@ public final class MenuClickListener implements Listener {
|
|
|
176
173
|
return;
|
|
177
174
|
}
|
|
178
175
|
|
|
179
|
-
// 4.
|
|
176
|
+
// 4. Read action from item PDC
|
|
180
177
|
String action = clickedItem.getItemMeta().getPersistentDataContainer()
|
|
181
178
|
.get(GuiItemBuilder.ACTION_KEY, PersistentDataType.STRING);
|
|
182
179
|
|
|
@@ -184,14 +181,14 @@ public final class MenuClickListener implements Listener {
|
|
|
184
181
|
return;
|
|
185
182
|
}
|
|
186
183
|
|
|
187
|
-
// 5.
|
|
184
|
+
// 5. Execute action routing
|
|
188
185
|
switch (action) {
|
|
189
186
|
case "buy_sword" -> {
|
|
190
|
-
player.sendMessage(MiniMessage.miniMessage().deserialize("<green>
|
|
187
|
+
player.sendMessage(MiniMessage.miniMessage().deserialize("<green>Successfully purchased weapon!</green>"));
|
|
191
188
|
player.closeInventory();
|
|
192
189
|
}
|
|
193
190
|
case "close_menu" -> player.closeInventory();
|
|
194
|
-
default -> player.sendMessage(MiniMessage.miniMessage().deserialize("<red>
|
|
191
|
+
default -> player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Unknown menu action.</red>"));
|
|
195
192
|
}
|
|
196
193
|
}
|
|
197
194
|
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
# Custom Items,
|
|
1
|
+
# Custom Items, Abilities & Combat in Paper
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Crafting custom items with magical abilities and combat mechanics (such as lightning wands, lifesteal swords, or explosive bows) is a hallmark of modern RPG and minigame servers.
|
|
4
4
|
|
|
5
|
-
Paper
|
|
5
|
+
Paper provides native APIs for entity raycasting, particle trails, vanilla visual cooldown animations, and Adventure sound effects without external library dependencies.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## 1.
|
|
9
|
+
## 1. Anatomy of a Custom Item with PDC
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Always identify custom items using `PersistentDataContainer` (PDC) rather than displayName or lore strings which players can forge via an Anvil:
|
|
12
12
|
|
|
13
13
|
```java
|
|
14
14
|
package com.example.plugin.item;
|
|
@@ -17,7 +17,6 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
|
17
17
|
import org.bukkit.Material;
|
|
18
18
|
import org.bukkit.NamespacedKey;
|
|
19
19
|
import org.bukkit.inventory.ItemStack;
|
|
20
|
-
import org.bukkit.inventory.meta.ItemMeta;
|
|
21
20
|
import org.bukkit.persistence.PersistentDataType;
|
|
22
21
|
|
|
23
22
|
import java.util.List;
|
|
@@ -29,18 +28,15 @@ public final class CustomItemFactory {
|
|
|
29
28
|
|
|
30
29
|
public static ItemStack createLightningWand() {
|
|
31
30
|
ItemStack item = new ItemStack(Material.BLAZE_ROD);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
meta.displayName(MiniMessage.miniMessage().deserialize("<gradient:#ffe259:#ffa751><bold>Tongkat Petir Kuno</bold></gradient>"));
|
|
31
|
+
item.editMeta(meta -> {
|
|
32
|
+
meta.displayName(MiniMessage.miniMessage().deserialize("<gradient:#ffe259:#ffa751><bold>Ancient Lightning Wand</bold></gradient>"));
|
|
35
33
|
meta.lore(List.of(
|
|
36
|
-
MiniMessage.miniMessage().deserialize("<gray>
|
|
37
|
-
MiniMessage.miniMessage().deserialize("<dark_gray>Cooldown: <gold>3
|
|
34
|
+
MiniMessage.miniMessage().deserialize("<gray>Right-click to discharge <yellow>Pure Lightning</yellow>.</gray>"),
|
|
35
|
+
MiniMessage.miniMessage().deserialize("<dark_gray>Cooldown: <gold>3 Seconds</gold></dark_gray>")
|
|
38
36
|
));
|
|
39
37
|
|
|
40
|
-
// Tandai ID item
|
|
41
38
|
meta.getPersistentDataContainer().set(ITEM_ID_KEY, PersistentDataType.STRING, "lightning_wand");
|
|
42
|
-
|
|
43
|
-
}
|
|
39
|
+
});
|
|
44
40
|
return item;
|
|
45
41
|
}
|
|
46
42
|
}
|
|
@@ -48,37 +44,35 @@ public final class CustomItemFactory {
|
|
|
48
44
|
|
|
49
45
|
---
|
|
50
46
|
|
|
51
|
-
## 2.
|
|
47
|
+
## 2. Cooldown Management: Visual Vanilla Animations
|
|
52
48
|
|
|
53
|
-
Minecraft
|
|
49
|
+
Minecraft features a native client API to render visual cooldown overlays (the sweeping gray overlay on hotbar slots):
|
|
54
50
|
|
|
55
51
|
```java
|
|
56
|
-
//
|
|
52
|
+
// Apply a 60-tick (3-second) vanilla cooldown to Blaze Rods
|
|
57
53
|
player.setCooldown(Material.BLAZE_ROD, 60);
|
|
58
54
|
|
|
59
|
-
//
|
|
55
|
+
// Query if cooldown is active
|
|
60
56
|
if (player.hasCooldown(Material.BLAZE_ROD)) {
|
|
61
|
-
player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>
|
|
57
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Ability is on cooldown!</red>"));
|
|
62
58
|
return;
|
|
63
59
|
}
|
|
64
60
|
```
|
|
65
61
|
|
|
66
62
|
---
|
|
67
63
|
|
|
68
|
-
## 3. Raycasting
|
|
64
|
+
## 3. Entity Raycasting & Particle Beams
|
|
69
65
|
|
|
70
|
-
Raycasting
|
|
66
|
+
Raycasting detects target entities along the player's line of sight using `World#rayTraceEntities`:
|
|
71
67
|
|
|
72
68
|
```java
|
|
73
69
|
package com.example.plugin.item;
|
|
74
70
|
|
|
75
71
|
import net.kyori.adventure.sound.Sound;
|
|
76
72
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
77
|
-
import org.bukkit.FluidCollisionMode;
|
|
78
73
|
import org.bukkit.Location;
|
|
79
74
|
import org.bukkit.Material;
|
|
80
75
|
import org.bukkit.Particle;
|
|
81
|
-
import org.bukkit.SoundCategory;
|
|
82
76
|
import org.bukkit.World;
|
|
83
77
|
import org.bukkit.entity.Damageable;
|
|
84
78
|
import org.bukkit.entity.Entity;
|
|
@@ -113,16 +107,15 @@ public final class CustomItemListener implements Listener {
|
|
|
113
107
|
event.setCancelled(true);
|
|
114
108
|
Player player = event.getPlayer();
|
|
115
109
|
|
|
116
|
-
// 1.
|
|
110
|
+
// 1. Check Cooldown
|
|
117
111
|
if (player.hasCooldown(Material.BLAZE_ROD)) {
|
|
118
|
-
player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>
|
|
112
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Ability cooling down!</red>"));
|
|
119
113
|
return;
|
|
120
114
|
}
|
|
121
115
|
|
|
122
|
-
// Set cooldown 3 detik (60 ticks)
|
|
123
116
|
player.setCooldown(Material.BLAZE_ROD, 60);
|
|
124
117
|
|
|
125
|
-
// 2.
|
|
118
|
+
// 2. Execute Spell Effect
|
|
126
119
|
castLightningBeam(player);
|
|
127
120
|
}
|
|
128
121
|
|
|
@@ -131,7 +124,7 @@ public final class CustomItemListener implements Listener {
|
|
|
131
124
|
Location eyeLoc = player.getEyeLocation();
|
|
132
125
|
Vector direction = eyeLoc.getDirection().normalize();
|
|
133
126
|
|
|
134
|
-
//
|
|
127
|
+
// Play cast sound
|
|
135
128
|
player.playSound(Sound.sound(
|
|
136
129
|
org.bukkit.Sound.ENTITY_ILLUSIONER_CAST_SPELL.key(),
|
|
137
130
|
Sound.Source.PLAYER,
|
|
@@ -139,13 +132,13 @@ public final class CustomItemListener implements Listener {
|
|
|
139
132
|
1.5f
|
|
140
133
|
));
|
|
141
134
|
|
|
142
|
-
//
|
|
135
|
+
// Spawn particle line (up to 30 blocks)
|
|
143
136
|
for (double d = 1; d <= 30; d += 0.5) {
|
|
144
137
|
Location point = eyeLoc.clone().add(direction.clone().multiply(d));
|
|
145
138
|
world.spawnParticle(Particle.ELECTRIC_SPARK, point, 2, 0.05, 0.05, 0.05, 0.01);
|
|
146
139
|
}
|
|
147
140
|
|
|
148
|
-
// Raytrace
|
|
141
|
+
// Raytrace entities colliding with beam
|
|
149
142
|
RayTraceResult result = world.rayTraceEntities(
|
|
150
143
|
eyeLoc,
|
|
151
144
|
direction,
|
|
@@ -158,13 +151,12 @@ public final class CustomItemListener implements Listener {
|
|
|
158
151
|
Entity target = result.getHitEntity();
|
|
159
152
|
Location hitLoc = target.getLocation();
|
|
160
153
|
|
|
161
|
-
// Panggil sambaran petir visual & berikan damage
|
|
162
154
|
world.strikeLightningEffect(hitLoc);
|
|
163
155
|
if (target instanceof Damageable damageable) {
|
|
164
|
-
damageable.damage(8.0, player); // 8 damage = 4
|
|
156
|
+
damageable.damage(8.0, player); // 8 damage = 4 hearts
|
|
165
157
|
}
|
|
166
158
|
|
|
167
|
-
player.sendActionBar(MiniMessage.miniMessage().deserialize("<yellow>
|
|
159
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<yellow>Target struck!</yellow>"));
|
|
168
160
|
}
|
|
169
161
|
}
|
|
170
162
|
}
|