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,171 @@
|
|
|
1
|
+
# Custom Items, Ability & Combat di Paper
|
|
2
|
+
|
|
3
|
+
Membuat item kustom dengan kemampuan sihir atau skill khusus (seperti tongkat sihir petir, pedang lifesteal, atau bow peledak) adalah fitur paling favorit di server RPG dan Mini-games.
|
|
4
|
+
|
|
5
|
+
Paper menyediakan API native untuk raycasting, particle, cooldown visual vanilla, dan Adventure sound effects tanpa perlu library eksternal.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Anatomi Custom Item dengan PDC
|
|
10
|
+
|
|
11
|
+
Selalu identifikasi item kustom menggunakan `PersistentDataContainer` (PDC), bukan nama atau lore yang bisa dipalsukan pemain via Anvil:
|
|
12
|
+
|
|
13
|
+
```java
|
|
14
|
+
package com.example.plugin.item;
|
|
15
|
+
|
|
16
|
+
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
17
|
+
import org.bukkit.Material;
|
|
18
|
+
import org.bukkit.NamespacedKey;
|
|
19
|
+
import org.bukkit.inventory.ItemStack;
|
|
20
|
+
import org.bukkit.inventory.meta.ItemMeta;
|
|
21
|
+
import org.bukkit.persistence.PersistentDataType;
|
|
22
|
+
|
|
23
|
+
import java.util.List;
|
|
24
|
+
|
|
25
|
+
public final class CustomItemFactory {
|
|
26
|
+
|
|
27
|
+
public static final NamespacedKey ITEM_ID_KEY = new NamespacedKey("myplugin", "item_id");
|
|
28
|
+
public static final NamespacedKey COOLDOWN_KEY = new NamespacedKey("myplugin", "ability_cooldown");
|
|
29
|
+
|
|
30
|
+
public static ItemStack createLightningWand() {
|
|
31
|
+
ItemStack item = new ItemStack(Material.BLAZE_ROD);
|
|
32
|
+
ItemMeta meta = item.getItemMeta();
|
|
33
|
+
if (meta != null) {
|
|
34
|
+
meta.displayName(MiniMessage.miniMessage().deserialize("<gradient:#ffe259:#ffa751><bold>Tongkat Petir Kuno</bold></gradient>"));
|
|
35
|
+
meta.lore(List.of(
|
|
36
|
+
MiniMessage.miniMessage().deserialize("<gray>Klik kanan untuk menembakkan <yellow>Petir Murni</yellow>.</gray>"),
|
|
37
|
+
MiniMessage.miniMessage().deserialize("<dark_gray>Cooldown: <gold>3 Detik</gold></dark_gray>")
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
// Tandai ID item
|
|
41
|
+
meta.getPersistentDataContainer().set(ITEM_ID_KEY, PersistentDataType.STRING, "lightning_wand");
|
|
42
|
+
item.setItemMeta(meta);
|
|
43
|
+
}
|
|
44
|
+
return item;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. Sistem Cooldown: Visual Vanilla vs Timestamp
|
|
52
|
+
|
|
53
|
+
Minecraft memiliki API bawaan untuk menampilkan cooldown visual (animasi abu-abu berputar pada hotbar slot):
|
|
54
|
+
|
|
55
|
+
```java
|
|
56
|
+
// Memberikan cooldown visual vanilla selama 60 tick (3 detik) pada Blaze Rod
|
|
57
|
+
player.setCooldown(Material.BLAZE_ROD, 60);
|
|
58
|
+
|
|
59
|
+
// Memeriksa apakah pemain sedang dalam cooldown
|
|
60
|
+
if (player.hasCooldown(Material.BLAZE_ROD)) {
|
|
61
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Kemampuan masih cooldown!</red>"));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 3. Raycasting Entitas & Partikel (Skill Sihir)
|
|
69
|
+
|
|
70
|
+
Raycasting digunakan untuk mendeteksi apa yang sedang dibidik pemain secara presisi menggunakan `World#rayTraceEntities` atau kalkulasi vektor:
|
|
71
|
+
|
|
72
|
+
```java
|
|
73
|
+
package com.example.plugin.item;
|
|
74
|
+
|
|
75
|
+
import net.kyori.adventure.sound.Sound;
|
|
76
|
+
import net.kyori.adventure.text.minimessage.MiniMessage;
|
|
77
|
+
import org.bukkit.FluidCollisionMode;
|
|
78
|
+
import org.bukkit.Location;
|
|
79
|
+
import org.bukkit.Material;
|
|
80
|
+
import org.bukkit.Particle;
|
|
81
|
+
import org.bukkit.SoundCategory;
|
|
82
|
+
import org.bukkit.World;
|
|
83
|
+
import org.bukkit.entity.Damageable;
|
|
84
|
+
import org.bukkit.entity.Entity;
|
|
85
|
+
import org.bukkit.entity.Player;
|
|
86
|
+
import org.bukkit.event.EventHandler;
|
|
87
|
+
import org.bukkit.event.Listener;
|
|
88
|
+
import org.bukkit.event.block.Action;
|
|
89
|
+
import org.bukkit.event.player.PlayerInteractEvent;
|
|
90
|
+
import org.bukkit.inventory.ItemStack;
|
|
91
|
+
import org.bukkit.persistence.PersistentDataType;
|
|
92
|
+
import org.bukkit.util.RayTraceResult;
|
|
93
|
+
import org.bukkit.util.Vector;
|
|
94
|
+
|
|
95
|
+
public final class CustomItemListener implements Listener {
|
|
96
|
+
|
|
97
|
+
@EventHandler
|
|
98
|
+
public void onInteract(PlayerInteractEvent event) {
|
|
99
|
+
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
ItemStack item = event.getItem();
|
|
104
|
+
if (item == null || !item.hasItemMeta()) return;
|
|
105
|
+
|
|
106
|
+
String itemId = item.getItemMeta().getPersistentDataContainer()
|
|
107
|
+
.get(CustomItemFactory.ITEM_ID_KEY, PersistentDataType.STRING);
|
|
108
|
+
|
|
109
|
+
if (!"lightning_wand".equals(itemId)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
event.setCancelled(true);
|
|
114
|
+
Player player = event.getPlayer();
|
|
115
|
+
|
|
116
|
+
// 1. Cek Cooldown
|
|
117
|
+
if (player.hasCooldown(Material.BLAZE_ROD)) {
|
|
118
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<red>Tunggu sejenak sebelum menembak lagi!</red>"));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Set cooldown 3 detik (60 ticks)
|
|
123
|
+
player.setCooldown(Material.BLAZE_ROD, 60);
|
|
124
|
+
|
|
125
|
+
// 2. Eksekusi Efek Serangan
|
|
126
|
+
castLightningBeam(player);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private void castLightningBeam(Player player) {
|
|
130
|
+
World world = player.getWorld();
|
|
131
|
+
Location eyeLoc = player.getEyeLocation();
|
|
132
|
+
Vector direction = eyeLoc.getDirection().normalize();
|
|
133
|
+
|
|
134
|
+
// Putar suara tembakan sihir
|
|
135
|
+
player.playSound(Sound.sound(
|
|
136
|
+
org.bukkit.Sound.ENTITY_ILLUSIONER_CAST_SPELL.key(),
|
|
137
|
+
Sound.Source.PLAYER,
|
|
138
|
+
1.0f,
|
|
139
|
+
1.5f
|
|
140
|
+
));
|
|
141
|
+
|
|
142
|
+
// Partikel beam sepanjang garis pandang (hingga 30 blok)
|
|
143
|
+
for (double d = 1; d <= 30; d += 0.5) {
|
|
144
|
+
Location point = eyeLoc.clone().add(direction.clone().multiply(d));
|
|
145
|
+
world.spawnParticle(Particle.ELECTRIC_SPARK, point, 2, 0.05, 0.05, 0.05, 0.01);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Raytrace entitas yang terkena garis tembakan
|
|
149
|
+
RayTraceResult result = world.rayTraceEntities(
|
|
150
|
+
eyeLoc,
|
|
151
|
+
direction,
|
|
152
|
+
30.0,
|
|
153
|
+
0.5,
|
|
154
|
+
entity -> entity != player && entity instanceof Damageable
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (result != null && result.getHitEntity() != null) {
|
|
158
|
+
Entity target = result.getHitEntity();
|
|
159
|
+
Location hitLoc = target.getLocation();
|
|
160
|
+
|
|
161
|
+
// Panggil sambaran petir visual & berikan damage
|
|
162
|
+
world.strikeLightningEffect(hitLoc);
|
|
163
|
+
if (target instanceof Damageable damageable) {
|
|
164
|
+
damageable.damage(8.0, player); // 8 damage = 4 hati
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize("<yellow>Tembakan Mengenai Target!</yellow>"));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# Penyimpanan Database Asinkron: SQLite & HikariCP di Paper
|
|
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.
|
|
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).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Aturan Emas Database di Minecraft
|
|
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.
|
|
13
|
+
|
|
14
|
+
Gunakan selalu **Asynchronous Execution (`CompletableFuture`)** yang me-return hasil ke thread region pemain saat selesai.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 2. Menyiapkan HikariCP (Connection Pooling)
|
|
19
|
+
|
|
20
|
+
HikariCP adalah library connection pool tercepat di ekosistem Java (digunakan secara ekstensif oleh **LuckPerms**).
|
|
21
|
+
|
|
22
|
+
### Dependensi Gradle:
|
|
23
|
+
```kotlin
|
|
24
|
+
dependencies {
|
|
25
|
+
implementation("com.zaxxer:HikariCP:5.1.0")
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Inisialisasi DataSource:
|
|
30
|
+
```java
|
|
31
|
+
package com.example.plugin.database;
|
|
32
|
+
|
|
33
|
+
import com.zaxxer.hikari.HikariConfig;
|
|
34
|
+
import com.zaxxer.hikari.HikariDataSource;
|
|
35
|
+
|
|
36
|
+
import java.sql.Connection;
|
|
37
|
+
import java.sql.SQLException;
|
|
38
|
+
|
|
39
|
+
public final class DatabaseManager {
|
|
40
|
+
|
|
41
|
+
private HikariDataSource dataSource;
|
|
42
|
+
|
|
43
|
+
public void initMySQL(String host, int port, String database, String user, String password) {
|
|
44
|
+
HikariConfig config = new HikariConfig();
|
|
45
|
+
config.setJdbcUrl("jdbc:mysql://" + host + ":" + port + "/" + database + "?useSSL=false&characterEncoding=utf-8");
|
|
46
|
+
config.setUsername(user);
|
|
47
|
+
config.setPassword(password);
|
|
48
|
+
|
|
49
|
+
// Pengaturan Pool HikariCP yang Direkomendasikan
|
|
50
|
+
config.setMaximumPoolSize(10);
|
|
51
|
+
config.setMinimumIdle(2);
|
|
52
|
+
config.setConnectionTimeout(10000); // 10 detik
|
|
53
|
+
config.setIdleTimeout(600000); // 10 menit
|
|
54
|
+
config.setMaxLifetime(1800000); // 30 menit
|
|
55
|
+
config.setPoolName("PluginHikariPool");
|
|
56
|
+
|
|
57
|
+
this.dataSource = new HikariDataSource(config);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public void initSQLite(String filePath) {
|
|
61
|
+
HikariConfig config = new HikariConfig();
|
|
62
|
+
config.setJdbcUrl("jdbc:sqlite:" + filePath);
|
|
63
|
+
config.setMaximumPoolSize(1); // SQLite hanya boleh 1 writer pool
|
|
64
|
+
config.setPoolName("PluginSQLitePool");
|
|
65
|
+
|
|
66
|
+
this.dataSource = new HikariDataSource(config);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public Connection getConnection() throws SQLException {
|
|
70
|
+
return dataSource.getConnection();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public void close() {
|
|
74
|
+
if (dataSource != null && !dataSource.isClosed()) {
|
|
75
|
+
dataSource.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Repositori Asinkron dengan `CompletableFuture`
|
|
84
|
+
|
|
85
|
+
Buat Data Access Object (DAO) yang memisahkan eksekusi SQL dari logika game:
|
|
86
|
+
|
|
87
|
+
```java
|
|
88
|
+
package com.example.plugin.database;
|
|
89
|
+
|
|
90
|
+
import java.sql.Connection;
|
|
91
|
+
import java.sql.PreparedStatement;
|
|
92
|
+
import java.sql.ResultSet;
|
|
93
|
+
import java.sql.SQLException;
|
|
94
|
+
import java.util.UUID;
|
|
95
|
+
import java.util.concurrent.CompletableFuture;
|
|
96
|
+
import java.util.concurrent.ExecutorService;
|
|
97
|
+
import java.util.concurrent.Executors;
|
|
98
|
+
|
|
99
|
+
public final class PlayerRepository {
|
|
100
|
+
|
|
101
|
+
private final DatabaseManager databaseManager;
|
|
102
|
+
private final ExecutorService dbExecutor = Executors.newFixedThreadPool(3);
|
|
103
|
+
|
|
104
|
+
public PlayerRepository(DatabaseManager databaseManager) {
|
|
105
|
+
this.databaseManager = databaseManager;
|
|
106
|
+
createTableIfNotExists();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private void createTableIfNotExists() {
|
|
110
|
+
try (Connection conn = databaseManager.getConnection();
|
|
111
|
+
PreparedStatement stmt = conn.prepareStatement(
|
|
112
|
+
"CREATE TABLE IF NOT EXISTS player_data (" +
|
|
113
|
+
"uuid VARCHAR(36) PRIMARY KEY, " +
|
|
114
|
+
"coins INT NOT NULL DEFAULT 0, " +
|
|
115
|
+
"level INT NOT NULL DEFAULT 1);"
|
|
116
|
+
)) {
|
|
117
|
+
stmt.executeUpdate();
|
|
118
|
+
} catch (SQLException e) {
|
|
119
|
+
e.printStackTrace();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Membaca data pemain secara asinkron
|
|
124
|
+
public CompletableFuture<Integer> getCoinsAsync(UUID uuid) {
|
|
125
|
+
return CompletableFuture.supplyAsync(() -> {
|
|
126
|
+
try (Connection conn = databaseManager.getConnection();
|
|
127
|
+
PreparedStatement stmt = conn.prepareStatement(
|
|
128
|
+
"SELECT coins FROM player_data WHERE uuid = ?"
|
|
129
|
+
)) {
|
|
130
|
+
stmt.setString(1, uuid.toString());
|
|
131
|
+
try (ResultSet rs = stmt.executeQuery()) {
|
|
132
|
+
if (rs.next()) {
|
|
133
|
+
return rs.getInt("coins");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch (SQLException e) {
|
|
137
|
+
e.printStackTrace();
|
|
138
|
+
}
|
|
139
|
+
return 0; // Default jika data belum ada
|
|
140
|
+
}, dbExecutor);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Menyimpan data pemain secara asinkron (Upsert)
|
|
144
|
+
public CompletableFuture<Void> setCoinsAsync(UUID uuid, int coins) {
|
|
145
|
+
return CompletableFuture.runAsync(() -> {
|
|
146
|
+
try (Connection conn = databaseManager.getConnection();
|
|
147
|
+
PreparedStatement stmt = conn.prepareStatement(
|
|
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)
|
|
150
|
+
)) {
|
|
151
|
+
stmt.setString(1, uuid.toString());
|
|
152
|
+
stmt.setInt(2, coins);
|
|
153
|
+
stmt.executeUpdate();
|
|
154
|
+
} catch (SQLException e) {
|
|
155
|
+
e.printStackTrace();
|
|
156
|
+
}
|
|
157
|
+
}, dbExecutor);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
public void shutdown() {
|
|
161
|
+
dbExecutor.shutdown();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 4. Menggunakan Hasil Database di Event Listener
|
|
169
|
+
|
|
170
|
+
Saat data selesai di-fetch dari database, gunakan scheduler Folia/Paper pemain untuk menerapkan perubahannya ke objek Bukkit:
|
|
171
|
+
|
|
172
|
+
```java
|
|
173
|
+
@EventHandler
|
|
174
|
+
public void onPlayerJoin(PlayerJoinEvent event) {
|
|
175
|
+
Player player = event.getPlayer();
|
|
176
|
+
|
|
177
|
+
// Fetch data koin di thread asinkron
|
|
178
|
+
playerRepository.getCoinsAsync(player.getUniqueId()).thenAccept(coins -> {
|
|
179
|
+
// Terapkan hasil ke entity pemain di thread region pemain
|
|
180
|
+
player.getScheduler().run(plugin, task -> {
|
|
181
|
+
player.sendActionBar(MiniMessage.miniMessage().deserialize(
|
|
182
|
+
"<gold>Saldo Koin: <yellow>" + coins + "</yellow></gold>"
|
|
183
|
+
));
|
|
184
|
+
}, null);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "papermc-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive CLI and architectural development skill for modern Paper & Folia Minecraft Java plugins",
|
|
5
|
+
"main": "bin/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"papermc-skill": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"templates",
|
|
13
|
+
"docs",
|
|
14
|
+
"skills",
|
|
15
|
+
"README.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"minecraft",
|
|
21
|
+
"paper",
|
|
22
|
+
"papermc",
|
|
23
|
+
"folia",
|
|
24
|
+
"minecraft-plugin",
|
|
25
|
+
"adventure-api",
|
|
26
|
+
"minimessage",
|
|
27
|
+
"spigot",
|
|
28
|
+
"bukkit",
|
|
29
|
+
"npx"
|
|
30
|
+
],
|
|
31
|
+
"author": "Ardian Ryan <https://github.com/ardianryan>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/ardianryan/papermc-skill.git"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/ardianryan/papermc-skill/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/ardianryan/papermc-skill#readme",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: paper-plugin-dev
|
|
3
|
+
description: Expert guidelines, modern standards, and architectural blueprints for developing Minecraft Java server plugins using the PaperMC API (1.20-1.21+), Folia, Adventure API, MiniMessage, and Paperweight.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Paper Minecraft Plugin Development Skill
|
|
7
|
+
|
|
8
|
+
Skill ini memberikan panduan otoritatif, standar arsitektur modern, dan resep coding siap pakai untuk membuat atau memodifikasi plugin Minecraft Java Server berbasis **PaperMC (1.20.6 / 1.21+)** dan **Folia (Threaded Regions)**.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Prasyarat Lingkungan & Dependensi
|
|
13
|
+
|
|
14
|
+
| Komponen | Versi yang Dibutuhkan | Catatan |
|
|
15
|
+
| :--- | :--- | :--- |
|
|
16
|
+
| **Java SDK** | **Java 21 (LTS)** atau lebih baru | Minecraft 1.20.5+ dan Paper modern **wajib** Java 21+. |
|
|
17
|
+
| **Gradle** | **Gradle 8.5+** (Disarankan 8.10+ / 9.x) | Gunakan Gradle Wrapper bawaan (`./gradlew`). |
|
|
18
|
+
| **Paper API** | `io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT` | Repositori: `https://repo.papermc.io/repository/maven-public/` |
|
|
19
|
+
| **Mojang Mappings** | `io.papermc.paperweight.userdev` | Hanya jika membutuhkan akses langsung ke internal `net.minecraft.*`. |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 2. Alur Kerja Standar (Workflow) untuk AI / Developer
|
|
24
|
+
|
|
25
|
+
Saat diminta membuat fitur atau plugin Paper baru, ikuti urutan berikut:
|
|
26
|
+
|
|
27
|
+
1. **Tentukan Manifest (`src/main/resources/paper-plugin.yml`)**:
|
|
28
|
+
- Berikan `folia-supported: true`.
|
|
29
|
+
- Cantumkan `bootstrapper:` jika menggunakan Paper Brigadier Command.
|
|
30
|
+
2. **Setup PDC Keys (`PluginDataKeys.java`)**:
|
|
31
|
+
- Buat konstanta `static final NamespacedKey` untuk semua data persisten (jangan instansiasi di dalam event loop).
|
|
32
|
+
3. **Pilih Penanganan Teks (Kyori Adventure)**:
|
|
33
|
+
- Gunakan `MiniMessage.miniMessage().deserialize(...)`.
|
|
34
|
+
- Jangan pernah menggunakan `§` atau `org.bukkit.ChatColor`.
|
|
35
|
+
4. **Pilih Pola Concurrency**:
|
|
36
|
+
- Akses entity/block? Gunakan `RegionScheduler` atau `EntityScheduler`.
|
|
37
|
+
- File/Database/Network? Gunakan `AsyncScheduler` atau `CompletableFuture`.
|
|
38
|
+
5. **Daftarkan Command & Event**:
|
|
39
|
+
- Command -> Daftarkan di `BootstrapContext` via `LifecycleEvents.COMMANDS`.
|
|
40
|
+
- Event -> Daftarkan di `JavaPlugin#onEnable()` via `registerEvents(...)`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 3. Resep Kode Cepat (Ready-to-Use Recipes)
|
|
45
|
+
|
|
46
|
+
### Resep A: Manifest Modern (`paper-plugin.yml`)
|
|
47
|
+
```yaml
|
|
48
|
+
name: MyAwesomePlugin
|
|
49
|
+
version: '1.0.0'
|
|
50
|
+
main: com.example.plugin.MyPlugin
|
|
51
|
+
bootstrapper: com.example.plugin.MyPluginBootstrap
|
|
52
|
+
api-version: '1.21'
|
|
53
|
+
folia-supported: true
|
|
54
|
+
authors:
|
|
55
|
+
- DeveloperName
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Resep B: Brigadier Command Modern (Tanpa `plugin.yml`)
|
|
59
|
+
```java
|
|
60
|
+
// Daftarkan di PluginBootstrap#bootstrap:
|
|
61
|
+
context.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {
|
|
62
|
+
event.registrar().register(
|
|
63
|
+
Commands.literal("warp")
|
|
64
|
+
.requires(source -> source.getSender().hasPermission("plugin.warp"))
|
|
65
|
+
.then(Commands.argument("target", StringArgumentType.word())
|
|
66
|
+
.suggests((ctx, builder) -> {
|
|
67
|
+
builder.suggest("spawn");
|
|
68
|
+
builder.suggest("arena");
|
|
69
|
+
return builder.buildFuture();
|
|
70
|
+
})
|
|
71
|
+
.executes(ctx -> {
|
|
72
|
+
String target = StringArgumentType.getString(ctx, "target");
|
|
73
|
+
ctx.getSource().getSender().sendMessage(
|
|
74
|
+
MiniMessage.miniMessage().deserialize("<green>Teleporting to <gold>" + target + "</gold>...</green>")
|
|
75
|
+
);
|
|
76
|
+
return Command.SINGLE_SUCCESS;
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
.build(),
|
|
80
|
+
"Warp command description",
|
|
81
|
+
List.of("gotowarp")
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Resep C: PersistentDataContainer (PDC) Item & Entity
|
|
87
|
+
```java
|
|
88
|
+
// Menyimpan data pada ItemStack
|
|
89
|
+
NamespacedKey KEY = new NamespacedKey(plugin, "item_level");
|
|
90
|
+
meta.getPersistentDataContainer().set(KEY, PersistentDataType.INTEGER, 5);
|
|
91
|
+
|
|
92
|
+
// Membaca data dengan fallback default:
|
|
93
|
+
int level = meta.getPersistentDataContainer().getOrDefault(KEY, PersistentDataType.INTEGER, 1);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Resep D: GUI Menu Aman dari Eksploitasi
|
|
97
|
+
```java
|
|
98
|
+
// 1. Buat Custom InventoryHolder
|
|
99
|
+
public class MenuHolder implements InventoryHolder {
|
|
100
|
+
private final Inventory inv;
|
|
101
|
+
public MenuHolder(Component title, int size) {
|
|
102
|
+
this.inv = Bukkit.createInventory(this, size, title);
|
|
103
|
+
}
|
|
104
|
+
@Override public Inventory getInventory() { return inv; }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 2. Di Listener:
|
|
108
|
+
@EventHandler
|
|
109
|
+
public void onMenuClick(InventoryClickEvent event) {
|
|
110
|
+
if (event.getInventory().getHolder() instanceof MenuHolder) {
|
|
111
|
+
event.setCancelled(true); // Selalu batalkan pergerakan item
|
|
112
|
+
ItemStack item = event.getCurrentItem();
|
|
113
|
+
if (item != null && item.hasItemMeta()) {
|
|
114
|
+
// Baca aksi dari PDC
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Resep E: Universal Folia / Paper Scheduler Adapter
|
|
121
|
+
```java
|
|
122
|
+
public static void runForPlayer(Plugin plugin, Player player, Runnable runnable) {
|
|
123
|
+
try {
|
|
124
|
+
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
|
|
125
|
+
player.getScheduler().run(plugin, task -> runnable.run(), null);
|
|
126
|
+
} catch (ClassNotFoundException e) {
|
|
127
|
+
Bukkit.getScheduler().runTask(plugin, runnable);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Resep F: Raycast & Partikel Sihir
|
|
133
|
+
```java
|
|
134
|
+
Location eye = player.getEyeLocation();
|
|
135
|
+
Vector dir = eye.getDirection().normalize();
|
|
136
|
+
|
|
137
|
+
for (double d = 1; d <= 25; d += 0.5) {
|
|
138
|
+
Location p = eye.clone().add(dir.clone().multiply(d));
|
|
139
|
+
player.getWorld().spawnParticle(Particle.FLAME, p, 1, 0, 0, 0, 0);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## 4. Anti-Patterns yang Wajib Dihindari
|
|
146
|
+
|
|
147
|
+
| Jangan Pernah Dilakukan ❌ | Cara yang Benar ✔️ |
|
|
148
|
+
| :--- | :--- |
|
|
149
|
+
| `ChatColor.RED + "Pesan"` | `MiniMessage.miniMessage().deserialize("<red>Pesan</red>")` |
|
|
150
|
+
| `public static Set<Player> players;` | `public static Set<UUID> playerUuids;` (Cegah memory leak) |
|
|
151
|
+
| `Bukkit.getScheduler()` di server Folia | Gunakan `RegionScheduler`, `EntityScheduler`, atau `AsyncScheduler` |
|
|
152
|
+
| `world.getChunkAt(x, z)` acak di loop | `world.getChunkAtAsync(x, z)` |
|
|
153
|
+
| `new NamespacedKey(...)` per hit/tick | Simpan `NamespacedKey` di `public static final` |
|
|
154
|
+
| Mencocokkan nama item lore untuk aksi GUI | Gunakan `PersistentDataContainer` (PDC) pada item |
|
|
155
|
+
| Query database di tick thread | Jalankan via `CompletableFuture.supplyAsync(...)` |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 5. Referensi Kode Asli di Folder `references/`
|
|
160
|
+
|
|
161
|
+
- **Arsitektur Enterprise & Database Async**: Periksa [`references/luckperms`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/luckperms).
|
|
162
|
+
- **Performa Async Chunks & Schedulers**: Periksa [`references/chunky`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/chunky).
|
|
163
|
+
- **Paperweight & Incendo Cloud Commands**: Periksa [`references/squaremap`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/squaremap).
|
|
164
|
+
- **MiniMessage & Paper Manifest**: Periksa [`references/miniplaceholders`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/miniplaceholders).
|
|
165
|
+
- **Ekonomi & Permissions Hooking**: Periksa [`references/vault-api`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/vault-api).
|
|
166
|
+
- **Setup Server Pengujian Otomatis**: Periksa [`references/run-paper`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/run-paper).
|
|
167
|
+
- **Display Packet & Hologram**: Periksa [`references/decentholograms`](file:///Users/ardianryan/Documents/minecraft-plugin-skill/references/decentholograms).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|