Knight docs
EN ID
GitHub
← Semua plugin

JSON

Parse, baca, bangun, dan simpan dokumen JSON dari gamemode.

Jenis
Go
Status
bawaan, sudah dikompilasi
Native
JsonParse JsonParseFile JsonStringify JsonFree JsonGetString JsonGetInt JsonGetFloat JsonGetBool JsonHas JsonType JsonCount JsonKeys JsonEscape JsonSaveFile
Hook
player::connect

Konfigurasi

VariableDefault
KNIGHT_JSON_ROOTscriptfiles

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} }