papermc-skill 1.0.1 → 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 +19 -0
- package/README.md +12 -12
- package/assets/banner-animated.svg +360 -0
- package/assets/banner.svg +360 -0
- package/bin/cli.js +107 -2
- 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 -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,17 +1,17 @@
|
|
|
1
|
-
# Command
|
|
1
|
+
# Modern Command Systems: Native Brigadier & Cloud in Paper
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
1. **Paper Native Brigadier API** (
|
|
5
|
-
2. **Incendo Cloud Framework** (`cloud-paper`):
|
|
3
|
+
The Minecraft Java command system has evolved substantially. Legacy Bukkit patterns like `CommandExecutor`, `TabCompleter`, and command declarations in `plugin.yml` have been superseded by:
|
|
4
|
+
1. **Paper Native Brigadier API** (introduced in Paper 1.20.6+ via `LifecycleEvents.COMMANDS`).
|
|
5
|
+
2. **Incendo Cloud Framework** (`cloud-paper`): The enterprise standard for annotation-driven and builder-based multi-platform command handling.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
## 1. Native Paper Brigadier Commands (Paper 1.20.6+)
|
|
10
10
|
|
|
11
|
-
Mojang Brigadier
|
|
11
|
+
Mojang Brigadier is Minecraft's official command dispatcher and parser engine. Paper exposes native Brigadier bindings directly without requiring NMS hacks or external shading.
|
|
12
12
|
|
|
13
|
-
###
|
|
14
|
-
|
|
13
|
+
### Registering Commands via Lifecycle Events
|
|
14
|
+
Commands are registered during the Bootstrap phase or when the `LifecycleEvents.COMMANDS` event triggers:
|
|
15
15
|
|
|
16
16
|
```java
|
|
17
17
|
package com.example.plugin;
|
|
@@ -24,7 +24,6 @@ import io.papermc.paper.plugin.bootstrap.BootstrapContext;
|
|
|
24
24
|
import io.papermc.paper.plugin.bootstrap.PluginBootstrap;
|
|
25
25
|
import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
|
|
26
26
|
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
27
|
-
import org.bukkit.entity.Player;
|
|
28
27
|
|
|
29
28
|
public final class CommandBootstrapper implements PluginBootstrap {
|
|
30
29
|
|
|
@@ -33,11 +32,11 @@ public final class CommandBootstrapper implements PluginBootstrap {
|
|
|
33
32
|
context.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
|
|
34
33
|
final Commands commands = event.registrar();
|
|
35
34
|
|
|
36
|
-
//
|
|
35
|
+
// Construct syntax tree: /teleportzone <name> [radius]
|
|
37
36
|
commands.register(
|
|
38
37
|
Commands.literal("teleportzone")
|
|
39
38
|
.requires(source -> source.getSender().hasPermission("plugin.command.teleportzone"))
|
|
40
|
-
.then(Commands.argument("
|
|
39
|
+
.then(Commands.argument("name", StringArgumentType.word())
|
|
41
40
|
.suggests((ctx, builder) -> {
|
|
42
41
|
builder.suggest("lobby");
|
|
43
42
|
builder.suggest("arena");
|
|
@@ -47,12 +46,12 @@ public final class CommandBootstrapper implements PluginBootstrap {
|
|
|
47
46
|
.then(Commands.argument("radius", IntegerArgumentType.integer(1, 100))
|
|
48
47
|
.executes(ctx -> {
|
|
49
48
|
final CommandSourceStack source = ctx.getSource();
|
|
50
|
-
final String zone = StringArgumentType.getString(ctx, "
|
|
49
|
+
final String zone = StringArgumentType.getString(ctx, "name");
|
|
51
50
|
final int radius = IntegerArgumentType.getInteger(ctx, "radius");
|
|
52
51
|
|
|
53
52
|
source.getSender().sendMessage(
|
|
54
53
|
MiniMessage.miniMessage().deserialize(
|
|
55
|
-
"<green>
|
|
54
|
+
"<green>Preparing teleportation to zone <gold>" + zone + "</gold> with radius <yellow>" + radius + "</yellow>!</green>"
|
|
56
55
|
)
|
|
57
56
|
);
|
|
58
57
|
return com.mojang.brigadier.Command.SINGLE_SUCCESS;
|
|
@@ -60,43 +59,42 @@ public final class CommandBootstrapper implements PluginBootstrap {
|
|
|
60
59
|
)
|
|
61
60
|
.executes(ctx -> {
|
|
62
61
|
final CommandSourceStack source = ctx.getSource();
|
|
63
|
-
final String zone = StringArgumentType.getString(ctx, "
|
|
62
|
+
final String zone = StringArgumentType.getString(ctx, "name");
|
|
64
63
|
source.getSender().sendMessage(
|
|
65
|
-
MiniMessage.miniMessage().deserialize("<green>
|
|
64
|
+
MiniMessage.miniMessage().deserialize("<green>Default teleportation to zone <gold>" + zone + "</gold>!</green>")
|
|
66
65
|
);
|
|
67
66
|
return com.mojang.brigadier.Command.SINGLE_SUCCESS;
|
|
68
67
|
})
|
|
69
68
|
)
|
|
70
69
|
.build(),
|
|
71
|
-
"
|
|
72
|
-
java.util.List.of("tpzone") //
|
|
70
|
+
"Modern zone teleportation command",
|
|
71
|
+
java.util.List.of("tpzone") // aliases
|
|
73
72
|
);
|
|
74
73
|
});
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
```
|
|
78
77
|
|
|
79
|
-
###
|
|
80
|
-
- **Client-Side Syntax Highlighting**: Minecraft
|
|
81
|
-
- **
|
|
82
|
-
- **
|
|
78
|
+
### Advantages of Native Paper Brigadier:
|
|
79
|
+
- **Client-Side Syntax Highlighting**: Minecraft clients highlight syntax errors in real-time before sending the command packet.
|
|
80
|
+
- **Automated Tab-Completion**: Eliminates complex manual `TabCompleter` string matching.
|
|
81
|
+
- **Strict Argument Typing**: Automatically validates argument bounds and rejects invalid types.
|
|
83
82
|
|
|
84
83
|
---
|
|
85
84
|
|
|
86
85
|
## 2. Incendo Cloud Command Framework (`cloud-paper`)
|
|
87
86
|
|
|
88
|
-
|
|
87
|
+
For large plugin codebases requiring dependency injection, automatic Bukkit argument parsing (`Player`, `OfflinePlayer`, `World`, `Duration`), and hundreds of sub-commands, **Incendo Cloud** is the leading solution.
|
|
89
88
|
|
|
90
|
-
###
|
|
89
|
+
### Gradle Dependencies:
|
|
91
90
|
```kotlin
|
|
92
91
|
dependencies {
|
|
93
92
|
implementation("org.incendo:cloud-paper:2.0.0-beta.10")
|
|
94
|
-
// atau cloud-annotations jika menyukai gaya @Command
|
|
95
93
|
implementation("org.incendo:cloud-annotations:2.0.0-beta.10")
|
|
96
94
|
}
|
|
97
95
|
```
|
|
98
96
|
|
|
99
|
-
###
|
|
97
|
+
### Initializing Cloud PaperCommandManager:
|
|
100
98
|
```java
|
|
101
99
|
import org.incendo.cloud.paper.PaperCommandManager;
|
|
102
100
|
import org.incendo.cloud.execution.ExecutionCoordinator;
|
|
@@ -111,8 +109,7 @@ public class PluginCommands {
|
|
|
111
109
|
ExecutionCoordinator.simpleCoordinator()
|
|
112
110
|
);
|
|
113
111
|
|
|
114
|
-
|
|
115
|
-
if (manager.hasCapability(org.incendo.cloud.paper.PaperCommandManager.Capability.BRIGADIER)) {
|
|
112
|
+
if (manager.hasCapability(PaperCommandManager.Capability.BRIGADIER)) {
|
|
116
113
|
manager.registerBrigadier();
|
|
117
114
|
}
|
|
118
115
|
|
|
@@ -121,7 +118,7 @@ public class PluginCommands {
|
|
|
121
118
|
}
|
|
122
119
|
```
|
|
123
120
|
|
|
124
|
-
###
|
|
121
|
+
### Building Commands with Cloud:
|
|
125
122
|
```java
|
|
126
123
|
import org.incendo.cloud.paper.PaperCommandManager;
|
|
127
124
|
import org.incendo.cloud.bukkit.parser.PlayerParser;
|
|
@@ -138,8 +135,8 @@ public void registerGiveRewardCommand(PaperCommandManager<CommandSender> manager
|
|
|
138
135
|
CommandSender sender = commandContext.sender();
|
|
139
136
|
Player target = commandContext.get("target");
|
|
140
137
|
|
|
141
|
-
target.sendMessage(MiniMessage.miniMessage().deserialize("<gold>
|
|
142
|
-
sender.sendMessage(MiniMessage.miniMessage().deserialize("<green>
|
|
138
|
+
target.sendMessage(MiniMessage.miniMessage().deserialize("<gold>You received a special reward!</gold>"));
|
|
139
|
+
sender.sendMessage(MiniMessage.miniMessage().deserialize("<green>Reward dispatched to " + target.getName() + ".</green>"));
|
|
143
140
|
})
|
|
144
141
|
);
|
|
145
142
|
}
|
|
@@ -147,11 +144,11 @@ public void registerGiveRewardCommand(PaperCommandManager<CommandSender> manager
|
|
|
147
144
|
|
|
148
145
|
---
|
|
149
146
|
|
|
150
|
-
## 3.
|
|
147
|
+
## 3. Comparison: When to Use Which?
|
|
151
148
|
|
|
152
|
-
|
|
|
149
|
+
| Aspect | Paper Native Brigadier | Incendo Cloud |
|
|
153
150
|
| :--- | :--- | :--- |
|
|
154
|
-
| **
|
|
155
|
-
| **
|
|
156
|
-
| **
|
|
157
|
-
| **
|
|
151
|
+
| **Artifact Overhead** | Zero-dependency (built directly into Paper) | Adds ~300KB-800KB (requires shadowing & relocating) |
|
|
152
|
+
| **Complexity Fit** | Best for small-to-medium command suites | Best for complex enterprise command structures |
|
|
153
|
+
| **Parsers** | Requires manual mapping for Bukkit types | Built-in parsers for Players, Locations, Selectors, etc. |
|
|
154
|
+
| **Portability** | Paper 1.20.6+ exclusive | Cross-platform (Velocity, BungeeCord, Fabric, Paper) |
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Concurrency, Threading & Folia Multi-Thread Compatibility
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One of the most revolutionary milestones in the PaperMC ecosystem is the emergence of **Folia**. Folia breaks Minecraft's monolithic "Server Main Thread" into hundreds of independent regional threads (**Threaded Regions**).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Consequently, legacy Bukkit concurrency assumptions like `Bukkit.getScheduler()` or mutating world state from arbitrary threads result in **`IllegalStateException` crashes or data corruption** on Folia.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
## 1.
|
|
9
|
+
## 1. Threading Architecture: Bukkit vs Paper vs Folia
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
[Bukkit / Vanilla]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
[Folia Regionized Threading]
|
|
16
|
-
Region Thread 1 ───
|
|
17
|
-
Region Thread 2 ───
|
|
18
|
-
Region Thread 3 ───
|
|
19
|
-
Global Thread ───
|
|
20
|
-
Async Threads ─── Network I/O, File Saving, Database Queries
|
|
13
|
+
Single Main Thread ─── Ticks All Worlds, Entities, Blocks, Redstone, AI, Chunks ───>
|
|
14
|
+
|
|
15
|
+
[Folia Regionized Multi-Threading]
|
|
16
|
+
Region Thread 1 ─── Ticks Overworld Region A (Chunks 0..32, 0..32) ───>
|
|
17
|
+
Region Thread 2 ─── Ticks Overworld Region B (Chunks 64..96, 64..96) ──>
|
|
18
|
+
Region Thread 3 ─── Ticks Nether Region C ────────────────────────────>
|
|
19
|
+
Global Thread ─── Ticks Weather, Time, Player List, Commands ───────>
|
|
20
|
+
Async Threads ─── Network I/O, File Saving, Database Queries ────────>
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
23
|
+
In Folia:
|
|
24
|
+
- There is no single "Main Thread".
|
|
25
|
+
- `World`, `Entity`, and `Block` instances may only be queried or mutated on the **region thread owning that coordinate/chunk**.
|
|
26
|
+
- Accessing an entity from a foreign region thread throws `IllegalStateException: Plugin attempted to access entity from wrong thread`.
|
|
27
27
|
|
|
28
28
|
---
|
|
29
29
|
|
|
30
|
-
## 2.
|
|
30
|
+
## 2. Modern Scheduler Hierarchy in Paper & Folia
|
|
31
31
|
|
|
32
|
-
Paper
|
|
32
|
+
Paper introduces clean scheduler primitives that operate seamlessly across both multi-threaded Folia and single-threaded Paper:
|
|
33
33
|
|
|
34
34
|
### 1. `RegionScheduler`
|
|
35
|
-
|
|
35
|
+
Used to execute tasks bound to specific world coordinates or chunks:
|
|
36
36
|
|
|
37
37
|
```java
|
|
38
38
|
import org.bukkit.Bukkit;
|
|
@@ -41,59 +41,59 @@ import org.bukkit.plugin.Plugin;
|
|
|
41
41
|
|
|
42
42
|
Location loc = player.getLocation();
|
|
43
43
|
|
|
44
|
-
//
|
|
44
|
+
// Run a one-time task on the region thread owning this location
|
|
45
45
|
Bukkit.getRegionScheduler().execute(plugin, loc, () -> {
|
|
46
46
|
loc.getBlock().setType(org.bukkit.Material.GOLD_BLOCK);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
//
|
|
49
|
+
// Run a repeating task on the region thread
|
|
50
50
|
Bukkit.getRegionScheduler().runAtFixedRate(plugin, loc, task -> {
|
|
51
51
|
loc.getWorld().spawnParticle(org.bukkit.Particle.FLAME, loc, 5);
|
|
52
|
-
}, 20L, 20L); // delay 20
|
|
52
|
+
}, 20L, 20L); // 20 ticks initial delay, 20 ticks period
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
### 2. `EntityScheduler`
|
|
56
|
-
|
|
56
|
+
Every `Entity` owns its own dedicated scheduler. Tasks execute directly on whichever region thread the entity currently occupies, even across chunk border transitions:
|
|
57
57
|
|
|
58
58
|
```java
|
|
59
59
|
player.getScheduler().run(plugin, task -> {
|
|
60
60
|
player.giveExp(10);
|
|
61
|
-
player.sendMessage("Bonus EXP
|
|
61
|
+
player.sendMessage("Bonus EXP received!");
|
|
62
62
|
}, () -> {
|
|
63
|
-
//
|
|
64
|
-
plugin.getLogger().warning("
|
|
63
|
+
// Retired callback if entity is dead or invalidated before execution
|
|
64
|
+
plugin.getLogger().warning("Player disconnected before reward was applied.");
|
|
65
65
|
});
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
### 3. `GlobalRegionScheduler`
|
|
69
|
-
|
|
69
|
+
Used for server-wide tasks unconfined to specific coordinates (e.g., global announcements, world time, or weather):
|
|
70
70
|
|
|
71
71
|
```java
|
|
72
72
|
Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, task -> {
|
|
73
|
-
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<gold>[
|
|
73
|
+
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<gold>[Announcement] Join our Discord server!</gold>"));
|
|
74
74
|
}, 100L, 1200L);
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
### 4. `AsyncScheduler`
|
|
78
|
-
|
|
78
|
+
Used for I/O, database interactions, HTTP calls, and non-Bukkit mathematical computations. Operates using wall-clock time (`java.time.TimeUnit`) rather than game ticks:
|
|
79
79
|
|
|
80
80
|
```java
|
|
81
81
|
import java.util.concurrent.TimeUnit;
|
|
82
82
|
|
|
83
83
|
Bukkit.getAsyncScheduler().runDelayed(plugin, task -> {
|
|
84
|
-
//
|
|
84
|
+
// Run database query off the game thread
|
|
85
85
|
database.savePlayerData(uuid, data);
|
|
86
86
|
}, 5, TimeUnit.SECONDS);
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
---
|
|
90
90
|
|
|
91
|
-
## 3.
|
|
91
|
+
## 3. Universal Scheduler Abstraction Pattern (Paper + Folia)
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Most production plugins must run identically on standard single-threaded Paper and multi-threaded Folia. The recommended approach is an abstraction layer (as pioneered by **Chunky** and **squaremap**):
|
|
94
94
|
|
|
95
95
|
```java
|
|
96
|
-
public final class
|
|
96
|
+
public final class UniversalScheduler {
|
|
97
97
|
|
|
98
98
|
private static final boolean IS_FOLIA = checkFolia();
|
|
99
99
|
|
|
@@ -110,7 +110,7 @@ public final class PlatformScheduler {
|
|
|
110
110
|
return IS_FOLIA;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
public static void
|
|
113
|
+
public static void runAtLocation(Plugin plugin, Location location, Runnable runnable) {
|
|
114
114
|
if (IS_FOLIA) {
|
|
115
115
|
Bukkit.getRegionScheduler().execute(plugin, location, runnable);
|
|
116
116
|
} else {
|
|
@@ -118,7 +118,7 @@ public final class PlatformScheduler {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
public static void
|
|
121
|
+
public static void runForEntity(Plugin plugin, Entity entity, Runnable runnable) {
|
|
122
122
|
if (IS_FOLIA) {
|
|
123
123
|
entity.getScheduler().run(plugin, task -> runnable.run(), null);
|
|
124
124
|
} else {
|
|
@@ -138,13 +138,13 @@ public final class PlatformScheduler {
|
|
|
138
138
|
|
|
139
139
|
---
|
|
140
140
|
|
|
141
|
-
## 4.
|
|
141
|
+
## 4. Mandatory Folia Compatibility Rules
|
|
142
142
|
|
|
143
|
-
1. **
|
|
144
|
-
|
|
145
|
-
2. **
|
|
146
|
-
|
|
147
|
-
3. **
|
|
148
|
-
|
|
149
|
-
4. **
|
|
150
|
-
|
|
143
|
+
1. **Declare Compatibility in Manifest**:
|
|
144
|
+
Always include `folia-supported: true` in `paper-plugin.yml`. Without this, Folia flags your plugin as potentially hazardous upon boot.
|
|
145
|
+
2. **Never Read Chunks/Blocks Synchronously Off-Thread**:
|
|
146
|
+
Always invoke `world.getChunkAtAsync(x, z)` which returns a `CompletableFuture<Chunk>`.
|
|
147
|
+
3. **Use TeleportAsync**:
|
|
148
|
+
Always call `entity.teleportAsync(location)` instead of `entity.teleport(location)`. Across Folia, cross-region movement must be processed asynchronously.
|
|
149
|
+
4. **Avoid Global Mutable Static Collections**:
|
|
150
|
+
Static lists of active players or entities experience severe race conditions when mutated by multiple region threads concurrently. Use thread-safe structures (`ConcurrentHashMap`) or delegate state to player PDC.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Event System & Paper Listeners
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The event bus is Paper's primary reactive mechanism for responding to player interactions, world updates, entity behaviors, and server lifecycle states. Paper provides exclusive high-performance events along with native Kyori Adventure Component integration.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## 1.
|
|
7
|
+
## 1. Anatomy of a Modern Listener
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Listeners implement the `org.bukkit.event.Listener` interface and decorate handler methods with `@EventHandler`:
|
|
10
10
|
|
|
11
11
|
```java
|
|
12
12
|
import org.bukkit.event.Listener;
|
|
@@ -26,35 +26,35 @@ public final class PlayerJoinListener implements Listener {
|
|
|
26
26
|
|
|
27
27
|
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
|
28
28
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
|
29
|
-
// Paper
|
|
29
|
+
// Paper natively uses Adventure Components for join messages
|
|
30
30
|
event.joinMessage(
|
|
31
31
|
MiniMessage.miniMessage().deserialize(
|
|
32
|
-
"<gray>[<green>+</green>] <yellow>" + event.getPlayer().getName() + "</yellow>
|
|
32
|
+
"<gray>[<green>+</green>] <yellow>" + event.getPlayer().getName() + "</yellow> joined the server!</gray>"
|
|
33
33
|
)
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
###
|
|
40
|
-
|
|
41
|
-
1. `LOWEST` (
|
|
39
|
+
### Understanding `EventPriority`:
|
|
40
|
+
Execution sequence from first to last:
|
|
41
|
+
1. `LOWEST` (Runs first, ideal for raw observation or early preprocessing)
|
|
42
42
|
2. `LOW`
|
|
43
|
-
3. `NORMAL` (
|
|
43
|
+
3. `NORMAL` (Default priority)
|
|
44
44
|
4. `HIGH`
|
|
45
|
-
5. `HIGHEST` (
|
|
46
|
-
6. `MONITOR` (**
|
|
45
|
+
5. `HIGHEST` (Runs last before final decision)
|
|
46
|
+
6. `MONITOR` (**READ-ONLY / LOGGING ONLY**; strictly forbidden from modifying or cancelling events, as downstream plugins cannot observe changes).
|
|
47
47
|
|
|
48
|
-
> **
|
|
48
|
+
> **Important**: Always specify `ignoreCancelled = true` if your listener does not need to process interactions already prevented by land protection plugins (such as WorldGuard or GriefPrevention).
|
|
49
49
|
|
|
50
50
|
---
|
|
51
51
|
|
|
52
|
-
## 2.
|
|
52
|
+
## 2. Modern Paper-Exclusive Events
|
|
53
53
|
|
|
54
|
-
Paper
|
|
54
|
+
Paper introduces modern, high-performance replacements for legacy Bukkit events:
|
|
55
55
|
|
|
56
|
-
### `AsyncChatEvent` (
|
|
57
|
-
Paper
|
|
56
|
+
### `AsyncChatEvent` (Replaces `AsyncPlayerChatEvent`)
|
|
57
|
+
Paper's `io.papermc.paper.event.player.AsyncChatEvent` operates directly with `Component` and MiniMessage:
|
|
58
58
|
|
|
59
59
|
```java
|
|
60
60
|
import io.papermc.paper.event.player.AsyncChatEvent;
|
|
@@ -63,7 +63,7 @@ import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
|
63
63
|
|
|
64
64
|
@EventHandler
|
|
65
65
|
public void onChat(AsyncChatEvent event) {
|
|
66
|
-
//
|
|
66
|
+
// Format chat messages cleanly using Adventure
|
|
67
67
|
event.renderer((source, sourceDisplayName, message, viewer) ->
|
|
68
68
|
MiniMessage.miniMessage().deserialize("<gray>[Member] </gray>")
|
|
69
69
|
.append(sourceDisplayName)
|
|
@@ -74,18 +74,18 @@ public void onChat(AsyncChatEvent event) {
|
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
### `PrePlayerAttackEntityEvent`
|
|
77
|
-
|
|
77
|
+
Fires prior to vanilla attack calculations. Essential for custom combat mechanics, weapon cooldown verification, or anti-griefing checks.
|
|
78
78
|
|
|
79
79
|
### `ServerTickStartEvent` & `ServerTickEndEvent`
|
|
80
|
-
|
|
80
|
+
Provides cycle-accurate server performance metrics per-tick without adding scheduler overhead.
|
|
81
81
|
|
|
82
82
|
---
|
|
83
83
|
|
|
84
|
-
## 3.
|
|
84
|
+
## 3. Creating Custom Events
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
You can create custom events to expose an extensible public API for other plugins (following the design of LuckPerms or WorldGuard):
|
|
87
87
|
|
|
88
|
-
###
|
|
88
|
+
### Custom Event Example:
|
|
89
89
|
```java
|
|
90
90
|
package com.example.plugin.event;
|
|
91
91
|
|
|
@@ -103,7 +103,7 @@ public final class PlayerLevelUpEvent extends Event implements Cancellable {
|
|
|
103
103
|
private boolean cancelled = false;
|
|
104
104
|
|
|
105
105
|
public PlayerLevelUpEvent(Player player, int newLevel) {
|
|
106
|
-
// false
|
|
106
|
+
// false for synchronous execution on main/region thread, true for asynchronous
|
|
107
107
|
super(false);
|
|
108
108
|
this.player = player;
|
|
109
109
|
this.newLevel = newLevel;
|
|
@@ -140,24 +140,23 @@ public final class PlayerLevelUpEvent extends Event implements Cancellable {
|
|
|
140
140
|
}
|
|
141
141
|
```
|
|
142
142
|
|
|
143
|
-
###
|
|
143
|
+
### Dispatching the Custom Event:
|
|
144
144
|
```java
|
|
145
145
|
PlayerLevelUpEvent event = new PlayerLevelUpEvent(player, 10);
|
|
146
146
|
Bukkit.getPluginManager().callEvent(event);
|
|
147
147
|
|
|
148
148
|
if (!event.isCancelled()) {
|
|
149
|
-
|
|
150
|
-
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Selamat, Anda naik level!</green>"));
|
|
149
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Congratulations, you leveled up!</green>"));
|
|
151
150
|
}
|
|
152
151
|
```
|
|
153
152
|
|
|
154
153
|
---
|
|
155
154
|
|
|
156
|
-
## 4.
|
|
155
|
+
## 4. Listener Unregistration & Memory Hygiene
|
|
157
156
|
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
157
|
+
To prevent memory leaks across plugin reload or disable cycles:
|
|
158
|
+
- All listeners registered via `pluginManager.registerEvents(listener, this)` are unregistered automatically upon `onDisable()`.
|
|
159
|
+
- If you register dynamic short-lived listeners (such as for a temporary minigame session), unregister them explicitly upon session completion:
|
|
161
160
|
```java
|
|
162
161
|
HandlerList.unregisterAll(matchListener);
|
|
163
162
|
```
|