Kite Wiki Kotlin Scripting cho Minecraft với Paper API mới — server Paper 1.21.11 · tổng hợp từ tài liệu chính thức, định hướng hiện đại
Kite là plugin cung cấp runtime scripting Kotlin cho server Minecraft dựa trên Paper. Script được biên dịch thành Java bytecode (hiệu năng gần bằng native), có toàn quyền truy cập Bukkit/Paper API.
Wiki này được viết theo định hướng chỉ dùng API Paper hiện đại: Adventure Component, sendRichMessage, Paper scheduler tương thích Folia — hạn chế tối đa các hàm Bukkit cũ đã deprecated.
📑 Mục lục
- Giới thiệu
- Cài đặt
- Nâng cấp Paper API
- Cấu trúc script
- Vòng đời script
- Script Context
- Adventure Text
- Bukkit cũ → Paper mới
- Event Listener
- Đăng ký lệnh
- Scheduler
- Quản lý thư viện
- Tích hợp IDE
- Xử lý sự cố
- Ví dụ 1 — Chào mừng
- Ví dụ 2 — Warp ngẫu nhiên
- Ví dụ 3 — Broadcast
- Ví dụ 4 — Plugin API
- Ví dụ 5 — HTTP async
- Ví dụ 6 — Đa file + thư viện
- Ví dụ 7 — Kit
- Ví dụ 8 — Rank
- Ví dụ 9 — Warp menu
- Ví dụ 10 — Fly
- Ví dụ 11 — PvP
- Ví dụ 12 — NPC FancyNpcs
- Ví dụ 13 — Bắt Custom Event
- Ví dụ 14 — Tạo & đọc file (message.yml)
- Ví dụ 15 — Quest NPC (flag bằng quyền)
1Giới thiệu về Kite
Kite là gì?
Kite là runtime scripting Kotlin cho server Minecraft. Thay vì một ngôn ngữ script riêng (như Skript), bạn viết bằng Kotlin và truy cập trực tiếp toàn bộ Bukkit/Paper API — với sức mạnh ngang một plugin Java thực thụ.
Điểm mạnh
- Hiệu năng cao: biên dịch thành Java bytecode, gần bằng native.
- Toàn quyền API: dùng Bukkit API, thư viện, và API của các plugin khác.
- Tương thích Folia: có sẵn wrapper scheduler tương thích Folia.
- Đăng ký lệnh đơn giản: chỉ với một hàm
command(...). - Lắng nghe event: Bukkit/Spigot/Paper hoặc custom event của plugin.
- Đa file: chia script thành nhiều file dễ quản lý.
Yêu cầu hệ thống
| Thành phần | Yêu cầu |
|---|---|
| Server | Dựa trên Paper — bạn đang dùng 1.21.11 |
| Java | Java 21 |
| Kiến thức | Kotlin cơ bản |
Kite đã có phiên bản hỗ trợ Paper 1.21.11 — khớp với server của bạn; chỉ cần đặt script lên server là chạy. Nếu dùng IntelliJ để có gợi ý code đúng bản API 1.21.11, bạn nên khai báo paper-api:1.21.11 tường minh trong starter project — xem mục 3.
2Cài đặt
- Tải JAR của Kite từ Modrinth, Hangar, hoặc GitHub (
github.com/EchoNineLabs/Kite). - Đặt file JAR vào thư mục plugins.
- Khởi động lại server → thư mục plugins/Kite/scripts được tạo tự động.
Tạo plugins/Kite/scripts/test.kite.kts:
onLoad {
println("Kite đang chạy! 🪁")
}
Chạy /kite reloadall và quan sát console.
3Nâng cấp Paper API lên bản mới
Đặt script trực tiếp lên server thì không cần phần này — Kite đã hỗ trợ Paper 1.21.11 và tự biên dịch. Mục này chỉ dành cho ai dùng IntelliJ và muốn gợi ý code khớp bản API 1.21.11.
Kite đã có bản hỗ trợ Paper 1.21.11 — bạn không cần tự build. Tuy nhiên, Kite mặc định đi kèm sẵn paper-api 1.21.1. Để IDE gợi ý code đúng bản 1.21.11 (khớp server của bạn), bạn khai báo paper-api:1.21.11 tường minh trong build.gradle.kts của starter project — không phải bằng annotation trong script:
dependencies {
// Loại bỏ Paper API 1.21.1 mà Kite đi kèm
api("dev.echonine.kite:kite:1.2.4") {
exclude(module = "paper-api")
}
// Khai báo Paper API khớp server của bạn (1.21.11)
api("io.papermc.paper:paper-api:1.21.11-R0.1-SNAPSHOT")
}
Sau khi sửa, reload Gradle trong IntelliJ. Script của bạn giờ được IDE biên dịch với Paper API 1.21.11, khớp với server và có đầy đủ method/class mới nhất.
- Annotation
@file:Dependencydùng để tải thư viện runtime phụ trợ, không để đổi Paper API version. - Việc khai báo version Paper API (cho IDE gợi ý code) chỉ thực hiện qua starter project Gradle.
- Server thực tế phải chạy bản Paper tương đương hoặc mới hơn API bạn khai báo.
4Cấu trúc script
Đuôi file & vị trí
Script có đuôi .kite.kts, đặt trong plugins/Kite/scripts (trực tiếp hoặc trong thư mục con).
Script một file
// scripts/hello.kite.kts
onLoad {
println("Xin chào từ script một file!")
}
Script đa file
Nếu đặ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! 🧰")
}
Script một file và đa file có thể dùng đồng thời trong cùng server.
5Vòng đời script
Mỗi script có hai khối: onLoad (sau khi nạp) và onUnload (trước khi gỡ). Dùng chúng để khởi tạo và dọn dẹp tài nguyên.
Hãy tận dụng triệt để hai khối này để hủy task, xóa dữ liệu tạm… khi script bị gỡ.
onLoad {
println("Hello, World!")
}
onUnload {
println("Goodbye, World!")
}
⚠️ Biên dịch bất đồng bộ
Kite biên dịch script thành bytecode (cache tại plugins/Kite/cache). Các lời gọi hàm, khởi tạo property/object ở top-level chạy ngay sau biên dịch, nhưng vì biên dịch là bất đồng bộ, bạn nên tránh gọi hàm non-thread-safe ở ngoài các khối onLoad/onUnload.
getTargetBlockExact chỉ gọi được trên main thread, nhưng đoạn dưới chạy ngay sau biên dịch:
val targetBlock = server.getPlayer("Player").getTargetBlockExact(10)
onLoad {
println(targetBlock.type)
}
onLoad {
val targetBlock = server.getPlayer("Player").getTargetBlockExact(10)
println(targetBlock.type)
}
6Script Context
Kite cung cấp các biến context luôn có sẵn trong mọi script. Hãy dùng chúng thay cho các truy cập static của Bukkit (xem mục Bukkit cũ → Paper mới).
| Biến | Kiểu | Ý nghĩa |
|---|---|---|
name | File | Tên script |
entryPoint | File | File điểm vào |
plugin | JavaPlugin | Instance plugin Kite |
server | Server | Instance server (Paper) |
logger | ComponentLogger | Logger hỗ trợ Adventure |
onLoad {
logger.info("Script " + name.name + " đã được nạp!")
println(server.name)
}
Import tự động
Hầu hết import Bukkit/Paper/Adventure được thêm tự động lúc biên dịch:
org.bukkitio.papermc.papercom.destroytokyo.papernet.kyori.adventure.text— Adventure đã có sẵn
API plugin khác phải import tường minh (xem Ví dụ 4).
7Adventure Text — hệ thống chat hiện đại
Adventure là thư viện chat của Paper, thay thế hoàn toàn ChatColor và chuỗi màu kiểu cũ. Kite tự import sẵn net.kyori.adventure.text.
7.1 — Rich Text (mini tags) — nhanh nhất
Kite cung cấp tiện ích sendRichMessage nhận chuỗi có tag màu:
player.sendRichMessage("<red>Máu thấp!</red> <yellow>Bạn có <gold>${player.health}</gold> HP.")
player.sendRichMessage("<bold>Nhấn để mở web: <click:open_url:'https://example.com'><underline>tại đây</click>.")
7.2 — Component trực tiếp
val msg = Component.text("Chào ")
.color(NamedTextColor.GREEN)
.append(Component.text(player.name).color(NamedTextColor.YELLOW))
.append(Component.text("!").decorate(TextDecoration.BOLD))
player.sendMessage(msg)
7.3 — Các tiện ích chính
| Ngữ cảnh | Hàm hiện đại |
|---|---|
| Gửi chat player | player.sendRichMessage("...") / player.sendMessage(Component) |
| Gửi chat player (không parse) | player.sendPlainMessage("...") |
| Broadcast toàn server | server.sendRichMessage("...") |
| Action bar | player.sendRichActionBar("...") / player.sendActionBar(Component) |
| Title | player.showTitle(Title.title(Component, Component)) |
| Kick | player.kick(Component) |
| Logger | logger.info(Component) — logger là ComponentLogger |
8Bukkit cũ → Paper mới
Bảng quy đổi nhanh các thói quen Bukkit API cũ sang Paper/Adventure hiện đại. Trong wiki này, mọi ví dụ đều dùng cột bên phải.
| ❌ Bukkit cũ (legacy) | ✅ Paper/Adventure mới |
|---|---|
player.sendMessage("&aHello") | player.sendRichMessage("<green>Hello") |
server.broadcastMessage("Hi") | server.sendRichMessage("Hi") |
Bukkit.getPlayer(name) | server.getPlayer(name) (dùng context server) |
Bukkit.getServer() | Biến context server có sẵn |
ChatColor.RED | NamedTextColor.RED / tag <red> |
player.getLocation() | player.location |
player.getWorld() | player.world |
player.sendTitle(a, b, 10, 20, 10) | player.showTitle(Title.title(Component, Component)) |
player.sendActionBar("...") (String) | player.sendRichActionBar("...") |
player.kick("reason") | player.kick(Component.text("reason")) |
Định thời qua Bukkit.getScheduler() | server.asyncScheduler / player.scheduler (xem Scheduler) |
Bukkit.isPrimaryThread() | server.isPrimaryThread |
- Ưu tiên Adventure để làm text, không dùng
ChatColor/mã&. - Dùng biến context (
server,plugin,logger) thay cho truy cập staticBukkit.*. - Dùng Paper scheduler (
EntityScheduler,AsyncScheduler,GlobalRegionScheduler) để tương thích Folia.
9Event Listener
Tạo listener bằng on<Event>. Event tự động hủy đăng ký khi script gỡ. Luôn dùng Adventure để gửi message.
Ví dụ cơ bản
on<PlayerJoinEvent> { event ->
if (event.player.hasPlayedBefore() == true) {
event.player.sendRichMessage("<gray>Chào mừng trở lại, <red>${event.player.name}!")
}
}
Chỉ định priority
on<PlayerDeathEvent>(priority = EventPriority.MONITOR) {
// xử lý sau cùng, không thay đổi kết quả event
}
Title chào mừng bằng Adventure
on<PlayerJoinEvent> { event ->
val player = event.player
player.showTitle(
Title.title(
Component.text("Chào mừng!").color(NamedTextColor.GOLD),
Component.text(player.name).color(NamedTextColor.YELLOW)
)
)
}
Lắng nghe event tùy biến của plugin khác bằng cách import loại event và dùng on<YourCustomEvent>.
10Đăng ký lệnh (Command)
Lệnh qua API Kite tự động hủy đăng ký khi script gỡ. Chỉ cần tên lệnh là bắt buộc.
command("echo") {
description = "In ra message người chơi truyền vào."
permission = "scripts.command.echo"
usage = "/echo [message]"
aliases = listOf("e", "print")
execute { sender, args ->
val message = args.joinToString(" ")
sender.sendRichMessage(message)
}
tabComplete { sender, args ->
return@tabComplete emptyList()
}
}
Tham số khối lệnh
| Thuộc tính | Ý nghĩa |
|---|---|
description | Mô tả lệnh |
permission | Permission cần thiết |
usage | Cú pháp sử dụng |
aliases | Danh sách lệnh tắt |
execute { sender, args -> } | Logic khi gõ lệnh |
tabComplete { sender, args -> } | Gợi ý tab |
Kiểm tra người gọi là player + truy cập world qua property
command("heal") {
permission = "scripts.command.heal"
execute { sender, args ->
if (sender is Player) {
sender.health = sender.maxHealth
sender.sendRichMessage("<green>Bạn đã được hồi máu!")
// sender.world — property Paper thay cho getWorld()
println(sender.world.name)
} else {
sender.sendRichMessage("<red>Lệnh này chỉ dùng cho người chơi.")
}
}
}
11Scheduler — bộ lập lịch Folia-ready
Task qua API Kite tự động hủy khi script gỡ. Nếu dùng trực tiếp Bukkit/Paper API, bạn phải tự quản lý vòng đời. Trong wiki này, ta ưu tiên Paper scheduler thay vì BukkitScheduler cũ (không tương thích Folia).
11.1 — EntityScheduler (khuyến nghị cho player)
Thực thi task trên region sở hữu entity — an toàn với Folia. Hàm: run, runDelayed, runAtFixedRate.
on<PlayerJoinEvent> { event ->
event.player.scheduler.run {
event.player.sendPlainMessage("Xin chào, ${it.name}, chào mừng đến server!")
}
event.player.scheduler.runDelayed(30, TimeUnit.SECONDS, {
event.player.kick(Component.text("Bạn đã chơi hơn 30 giây... Quá lâu rồi!"))
})
event.player.scheduler.runAtFixedRate(5, 5, TimeUnit.SECONDS, {
event.player.level = event.player.level + 1
})
}
11.2 — AsyncScheduler (cho I/O, HTTP)
Chạy bất đồng bộ, tách khỏi tick server. Hàm: runNow, runDelayed, runAtFixedRate.
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
val client = HttpClient.newHttpClient()
command("country") {
permission = "scripts.country"
execute { sender, args ->
if (sender is Player) {
server.asyncScheduler.runNow {
// Kiểm tra thread bằng Paper API
sender.sendRichMessage("<gray>Thread: <yellow>${Thread.currentThread().name}")
val req = HttpRequest.newBuilder().GET()
.uri(URI("https://get.geojs.io/v1/ip/country/full/${sender.address.address}"))
.build()
val country = client.send(req, HttpResponse.BodyHandlers.ofString()).body().trim()
sender.sendRichMessage("<green>Quốc gia: <yellow>$country")
}
}
}
}
11.3 — GlobalRegionScheduler
Chạy trên global region thread; nếu không dùng Folia thì quay về main thread.
server.globalRegionScheduler.runDelayed(10, TimeUnit.SECONDS) {
server.sendRichMessage("<gold>10 giây đã trôi qua!")
}
11.4 — BukkitScheduler (chỉ khi không dùng Folia)
Bám theo tick server, không tương thích Folia. Hàm: runTask, runTaskAsync, runTaskTimer, runTaskTimerAsync.
server.scheduler.runTaskTimer(delayTicks = 20, periodTicks = 20 * 10) {
server.sendRichMessage("<aqua>Thông báo định kỳ (BukkitScheduler)")
}
- Thao tác trên một entity/player →
player.scheduler(EntityScheduler). - I/O, HTTP, tính toán nặng →
server.asyncScheduler. - Task toàn server, không theo entity →
server.globalRegionScheduler. - Chỉ chạy không Folia →
server.scheduler(BukkitScheduler).
12Quản lý thư viện (Dependencies)
Kite tự tải và nạp thư viện từ Maven repository lúc chạy.
Khai báo Repository
Dùng @file:Repository (lặp lại được). Nên dùng mirror thay vì Maven Central gốc:
@file:Repository("https://maven-central.storage-download.googleapis.com/maven2")
Khai báo Dependency
@file:Dependency("com.github.ben-manes.caffeine:caffeine:3.2.3")
// Tắt giải quyết transitive nếu cần:
@file:Dependency("groupId:artifactId:version", withTransitiveDependencies = false)
Relocation (đổi package) — tùy chọn
@file:Relocation("pattern.from", "pattern.to")
Dependency tải vào plugins/Kite/libs. Reload script để biên dịch; thành công in Script ... successfully loaded.
Repository/dependency khai báo bằng annotation cũng nên thêm vào build.gradle.kts để có gợi ý code.
13Tích hợp IDE (IntelliJ IDEA) — tùy chọn
Script chạy trực tiếp trên server: chỉ cần đặt file .kite.kts vào plugins/Kite/scripts/ rồi chạy /kite reloadall — Kite tự biên dịch ngay tại runtime. Không cần Gradle hay IntelliJ để chạy.
Mục này dành cho ai muốn có gợi ý code, bắt lỗi sớm, và xem tài liệu API khi viết — hoàn toàn tùy chọn.
- Clone starter project:
Mở trong IntelliJ IDEA và load Gradle.git clone https://github.com/EchoNineLabs/KiteScripting.git - Kích hoạt Kite Script definition: Settings → Languages & Frameworks → Kotlin → Kotlin Scripting → Scan Classpath. Khi thấy "1 new definition(s) found" → Apply → OK. Xác nhận Kite Script (.kite.kts) đã bật.
- Nâng cấp Paper API (nếu muốn) — xem mục 3.
Thêm dependency remote tùy biến (ví dụ PlaceholderAPI) — phải khai báo cả annotation lẫn build script:
// example.kite.kts
@file:Repository("https://repo.extendedclip.com/releases/")
@file:Dependency("me.clip:placeholderapi:2.11.7")
// build.gradle.kts
repositories {
maven { url = uri("https://repo.extendedclip.com/releases/") }
}
dependencies {
api("me.clip:placeholderapi:2.11.7")
}
Nhớ thêm dependency vào cả build script, nếu không sẽ không có gợi ý code.
Ghi chú viết script trong IDE
- Script có thể đặt bất kỳ đâu, khuyến nghị thư mục src.
- File import qua
@file:Importkhông xuất hiện trong gợi ý code do lỗi editor.
14Xử lý sự cố
14.1 — Import xung đột
Các API tự import có thể xung đột kiểu (vd org.bukkit.Sound vs net.kyori.adventure.sound.Sound). Giải pháp: import tường minh, dùng tên đầy đủ, hoặc alias:
import net.kyori.adventure.sound.Sound as AdventureSound
14.2 — Unresolved References (fork server)
Một số fork làm hỏng class loading, khiến mọi script biên dịch thất bại. Không xảy ra trên Paper/Folia/Purpur/Pufferfish. Giải pháp: thêm JVM flag
-Dkite.compat.dynamic-server-jar=true
Tính năng này thử nghiệm, không đảm bảo hoạt động mọi trường hợp.
14.3 — Trùng tên class (Duplicate JVM Class Name)
Xảy ra với @Import: hai script cùng tên file (dù khác thư mục) biên dịch thành cùng class Name_kite. Giải pháp: dùng tên khác, hoặc khai báo package:
// extras/main.kite.kts
package extras
fun fromExtras() = "Từ thư mục extras"
// my_script/main.kite.kts
@file:Import("../extras/main.kite.kts")
import extras.*
onLoad {
println(fromExtras())
}
15Ví dụ script hoàn chỉnh (Paper API mới)
Các script mẫu đều dùng Adventure, context server, và Paper scheduler. Lưu mỗi đoạn thành file .kite.kts trong plugins/Kite/scripts rồi reload.
Ví dụ 1 — Chào mừng thành viên
Gửi lời chào khác nhau cho thành viên mới/cũ, kèm task chào muộn sau 5 giây bằng server.scheduler.
// welcome.kite.kts
on<PlayerJoinEvent> { event ->
val player = event.player
val isNew = !player.hasPlayedBefore()
if (isNew) {
player.sendRichMessage("<gold>🎉 Chào mừng lần đầu, <bold>${player.name}</bold> đến server!")
} else {
player.sendRichMessage("<gray>Chào mừng trở lại, <red>${player.name}</gray>!")
}
// Task chào muộn sau 5 giây (100 tick)
server.scheduler.runTask(100) {
if (player.isOnline) {
player.sendRichMessage("<aqua>Nhớ xem luật server: <click:open_url:'https://example.com/rules'><underline>tại đây</click>!")
}
}
}
onUnload {
println("Đã gỡ script welcome.")
}
Ví dụ 2 — Lệnh warp ngẫu nhiên
Lệnh /randomtp dịch chuyển người chơi ngẫu nhiên. Dùng property sender.world, getHighestBlockYAt (Paper) và EntityScheduler.
// randomtp.kite.kts
import org.bukkit.Location
command("randomtp") {
description = "Dịch chuyển ngẫu nhiên trong bán kính cho trước."
permission = "scripts.randomtp"
usage = "/randomtp <bán_kính>"
aliases = listOf("rtp")
execute { sender, args ->
if (sender !is Player) {
sender.sendRichMessage("<red>Chỉ người chơi mới dùng được lệnh này.")
return@execute
}
val radius = args.firstOrNull()?.toIntOrNull() ?: 500
val world = sender.world // Paper property, thay cho getWorld()
val x = (-radius..radius).random()
val z = (-radius..radius).random()
val y = world.getHighestBlockYAt(x, z) // Paper API
val target = Location(world, x.toDouble(), (y + 1).toDouble(), z.toDouble())
// EntityScheduler — an toàn với Folia
sender.scheduler.run {
sender.teleport(target)
sender.sendRichMessage("<green>Đã dịch chuyển đến $x, $z!")
}
}
}
Ví dụ 3 — Broadcast định kỳ
Mỗi 30 giây gửi một thông báo xoay vòng. Dùng server.sendRichMessage và BukkitScheduler (chỉ chạy không-Folia; nếu dùng Folia hãy chuyển sang globalRegionScheduler).
// auto_announce.kite.kts
val announcements = listOf(
"<gold>💡 Mẹo: <white>Nhấn <yellow>Ctrl+F</white> để tìm kiếm trên web.",
"<gold>💡 Mẹo: <white>Sử dụng <yellow>/help</white> để xem danh sách lệnh.",
"<gold>💡 Mẹo: <white>Tham gia Discord của chúng tôi!"
)
var index = 0
onLoad {
server.scheduler.runTaskTimer(delayTicks = 20 * 5, periodTicks = 20 * 30) {
server.sendRichMessage(announcements[index])
index = (index + 1) % announcements.size
}
server.sendRichMessage("<green>✓ Auto-announce đã bật.")
}
onUnload {
server.sendRichMessage("<red>✕ Auto-announce đã tắt.")
}
Ví dụ 4 — Gọi API plugin khác (PlaceholderAPI)
Import tường minh vì không phải API Bukkit/Paper. Gửi kết quả bằng Adventure.
// papi.kite.kts
@file:Repository("https://repo.extendedclip.com/releases/")
@file:Dependency("me.clip:placeholderapi:2.11.7")
import me.clip.placeholderapi.PlaceholderAPI
command("stats") {
permission = "scripts.stats"
execute { sender, args ->
if (sender is Player) {
val uptime = PlaceholderAPI.setPlaceholders(sender, "%server_uptime%")
val pings = PlaceholderAPI.setPlaceholders(sender, "%player_ping%")
sender.sendRichMessage("<yellow>Uptime: <white>$uptime\n<yellow>Ping: <white>$pings ms")
}
}
}
Ví dụ 5 — HTTP request bất đồng bộ
Tra cứu quốc gia theo IP. Dùng AsyncScheduler để không chặn main thread.
// country.kite.kts
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
val httpClient = HttpClient.newBuilder().connectTimeout(java.time.Duration.ofSeconds(5)).build()
command("country") {
permission = "scripts.country"
execute { sender, args ->
if (sender is Player) {
sender.sendRichMessage("<gray>Đang tra cứu quốc gia của bạn...")
server.asyncScheduler.runNow {
try {
val ip = sender.address.address.hostAddress
val req = HttpRequest.newBuilder().GET().uri(URI("https://get.geojs.io/v1/ip/country/full/$ip")).build()
val country = httpClient.send(req, HttpResponse.BodyHandlers.ofString()).body().trim()
sender.sendRichMessage("<green>Quốc gia: <yellow>$country")
} catch (e: Exception) {
sender.sendRichMessage("<red>Không tra cứu được: <white>${e.message}")
}
}
}
}
}
Ví dụ 6 — Đa file + tự tải thư viện (Caffeine)
Dự án đa file, dùng thư viện cache Caffeine tải từ Maven, kết hợp Adventure để gửi message.
File 1: plugins/Kite/scripts/cache_demo/helpers.kite.kts
// helpers.kite.kts
fun greet(playerName: String): String = "Xin chào $playerName! 👋"
File 2: plugins/Kite/scripts/cache_demo/main.kite.kts
// main.kite.kts (entry point của cache_demo)
@file:Repository("https://maven-central.storage-download.googleapis.com/maven2")
@file:Dependency("com.github.ben-manes.caffeine:caffeine:3.2.3")
@file:Import("helpers.kite.kts")
import com.github.benmanes.caffeine.cache.Caffeine
val cache = Caffeine.newBuilder()
.expireAfterWrite(java.time.Duration.ofMinutes(5))
.build<String, String>()
command("greet") {
permission = "scripts.greet"
execute { sender, args ->
val name = args.firstOrNull() ?: sender.name
val cached = cache.getIfPresent(name)
if (cached != null) {
sender.sendRichMessage("<gray>(cache) $cached")
} else {
val message = greet(name)
cache.put(name, message)
sender.sendRichMessage("<green>$message")
}
}
}
Thư mục con phải có main.kite.kts làm entry point. Nếu hai thư mục có file cùng tên, khai báo package để tránh lỗi trùng class (xem mục 14.3).
Ví dụ 7 — Kit (lệnh cấp trang bị)
Lệnh /kit cấp các bộ trang bị theo tên, kèm gợi ý tab tự động. Item dùng Material, message dùng Adventure.
// kit.kite.kts
import org.bukkit.Material
import org.bukkit.inventory.ItemStack
val kits = mapOf(
"starter" to listOf(
ItemStack(Material.STONE_SWORD),
ItemStack(Material.COOKED_BEEF, 16),
ItemStack(Material.LEATHER_HELMET)
),
"diamond" to listOf(
ItemStack(Material.DIAMOND_SWORD),
ItemStack(Material.DIAMOND_CHESTPLATE),
ItemStack(Material.GOLDEN_APPLE, 4)
)
)
command("kit") {
description = "Cấp kit cho người chơi."
permission = "scripts.kit"
usage = "/kit <starter|diamond>"
execute { sender, args ->
if (sender !is Player) {
sender.sendRichMessage("<red>Chỉ người chơi mới dùng được lệnh này.")
return@execute
}
val kit = args.firstOrNull() ?: "starter"
val items = kits[kit]
if (items == null) {
sender.sendRichMessage("<red>Kit '<yellow>$kit</red>' không tồn tại. Có: <yellow>${kits.keys.joinToString(", ")}")
return@execute
}
items.forEach { sender.inventory.addItem(it) }
sender.sendRichMessage("<green>Đã nhận kit <yellow>$kit</green>! 🎒")
}
tabComplete { sender, args ->
return@tabComplete kits.keys.filter { it.startsWith(args.lastOrNull() ?: "") }
}
}
Ví dụ 8 — Rank (prefix theo quyền)
Gán prefix cho người chơi dựa trên permission node (rank.vip, rank.mvp, rank.admin). Hiển thị khi vào server và khi chạy lệnh /rank.
// rank.kite.kts
val ranks = linkedMapOf(
"vip" to "<gold>[VIP] ",
"mvp" to "<light_purple>[MVP] ",
"admin" to "<red>[ADMIN] "
)
// Hàm tìm rank của player theo permission node
fun getRank(player: Player): String? = ranks.keys.find { player.hasPermission("rank.$it") }
on<PlayerJoinEvent> { event ->
val prefix = getRank(event.player)?.let { ranks[it] } ?: ""
event.player.sendRichMessage("$prefix<gray>Xin chào ${event.player.name}!")
}
command("rank") {
permission = "scripts.rank"
execute { sender, args ->
if (sender !is Player) return@execute
val rank = getRank(sender)
if (rank == null) {
sender.sendRichMessage("<gray>Bạn chưa có rank.")
return@execute
}
sender.sendRichMessage("${ranks[rank]}<green>Rank của bạn: <yellow>$rank")
}
}
Ví dụ 9 — Warp menu (GUI Inventory)
Mở một GUI 9×3 chứa các nút warp. Click vào item sẽ đóng menu và dịch chuyển người chơi. Dùng Adventure để đặt tên item và title menu.
// warpmenu.kite.kts
import org.bukkit.Bukkit
import org.bukkit.Location
import org.bukkit.Material
import org.bukkit.inventory.Inventory
import org.bukkit.inventory.ItemStack
import org.bukkit.event.inventory.InventoryClickEvent
import net.kyori.adventure.text.Component
import net.kyori.adventure.text.format.NamedTextColor
data class Warp(val label: String, val material: Material, val x: Int, val z: Int)
val warps = listOf(
Warp("Spawn", Material.RED_BED, 0, 0),
Warp("Mine", Material.STONE_PICKAXE, 100, -50),
Warp("Farm", Material.WHEAT, -200, 80)
)
val menuTitle = Component.text("Warp Menu").color(NamedTextColor.GOLD)
fun teleportToWarp(player: Player, warp: Warp) {
val y = player.world.getHighestBlockYAt(warp.x, warp.z)
val loc = Location(player.world, warp.x + 0.5, (y + 1).toDouble(), warp.z + 0.5)
player.scheduler.run { // EntityScheduler, Folia-safe
player.teleport(loc)
player.sendRichMessage("<green>Đã dịch chuyển đến <yellow>${warp.label}</green>!")
}
}
command("warp") {
description = "Mở menu warp."
permission = "scripts.warp"
execute { sender, args ->
if (sender !is Player) {
sender.sendRichMessage("<red>Chỉ người chơi dùng được lệnh này.")
return@execute
}
val target = args.firstOrNull()?.lowercase()
if (target != null) {
val warp = warps.find { it.label.lowercase() == target }
if (warp != null) { teleportToWarp(sender, warp); return@execute }
sender.sendRichMessage("<red>Warp '<yellow>$target</red>' không tồn tại.")
return@execute
}
openWarpMenu(sender)
}
}
fun openWarpMenu(player: Player) {
val inv: Inventory = Bukkit.createInventory(null, 9 * 3, menuTitle)
warps.forEachIndexed { i, warp ->
val icon = ItemStack(warp.material)
icon.editMeta { displayName(Component.text(warp.label).color(NamedTextColor.GREEN)) }
inv.setItem(i, icon)
}
player.openInventory(inv)
}
on<InventoryClickEvent> { event ->
if (event.view.title() != menuTitle) return@on
event.isCancelled = true
val player = event.whoClicked as? Player ?: return@on
val slot = event.rawSlot
if (slot in warps.indices) {
player.closeInventory()
teleportToWarp(player, warps[slot])
}
}
Ví dụ 10 — Fly toggle
Lệnh /fly bật/tắt bay. Dùng property isFlying và Adventure để báo trạng thái.
// fly.kite.kts
command("fly") {
description = "Bật/tắt chế độ bay."
permission = "scripts.fly"
execute { sender, args ->
if (sender !is Player) {
sender.sendRichMessage("<red>Chỉ người chơi dùng được lệnh này.")
return@execute
}
sender.isFlying = !sender.isFlying
sender.sendRichMessage(
"<green>Chế độ bay: <${if (sender.isFlying) "green" else "red"}>${if (sender.isFlying) "BẬT" else "TẮT"} ✈️"
)
}
}
Ví dụ 11 — PvP toggle theo từng người chơi
Mỗi người chơi tự bật/tắt PvP của mình. Nếu một trong hai bên tắt PvP thì hủy sát thương.
// pvp.kite.kts
import org.bukkit.event.entity.EntityDamageByEntityEvent
// Lưu tên những player đang bật PvP
val pvpEnabled = mutableSetOf<String>()
command("pvp") {
description = "Bật/tắt PvP của bản thân."
permission = "scripts.pvp"
execute { sender, args ->
if (sender !is Player) return@execute
val name = sender.name
val on = if (name in pvpEnabled) { pvpEnabled.remove(name); false } else { pvpEnabled.add(name); true }
sender.sendRichMessage(
"<gold>PvP: <${if (on) "green" else "red"}>${if (on) "BẬT" else "TẮT"} ⚔️"
)
}
}
on<EntityDamageByEntityEvent> { event ->
val damager = event.damager as? Player ?: return@on
val victim = event.entity as? Player ?: return@on
if (damager.name !in pvpEnabled || victim.name !in pvpEnabled) {
event.isCancelled = true
damager.sendRichMessage("<red>PvP đang tắt — không thể gây sát thương lẫn nhau!")
}
}
Ví dụ 12 — Tương tác NPC với FancyNpcs
Lắng nghe event tương tác NPC của plugin FancyNpcs (NpcInteractEvent) để chạy hành động khi người chơi click vào NPC. Vì là API của plugin khác, bạn phải import tường minh. Không cần khai báo version — xem giải thích bên dưới.
- FancyNpcs đã cài trên server → Kite truy cập thẳng classes của plugin đang chạy qua classpath, nên không cần
@file:Dependency(và không cần version). @file:Dependencychỉ bắt buộc cho thư viện chưa có trên server (Caffeine, HTTP client…) — lúc đó Maven mới cần version để tải artifact.- Placeholder
{version}không hoạt động trong annotation (chuỗi tĩnh) — nó chỉ có tác dụng trong Gradle build script. - Bạn chỉ cần khai báo build.gradle.kts dạng
compileOnlyđể IDE gợi ý code, không phải để chạy.
- Cài plugin FancyNpcs trên server (Paper 1.21.11).
- Tạo NPC bằng /npc create <tên>.
- Dùng đúng version API tương ứng bản FancyNpcs server đang chạy trong build.gradle.kts.
// npc.kite.kts — không cần @file:Dependency vì FancyNpcs đã có trên server
import de.oliver.fancynpcs.api.events.NpcInteractEvent
on<NpcInteractEvent> { event ->
val player = event.player // người chơi click
val npcName = event.npc.data.name // tên NPC bị click
when (npcName) {
"shop" -> {
player.sendRichMessage("<gold>🛒 Chào mừng đến cửa hàng!")
// mở GUI shop, gọi plugin economy... tại đây
}
"quest" -> player.sendRichMessage("<green>📜 Bạn đã bắt đầu nhiệm vụ!")
"heal" -> {
player.health = player.maxHealth
player.sendRichMessage("<green>💖 NPC đã hồi máu cho bạn!")
}
else -> player.sendRichMessage("<gray>Bạn tương tác với NPC <yellow><bold>$npcName")
}
}
Chỉ để IDE gợi ý code, thêm vào build.gradle.kts — dùng compileOnly (không đóng gói, vì plugin đã có trên server):
repositories {
maven { url = uri("https://repo.fancyinnovations.com/releases") }
}
dependencies {
compileOnly("de.oliver:FancyNpcs:2.10.0.356") // version khớp bản FancyNpcs trên server
}
- Nếu cần tên NPC để so sánh với id trong config, dùng
event.npc.data.name(id NPC FancyNpcs). - Muốn phân biệt click chuột trái/phải, dùng property
event.clickTypecủa event. - Bạn có thể kết hợp
whenvới nhiều hành động: mở GUI, trao item, gọi Vault/PlaceholderAPI…
Check permission + gửi message từ message.yml
Nâng cấp: khi click NPC, check quyền (player.hasPermission(...)). Nếu chưa có quyền thì load message từ file message.yml (đã tạo ở Ví dụ 14) và gửi cho người chơi. Message có thể là 1 dòng hoặc nhiều dòng — dùng key lines: để khai báo danh sách.
Vì message.yml nằm trên đĩa, bất kỳ script nào cũng đọc được bằng cùng pattern YamlConfiguration.loadConfiguration(File(scriptFile, "message.yml")) — không cần gọi hàm từ script khác. Mỗi script tự load file khi onLoad là cách đơn giản và chắc chắn nhất.
Thêm vào message.yml (từ Ví dụ 14) các key liên quan NPC:
# message.yml — thêm phần này
npc:
no_perm: "<red>⛔ Bạn chưa có quyền dùng NPC này!"
no_perm_lines: # dạng nhiều dòng (block)
- "<red>⛔ Không đủ quyền."
- "<gray>Cần quyền <yellow>vip.npc.shop"
- "<gray>Liên hệ admin để mua VIP."
deny_perm: "<gold><bold>✋ <gold>NPC tạm khóa"
// npc_perm.kite.kts — check quyền + gửi message từ message.yml
import de.oliver.fancynpcs.api.events.NpcInteractEvent
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
// Load message.yml nằm cạnh script (giống Ví dụ 14):
val msg = YamlConfiguration.loadConfiguration(File(scriptFile, "message.yml"))
on<NpcInteractEvent> { event ->
val player = event.player
val npcName = event.npc.data.name
// Chỉ check quyền cho NPC "shop" (đổi tùy bạn)
if (npcName == "shop") {
if (player.hasPermission("vip.npc.shop")) {
// ✅ CÓ quyền → thực thi actions / lệnh
player.sendRichMessage("<green>🛒 Chào mừng VIP!")
// player tự chạy lệnh (mở GUI shop, gọi economy...):
player.performCommand("shop")
// console chạy lệnh (trao quyền, gửi quà, teleport...):
server.dispatchCommand(server.consoleSender, "lp user ${player.name} permission set vip.gift true")
server.dispatchCommand(server.consoleSender, "give ${player.name} diamond 5")
// các action khác: hồi máu, cấp item, đổi gamemode... tùy bạn
player.health = player.maxHealth
player.inventory.addItem(ItemStack(Material.GOLDEN_APPLE, 1))
} else {
// ❌ CHƯA quyền → gửi message từ message.yml
sendNpcMessage(player, "npc.no_perm_lines")
}
}
}
// Gửi message: 1 dòng (String) hoặc nhiều dòng (List), tự nhận diện
fun sendNpcMessage(player: Player, path: String) {
val obj = msg.get(path) ?: return
when (obj) {
is String -> player.sendRichMessage(obj) // 1 dòng
is List<*> -> obj.filterIsInstance<String>()
.forEach { player.sendRichMessage(it) } // block nhiều dòng
}
}
player.hasPermission("vip.npc.shop")— trả vềtrue/falsedựa trên quyền hiện có của player (bao gồm cả quyền từ group/plugin quản lý quyền như LuckPerms).- Có quyền → chạy actions: gửi message,
player.performCommand("shop")(player tự chạy lệnh) hoặcserver.dispatchCommand(server.consoleSender, "...")(console chạy lệnh — trao quyền, gửi quà, teleport…). Bạn có thể viết thêmwhen (npcName)để NPC "shop" / "quest" / "heal" mỗi cái có chuỗi actions riêng. - Hàm
sendNpcMessagenhận diện tự động: nếu value làString→ gửi 1 dòng; nếu làList→ gửi từng dòng (block nhiều line). Nhờ vậy cùng 1 hàm dùng được cho cả 2 dạng. msg.get(path)trả vềAny?, nên dùngis String/is List<*>để rẽ nhánh an toàn kiểu.- Đổi
npc.no_perm_lines→npc.no_permđể gửi message 1 dòng thay vì block.
Ví dụ 13 — Bắt Custom Event của plugin khác
Ngoài event Bukkit/Paper, script có thể bắt event tùy biến do plugin khác call. Ví dụ: sau khi login thành công, plugin của bạn gọi PlayerAuthSuccessEvent → script bắt event đó và chạy các actions / commands.
- Class event phải extends
org.bukkit.event.Event— class bạn đã viết là đạt chuẩn. - Plugin phải gọi event bằng
Bukkit.getPluginManager().callEvent(event)ở đúng thời điểm. - Import tường minh class event (gói không tự import):
import me.ipapervn.modules.login.PlayerAuthSuccessEvent. - Không cần version — plugin đã cài trên server, Kite truy cập thẳng class của nó qua classpath.
Script mẫu: JOIN → LOGIN THÀNH CÔNG → chạy commands
// auth_flow.kite.kts — đặt vào plugins/Kite/scripts/
import me.ipapervn.modules.login.PlayerAuthSuccessEvent // event plugin của bạn
// 1) Player vừa JOIN (event Bukkit chuẩn, tự động có)
on<PlayerJoinEvent> { event ->
val player = event.player
player.sendRichMessage("<gray>🔐 Vui lòng đăng nhập: <yellow>/login <mật_khẩu>")
// VD: console chạy lệnh — đặt player về spectator chờ xác thực:
server.dispatchCommand(server.consoleSender, "gamemode spectator ${player.name}")
// có thể gọi lệnh plugin khác, vd ẩn danh / bảo vệ spawn...
// actions trước khác: lưu vị trí, ẩn danh, set gamemode... tùy bạn
}
// 2) LOGIN THÀNH CÔNG → chạy actions / commands
on<PlayerAuthSuccessEvent> { event ->
val player = event.player
player.sendRichMessage("<green>✅ Đăng nhập thành công! Chào mừng <gold>${player.name}")
player.performCommand("spawn") // player tự chạy lệnh
server.dispatchCommand(server.consoleSender, "give ${player.name} diamond 1") // console chạy lệnh
server.dispatchCommand(server.consoleSender, "lp user ${player.name} parent add member")
player.showTitle(Title.title(
Component.text("Chào mừng!").color(NamedTextColor.GOLD),
Component.text(player.name).color(NamedTextColor.YELLOW)
))
player.inventory.addItem(ItemStack(Material.GOLDEN_APPLE, 1))
}
player.performCommand("...")— người chơi 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 vào chuỗi lệnh:
"give ${player.name} diamond 1".
Về phía plugin — cần đúng 2 điều
- Class event hợp lệ: extends
Eventvà cógetHandlers()— classPlayerAuthSuccessEventbạn viết đã đạt chuẩn. - Gọi event khi login thành công:
Bukkit.getPluginManager().callEvent(PlayerAuthSuccessEvent(player))
on<Event>tự đăng ký/hủy listener theo vòng đời script — không cần tự quản lý listener.- Muốn chạy thêm actions trước khi login thành công, dùng
on<PlayerJoinEvent>(đã có trong ví dụ) — không cần thêm class/event nào trong plugin. - Muốn sắp thứ tự giữa các listener cùng 1 event, dùng
priority = EventPriority.HIGHEST/LOWEST.
Ví dụ 14 — Tạo & đọc file (message.yml)
Kite cho phép bạn thao tác trực tiếp với file trên ổ đĩa bằng Kotlin/Java API chuẩn. Cách phổ biến nhất: dùng file YAML (message.yml, config.yml, lang/vi.yml…) để chứa thông điệp/khai báo mà nhiều script cùng dùng chung. Nhờ đó bạn đổi nội dung mà không cần sửa code — chỉ sửa file rồi reload.
1) Tạo file message.yml (đặt ở plugins/Kite/scripts/message.yml)
# ===== message.yml =====
join:
welcome: "<gold>Chào mừng <yellow>{player}<gold> đến server! 🎉"
tip: "<gray>Mẹo: gõ <green>/help</green> để xem lệnh."
death:
message: "<red>💀 {player} đã tử vong bởi {killer}"
reward:
daily: "<green>✅ Nhận quà hằng ngày: {amount} 🌟"
reload:
done: "<green>Đã reload message.yml"
2) Script đọc file — message_loader.kite.kts
Dùng YamlConfiguration (có sẵn trong Paper, không cần thêm thư viện) để nạp file thành object rồi truy cập theo path. File nằm cạnh script nên dùng File(scriptFile, "message.yml"):
// message_loader.kite.kts
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
// File message.yml nằm CÙNG thư mục script:
val msg = YamlConfiguration.loadConfiguration(File(scriptFile, "message.yml"))
fun sendJoin(player: Player) {
val welcome = msg.getString("join.welcome")!!.replace("{player}", player.name)
player.sendRichMessage(welcome)
player.sendRichMessage(msg.getString("join.tip")!!)
}
on<PlayerJoinEvent> { e -> sendJoin(e.player) }
on<PlayerDeathEvent> { e ->
val killer = e.damageSource?.causingEntity?.name ?: "tự nhiên"
val text = msg.getString("death.message")!!
.replace("{player}", e.player.name)
.replace("{killer}", killer)
server.broadcast(Component.text(replaceColor(text)))
}
fun replaceColor(s: String) = s.replace("<", "<").replace(">", ">").replace("&", "&")
Kite cung cấp biến scriptFile (kiểu File) trỏ tới file .kite.kts đang chạy. Dùng File(scriptFile, "message.yml") để mở file cùng thư mục một cách an toàn, không phụ thuộc working directory.
3) Các script khác CÙNG dùng file này
Vì message.yml nằm trên đĩa, bất kỳ script nào cũng đọc được. Cách gọn: đặt code nạp + lấy thông điệp vào một hàm dùng chung, mỗi script onLoad sẽ nạp file. Ví dụ script thứ 2 dùng message.yml để gửi quà:
// daily_reward.kite.kts — script riêng, vẫn đọc chung message.yml
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
val msg = YamlConfiguration.loadConfiguration(File(scriptFile, "message.yml"))
on<PlayerJoinEvent> { e ->
val player = e.player
if (hasClaimedToday(player.uniqueId)) return@on
player.sendRichMessage(
msg.getString("reward.daily")!!.replace("{amount}", "50")
)
player.inventory.addItem(ItemStack(Material.DIAMOND, 1))
}
fun hasClaimedToday(uuid: java.util.UUID): Boolean = false // bạn ghi vào file khác để nhớ
4) Ghi / tạo file từ script
Muốn tạo file hoặc cập nhật dữ liệu, dùng YamlConfiguration.save(File). Đây là cách lưu điểm, quà hằng ngày, số lần login… Mẹo: tự sinh message.yml mặc định khi chưa có:
// config_init.kite.kts — tạo message.yml nếu chưa tồn tại
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
val f = File(scriptFile, "message.yml")
if (!f.exists()) {
f.parentFile.mkdirs() // tạo thư mục nếu thiếu
val def = YamlConfiguration()
def.set("join.welcome", "<gold>Chào mừng {player}!")
def.set("reward.daily", "<green>Nhận {amount} 🌟")
def.save(f) // ghi ra đĩa
server.consoleSender.sendPlainMessage("[Kite] Đã tạo message.yml")
}
- Đường dẫn an toàn: luôn đặt file trong thư mục script (
File(scriptFile, "...")) để tránh ghi ra ngoài — là lỗ hổng bảo mật nếu cho người khác nạp script. - Charset: dùng
YamlConfigurationthì tiếng Việt lưu tốt. Nếu tự đọc bằngFile.readText(), chỉ rõ encoding:File(...).readText(Charsets.UTF_8). - Reload: sau khi sửa
message.yml, chạy/kite reload message_loaderđể script đọc lại file mới.
Ví dụ 15 — Quest NPC: flag trạng thái bằng quyền (kiểu ConditionalEvents)
Ví dụ này hookup FancyNpcs làm NPC — lắng nghe NpcInteractEvent khi người chơi click NPC. Không cần @file:Dependency vì FancyNpcs đã cài trên server (chi tiết ở Ví dụ 12).
File npc_single.kite.kts đọc cấu hình từ npc.yml. 1 script duy nhất xử lý tất cả NPC. Muốn thêm NPC chỉ cần thêm 1 khối trong npc.yml rồi /kite reloadall — không sửa code.
- Cài plugin FancyNpcs (Paper 1.21.11).
- Đứng vị trí muốn đặt, tạo NPC: /npc create shop (hoặc /npc create healer…). Tên NPC = id, khớp với key trong
npc.yml. - Đặt skin/model: /npc skin <tên>.
Bước 1 — Đặt script vào plugins/Kite/scripts/. Lần chạy đầu tiên nó tự tạo file npc.yml mẫu (khối shop). Mở ra sửa theo ý bạn — và thêm thêm các NPC khác bằng cách copy khối rồi đổi tên.
healer# npc.yml npcs: shop: # /npc create shop perm: "vip.npc.shop" need: "GOLD_INGOT" # item cần nộp để mở amount: 10 # số lượng cần cmd_gained: # lệnh chạy khi đã mở - "shop" - "give {player} diamond 1" healer: # /npc create healer — NPC mới perm: "vip.npc.healer" need: "DIAMOND" amount: 1 cmd_gained: []
perm → quyền = flag "đã mở"; player có nó là xong quest need → item phải nộp để mở (tên Material, in hoa) amount → số lượng item cần nộp cmd_gained → lệnh chạy khi đã mở (danh sách, chạy theo thứ tự) Thêm NPC mới: copy 1 khối → đổi tên + perm → tạo NPC cùng tên trong FancyNpcs → /kite reloadall không cần sửa script.
Khác với Ví dụ 14 (message.yml để ngoài cho nhiều script dùng chung), npc_single tự chứa message bằng sendRichMessage để đúng tiêu chí "1 file gọn, không tách rời". Sau này muốn tách message ra ngoài thì tham khảo Ví dụ 14.
Bước 2 — Script npc_single.kite.kts (chỉ 1 file, tự chứa tất cả).
Bạn không cần gõ lại — file npc_single.kite.kts (đầy đủ comment, xử lý lỗi Material, log onLoad/onUnload) đã được tạo cùng thư mục. Đặt nó vào plugins/Kite/scripts/ rồi /kite load npc_single là chạy.
- Tự tạo npc.yml: nếu chưa có, ghi khối
shopmẫu vàoFile(scriptFile, "npc.yml"). on<NpcInteractEvent>: lấynpcId = event.npc.data.name→cfg.getConfigurationSection("npcs.$npcId");null→ bỏ qua an toàn.- Đã mở (
hasPermission(perm)) → chạycmd_gained(thay{player}) → gửi message "đã mở", không làm lại nv. - Chưa mở → đếm item (
need+amount): đủ →removeItems+ console gán quyền + thông báo; chưa đủ → nhắc nhiệm vụ. - Hàm
removeItemstrừ item khỏi inventory. onLoad/onUnload: log các NPC đang quản lý khi nạp/gỡ.
Toàn bộ script (để copy):
// npc_single.kite.kts — Quest NPC, MỘT file duy nhất, tự chứa tất cả
import de.oliver.fancynpcs.api.events.NpcInteractEvent
import org.bukkit.configuration.file.YamlConfiguration
import java.io.File
// ---- 1. CẤU HÌNH (tự tạo npc.yml nếu chưa có) ----
val npcFile = File(scriptFile, "npc.yml")
if (!npcFile.exists()) {
npcFile.parentFile.mkdirs()
val def = YamlConfiguration()
def.set("npcs.shop.perm", "vip.npc.shop")
def.set("npcs.shop.need", "GOLD_INGOT")
def.set("npcs.shop.amount", 10)
def.set("npcs.shop.cmd_gained", listOf("shop", "give {player} diamond 1"))
def.save(npcFile)
logger.info("[npc_single] Đã tự tạo npc.yml mặc định")
}
val cfg = YamlConfiguration.loadConfiguration(npcFile)
// ---- 2. SỰ KIỆN TƯƠNG TÁC NPC (FancyNpcs) ----
on<NpcInteractEvent> { event ->
val player = event.player
val npcId = event.npc.data.name
val sec = cfg.getConfigurationSection("npcs.$npcId") ?: return@on
val perm = sec.getString("perm") ?: return@on
// ĐÃ mở (có quyền) → chạy lệnh, không làm lại nhiệm vụ
if (player.hasPermission(perm)) {
sec.getStringList("cmd_gained")
.filter { it.isNotBlank() }
.forEach { raw -> player.performCommand(raw.replace("{player}", player.name)) }
player.sendRichMessage("<green>✨ Bạn đã mở NPC này rồi!")
return@on
}
// CHƯA mở → kiểm tra nộp item
val mat = Material.valueOf(sec.getString("need")!!.uppercase())
val amount = sec.getInt("amount", 1)
val have = player.inventory.all(mat).values.sumOf { it.amount }
if (have < amount) {
player.sendRichMessage("<gold>📜 Nhiệm vụ: mang <yellow>$amount ${mat.name.lowercase()}</yellow> cho ta.")
player.sendRichMessage("<gray>Quay lại khi đủ để mở quyền.")
} else {
removeItems(player, mat, amount)
server.dispatchCommand(server.consoleSender, "lp user ${player.name} permission set $perm true")
player.sendRichMessage("<green>✅ Hoàn thành! Đã mở quyền cho bạn.")
}
}
// ---- 3. HÀM TIỆN ÍCH ----
fun removeItems(player: Player, mat: Material, count: Int) {
var left = count
player.inventory.all(mat).forEach { (_, item) ->
if (left <= 0) return
val take = minOf(left, item.amount)
item.amount -= take
left -= take
}
}
- Quyền = flag:
player.hasPermission(perm)trả lời "đã hoàn thành chưa?". Vừa check điều kiện vừa lưu trạng thái — không cần file ghi nhớ riêng. - Thêm NPC mới = thêm 1 khối trong npc.yml, không sửa script. Copy khối
healer:→ đổi tên +perm→ tạo NPC cùng tên →/kite reloadall. - NPC không nằm trong npc.yml →
getConfigurationSection("npcs.$id")trảnull→ bị bỏ qua an toàn. - Lần đầu: chưa quyền → nhắc nhiệm vụ, yêu cầu nộp item.
- Đủ item:
removeItemsthu item →lp user ... permission set ... truegán quyền. - Lần sau: nhánh
if (hasPermission)chạy lệnh từcmd_gained→ không làm lại nv. - Placeholder
{player}được thay trong lệnh; message dùng MiniMessage (<green>…). - Xóa quyền để "reset" quest cho player:
lp user <tên> permission unset <perm>.
16Lệnh quản lý & tài nguyên
Các lệnh quản lý (yêu cầu permission kite.manage)
| Lệnh | Chức năng |
|---|---|
/kite list (script) | Liệt kê tất cả script |
/kite load (script) | Nạp script theo tên |
/kite unload (script) | Gỡ script đang nạp |
/kite reload (script) | Reload một script |
/kite reloadall | Reload tất cả script |
Phần (script) là tùy chọn — nếu bỏ qua, áp dụng cho toàn bộ script.
Chỉ cấp quyền các lệnh này cho những người bạn tin tưởng, vì chúng có thể nạp/gỡ/reload mã chạy trên server.
Đường dẫn hữu ích
- Kite Documentation: echonine.dev/kite
- Kotlin Documentation: kotlinlang.org
- Paper Documentation: docs.papermc.io
- Paper JavaDocs: jd.papermc.io (API Paper/Bukkit)
- Adventure API: javaDoc Adventure
- Kite GitHub: EchoNineLabs/Kite
- Starter project: EchoNineLabs/KiteScripting
- Kite Discord: hỗ trợ cộng đồng