JSON
Parse, baca, bangun, dan simpan dokumen JSON dari gamemode.
- Jenis
- Go
- Status
- bawaan, sudah dikompilasi
- Native
JsonParseJsonParseFileJsonStringifyJsonFreeJsonGetStringJsonGetIntJsonGetFloatJsonGetBoolJsonHasJsonTypeJsonCountJsonKeysJsonEscapeJsonSaveFile- Hook
player::connect
Konfigurasi
| Variable | Default |
|---|---|
KNIGHT_JSON_ROOT | scriptfiles |
Mengaktifkan
Tambahkan path ke server.plugins (atau `plugins` di server.cfg).
knight
server.plugins = ["plugins/json"];Alur kerja
flowchart TD A["Gamemode .sa"] -->|JsonParse| B["Parse document"] B --> C["Store in memory<br/>(document id)"] C -->|JsonGetString / JsonGetInt| D["Read values by dot path"] C -->|JsonStringify| E["Serialise back to text"] C -->|JsonSaveFile| F["Write inside scriptfiles"] F -->|JsonParseFile| B C -->|JsonFree| G["Release document"]
Kode sumber
plugins/json/json.go
504 baris
go
// Package json is a Knight plugin that exposes JSON parsing and building to
// gamemodes.
//
// It demonstrates the plugin extension point with a self-contained feature:
//
// - natives callable from .sa (JsonParse, JsonGetString, JsonGetInt, ...)
// - a hook on player::connect that loads a per-player JSON file
//
// Activate it by blank-importing the package from the server build:
//
// import _ "knight/plugins/json"
package json
import (
"encoding/json"
"errors"
"math"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"knight/pkg/plugin"
)
// Config tunes the plugin. Zero values fall back to the defaults below.
type Config struct {
// Root is the directory used by the file natives. Default "scriptfiles".
Root string
// MaxBytes caps the size of a file the natives will read. Default 1 MiB.
MaxBytes int64
}
const (
defaultRoot = "scriptfiles"
defaultMaxBytes = 1 << 20
)
type jsonPlugin struct {
api plugin.API
cfg Config
mu sync.Mutex
docs map[string]any // documents held between call (id -> parsed value)
next int
}
func init() { plugin.Register(func() plugin.Plugin { return &jsonPlugin{} }) }
func (p *jsonPlugin) Info() plugin.Info {
return plugin.Info{Name: "json", Version: "1.0.0", Author: "Knight"}
}
func (p *jsonPlugin) Init(api plugin.API) error {
p.api = api
if p.cfg.Root == "" {
// KNIGHT_JSON_ROOT lets a deployment point the plugin at an existing
// scriptfiles directory without rebuilding.
p.cfg.Root = os.Getenv("KNIGHT_JSON_ROOT")
if p.cfg.Root == "" {
p.cfg.Root = defaultRoot
}
}
if p.cfg.MaxBytes <= 0 {
p.cfg.MaxBytes = defaultMaxBytes
}
if p.docs == nil {
p.docs = map[string]any{}
}
api.RegisterNative("JsonParse", p.parse)
api.RegisterNative("JsonParseFile", p.parseFile)
api.RegisterNative("JsonFree", p.free)
api.RegisterNative("JsonStringify", p.stringify)
api.RegisterNative("JsonGetString", p.getString)
api.RegisterNative("JsonGetInt", p.getInt)
api.RegisterNative("JsonGetFloat", p.getFloat)
api.RegisterNative("JsonGetBool", p.getBool)
api.RegisterNative("JsonHas", p.has)
api.RegisterNative("JsonType", p.typeOf)
api.RegisterNative("JsonCount", p.count)
api.RegisterNative("JsonKeys", p.keys)
api.RegisterNative("JsonEscape", p.escape)
api.RegisterNative("JsonSaveFile", p.saveFile)
api.Hook("player::connect", plugin.PriorityNormal, p.onConnect)
return nil
}
func (p *jsonPlugin) Shutdown() {
p.mu.Lock()
p.docs = map[string]any{}
p.mu.Unlock()
}
// onConnect loads <root>/players/<name>.json when it exists, so a gamemode can
// read saved profile data with JsonParseFile or the cached id.
func (p *jsonPlugin) onConnect(ctx plugin.Ctx) bool {
name, ok := ctx.PlayerName(int(ctx.Int(0)))
if !ok || name == "" {
return true
}
path := filepath.Join(p.cfg.Root, "players", safeName(name)+".json")
if _, err := os.Stat(path); err == nil {
ctx.Log("json: profile available for %s", name)
}
return true
}
// --- helpers ---
func safeName(name string) string {
var b strings.Builder
for _, r := range name {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-', r == '.':
b.WriteRune(r)
default:
b.WriteByte('_')
}
}
return b.String()
}
// keyPath splits "a.b.0.c" into ["a","b","0","c"].
func keyPath(path string) []string {
if path == "" {
return nil
}
return strings.Split(path, ".")
}
func lookup(doc any, path string) (any, bool) {
cur := doc
for _, key := range keyPath(path) {
switch node := cur.(type) {
case map[string]any:
v, ok := node[key]
if !ok {
return nil, false
}
cur = v
case []any:
i, err := strconv.Atoi(key)
if err != nil || i < 0 || i >= len(node) {
return nil, false
}
cur = node[i]
default:
return nil, false
}
}
return cur, true
}
func (p *jsonPlugin) store(v any) string {
p.mu.Lock()
defer p.mu.Unlock()
p.next++
id := "doc" + strconv.Itoa(p.next)
p.docs[id] = v
return id
}
func (p *jsonPlugin) load(id string) (any, bool) {
p.mu.Lock()
defer p.mu.Unlock()
v, ok := p.docs[id]
return v, ok
}
func arg(args []plugin.Value, i int) string {
if i < len(args) {
return args[i].Str
}
return ""
}
func (p *jsonPlugin) readFile(rel string) ([]byte, error) {
path := rel
if !filepath.IsAbs(path) {
path = filepath.Join(p.cfg.Root, path)
}
clean := filepath.Clean(path)
root, err := filepath.Abs(p.cfg.Root)
if err == nil {
abs, aerr := filepath.Abs(clean)
if aerr == nil && !strings.HasPrefix(abs, root) {
return nil, errors.New("json: path escapes the root directory")
}
}
st, err := os.Stat(clean)
if err != nil {
return nil, err
}
if st.Size() > p.cfg.MaxBytes {
return nil, errors.New("json: file too large")
}
return os.ReadFile(clean)
}
// --- natives ---
// JsonParse(text) -> id or "" on error.
func (p *jsonPlugin) parse(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
var v any
if err := json.Unmarshal([]byte(arg(args, 0)), &v); err != nil {
ctx.Log("json: parse error: %v", err)
return plugin.StrV("")
}
return plugin.StrV(p.store(v))
}
// JsonParseFile(path) -> id or "" on error.
func (p *jsonPlugin) parseFile(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
data, err := p.readFile(arg(args, 0))
if err != nil {
ctx.Log("json: %v", err)
return plugin.StrV("")
}
var v any
if err := json.Unmarshal(data, &v); err != nil {
ctx.Log("json: parse %s: %v", arg(args, 0), err)
return plugin.StrV("")
}
return plugin.StrV(p.store(v))
}
// JsonFree(id) -> bool.
func (p *jsonPlugin) free(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
p.mu.Lock()
defer p.mu.Unlock()
id := arg(args, 0)
_, ok := p.docs[id]
delete(p.docs, id)
return plugin.BoolV(ok)
}
// JsonStringify(id, indent) -> string ("" when the id is unknown).
func (p *jsonPlugin) stringify(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.StrV("")
}
indent := ""
if len(args) > 1 && args[1].Int > 0 {
indent = strings.Repeat(" ", int(args[1].Int))
}
var out []byte
var err error
if indent == "" {
out, err = json.Marshal(doc)
} else {
out, err = json.MarshalIndent(doc, "", indent)
}
if err != nil {
return plugin.StrV("")
}
return plugin.StrV(string(out))
}
// JsonGetString(id, path) -> string.
func (p *jsonPlugin) getString(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.StrV("")
}
v, ok := lookup(doc, arg(args, 1))
if !ok {
return plugin.StrV("")
}
switch node := v.(type) {
case string:
return plugin.StrV(node)
case float64:
return plugin.StrV(strconv.FormatFloat(node, 'f', -1, 64))
case bool:
return plugin.StrV(strconv.FormatBool(node))
case nil:
return plugin.StrV("")
}
raw, err := json.Marshal(v)
if err != nil {
return plugin.StrV("")
}
return plugin.StrV(string(raw))
}
func (p *jsonPlugin) getInt(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.IntV(0)
}
v, ok := lookup(doc, arg(args, 1))
if !ok {
return plugin.IntV(0)
}
switch node := v.(type) {
case float64:
return plugin.IntV(int64(node))
case string:
n, _ := strconv.ParseInt(node, 10, 64)
return plugin.IntV(n)
case bool:
if node {
return plugin.IntV(1)
}
}
return plugin.IntV(0)
}
func (p *jsonPlugin) getFloat(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.FloatV(0)
}
v, ok := lookup(doc, arg(args, 1))
if !ok {
return plugin.FloatV(0)
}
switch node := v.(type) {
case float64:
return plugin.FloatV(node)
case string:
f, _ := strconv.ParseFloat(node, 64)
return plugin.FloatV(f)
}
return plugin.FloatV(0)
}
func (p *jsonPlugin) getBool(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.BoolV(false)
}
v, ok := lookup(doc, arg(args, 1))
if !ok {
return plugin.BoolV(false)
}
switch node := v.(type) {
case bool:
return plugin.BoolV(node)
case float64:
return plugin.BoolV(node != 0 && !math.IsNaN(node))
case string:
return plugin.BoolV(node != "" && node != "false" && node != "0")
}
return plugin.BoolV(false)
}
func (p *jsonPlugin) has(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.BoolV(false)
}
_, found := lookup(doc, arg(args, 1))
return plugin.BoolV(found)
}
func (p *jsonPlugin) typeOf(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.StrV("missing")
}
v, found := lookup(doc, arg(args, 1))
if !found {
return plugin.StrV("missing")
}
switch v.(type) {
case nil:
return plugin.StrV("null")
case bool:
return plugin.StrV("bool")
case float64:
return plugin.StrV("number")
case string:
return plugin.StrV("string")
case []any:
return plugin.StrV("array")
case map[string]any:
return plugin.StrV("object")
}
return plugin.StrV("unknown")
}
// JsonCount(id) -> top-level element count (array/object), 0 otherwise.
func (p *jsonPlugin) count(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.IntV(0)
}
v := doc
if len(args) > 1 && arg(args, 1) != "" {
child, found := lookup(doc, arg(args, 1))
if !found {
return plugin.IntV(0)
}
v = child
}
switch node := v.(type) {
case []any:
return plugin.IntV(int64(len(node)))
case map[string]any:
return plugin.IntV(int64(len(node)))
case string:
return plugin.IntV(int64(len(node)))
}
return plugin.IntV(0)
}
// JsonKeys(id, path) -> comma-separated keys (object) or indexes (array).
func (p *jsonPlugin) keys(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.StrV("")
}
v := doc
if len(args) > 1 && arg(args, 1) != "" {
child, found := lookup(doc, arg(args, 1))
if !found {
return plugin.StrV("")
}
v = child
}
switch node := v.(type) {
case map[string]any:
keys := make([]string, 0, len(node))
for k := range node {
keys = append(keys, k)
}
sortStrings(keys)
return plugin.StrV(strings.Join(keys, ","))
case []any:
idx := make([]string, len(node))
for i := range node {
idx[i] = strconv.Itoa(i)
}
return plugin.StrV(strings.Join(idx, ","))
}
return plugin.StrV("")
}
// JsonEscape(text) -> JSON string body with quotes.
func (p *jsonPlugin) escape(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
out, err := json.Marshal(arg(args, 0))
if err != nil {
return plugin.StrV("\"\"")
}
return plugin.StrV(string(out))
}
// JsonSaveFile(id, path, indent) -> bool.
func (p *jsonPlugin) saveFile(ctx plugin.Ctx, args []plugin.Value) plugin.Value {
doc, ok := p.load(arg(args, 0))
if !ok {
return plugin.BoolV(false)
}
path := arg(args, 1)
if path == "" {
return plugin.BoolV(false)
}
if !filepath.IsAbs(path) {
path = filepath.Join(p.cfg.Root, path)
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
ctx.Log("json: save %s: %v", path, err)
return plugin.BoolV(false)
}
indent := ""
if len(args) > 2 && args[2].Int > 0 {
indent = strings.Repeat(" ", int(args[2].Int))
}
var data []byte
var err error
if indent == "" {
data, err = json.Marshal(doc)
} else {
data, err = json.MarshalIndent(doc, "", indent)
}
if err != nil {
return plugin.BoolV(false)
}
if err := os.WriteFile(path, data, 0o644); err != nil {
ctx.Log("json: save %s: %v", path, err)
return plugin.BoolV(false)
}
return plugin.BoolV(true)
}
func sortStrings(s []string) {
for i := 1; i < len(s); i++ {
for j := i; j > 0 && s[j] < s[j-1]; j-- {
s[j], s[j-1] = s[j-1], s[j]
}
}
}
// SetConfig tunes the plugin before Init (used by tests and embedders).
func (p *jsonPlugin) SetConfig(c Config) { p.cfg = c }
// New returns a plugin instance with the given configuration.
func New(c Config) plugin.Plugin { return &jsonPlugin{cfg: c} }