Lộ trình học KiteHướng dẫn từ A → Z: cài đặt → script đầu tiên → event → lệnh → file → plugin ngoài → dự án hoàn chỉnh
← Quay lại wiki chính để xem chi tiết tham chiếuSau lộ trình này bạn sẽ tự viết được script Kite chạy trên Paper 1.21.11: bắt event, gửi tin, chạy lệnh, đọc file cấu hình, và làm một hệ thống quest NPC hoàn chỉnh bằng cách liên kết các bài học lại với nhau.
1Kite là gì?
Kite là một công cụ cho phép bạn viết plugin Minecraft bằng Kotlin mà không cần biên dịch / đóng gói / restart server. Bạn đặt file .kite.kts vào thư mục script, Kite tự động nạp và biên dịch sang bytecode, rồi chạy ngay.
| Bạn không cần | Bạn chỉ cần |
|---|---|
| Biết Java | Biết Kotlin cơ bản (biến, hàm, if/else, class) |
| Môi trường build (Maven/Gradle) | Một file .kite.kts đúng chuẩn |
| Restart server mỗi lần sửa | /kite reload <script> |
| IDE (IntelIJ) bắt buộc | Đặt file vào plugins/Kite/scripts/ |
Plugin viết bằng Java/Kotlin phải build ra .jar rồi restart. Script Kite nằm sẵn trong thư mục server, sửa xong chỉ cần reload — nhanh gọn, phù hợp thay đổi logic thường xuyên.
2Cài đặt
- Máy chủ Paper 1.21.11 (Java 21).
- Tải plugin Kite từ trang chính thức và bỏ vào plugins/.
- Khởi động server — Kite tạo thư mục plugins/Kite/scripts/.
- Đặt các file
.kite.ktsvào thư mục đó.
Vì script nằm trong server, Kite truy cập thẳng class của plugin đang chạy qua classpath. Không cần @file:Dependency cho các plugin như LuckPerms, FancyNpcs… (chỉ dùng compileOnly trong Gradle nếu bạn dùng IDE để gợi ý code). Xem bài Quản lý thư viện.
3Script đầu tiên
Tạo file hello.kite.kts trong plugins/Kite/scripts/:
onLoad {
println("Hello, World!")
}
Lưu file → chạy /kite reload hello → xem console có dòng Hello, World!. 🎉
Kite tự nạp script đã đặt trong thư mục khi server khởi động. Với script mới thêm khi server đang chạy, dùng /kite load <tên> để nạp mà không cần restart.
4Cấu trúc script
Kite tự import sẵn các gói phổ biến của Paper, nên bạn dùng ngay Player, Material, server, logger… mà không cần import. API của plugin khác thì phải import tường minh. Xem chi tiết: Cấu trúc script.
Script một file
onLoad {
println("Xin chào từ script một file!")
}
Script đa file
Đặt trong thư mục con — thư mục đó bắt buộc có file entry point tên main.kite.kts. Các file khác import qua @file:Import.
// scripts/my_plugin/main.kite.kts
@file:Import("helpers.kite.kts")
onLoad {
println("Đã nạp my_plugin!")
printHelper() // hàm từ helpers.kite.kts
}
// scripts/my_plugin/helpers.kite.kts
fun printHelper() {
println("Tôi là helper! 🧰")
}
5Vòng đời script
Hai khối chính: onLoad (sau khi nạp) và onUnload (trước khi gỡ). Dùng để khởi tạo và dọn dẹp.
onLoad {
println("Hello!")
}
onUnload {
println("Goodbye!")
}
Kite biên dịch script bất đồng bộ. Tránh gọi hàm non-thread-safe (vd getTargetBlockExact) ở top-level — đưa vào onLoad cho an toàn.
6Gửi tin nhắn (Adventure)
Paper hiện đại dùng Adventure API. sendRichMessage hỗ trợ mã màu kiểu MiniMessage <gold>, <red>…
player.sendRichMessage("<gold>Chào <bold>bạn</bold>!")
Title, kick, broadcast… đều dùng Adventure. Xem Adventure Text và Bukkit cũ → Paper mới để tránh code kiểu cũ.
7Bắt event
Dùng on<Event> để phản ứng khi sự kiện xảy ra. Event Bukkit chuẩn (PlayerJoinEvent…) tự có sẵn, không cần import.
on<PlayerJoinEvent> { event ->
event.player.sendRichMessage("<green>Chào mừng đến server!")
}
Muốn sắp thứ tự giữa các listener, dùng priority. Chi tiết: Event Listener.
8Chạy lệnh
player.performCommand("...")— player tự chạy lệnh.server.dispatchCommand(server.consoleSender, "...")— console chạy lệnh (cấp quyền, trao item, lệnh admin).- Chèn biến:
"give ${player.name} diamond 1".
server.dispatchCommand(server.consoleSender, "lp user ${player.name} permission set vip true")
player.performCommand("spawn")
9Hẹn giờ (Scheduler)
Paper cung cấp scheduler Folia-ready. Ví dụ chạy lặp mỗi 60 giây:
server.asyncScheduler.runAtFixedRate(server, { task ->
server.broadcast(Component.text("⏰ Đã 1 phút!"))
}, 60 * 20, 60 * 20)
asyncScheduler chạy off-thread — đừng chạm tới world/player ở đó nếu không cần. Chi tiết: Scheduler.
10Đọc file YAML
Dùng YamlConfiguration (có sẵn trong Paper) để đọc file cấu hình đặt cạnh script:
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
val msg = YamlConfiguration.loadConfiguration(File(scriptFile, "message.yml"))
on<PlayerJoinEvent> { e ->
e.player.sendRichMessage(msg.getString("join.welcome")!!.replace("{player}", e.player.name))
}
Biến scriptFile trỏ tới script hiện tại. Muốn tạo file nếu chưa có, xem Ví dụ 14 — message.yml.
11Dùng API plugin khác
Plugin đã cài trên server (FancyNpcs, LuckPerms…) → import tường minh class của nó, không cần @file:Dependency.
import de.oliver.fancynpcs.api.events.NpcInteractEvent
on<NpcInteractEvent> { event ->
event.player.sendRichMessage("<gold>Bạn click NPC ${event.npc.data.name}")
}
Chi tiết về FancyNpcs ở Ví dụ 12, về PlaceholderAPI ở Ví dụ 4, về bắt event của plugin khác ở Ví dụ 13.
12Dự án hoàn chỉnh: Quest NPC
Áp dụng mọi bài đã học thành một hệ thống: NPC yêu cầu nộp item → hoàn thành thì gán quyền → lần sau vào thẳng. Dùng quyền làm flag lưu trạng thái, cấu hình trong npc.yml.
Cấu hình NPC để trong npc.yml. Thêm NPC = thêm 1 khối + /kite reload, không sửa code. Xem Ví dụ 15 đầy đủ.
Vòng khép kín (giống ConditionalEvents):
- Lần đầu click NPC → chưa có quyền → đọc message nhiệm vụ, yêu cầu nộp item.
- Đủ item → thu item →
lp user ... permission set ... truegán quyền. - Lần sau → có quyền → chạy
cmd_gained, không làm lại.
// npc.yml — cấu hình mọi NPC
npcs:
shop:
perm: "vip.npc.shop"
need: "GOLD_INGOT"
amount: 10
cmd_gained:
- "shop"
- "give {player} diamond 1"
13Script hoàn chỉnh (copy-paste)
Đây là toàn bộ script daily_reward.kite.kts — một ví dụ khác hẳn với các ví dụ trong wiki. Nó tổng hợp các kỹ năng đã học: bắt event, đăng ký lệnh, đọc/ghi file YAML, gửi tin Adventure, hẹn giờ. Chỉ cần 1 file duy nhất — file data.yml được tự tạo để ghi nhớ lần nhận quà, bạn không phải tạo file cấu hình nào.
- Không dùng FancyNpcs, không quest — hoàn toàn độc lập, dùng event Bukkit chuẩn.
- Tự tạo file
data.ymlkhi chạy lần đầu (minh họa ghi file). - Có lệnh
/rewardtự đăng ký. - Có hẹn giờ broadcast nhắc nhở mỗi 30 phút.
Cách dùng:
- Copy nguyên khối dưới đây → tạo file daily_reward.kite.kts.
- Đặt vào plugins/Kite/scripts/.
- Chạy /kite load daily_reward.
- Vào game — được chào mừng + nhận quà 1 lần/ngày; gõ
/rewardđể nhận.
// ============================================================
// daily_reward.kite.kts — Chào mừng + quà hằng ngày
// Khác hẳn wiki: event Bukkit, lệnh, file, scheduler
// Cách dùng:
// 1) Đặt file vào plugins/Kite/scripts/
// 2) Nạp: /kite load daily_reward
// 3) Player vào server → chào mừng + nhận quà 1 lần/ngày
// 4) Gõ /reward → nhận quà qua lệnh
// File data.yml tự tạo để ghi nhớ lần nhận cuối.
// ============================================================
import net.kyori.adventure.text.minimessage.MiniMessage
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
import java.time.LocalDate
import java.util.logging.Level
// ---- 1. FILE DỮ LIỆU (tự tạo nếu chưa có) ----
val dataFile = File(scriptFile, "data.yml")
val data = YamlConfiguration()
if (dataFile.exists()) {
data.load(dataFile) // đọc dữ liệu cũ
} else {
dataFile.parentFile.mkdirs() // tạo thư mục nếu thiếu
data.set("meta.created", LocalDate.now().toString())
data.save(dataFile) // ghi file lần đầu
logger.info("[daily_reward] Đã tạo data.yml lần đầu.")
}
// ---- 2. BẮT EVENT: chào mừng + quà khi vào server ----
on<PlayerJoinEvent> { event ->
val player = event.player
val uuid = player.uniqueId.toString()
// Chào mừng bằng Adventure (màu MiniMessage)
player.sendRichMessage("<gold>👋 Chào mừng <yellow><bold>${player.name}</bold></yellow> đến server!")
player.sendRichMessage("<gray>Gõ <green>/reward</green> để nhận quà hằng ngày.")
// Kiểm tra hôm nay đã nhận chưa
val last = data.getString("rewards.$uuid")
val today = LocalDate.now().toString()
if (last == today) {
player.sendRichMessage("<red>Bạn đã nhận quà hôm nay rồi. Quay lại ngày mai nhé! ⏰")
} else {
// Chưa nhận → tặng quà + ghi nhớ
player.inventory.addItem(ItemStack(Material.DIAMOND, 1))
player.inventory.addItem(ItemStack(Material.GOLDEN_APPLE, 2))
data.set("rewards.$uuid", today)
saveData()
player.sendRichMessage("<green>✅ Đã tặng quà hằng ngày cho bạn!")
}
}
// ---- 3. ĐĂNG KÝ LỆNH /reward ----
command("reward") { player, args ->
val uuid = player.uniqueId.toString()
val last = data.getString("rewards.$uuid")
val today = LocalDate.now().toString()
if (last == today) {
player.sendRichMessage("<red>Hôm nay bạn đã nhận quà rồi. Quay lại vào <yellow>ngày mai</yellow>! ⏰")
} else {
// Cho phép nhận ngay qua lệnh
player.inventory.addItem(ItemStack(Material.DIAMOND, 1))
player.inventory.addItem(ItemStack(Material.GOLDEN_APPLE, 2))
data.set("rewards.$uuid", today)
saveData()
player.sendRichMessage("<green>🎁 Nhận quà thành công qua lệnh!")
}
}
// ---- 4. HẸN GIỜ: nhắc mọi người nhận quà mỗi 30 phút ----
// Dùng MiniMessage.miniMessage() để parse tag màu <yellow> cho đúng Adventure.
server.asyncScheduler.runAtFixedRate(server, {
server.broadcast(
MiniMessage.miniMessage().deserialize("<yellow>⏰ Đừng quên nhận quà hằng ngày: gõ /reward!")
)
}, 30 * 20L, 30 * 60 * 20L)
// ---- 5. HÀM TIỆN ÍCH ----
/** Lưu dữ liệu xuống file, bọc try/catch để an toàn. */
fun saveData() {
try {
data.save(dataFile)
} catch (e: Exception) {
logger.log(Level.SEVERE, "[daily_reward] Không lưu được data.yml!", e)
}
}
// ---- 6. LOG KHI NẠP / GỠ ----
onLoad {
logger.info("[daily_reward] Đã nạp. Số người đã từng nhận quà: " +
(data.getConfigurationSection("rewards")?.getKeys(false)?.size ?: 0))
}
onUnload {
saveData()
logger.info("[daily_reward] Đã gỡ script.")
}
- Tự tạo file:
if (!dataFile.exists())→save()để ghi file lần đầu. - Đọc file:
YamlConfiguration.load/loadConfigurationđưa dữ liệu cũ vào bộ nhớ. - Đăng ký lệnh:
command("reward") { player, args -> ... }. - Adventure:
sendRichMessage(màu MiniMessage) vàMiniMessage.miniMessage().deserialize()cho broadcast. - Scheduler:
server.asyncScheduler.runAtFixedRateđể chạy lặp.
14Message Loader dùng chung
Khi có nhiều script cùng dùng một message.yml, bạn không muốn copy-paste hàm gửi tin vào từng file. Kite có cơ chế để chia sẻ hàm giữa các script — nhưng có giới hạn. Bài này chỉ rõ 2 cách và cách nào tốt hơn.
@file:Import("tên.kite.kts") chỉ hoạt động cho các file trong CÙNG một thư mục con (thư mục phải có entry point main.kite.kts). Các script độc lập đặt thẳng trong scripts/ không import lẫn nhau được — mỗi script tự load message.yml riêng.
Cách A — Import dùng chung (1 loader, nhiều script gọi)
Đặt cả project trong 1 thư mục con của scripts/. Trong folder đó có file entry point bắt buộc tên main.kite.kts, loader chứa hàm sendMsg, và các script khác import + gọi thẳng.
plugins/Kite/scripts/
└── my_project/ // TẠO THÊM folder con
├── main.kite.kts // bắt buộc tên "main" (entry point)
├── message_loader.kite.kts // loader — hàm dùng chung
├── welcome.kite.kts // script khác dùng sendMsg
└── message.yml // thông điệp dùng chung
- Folder con = 1 "project". Nạp bằng:
/kite load my_project(tên = folder). - File entry point bắt buộc tên main.kite.kts.
- Loader + các script dùng chung đặt cùng trong folder.
message.ymlcũng nằm trong folder (vìFile(scriptFile, ...)mở file cùng chỗ).
// main.kite.kts — entry point, import loader
@file:Import("message_loader.kite.kts")
on<PlayerJoinEvent> { event ->
val player = event.player
sendMsg(player, "join.welcome", "{player}" to player.name) // hàm từ loader
sendMsg(player, "join.tip")
}
// message_loader.kite.kts — hàm dùng chung
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
val msg = YamlConfiguration.loadConfiguration(File(scriptFile, "message.yml"))
fun sendMsg(player: Player, path: String, vararg repl: Pair<String, String>) {
val lines: List<String> = when (val v = msg.get(path)) {
is String -> listOf(v)
is List<*> -> v.filterIsInstance<String>()
else -> return
}
lines.forEach { line ->
var out = line
repl.forEach { (k, v) -> out = out.replace(k, v) }
player.sendRichMessage(out)
}
}
Cách B — Script độc lập, mỗi script tự load
Các script đặt thẳng trong scripts/ (không có thư mục con). Mỗi script tự load message.yml + tự khai báo hàm sendMsg. Đây là cách dùng trong Bài 13.
// welcome.kite.kts — script độc lập, tự load message.yml
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
val msg = YamlConfiguration.loadConfiguration(File(scriptFile, "message.yml"))
on<PlayerJoinEvent> { event ->
sendMsg(event.player, "join.welcome", "{player}" to event.player.name)
}
fun sendMsg(player: Player, path: String, vararg repl: Pair<String, String>) {
val lines: List<String> = when (val v = msg.get(path)) {
is String -> listOf(v)
is List<*> -> v.filterIsInstance<String>()
else -> return
}
lines.forEach { line ->
var out = line
repl.forEach { (k, v) -> out = out.replace(k, v) }
player.sendRichMessage(out)
}
}
⚖️ So sánh — Cách nào tốt hơn?
| Tiêu chí | A — Import chung | B — Tự load |
|---|---|---|
| Viết hàm 1 lần | ✅ 1 nơi | ❌ lặp lại mỗi script |
| Nhiều script dùng chung | ✅ dễ | ⚠️ phải copy hàm |
| Sửa message.yml | ✅ 1 chỗ, mọi script cập nhật | ✅ vẫn 1 file yml chung |
| Độc lập, đơn giản | ⚠️ phải theo cấu trúc thư mục | ✅ gọn, dễ hiểu |
| Phù hợp khi | Nhiều script, cùng 1 hệ thống | Vài script đơn lẻ, mới bắt đầu |
- Mới bắt đầu / vài script đơn lẻ → dùng Cách B (tự load). Đơn giản, không phải học cấu trúc thư mục, không rủi ro lỗi import. Hàm
sendMsgngắn nên copy vài lần cũng không sao. - Nhiều script trong 1 hệ thống lớn (quest, kit, rank…) → dùng Cách A (import chung). Viết hàm 1 lần, mọi script gọi chung, dễ bảo trì khi message.yml thay đổi.
- Dù dùng cách nào, message.yml vẫn là 1 file chung trên đĩa — chỉ khác cách mỗi script lấy nội dung.
Nếu bạn chỉ có 1–2 script đơn giản, đừng phức tạp hóa bằng thư mục con. Bắt đầu với Cách B, khi dự án lớn dần lên rồi mới chuyển sang cấu trúc Cách A.
Bây giờ bạn có thể quay lại wiki chính để xem thêm: Rank (vd8), Warp menu GUI (vd9), Fly (vd10), PvP (vd11), HTTP async (vd5)… và các lệnh quản lý như /kite reload, /kite list.