Sistem Plugin
Cara Knight diperluas: modul skrip untuk semua orang, plugin Go untuk fitur berat.
Knight punya satu titik ekstensi dengan dua jenis perpanjangan. Modul skrip (.sa) adalah yang dipakai hampir semua pemilik server: tanpa pengetahuan Go, tanpa rebuild, cukup taruh file lalu import. Plugin Go untuk pembuat server yang butuh fitur berkecepatan native seperti driver database, klien HTTP, atau native baru. Keduanya masuk ke registri event dan fungsi yang sama, jadi modul bisa memanggil plugin Go dan plugin bisa mendengarkan event yang sama.
1. Yang bisa dilakukan modul
| Kemampuan | Cara |
|---|---|
| Membagi fungsi | export function nama(...) di modul, panggil nama(...) setelah import "modul"; |
| Menambah command | player::command("/nama") { ... }; |
| Mereaksi event | on player::spawn() { ... }; atau gaya klasik player::spawn() { ... }; |
| Memblokir event | return false; dari handler boolean |
| Menyimpan state | let / const level atas di modul |
| Menjalankan timer | timer::every(60s) { ... }; |
2. Siklus hidup modul
| Tahap | Yang terjadi |
|---|---|
| Muat | import "x"; dicari, file dimasukkan sekali lalu di-parse |
| Cek | Semua fungsi dalam program didaftarkan, lalu semua file dicek tipenya |
| Init | let/const level atas dijalankan, lalu system::start dipanggil |
| Runtime | Fungsi export dan hook on bisa dipanggil |
| Stop | system::stop dipanggil; state modul hilang bersama server |
3. Menulis modul
// Gamemodes/modules/jobs.sa
module "jobs" {
version: "1.0.0";
author: "Euporia";
};
const JOB_TRUCKER = 1;
const PAYMENT = 250;
let active = {};
export function startJob(int id, int job) {
if (at(active, id) != null) { return false; }
active[id] = job;
SendClientMessage(id, "Pekerjaan dimulai.");
return true;
}
export function finishJob(int id) {
let job = at(active, id);
if (job == null) { return 0; }
del(active, id);
GivePlayerMoney(id, PAYMENT);
return PAYMENT;
}
player::command("/job", int job) {
if (!startJob(playerid, job)) {
SendClientMessage(playerid, "Kamu sudah punya pekerjaan.");
}
};
Pakai dari gamemode:
import "jobs";
system::start() {
AddPlayerClass(0, 1958.4, 1343.2, 15.4, 270.0, 24, 50);
};
player::command("/selesai") {
let paid = finishJob(playerid);
SendClientMessage(playerid, paid > 0 ? "Dibayar {paid}" : "Tidak ada pekerjaan.");
};
4. Hook dan veto
Handler event boolean (seperti player::chat) bisa memveto. Handler dijalankan sesuai urutan muat; return false pertama menghentikan rantai dan melewati aksi default engine (untuk chat: tidak di-relay). Ini membuat modul bisa menegakkan aturan tanpa mengubah gamemode utama.
// Gamemodes/modules/mute.sa
module "mute" { version: "1.0.0"; };
let muted = {};
export function mute(int id) { muted[id] = true; }
export function unmute(int id) { del(muted, id); }
on player::chat(string message) {
if (has(muted, playerid)) {
SendClientMessage(playerid, "Kamu dibisukan.");
return false; // blokir baris
}
return true; // teruskan
};
5. Lokasi modul
| Urutan | Path |
|---|---|
| 1 | Gamemodes/<nama>.sa |
| 2 | Gamemodes/modules/<nama>.sa |
| 3 | modules/<nama>.sa (satu level di atas gamemode) |
| 4 | modules/<nama>.sa di direktori kerja |
Modul dimuat sekali walau beberapa file meng-import-nya. Import melingkar dan file hilang adalah error saat start, dilaporkan dengan rantai lengkap.
6. Plugin Go (untuk pembuat server)
Bila fitur perlu menyentuh socket, file, atau driver, tulis di Go lalu daftarkan. Plugin dikompilasi ke dalam server, jadi tidak ada risiko ABI, tidak ada batas GC, dan -race tetap berlaku. Pengguna memasang binary hasilnya; mereka tidak pernah melihat kode Go.
// plugins/discord/discord.go
package discord
type plugin struct{ api API }
func init() { Register(func() Plugin { return &plugin{} }) }
func (p *plugin) Info() Info { return Info{Name: "discord", Version: "1.0.0"} }
func (p *plugin) Init(api API) error {
p.api = api
api.RegisterNative("DiscordSend", p.send) // bisa dipanggil dari .sa
api.Hook("player::connect", 100, p.onConnect) // event sama dengan modul
return nil
}
func (p *plugin) send(ctx Ctx, args []Value) Value {
return Int(1) // DiscordSend(...) -> int
}
func (p *plugin) onConnect(ctx Ctx) bool {
ctx.Log("player %d connected", ctx.Int(0))
return true
}
| API plugin | Kegunaan |
|---|---|
Register(fn) | Mendeklarasikan plugin di init() |
Info | Nama/versi yang tampil di log start |
RegisterNative(name, fn) | Membuka fungsi ke gamemode .sa |
Hook(event, priority, fn) | Mengamati atau memveto event bersama modul |
Log, Int, String, Bool | Helper nilai kecil untuk argumen native |
Memuat plugin dari path
Selain blank import, plugin bisa dideklarasikan dari gamemode. server.plugins menerima daftar path. Path bisa berupa folder atau satu file .sa.
system::config() {
server.name = "Euporia RP";
server.plugins = ["plugins", "mods/anticheat.sa"];
};
| Path | Yang dimuat |
|---|---|
plugins (folder) | Semua file *.sa di dalamnya dan setiap subfolder langsung yang berisi plugin.go |
plugins/json (folder) | Satu paket plugin Go itu |
plugins/extra.sa (file) | Satu plugin skrip itu |
Struktur folder
plugins/
├── json/ <- plugin Go (plugin.go, dikompilasi ke server)
│ ├── plugin.go
│ └── README.md
├── anticheat/ <- plugin Go
│ └── plugin.go
└── welcome.sa <- plugin skrip (tanpa rebuild)
Semua isi folder dimuat bersama, terurut berdasarkan nama. Folder tanpa plugin.go diabaikan; file .sa selalu dimuat sebagai plugin skrip.
Plugin Go harus dikompilasi
Plugin Go adalah kode nyata yang berjalan di dalam proses server, jadi paketnya harus menjadi bagian binary. Import sekali di cmd/knight-server/plugins_enabled.go (atau build Anda sendiri), lalu arahkan server.plugins ke foldernya:
package main
import (
_ "knight/plugins/json"
_ "knight/plugins/anticheat"
)
plugin.go tanpa paket yang dikompilasi adalah error saat start, jadi salah ketik tidak pernah diam-diam menonaktifkan fitur.Plugin skrip tanpa rebuild
Plugin .sa berperilaku seperti modul yang di-import: function-nya bisa dipanggil gamemode dan handler on-nya berjalan bersama handler gamemode. Ini jalur untuk apa pun yang ditulis sendiri oleh pemilik server.
// plugins/welcome.sa
module "welcome" { version: "1.0.0"; };
export function welcomeGreeting() { return "Selamat datang!"; };
on player::connect() {
SendClientMessage(playerid, welcomeGreeting());
};
player::command("/help2") {
SendClientMessage(playerid, "Ditambahkan oleh plugin .sa");
};
plugins anticheat.sa extra/ di server.cfg juga bekerja, bagi operator yang lebih suka file. Nilai di system::config() menang, jadi satu sumber kebenaran sudah cukup.8. Mengaktifkan plugin Go
Plugin menjadi bagian server lewat blank import. Buat titik masuk kecil yang meng-import plugin yang Anda distribusikan lalu menjalankan server seperti biasa:
// cmd/knight-server/plugins.go
package main
import (
_ "knight/plugins/discord"
_ "knight/plugins/anticheat"
)
Build ulang server. Saat start, log menampilkan setiap plugin yang dimuat, dan gamemode bisa langsung memanggil native-nya. Tidak ada perubahan bagi pemain: mereka memasang binary seperti biasa.
| Log start | Arti |
|---|---|
plugin loaded: welcome 1.0.0 | Init berhasil |
plugin nama: ... error | Init mengembalikan error; server menolak start |
native X overrides an earlier plugin | Dua plugin mendaftarkan nama sama; yang terakhir menang |
9. Pilih yang mana?
| Kebutuhan | Gunakan |
|---|---|
| Aturan gameplay, job, ekonomi, anticheat | Modul skrip (.sa) |
| Command admin dan kebijakan chat | Modul skrip (.sa) |
| Skema database dan persistensi | Modul skrip dengan SQLite/MySQL |
| Callback HTTP, bot Discord, API eksternal | Plugin Go |
| Primitif baru (socket, file watcher) | Plugin Go |
| Mengubah gameplay tanpa restart | Modul skrip: edit lalu restart gamemode |
10. Kesalahan umum
| Gejala | Penyebab |
|---|---|
module not found: x | File tidak ada di jalur pencarian, atau ekstensi lupa |
circular import: a -> b -> a | Dua modul saling import |
expected ';' after event block | on ... { } atau ns::name(...) { } tanpa ; di akhir |
| Bentrok nama dengan native | Pakai nama lain; nama native sudah dipesan |
| Filter chat tidak bekerja | Handler mengembalikan true, bukan false |
| State tiba-tiba reset | State modul di memori; simpan dengan SQLite/MySQL |