Knight docs
EN ID
GitHub

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

KemampuanCara
Membagi fungsiexport function nama(...) di modul, panggil nama(...) setelah import "modul";
Menambah commandplayer::command("/nama") { ... };
Mereaksi eventon player::spawn() { ... }; atau gaya klasik player::spawn() { ... };
Memblokir eventreturn false; dari handler boolean
Menyimpan statelet / const level atas di modul
Menjalankan timertimer::every(60s) { ... };
Note
Modul dicek tipenya bersama gamemode, jadi salah ketik di modul yang di-import dilaporkan saat start, bukan saat runtime.

2. Siklus hidup modul

TahapYang terjadi
Muatimport "x"; dicari, file dimasukkan sekali lalu di-parse
CekSemua fungsi dalam program didaftarkan, lalu semua file dicek tipenya
Initlet/const level atas dijalankan, lalu system::start dipanggil
RuntimeFungsi export dan hook on bisa dipanggil
Stopsystem::stop dipanggil; state modul hilang bersama server

3. Menulis modul

knight
// 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:

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

knight
// 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

UrutanPath
1Gamemodes/<nama>.sa
2Gamemodes/modules/<nama>.sa
3modules/<nama>.sa (satu level di atas gamemode)
4modules/<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.

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 pluginKegunaan
Register(fn)Mendeklarasikan plugin di init()
InfoNama/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, BoolHelper nilai kecil untuk argumen native
Warning
Plugin Go berjalan di dalam proses server dengan hak penuh. Hanya build dengan plugin yang dipercaya. Modul skrip tidak bisa membuat server crash: error-nya dicatat dan event lanjut.

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.

knight
system::config() {
    server.name = "Euporia RP";
    server.plugins = ["plugins", "mods/anticheat.sa"];
};
PathYang 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

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

go
package main

import (
    _ "knight/plugins/json"
    _ "knight/plugins/anticheat"
)
Note
Menyebut foldernya adalah yang mengaktifkan plugin: bila folder tidak terdaftar, plugin yang sudah dikompilasi tetap tidak aktif. Folder berisi 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.

knight
// 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");
};
Tip
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:

go
// 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 startArti
plugin loaded: welcome 1.0.0Init berhasil
plugin nama: ... errorInit mengembalikan error; server menolak start
native X overrides an earlier pluginDua plugin mendaftarkan nama sama; yang terakhir menang

9. Pilih yang mana?

KebutuhanGunakan
Aturan gameplay, job, ekonomi, anticheatModul skrip (.sa)
Command admin dan kebijakan chatModul skrip (.sa)
Skema database dan persistensiModul skrip dengan SQLite/MySQL
Callback HTTP, bot Discord, API eksternalPlugin Go
Primitif baru (socket, file watcher)Plugin Go
Mengubah gameplay tanpa restartModul skrip: edit lalu restart gamemode

10. Kesalahan umum

GejalaPenyebab
module not found: xFile tidak ada di jalur pencarian, atau ekstensi lupa
circular import: a -> b -> aDua modul saling import
expected ';' after event blockon ... { } atau ns::name(...) { } tanpa ; di akhir
Bentrok nama dengan nativePakai nama lain; nama native sudah dipesan
Filter chat tidak bekerjaHandler mengembalikan true, bukan false
State tiba-tiba resetState modul di memori; simpan dengan SQLite/MySQL