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ếu
🎯 Mục tiêu

Sau 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 Kotlinkhô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ầnBạn chỉ cần
Biết JavaBiế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/
💡 Điểm khác biệt so với plugin thường

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

  1. Máy chủ Paper 1.21.11 (Java 21).
  2. Tải plugin Kite từ trang chính thức và bỏ vào plugins/.
  3. Khởi động server — Kite tạo thư mục plugins/Kite/scripts/.
  4. Đặt các file .kite.kts vào thư mục đó.
✅ Không cần khai báo version cho plugin đã cài

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!. 🎉

⚠️ Nạp script chưa nằm trong thư mục

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!")
}
🚫 Biên dịch bất đồng bộ

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>!")
💡 Còn gì nữa?

Title, kick, broadcast… đều dùng Adventure. Xem Adventure TextBukkit 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!")
}
💡 Xem thêm

Muốn sắp thứ tự giữa các listener, dùng priority. Chi tiết: Event Listener.

8Chạy lệnh

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)
⚠️ Luồng chạy

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))
}
💡 Lộ trình liên quan

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}")
}
💡 Xem thêm

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.

✅ 1 script quản lý mọi NPC — không tạo script mới mỗi NPC

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):

  1. Lần đầu click NPC → chưa có quyền → đọc message nhiệm vụ, yêu cầu nộp item.
  2. Đủ item → thu item → lp user ... permission set ... true gán quyền.
  3. 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.

✅ Script này khác gì?
  • 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.yml khi chạy lần đầu (minh họa ghi file).
  • lệnh /reward tự đăng ký.
  • hẹn giờ broadcast nhắc nhở mỗi 30 phút.

Cách dùng:

  1. Copy nguyên khối dưới đây → tạo file daily_reward.kite.kts.
  2. Đặt vào plugins/Kite/scripts/.
  3. Chạy /kite load daily_reward.
  4. 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.")
}
🧠 Script này dạy bạn điều gì
  • 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.

⚠️ Điều quan trọng nhất: hiểu giới hạn của @file:Import

@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.

🗂️ Cấu trúc folder để dùng chung
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.yml cũ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 chungB — 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 khiNhiều script, cùng 1 hệ thốngVài script đơn lẻ, mới bắt đầu
🏆 Kết luận
  • 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 sendMsg ngắ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.
💡 Lời khuyên

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.

🎓 Chúc mừng — bạn đã đi hết lộ trình!

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.