papermc-skill 1.0.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/LICENSE +21 -0
- package/README.md +169 -0
- package/bin/cli.js +307 -0
- package/docs/01-modern-paper-architecture.md +172 -0
- package/docs/02-adventure-and-minimessage.md +160 -0
- package/docs/03-persistent-data-container.md +179 -0
- package/docs/04-commands-and-brigadier.md +157 -0
- package/docs/05-concurrency-and-folia.md +150 -0
- package/docs/06-events-and-listeners.md +163 -0
- package/docs/07-best-practices-and-perf.md +118 -0
- package/docs/08-gui-and-menus.md +198 -0
- package/docs/09-custom-items-and-combat.md +171 -0
- package/docs/10-database-and-storage.md +187 -0
- package/package.json +44 -0
- package/skills/paper-plugin-dev/SKILL.md +167 -0
- package/templates/paper-modern-template/.editorconfig +8 -0
- package/templates/paper-modern-template/.gradle/9.4.1/checksums/checksums.lock +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/checksums/md5-checksums.bin +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.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/fileChanges/last-build.bin +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.bin +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.lock +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/resourceHashesCache.bin +0 -0
- package/templates/paper-modern-template/.gradle/9.4.1/gc.properties +0 -0
- package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/templates/paper-modern-template/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/templates/paper-modern-template/.gradle/file-system.probe +0 -0
- package/templates/paper-modern-template/.gradle/vcs-1/gc.properties +0 -0
- 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/data/PluginDataKeys.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 +54 -0
- package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/templates/paper-modern-template/gradlew +248 -0
- package/templates/paper-modern-template/gradlew.bat +93 -0
- package/templates/paper-modern-template/settings.gradle.kts +5 -0
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +35 -0
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +30 -0
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +55 -0
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +22 -0
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +61 -0
- package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +77 -0
- package/templates/paper-modern-template/src/main/resources/config.yml +10 -0
- package/templates/paper-modern-template/src/main/resources/paper-plugin.yml +9 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Konkurensi, Threading & Kompatibilitas Folia
|
|
2
|
+
|
|
3
|
+
Salah satu perubahan paling revolusioner di ekosistem Paper adalah kedatangan **Folia**. Folia memecah satu "Server Main Thread" Minecraft menjadi ratusan thread regional mandiri (**Threaded Regions**).
|
|
4
|
+
|
|
5
|
+
Konsekuensinya, metode konkurensi lama seperti `Bukkit.getScheduler()` atau mengakses state world dari sembarang thread akan menyebabkan **`IllegalStateException` atau crash fatal** di Folia.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Model Threading: Bukkit vs Paper vs Folia
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
[Bukkit / Vanilla]
|
|
13
|
+
Satu Main Thread ─── Tick Semua Dunia, Entity, Blok, Redstone, AI, Chunk ───>
|
|
14
|
+
|
|
15
|
+
[Folia Regionized Threading]
|
|
16
|
+
Region Thread 1 ─── Tick Region Dunia A (Chunk 0..32, 0..32) ───>
|
|
17
|
+
Region Thread 2 ─── Tick Region Dunia A (Chunk 64..96, 64..96) ──>
|
|
18
|
+
Region Thread 3 ─── Tick Nether (Region B) ──────────────────────>
|
|
19
|
+
Global Thread ─── Tick Weather, Time, Player List, Commands ───>
|
|
20
|
+
Async Threads ─── Network I/O, File Saving, Database Queries ──>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Di Folia:
|
|
24
|
+
- Tidak ada lagi konsep "The Main Thread".
|
|
25
|
+
- Objek `World`, `Entity`, dan `Block` hanya boleh diakses di **thread region yang memiliki chunk/lokasi tersebut**.
|
|
26
|
+
- Mengakses entity pemain dari thread lain akan melempar error `Plugin attempted to access entity from wrong thread`.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Hirarki Scheduler Modern di Folia & Paper
|
|
31
|
+
|
|
32
|
+
Paper memperkenalkan scheduler baru yang kompatibel dengan Folia dan Paper standar:
|
|
33
|
+
|
|
34
|
+
### 1. `RegionScheduler`
|
|
35
|
+
Digunakan untuk mengeksekusi task pada lokasi dunia atau koordinat chunk tertentu.
|
|
36
|
+
|
|
37
|
+
```java
|
|
38
|
+
import org.bukkit.Bukkit;
|
|
39
|
+
import org.bukkit.Location;
|
|
40
|
+
import org.bukkit.plugin.Plugin;
|
|
41
|
+
|
|
42
|
+
Location loc = player.getLocation();
|
|
43
|
+
|
|
44
|
+
// Menjalankan task 1-kali pada thread region lokasi tersebut
|
|
45
|
+
Bukkit.getRegionScheduler().execute(plugin, loc, () -> {
|
|
46
|
+
loc.getBlock().setType(org.bukkit.Material.GOLD_BLOCK);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Menjalankan task berulang (repeating) pada region
|
|
50
|
+
Bukkit.getRegionScheduler().runAtFixedRate(plugin, loc, task -> {
|
|
51
|
+
loc.getWorld().spawnParticle(org.bukkit.Particle.FLAME, loc, 5);
|
|
52
|
+
}, 20L, 20L); // delay 20 tick, period 20 tick
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. `EntityScheduler`
|
|
56
|
+
Setiap `Entity` memiliki scheduler tersendiri. Task akan dieksekusi tepat pada thread region di mana entity tersebut sedang berada, bahkan jika entity tersebut bergerak/teleport antar region.
|
|
57
|
+
|
|
58
|
+
```java
|
|
59
|
+
player.getScheduler().run(plugin, task -> {
|
|
60
|
+
player.giveExp(10);
|
|
61
|
+
player.sendMessage("Bonus EXP didapatkan!");
|
|
62
|
+
}, () -> {
|
|
63
|
+
// Callback jika entity telah mati atau tidak valid sebelum task sempat berjalan
|
|
64
|
+
plugin.getLogger().warning("Pemain logout sebelum exp diberikan.");
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 3. `GlobalRegionScheduler`
|
|
69
|
+
Digunakan untuk hal-hal yang bersifat global server dan tidak terikat pada koordinat block/entity tertentu (misal: pengumuman berkala chat global, update scoreboard global, waktu dunia).
|
|
70
|
+
|
|
71
|
+
```java
|
|
72
|
+
Bukkit.getGlobalRegionScheduler().runAtFixedRate(plugin, task -> {
|
|
73
|
+
Bukkit.broadcast(MiniMessage.miniMessage().deserialize("<gold>[Pengumuman] Kunjungi discord kami!</gold>"));
|
|
74
|
+
}, 100L, 1200L);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 4. `AsyncScheduler`
|
|
78
|
+
Untuk I/O, database query, request HTTP, komputasi berat non-Bukkit API. Menggunakan waktu nyata (`java.time.TimeUnit`), bukan tick game:
|
|
79
|
+
|
|
80
|
+
```java
|
|
81
|
+
import java.util.concurrent.TimeUnit;
|
|
82
|
+
|
|
83
|
+
Bukkit.getAsyncScheduler().runDelayed(plugin, task -> {
|
|
84
|
+
// Menjalankan query database MySQL / SQLite
|
|
85
|
+
database.savePlayerData(uuid, data);
|
|
86
|
+
}, 5, TimeUnit.SECONDS);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 3. Pola Abstraksi Scheduler Universal (Paper + Folia)
|
|
92
|
+
|
|
93
|
+
Banyak plugin perlu berjalan di server Paper standar (single-thread) dan Folia sekaligus. Cara terbaik adalah membuat lapisan pembungkus (*abstraction wrapper*) seperti yang digunakan oleh **Chunky** dan **squaremap**:
|
|
94
|
+
|
|
95
|
+
```java
|
|
96
|
+
public final class PlatformScheduler {
|
|
97
|
+
|
|
98
|
+
private static final boolean IS_FOLIA = checkFolia();
|
|
99
|
+
|
|
100
|
+
private static boolean checkFolia() {
|
|
101
|
+
try {
|
|
102
|
+
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
|
|
103
|
+
return true;
|
|
104
|
+
} catch (ClassNotFoundException e) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public static boolean isFolia() {
|
|
110
|
+
return IS_FOLIA;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public static void runLocation(Plugin plugin, Location location, Runnable runnable) {
|
|
114
|
+
if (IS_FOLIA) {
|
|
115
|
+
Bukkit.getRegionScheduler().execute(plugin, location, runnable);
|
|
116
|
+
} else {
|
|
117
|
+
Bukkit.getScheduler().runTask(plugin, runnable);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public static void runEntity(Plugin plugin, Entity entity, Runnable runnable) {
|
|
122
|
+
if (IS_FOLIA) {
|
|
123
|
+
entity.getScheduler().run(plugin, task -> runnable.run(), null);
|
|
124
|
+
} else {
|
|
125
|
+
Bukkit.getScheduler().runTask(plugin, runnable);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public static void runAsync(Plugin plugin, Runnable runnable) {
|
|
130
|
+
if (IS_FOLIA) {
|
|
131
|
+
Bukkit.getAsyncScheduler().runNow(plugin, task -> runnable.run());
|
|
132
|
+
} else {
|
|
133
|
+
Bukkit.getScheduler().runTaskAsynchronously(plugin, runnable);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 4. Aturan Wajib untuk Kompatibilitas Folia
|
|
142
|
+
|
|
143
|
+
1. **Deklarasikan di Manifest**:
|
|
144
|
+
Tambahkan `folia-supported: true` di `paper-plugin.yml`. Jika tidak, server Folia akan menampilkan peringatan bahwa plugin Anda berpotensi tidak aman.
|
|
145
|
+
2. **Jangan Pernah Mengakses Chunk/Block dari Async Thread**:
|
|
146
|
+
Gunakan `world.getChunkAtAsync(x, z)` yang mengembalikan `CompletableFuture<Chunk>`.
|
|
147
|
+
3. **Gunakan TeleportAsync**:
|
|
148
|
+
Gunakan `entity.teleportAsync(location)` alih-alih `entity.teleport(location)`. Di Folia, teleportasi melintasi region thread yang berbeda harus dilakukan secara asinkron.
|
|
149
|
+
4. **Hindari State Statis Global**:
|
|
150
|
+
Variabel static yang menyimpan list player atau entity aktif dapat mengalami *race condition* jika dimodifikasi oleh beberapa thread region secara paralel. Gunakan struktur data thread-safe (`ConcurrentHashMap`, `CopyOnWriteArrayList`) bila terpaksa.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Sistem Event & Listener di Paper
|
|
2
|
+
|
|
3
|
+
Sistem event adalah mekanisme reaktif utama di Paper untuk merespons aksi pemain, lingkungan dunia, entitas, dan siklus server. Paper menyediakan banyak event eksklusif berkinerja tinggi serta integrasi penuh dengan Adventure Component.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Anatomi Listener Standar
|
|
8
|
+
|
|
9
|
+
Listener mengimplementasikan antarmuka `org.bukkit.event.Listener` dan menggunakan anotasi `@EventHandler`:
|
|
10
|
+
|
|
11
|
+
```java
|
|
12
|
+
import org.bukkit.event.Listener;
|
|
13
|
+
import org.bukkit.event.EventHandler;
|
|
14
|
+
import org.bukkit.event.EventPriority;
|
|
15
|
+
import org.bukkit.event.player.PlayerJoinEvent;
|
|
16
|
+
import org.bukkit.plugin.java.JavaPlugin;
|
|
17
|
+
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
18
|
+
|
|
19
|
+
public final class PlayerJoinListener implements Listener {
|
|
20
|
+
|
|
21
|
+
private final JavaPlugin plugin;
|
|
22
|
+
|
|
23
|
+
public PlayerJoinListener(JavaPlugin plugin) {
|
|
24
|
+
this.plugin = plugin;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
|
28
|
+
public void onPlayerJoin(PlayerJoinEvent event) {
|
|
29
|
+
// Paper menggunakan Adventure Component untuk joinMessage
|
|
30
|
+
event.joinMessage(
|
|
31
|
+
MiniMessage.miniMessage().deserialize(
|
|
32
|
+
"<gray>[<green>+</green>] <yellow>" + event.getPlayer().getName() + "</yellow> bergabung ke server!</gray>"
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Memahami `EventPriority`:
|
|
40
|
+
Urutan eksekusi event dari pertama hingga terakhir:
|
|
41
|
+
1. `LOWEST` (dijalankan paling awal, bagus untuk observasi mentah)
|
|
42
|
+
2. `LOW`
|
|
43
|
+
3. `NORMAL` (default)
|
|
44
|
+
4. `HIGH`
|
|
45
|
+
5. `HIGHEST` (dijalankan paling akhir sebelum event final)
|
|
46
|
+
6. `MONITOR` (**HANYA UNTUK MEMBACA/LOGGING**; DILARANG memodifikasi atau membatalkan event di priority ini karena plugin lain tidak akan dapat merespons perubahan Anda).
|
|
47
|
+
|
|
48
|
+
> **Penting**: Selalu tambahkan `ignoreCancelled = true` jika listener Anda tidak perlu memproses aksi yang sudah dibatalkan oleh plugin proteksi area (seperti WorldGuard/GriefPrevention).
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 2. Event Modern Eksklusif Paper
|
|
53
|
+
|
|
54
|
+
Paper mengganti atau melengkapi beberapa event Bukkit lama dengan alternatif yang lebih efisien dan modern:
|
|
55
|
+
|
|
56
|
+
### `AsyncChatEvent` (Menggantikan `AsyncPlayerChatEvent`)
|
|
57
|
+
Paper menggunakan `io.papermc.paper.event.player.AsyncChatEvent` yang bekerja langsung dengan `Component` dan MiniMessage:
|
|
58
|
+
|
|
59
|
+
```java
|
|
60
|
+
import io.papermc.paper.event.player.AsyncChatEvent;
|
|
61
|
+
import net.kyori.adventure.text.Component;
|
|
62
|
+
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
63
|
+
|
|
64
|
+
@EventHandler
|
|
65
|
+
public void onChat(AsyncChatEvent event) {
|
|
66
|
+
// Memodifikasi format pesan dengan Adventure
|
|
67
|
+
event.renderer((source, sourceDisplayName, message, viewer) ->
|
|
68
|
+
MiniMessage.miniMessage().deserialize("<gray>[Member] </gray>")
|
|
69
|
+
.append(sourceDisplayName)
|
|
70
|
+
.append(Component.text(": "))
|
|
71
|
+
.append(message)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `PrePlayerAttackEntityEvent`
|
|
77
|
+
Dijalankan sebelum kalkulasi serangan vanilla terjadi. Sangat ideal untuk sistem combat kustom, cooldown attack senjata khusus, atau verifikasi proteksi PvP.
|
|
78
|
+
|
|
79
|
+
### `ServerTickStartEvent` & `ServerTickEndEvent`
|
|
80
|
+
Memonitor performa server secara presisi per-tick tanpa membebani scheduler.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 3. Membuat Custom Event
|
|
85
|
+
|
|
86
|
+
Anda dapat membuat event sendiri untuk mengekspos API plugin Anda kepada developer lain (seperti yang dilakukan oleh LuckPerms atau WorldGuard).
|
|
87
|
+
|
|
88
|
+
### Contoh Custom Event:
|
|
89
|
+
```java
|
|
90
|
+
package com.example.plugin.event;
|
|
91
|
+
|
|
92
|
+
import org.bukkit.entity.Player;
|
|
93
|
+
import org.bukkit.event.Cancellable;
|
|
94
|
+
import org.bukkit.event.Event;
|
|
95
|
+
import org.bukkit.event.HandlerList;
|
|
96
|
+
import org.jetbrains.annotations.NotNull;
|
|
97
|
+
|
|
98
|
+
public final class PlayerLevelUpEvent extends Event implements Cancellable {
|
|
99
|
+
|
|
100
|
+
private static final HandlerList HANDLERS = new HandlerList();
|
|
101
|
+
private final Player player;
|
|
102
|
+
private final int newLevel;
|
|
103
|
+
private boolean cancelled = false;
|
|
104
|
+
|
|
105
|
+
public PlayerLevelUpEvent(Player player, int newLevel) {
|
|
106
|
+
// false jika berjalan di main/region thread (Sync), true jika Async
|
|
107
|
+
super(false);
|
|
108
|
+
this.player = player;
|
|
109
|
+
this.newLevel = newLevel;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public Player getPlayer() {
|
|
113
|
+
return player;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public int getNewLevel() {
|
|
117
|
+
return newLevel;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Override
|
|
121
|
+
public boolean isCancelled() {
|
|
122
|
+
return cancelled;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Override
|
|
126
|
+
public void setCancelled(boolean cancel) {
|
|
127
|
+
this.cancelled = cancel;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@NotNull
|
|
131
|
+
@Override
|
|
132
|
+
public HandlerList getHandlers() {
|
|
133
|
+
return HANDLERS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@NotNull
|
|
137
|
+
public static HandlerList getHandlerList() {
|
|
138
|
+
return HANDLERS;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Memanggil (Dispatch) Event:
|
|
144
|
+
```java
|
|
145
|
+
PlayerLevelUpEvent event = new PlayerLevelUpEvent(player, 10);
|
|
146
|
+
Bukkit.getPluginManager().callEvent(event);
|
|
147
|
+
|
|
148
|
+
if (!event.isCancelled()) {
|
|
149
|
+
// Lanjutkan aksi jika event tidak dibatalkan oleh plugin lain
|
|
150
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<green>Selamat, Anda naik level!</green>"));
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 4. Unregistering Event & Pembersihan
|
|
157
|
+
|
|
158
|
+
Untuk mencegah memory leak saat reload atau saat plugin dinonaktifkan:
|
|
159
|
+
- Semua event yang didaftarkan melalui `pluginManager.registerEvents(listener, this)` otomatis di-unregister saat `onDisable()`.
|
|
160
|
+
- Jika Anda mendaftarkan event dinamis (misal per-minigame match), unregister secara manual ketika game selesai:
|
|
161
|
+
```java
|
|
162
|
+
HandlerList.unregisterAll(matchListener);
|
|
163
|
+
```
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Best Practices & Optimasi Performa Plugin Paper
|
|
2
|
+
|
|
3
|
+
Pengembangan plugin Minecraft di server berkapasitas besar menuntut efisiensi tinggi, bebas memory leak, dan tidak memblokir tick game. Dokumen ini merangkum aturan emas dan anti-pattern yang sering terjadi.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Mencegah Memory Leak (Kebocoran Memori)
|
|
8
|
+
|
|
9
|
+
Minecraft memiliki arsitektur objek yang saling terhubung erat. Menyimpan referensi objek Bukkit di memori Java lebih lama dari masa hidupnya dapat mencegah Garbage Collector (GC) membersihkan ratusan megabyte data.
|
|
10
|
+
|
|
11
|
+
### 🔴 Anti-Pattern: Menyimpan Objek Bukkit di Collection Statis
|
|
12
|
+
```java
|
|
13
|
+
// SANGAT BERBAHAYA: Player objek menahan referensi ke World, Chunk, Connection, dll.
|
|
14
|
+
public static final Set<Player> activePlayers = new HashSet<>();
|
|
15
|
+
public static final Map<Player, PlayerData> dataMap = new HashMap<>();
|
|
16
|
+
```
|
|
17
|
+
Jika pemain disconnect, objek `Player` tidak akan bisa di-garbage collect!
|
|
18
|
+
|
|
19
|
+
### 🟢 Solusi: Simpan `UUID`
|
|
20
|
+
```java
|
|
21
|
+
// AMAN: UUID hanya 16-byte identifier yang tidak menahan referensi dunia/koneksi
|
|
22
|
+
public static final Set<UUID> activePlayerUuids = new HashSet<>();
|
|
23
|
+
public static final Map<UUID, PlayerData> dataMap = new ConcurrentHashMap<>();
|
|
24
|
+
|
|
25
|
+
// Ambil instance player hanya saat dibutuhkan:
|
|
26
|
+
Player player = Bukkit.getPlayer(uuid);
|
|
27
|
+
if (player != null) {
|
|
28
|
+
// Gunakan
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> **Aturan Umum**: Jangan pernah menyimpan `Player`, `Entity`, `World`, `Chunk`, atau `Block` di dalam field permanen/statis. Gunakan `UUID`, `NamespacedKey`, atau koordinat primitif (`worldName`, `x`, `y`, `z`).
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Operasi I/O & Database Selalu Asinkron
|
|
37
|
+
|
|
38
|
+
Setiap tick server berjalan dalam ~50 milidetik (20 TPS). Operasi disk I/O, pembacaan file besar, koneksi web/HTTP, atau query database yang membutuhkan waktu 100ms akan langsung menyebabkan server lag/freeze (tps drop).
|
|
39
|
+
|
|
40
|
+
### 🟢 Solusi: Gunakan Async Threads & CompletableFuture
|
|
41
|
+
```java
|
|
42
|
+
import java.util.concurrent.CompletableFuture;
|
|
43
|
+
import org.bukkit.Bukkit;
|
|
44
|
+
|
|
45
|
+
public CompletableFuture<PlayerData> loadDataAsync(UUID uuid) {
|
|
46
|
+
return CompletableFuture.supplyAsync(() -> {
|
|
47
|
+
// Berjalan di luar thread game (off-main-thread)
|
|
48
|
+
return database.fetchUser(uuid);
|
|
49
|
+
}, myPluginThreadPool).thenApply(data -> {
|
|
50
|
+
return data != null ? data : new PlayerData(uuid);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Menggunakan hasilnya:
|
|
55
|
+
loadDataAsync(player.getUniqueId()).thenAccept(data -> {
|
|
56
|
+
// Jika perlu memodifikasi state game Bukkit (seperti memberi item),
|
|
57
|
+
// jadwalkan kembali ke scheduler region pemain:
|
|
58
|
+
player.getScheduler().run(plugin, task -> {
|
|
59
|
+
player.giveExp(data.getPendingExp());
|
|
60
|
+
}, null);
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 3. Menghindari Synchronous Chunk Loading
|
|
67
|
+
|
|
68
|
+
Memanggil `world.getChunkAt(x, z)` atau `location.getBlock()` pada chunk yang belum di-load akan memaksa server memuat chunk tersebut dari disk secara synchronous (membekukan tick thread hingga chunk selesai dibaca).
|
|
69
|
+
|
|
70
|
+
### 🔴 Anti-Pattern
|
|
71
|
+
```java
|
|
72
|
+
Chunk chunk = world.getChunkAt(x, z); // Membekukan server jika chunk belum di-load!
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 🟢 Solusi Paper: Asynchronous Chunk Loading
|
|
76
|
+
Gunakan API Paper `getChunkAtAsync`:
|
|
77
|
+
|
|
78
|
+
```java
|
|
79
|
+
world.getChunkAtAsync(x, z, true).thenAccept(chunk -> {
|
|
80
|
+
// Chunk berhasil dimuat tanpa lag spike pada TPS
|
|
81
|
+
processChunkBlocks(chunk);
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 4. Konfigurasi (`config.yml`) yang Tangguh
|
|
88
|
+
|
|
89
|
+
Di Paper, file YAML secara default menggunakan encoding UTF-8. Pastikan membaca dan menyimpan file konfigurasi dengan aman:
|
|
90
|
+
|
|
91
|
+
```java
|
|
92
|
+
// Menyimpan default config dari jar jika belum ada
|
|
93
|
+
saveDefaultConfig();
|
|
94
|
+
|
|
95
|
+
// Reload config
|
|
96
|
+
reloadConfig();
|
|
97
|
+
FileConfiguration config = getConfig();
|
|
98
|
+
|
|
99
|
+
// Mengambil pesan dengan fallback default
|
|
100
|
+
String rawMessage = config.getString("messages.welcome", "<green>Selamat datang!</green>");
|
|
101
|
+
Component message = MiniMessage.miniMessage().deserialize(rawMessage);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Untuk struktur config yang sangat kompleks, pertimbangkan menggunakan library **Configurate** (`org.spongepowered:configurate-yaml`) yang mendukung pemeliharaan komentar file YAML, schema validation, dan serialisasi otomatis ke record Java.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## 5. Ringkasan Checklist Anti-Pattern
|
|
109
|
+
|
|
110
|
+
| Jangan Lakukan ❌ | Lakukan Ini ✔️ |
|
|
111
|
+
| :--- | :--- |
|
|
112
|
+
| `ChatColor.RED + "Teks"` | `MiniMessage.miniMessage().deserialize("<red>Teks</red>")` |
|
|
113
|
+
| `player.sendMessage("§a...")` | `player.sendMessage(Component)` |
|
|
114
|
+
| `new NamespacedKey(...)` di per-event loop | Simpan instance `NamespacedKey` sebagai `static final` |
|
|
115
|
+
| `world.getBlockAt(x, y, z)` acak di async thread | Akses block hanya di thread region yang bersangkutan |
|
|
116
|
+
| Mengabaikan `paper-plugin.yml` untuk plugin baru | Manfaatkan `paper-plugin.yml` dan `PluginBootstrap` |
|
|
117
|
+
| Menahan koneksi database terbuka di main thread | Gunakan connection pool (seperti HikariCP) & query async |
|
|
118
|
+
| Mengabaikan `folia-supported` | Rancang scheduler menggunakan abstraksi region / universal |
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Pembuatan GUI / Menu Inventory Modern di Paper
|
|
2
|
+
|
|
3
|
+
Membuat antarmuka interaktif (GUI Menu) menggunakan inventaris chest adalah salah satu fitur paling sering dibuat di plugin Minecraft. Pendekatan lama yang memeriksa judul GUI dengan string berwarna (`ChatColor.stripColor`) atau mencocokkan nomor slot sangat rapuh dan rawan bug duplikasi item.
|
|
4
|
+
|
|
5
|
+
Paper modern menawarkan cara yang jauh lebih aman, bersih, dan type-safe menggunakan **Kyori Adventure** dan **PersistentDataContainer (PDC)**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Masalah pada Pendekatan Lama vs Solusi Modern
|
|
10
|
+
|
|
11
|
+
| Masalah Legacy (Lama) | Solusi Modern Paper |
|
|
12
|
+
| :--- | :--- |
|
|
13
|
+
| Memeriksa judul menu dengan `event.getView().getTitle().equals("§8Menu")` | Memeriksa `InventoryHolder` kustom atau PDC metadata pada item. |
|
|
14
|
+
| Mencocokkan fungsi tombol berdasarkan `event.getSlot() == 13` | Menandai tombol dengan `PDC` (`NamespacedKey: "gui_action" -> "buy_item"`). |
|
|
15
|
+
| Merusak format warna teks jika ada reload atau translate | Menggunakan Adventure `Component` untuk title inventaris. |
|
|
16
|
+
| Rawan bug eksploitasi duplikasi jika `event.setCancelled(true)` luput | Selalu batalkan event jika holder terverifikasi sebagai custom GUI. |
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. Pola Custom `InventoryHolder`
|
|
21
|
+
|
|
22
|
+
Cara paling handal dan standar industri untuk mengenali GUI buatan plugin Anda adalah membuat kelas yang mengimplementasikan `InventoryHolder`:
|
|
23
|
+
|
|
24
|
+
```java
|
|
25
|
+
package com.example.plugin.gui;
|
|
26
|
+
|
|
27
|
+
import net.kyori.adventure.text.Component;
|
|
28
|
+
import org.bukkit.Bukkit;
|
|
29
|
+
import org.bukkit.inventory.Inventory;
|
|
30
|
+
import org.bukkit.inventory.InventoryHolder;
|
|
31
|
+
import org.jetbrains.annotations.NotNull;
|
|
32
|
+
|
|
33
|
+
public class CustomMenuHolder implements InventoryHolder {
|
|
34
|
+
|
|
35
|
+
private final Inventory inventory;
|
|
36
|
+
private final String menuId;
|
|
37
|
+
|
|
38
|
+
public CustomMenuHolder(Component title, int rows, String menuId) {
|
|
39
|
+
this.menuId = menuId;
|
|
40
|
+
// Bukkit.createInventory mendukung Adventure Component secara native di Paper
|
|
41
|
+
this.inventory = Bukkit.createInventory(this, rows * 9, title);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public String getMenuId() {
|
|
45
|
+
return menuId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@Override
|
|
49
|
+
public @NotNull Inventory getInventory() {
|
|
50
|
+
return inventory;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 3. Membuat Tombol dengan Penanda PDC
|
|
58
|
+
|
|
59
|
+
Daripada mengingat nomor slot, tandai setiap item tombol dengan aksi spesifik di `PersistentDataContainer`:
|
|
60
|
+
|
|
61
|
+
```java
|
|
62
|
+
package com.example.plugin.gui;
|
|
63
|
+
|
|
64
|
+
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
65
|
+
import org.bukkit.Material;
|
|
66
|
+
import org.bukkit.NamespacedKey;
|
|
67
|
+
import org.bukkit.inventory.ItemStack;
|
|
68
|
+
import org.bukkit.inventory.meta.ItemMeta;
|
|
69
|
+
import org.bukkit.persistence.PersistentDataType;
|
|
70
|
+
import org.bukkit.plugin.Plugin;
|
|
71
|
+
|
|
72
|
+
import java.util.List;
|
|
73
|
+
|
|
74
|
+
public final class GuiItemBuilder {
|
|
75
|
+
|
|
76
|
+
public static final NamespacedKey ACTION_KEY = new NamespacedKey("myplugin", "gui_action");
|
|
77
|
+
|
|
78
|
+
public static ItemStack createButton(Material material, String nameMiniMessage, List<String> loreMiniMessage, String actionId) {
|
|
79
|
+
ItemStack item = new ItemStack(material);
|
|
80
|
+
ItemMeta meta = item.getItemMeta();
|
|
81
|
+
if (meta != null) {
|
|
82
|
+
meta.displayName(MiniMessage.miniMessage().deserialize(nameMiniMessage));
|
|
83
|
+
|
|
84
|
+
meta.lore(loreMiniMessage.stream()
|
|
85
|
+
.map(line -> MiniMessage.miniMessage().deserialize(line))
|
|
86
|
+
.toList()
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Tandai tombol dengan ID aksi unik
|
|
90
|
+
meta.getPersistentDataContainer().set(ACTION_KEY, PersistentDataType.STRING, actionId);
|
|
91
|
+
item.setItemMeta(meta);
|
|
92
|
+
}
|
|
93
|
+
return item;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 4. Merakit Menu
|
|
101
|
+
|
|
102
|
+
```java
|
|
103
|
+
public void openShopMenu(Player player) {
|
|
104
|
+
CustomMenuHolder holder = new CustomMenuHolder(
|
|
105
|
+
MiniMessage.miniMessage().deserialize("<gradient:#f12711:#f5af19><bold>Toko Server</bold></gradient>"),
|
|
106
|
+
3, // 3 baris = 27 slot
|
|
107
|
+
"server_shop"
|
|
108
|
+
);
|
|
109
|
+
Inventory inv = holder.getInventory();
|
|
110
|
+
|
|
111
|
+
// Border pengisi (filler)
|
|
112
|
+
ItemStack filler = GuiItemBuilder.createButton(
|
|
113
|
+
Material.GRAY_STAINED_GLASS_PANE,
|
|
114
|
+
"<gray> </gray>",
|
|
115
|
+
List.of(),
|
|
116
|
+
"noop" // No operation
|
|
117
|
+
);
|
|
118
|
+
for (int i = 0; i < inv.getSize(); i++) {
|
|
119
|
+
inv.setItem(i, filler);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Tombol aksi
|
|
123
|
+
inv.setItem(11, GuiItemBuilder.createButton(
|
|
124
|
+
Material.DIAMOND_SWORD,
|
|
125
|
+
"<aqua><bold>Beli Senjata Legendaris</bold></aqua>",
|
|
126
|
+
List.of("<gray>Harga: <gold>500 Koin</gold></gray>", "<yellow>Klik untuk membeli!</yellow>"),
|
|
127
|
+
"buy_sword"
|
|
128
|
+
));
|
|
129
|
+
|
|
130
|
+
inv.setItem(15, GuiItemBuilder.createButton(
|
|
131
|
+
Material.BARRIER,
|
|
132
|
+
"<red><bold>Tutup Menu</bold></red>",
|
|
133
|
+
List.of("<gray>Klik untuk keluar</gray>"),
|
|
134
|
+
"close_menu"
|
|
135
|
+
));
|
|
136
|
+
|
|
137
|
+
player.openInventory(inv);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 5. Event Listener Menu yang Aman dari Eksploitasi
|
|
144
|
+
|
|
145
|
+
```java
|
|
146
|
+
package com.example.plugin.gui;
|
|
147
|
+
|
|
148
|
+
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
149
|
+
import org.bukkit.entity.Player;
|
|
150
|
+
import org.bukkit.event.EventHandler;
|
|
151
|
+
import org.bukkit.event.EventPriority;
|
|
152
|
+
import org.bukkit.event.Listener;
|
|
153
|
+
import org.bukkit.event.inventory.InventoryClickEvent;
|
|
154
|
+
import org.bukkit.inventory.ItemStack;
|
|
155
|
+
import org.bukkit.persistence.PersistentDataType;
|
|
156
|
+
|
|
157
|
+
public final class MenuClickListener implements Listener {
|
|
158
|
+
|
|
159
|
+
@EventHandler(priority = EventPriority.HIGHEST)
|
|
160
|
+
public void onInventoryClick(InventoryClickEvent event) {
|
|
161
|
+
// 1. Verifikasi apakah inventory yang diklik adalah custom holder kita
|
|
162
|
+
if (!(event.getInventory().getHolder() instanceof CustomMenuHolder holder)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 2. SELALU batalkan klik untuk mencegah pemain mengambil/memindahkan item GUI!
|
|
167
|
+
event.setCancelled(true);
|
|
168
|
+
|
|
169
|
+
// 3. Abaikan jika klik di luar jendela atau slot kosong
|
|
170
|
+
ItemStack clickedItem = event.getCurrentItem();
|
|
171
|
+
if (clickedItem == null || !clickedItem.hasItemMeta()) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!(event.getWhoClicked() instanceof Player player)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 4. Baca aksi dari PDC item
|
|
180
|
+
String action = clickedItem.getItemMeta().getPersistentDataContainer()
|
|
181
|
+
.get(GuiItemBuilder.ACTION_KEY, PersistentDataType.STRING);
|
|
182
|
+
|
|
183
|
+
if (action == null || action.equals("noop")) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 5. Eksekusi aksi berdasarkan action ID
|
|
188
|
+
switch (action) {
|
|
189
|
+
case "buy_sword" -> {
|
|
190
|
+
player.sendMessage(MiniMessage.miniMessage().deserialize("<green>Sukses membeli senjata!</green>"));
|
|
191
|
+
player.closeInventory();
|
|
192
|
+
}
|
|
193
|
+
case "close_menu" -> player.closeInventory();
|
|
194
|
+
default -> player.sendMessage(MiniMessage.miniMessage().deserialize("<red>Aksi tidak dikenal.</red>"));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|