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.
Files changed (53) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +169 -0
  4. package/bin/cli.js +307 -0
  5. package/docs/01-modern-paper-architecture.md +172 -0
  6. package/docs/02-adventure-and-minimessage.md +160 -0
  7. package/docs/03-persistent-data-container.md +179 -0
  8. package/docs/04-commands-and-brigadier.md +157 -0
  9. package/docs/05-concurrency-and-folia.md +150 -0
  10. package/docs/06-events-and-listeners.md +163 -0
  11. package/docs/07-best-practices-and-perf.md +118 -0
  12. package/docs/08-gui-and-menus.md +198 -0
  13. package/docs/09-custom-items-and-combat.md +171 -0
  14. package/docs/10-database-and-storage.md +187 -0
  15. package/package.json +44 -0
  16. package/skills/paper-plugin-dev/SKILL.md +167 -0
  17. package/templates/paper-modern-template/.editorconfig +8 -0
  18. package/templates/paper-modern-template/.gradle/9.4.1/checksums/checksums.lock +0 -0
  19. package/templates/paper-modern-template/.gradle/9.4.1/checksums/md5-checksums.bin +0 -0
  20. package/templates/paper-modern-template/.gradle/9.4.1/checksums/sha1-checksums.bin +0 -0
  21. package/templates/paper-modern-template/.gradle/9.4.1/executionHistory/executionHistory.bin +0 -0
  22. package/templates/paper-modern-template/.gradle/9.4.1/executionHistory/executionHistory.lock +0 -0
  23. package/templates/paper-modern-template/.gradle/9.4.1/fileChanges/last-build.bin +0 -0
  24. package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.bin +0 -0
  25. package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/fileHashes.lock +0 -0
  26. package/templates/paper-modern-template/.gradle/9.4.1/fileHashes/resourceHashesCache.bin +0 -0
  27. package/templates/paper-modern-template/.gradle/9.4.1/gc.properties +0 -0
  28. package/templates/paper-modern-template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  29. package/templates/paper-modern-template/.gradle/buildOutputCleanup/cache.properties +2 -0
  30. package/templates/paper-modern-template/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  31. package/templates/paper-modern-template/.gradle/file-system.probe +0 -0
  32. package/templates/paper-modern-template/.gradle/vcs-1/gc.properties +0 -0
  33. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/ExamplePaperPlugin.class +0 -0
  34. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/PaperPluginBootstrap.class +0 -0
  35. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/command/HelloCommand.class +0 -0
  36. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/data/PluginDataKeys.class +0 -0
  37. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/listener/PlayerEventListener.class +0 -0
  38. package/templates/paper-modern-template/build/classes/java/main/com/example/paperplugin/util/SchedulerAdapter.class +0 -0
  39. package/templates/paper-modern-template/build/tmp/compileJava/previous-compilation-data.bin +0 -0
  40. package/templates/paper-modern-template/build.gradle.kts +54 -0
  41. package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.jar +0 -0
  42. package/templates/paper-modern-template/gradle/wrapper/gradle-wrapper.properties +7 -0
  43. package/templates/paper-modern-template/gradlew +248 -0
  44. package/templates/paper-modern-template/gradlew.bat +93 -0
  45. package/templates/paper-modern-template/settings.gradle.kts +5 -0
  46. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/ExamplePaperPlugin.java +35 -0
  47. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/PaperPluginBootstrap.java +30 -0
  48. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/command/HelloCommand.java +55 -0
  49. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/data/PluginDataKeys.java +22 -0
  50. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/listener/PlayerEventListener.java +61 -0
  51. package/templates/paper-modern-template/src/main/java/com/example/paperplugin/util/SchedulerAdapter.java +77 -0
  52. package/templates/paper-modern-template/src/main/resources/config.yml +10 -0
  53. 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).
@@ -0,0 +1,8 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ indent_size = 2
6
+ indent_style = space
7
+ insert_final_newline = true
8
+ max_line_length = off
@@ -0,0 +1,2 @@
1
+ #Sat Sep 05 14:25:51 UTC 2026
2
+ gradle.version=9.4.1