Add Cloth Config plugin management screen

This commit is contained in:
Dmitri Shimanski
2026-07-20 04:15:57 +03:00
commit 020da28470
66 changed files with 3351 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
package sdk
import (
"fmt"
"sync/atomic"
"time"
)
var callSequence atomic.Uint64
type Context struct {
actions []ActionRequest
systemCalls []SystemCallRequest
logs []LogEntry
snapshot *SnapshotSubscription
}
func (context *Context) Broadcast(message string) {
context.actions = append(context.actions, ActionRequest{
Type: "minecraft:chat.broadcast",
Payload: map[string]any{
"message": message,
},
})
}
func (context *Context) SendMessage(playerUUID, message string) {
context.actions = append(context.actions, ActionRequest{
Type: "minecraft:chat.player",
Payload: map[string]any{
"playerUuid": playerUUID,
"message": message,
},
})
}
func (context *Context) SystemCall(name string, payload any) string {
id := fmt.Sprintf("call-%d", callSequence.Add(1))
context.systemCalls = append(context.systemCalls, SystemCallRequest{
ID: id,
Name: name,
Payload: payload,
})
return id
}
func (context *Context) SubscribeSnapshot(entities bool, blocks ...BlockReference) {
context.snapshot = &SnapshotSubscription{
Entities: entities,
Blocks: append([]BlockReference(nil), blocks...),
}
}
func (context *Context) Log(level, message string) {
context.logs = append(context.logs, LogEntry{
Stream: "sdk",
Level: level,
Message: message,
TimestampUnixMilli: time.Now().UnixMilli(),
})
}
+3
View File
@@ -0,0 +1,3 @@
module github.com/yawaflua/GoMinecraftBridge/sdk
go 1.24.0
+63
View File
@@ -0,0 +1,63 @@
package sdk
import (
"encoding/json"
"fmt"
"testing"
)
func BenchmarkJSONSnapshot1000Entities(b *testing.B) {
snapshot := benchmarkSnapshot(1000)
encoded, err := json.Marshal(snapshot)
if err != nil {
b.Fatal(err)
}
b.Run("encode", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
if _, err := json.Marshal(snapshot); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(len(encoded)), "bytes/snapshot")
})
b.Run("decode", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
var decoded ServerSnapshot
if err := json.Unmarshal(encoded, &decoded); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(len(encoded)), "bytes/snapshot")
})
}
func benchmarkSnapshot(entityCount int) ServerSnapshot {
entities := make([]EntitySnapshot, entityCount)
for index := range entities {
health := float32(20)
entities[index] = EntitySnapshot{
RuntimeID: index,
UUID: fmt.Sprintf("00000000-0000-0000-0000-%012d", index),
Type: "minecraft:zombie",
Name: "Zombie",
Dimension: "minecraft:overworld",
X: float64(index),
Y: 64,
Z: float64(-index),
Alive: true,
Health: &health,
MaxHealth: &health,
}
}
return ServerSnapshot{
Tick: 1,
TimestampUnixMilli: 1,
Levels: []LevelSnapshot{{
Dimension: "minecraft:overworld",
}},
Entities: entities,
}
}
+68
View File
@@ -0,0 +1,68 @@
//go:build cgo && !wasm
package sdk
/*
#include <stdint.h>
#include <stdlib.h>
*/
import "C"
import (
"fmt"
"unsafe"
)
//export gmb_abi_version
func gmb_abi_version() C.int32_t {
return C.int32_t(ABIVersion)
}
//export gmb_call
func gmb_call(
operation C.int32_t,
input *C.uint8_t,
inputLength C.uint64_t,
output **C.uint8_t,
outputLength *C.uint64_t,
) (status C.int32_t) {
*output = nil
*outputLength = 0
defer func() {
if recovered := recover(); recovered != nil {
fallback := []byte(fmt.Sprintf(`{"status":"panic","error":%q}`, fmt.Sprint(recovered)))
writeNativeOutput(fallback, output, outputLength)
status = 0
}
}()
if inputLength > 64*1024*1024 {
return 2
}
var payload []byte
if inputLength > 0 {
if input == nil {
return 3
}
payload = C.GoBytes(unsafe.Pointer(input), C.int(inputLength))
}
writeNativeOutput(Dispatch(int(operation), payload), output, outputLength)
return 0
}
//export gmb_free
func gmb_free(pointer unsafe.Pointer) {
C.free(pointer)
}
func writeNativeOutput(payload []byte, output **C.uint8_t, outputLength *C.uint64_t) {
if len(payload) == 0 {
return
}
pointer := C.CBytes(payload)
*output = (*C.uint8_t)(pointer)
*outputLength = C.uint64_t(len(payload))
}
+115
View File
@@ -0,0 +1,115 @@
package sdk
import (
"bufio"
"fmt"
"io"
"log"
"os"
"strings"
"sync"
"time"
)
var captured = struct {
sync.Mutex
logs []LogEntry
}{}
var outputCaptureOnce sync.Once
type streamCapture struct {
writer *os.File
marker string
ack chan struct{}
mu sync.Mutex
}
var outputStreams []*streamCapture
func enableOutputCapture() {
outputCaptureOnce.Do(func() {
outputStreams = append(outputStreams, captureStream("stdout", &os.Stdout))
outputStreams = append(outputStreams, captureStream("stderr", &os.Stderr))
log.SetOutput(os.Stderr)
})
}
func captureStream(name string, destination **os.File) *streamCapture {
reader, writer, err := os.Pipe()
if err != nil {
return nil
}
stream := &streamCapture{
writer: writer,
marker: fmt.Sprintf("\x00gmb-flush-%s-%d\x00", name, time.Now().UnixNano()),
ack: make(chan struct{}),
}
*destination = writer
go func() {
buffered := bufio.NewReader(reader)
for {
line, readErr := buffered.ReadString('\n')
line = strings.TrimSuffix(strings.TrimSuffix(line, "\n"), "\r")
if markerIndex := strings.Index(line, stream.marker); markerIndex >= 0 {
captureLine(name, line[:markerIndex])
captureLine(name, line[markerIndex+len(stream.marker):])
stream.ack <- struct{}{}
} else {
captureLine(name, line)
}
if readErr != nil {
if readErr != io.EOF {
captureLine("stderr", "stdout capture failed: "+readErr.Error())
}
return
}
}
}()
return stream
}
func captureLine(stream, message string) {
if message == "" {
return
}
captured.Lock()
captured.logs = append(captured.logs, LogEntry{
Stream: stream,
Level: streamLevel(stream),
Message: message,
TimestampUnixMilli: time.Now().UnixMilli(),
})
captured.Unlock()
}
func drainCapturedLogs() []LogEntry {
for _, stream := range outputStreams {
if stream != nil {
stream.flush()
}
}
captured.Lock()
defer captured.Unlock()
logs := append([]LogEntry(nil), captured.logs...)
captured.logs = captured.logs[:0]
return logs
}
func (stream *streamCapture) flush() {
stream.mu.Lock()
defer stream.mu.Unlock()
if _, err := stream.writer.WriteString(stream.marker + "\n"); err != nil {
return
}
<-stream.ack
}
func streamLevel(stream string) string {
if stream == "stderr" {
return "error"
}
return "info"
}
+29
View File
@@ -0,0 +1,29 @@
package sdk
type Plugin interface {
Metadata() Metadata
}
type Initializer interface {
Init(context *Context, event InitEvent) error
}
type TickHandler interface {
Tick(context *Context, snapshot ServerSnapshot) error
}
type ChatHandler interface {
Chat(context *Context, event ChatEvent) error
}
type DeathHandler interface {
Death(context *Context, event DeathEvent) error
}
type SystemCallResultHandler interface {
SystemCallResult(context *Context, result SystemCallResult) error
}
type Deinitializer interface {
Deinit(context *Context, event DeinitEvent) error
}
+141
View File
@@ -0,0 +1,141 @@
package sdk
import (
"encoding/json"
"errors"
"fmt"
"runtime/debug"
"sync"
)
var (
pluginMu sync.RWMutex
registeredPlugin Plugin
)
func Register(plugin Plugin) {
if plugin == nil {
panic("sdk: cannot register a nil plugin")
}
pluginMu.Lock()
defer pluginMu.Unlock()
if registeredPlugin != nil {
panic("sdk: a plugin is already registered")
}
registeredPlugin = plugin
enableOutputCapture()
}
func Dispatch(operation int, input []byte) (output []byte) {
context := &Context{}
result := response{Status: "ok"}
defer func() {
if recovered := recover(); recovered != nil {
result.Status = "panic"
result.Error = fmt.Sprint(recovered)
result.Stack = string(debug.Stack())
}
result.Logs = append(result.Logs, context.logs...)
result.Logs = append(result.Logs, drainCapturedLogs()...)
result.Actions = context.actions
result.SystemCalls = context.systemCalls
result.Snapshot = context.snapshot
encoded, err := json.Marshal(result)
if err != nil {
output = []byte(`{"status":"panic","error":"cannot encode plugin response"}`)
return
}
output = encoded
}()
plugin := currentPlugin()
var err error
switch operation {
case OperationMetadata:
metadata := plugin.Metadata()
if metadata.APIVersion == 0 {
metadata.APIVersion = ABIVersion
}
result.Data = metadata
case OperationInit:
var event InitEvent
err = decode(input, &event)
if err == nil {
if handler, ok := plugin.(Initializer); ok {
err = handler.Init(context, event)
}
}
case OperationTick:
var snapshot ServerSnapshot
err = decode(input, &snapshot)
if err == nil {
if handler, ok := plugin.(TickHandler); ok {
err = handler.Tick(context, snapshot)
}
}
case OperationChat:
var event ChatEvent
err = decode(input, &event)
if err == nil {
if handler, ok := plugin.(ChatHandler); ok {
err = handler.Chat(context, event)
}
}
case OperationDeath:
var event DeathEvent
err = decode(input, &event)
if err == nil {
if handler, ok := plugin.(DeathHandler); ok {
err = handler.Death(context, event)
}
}
case OperationSystemCallResult:
var event SystemCallResult
err = decode(input, &event)
if err == nil {
if handler, ok := plugin.(SystemCallResultHandler); ok {
err = handler.SystemCallResult(context, event)
}
}
case OperationDeinit:
var event DeinitEvent
err = decode(input, &event)
if err == nil {
if handler, ok := plugin.(Deinitializer); ok {
err = handler.Deinit(context, event)
}
}
default:
err = fmt.Errorf("sdk: unknown operation %d", operation)
}
if err != nil {
result.Status = "error"
result.Error = err.Error()
}
return nil
}
func currentPlugin() Plugin {
pluginMu.RLock()
defer pluginMu.RUnlock()
if registeredPlugin == nil {
panic("sdk: plugin was not registered; call sdk.Register from init")
}
return registeredPlugin
}
func decode(input []byte, target any) error {
if len(input) == 0 {
return errors.New("sdk: operation requires an input payload")
}
if err := json.Unmarshal(input, target); err != nil {
return fmt.Errorf("sdk: decode input: %w", err)
}
return nil
}
+54
View File
@@ -0,0 +1,54 @@
package sdk
import (
"encoding/json"
"errors"
"testing"
)
type testPlugin struct{}
func (testPlugin) Metadata() Metadata {
return Metadata{ID: "test_plugin", Name: "Test", Version: "1.0.0"}
}
func (testPlugin) Chat(context *Context, event ChatEvent) error {
if event.Message == "panic" {
panic("expected panic")
}
if event.Message == "error" {
return errors.New("expected error")
}
context.SendMessage(event.PlayerUUID, "received")
return nil
}
func TestDispatch(t *testing.T) {
pluginMu.Lock()
registeredPlugin = testPlugin{}
pluginMu.Unlock()
input, _ := json.Marshal(ChatEvent{PlayerUUID: "player", Message: "hello"})
var got response
if err := json.Unmarshal(Dispatch(OperationChat, input), &got); err != nil {
t.Fatal(err)
}
if got.Status != "ok" || len(got.Actions) != 1 {
t.Fatalf("unexpected response: %#v", got)
}
}
func TestDispatchRecoversPanic(t *testing.T) {
pluginMu.Lock()
registeredPlugin = testPlugin{}
pluginMu.Unlock()
input, _ := json.Marshal(ChatEvent{Message: "panic"})
var got response
if err := json.Unmarshal(Dispatch(OperationChat, input), &got); err != nil {
t.Fatal(err)
}
if got.Status != "panic" || got.Stack == "" {
t.Fatalf("panic was not captured: %#v", got)
}
}
+144
View File
@@ -0,0 +1,144 @@
package sdk
import "encoding/json"
const ABIVersion = 1
const (
OperationMetadata = 1
OperationInit = 2
OperationTick = 3
OperationChat = 4
OperationDeath = 5
OperationSystemCallResult = 6
OperationDeinit = 7
)
type Metadata struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Description string `json:"description,omitempty"`
Authors []string `json:"authors,omitempty"`
Website string `json:"website,omitempty"`
APIVersion int `json:"apiVersion"`
ConfigSchema map[string]any `json:"configSchema,omitempty"`
}
type InitEvent struct {
MinecraftVersion string `json:"minecraftVersion"`
Dedicated bool `json:"dedicated"`
DataDirectory string `json:"dataDirectory"`
}
type ServerSnapshot struct {
Tick int64 `json:"tick"`
TimestampUnixMilli int64 `json:"timestampUnixMilli"`
Levels []LevelSnapshot `json:"levels"`
Entities []EntitySnapshot `json:"entities"`
Blocks []BlockSnapshot `json:"blocks"`
}
type LevelSnapshot struct {
Dimension string `json:"dimension"`
GameTime int64 `json:"gameTime"`
DayTime int64 `json:"dayTime"`
Raining bool `json:"raining"`
Thundering bool `json:"thundering"`
}
type EntitySnapshot struct {
RuntimeID int `json:"runtimeId"`
UUID string `json:"uuid"`
Type string `json:"type"`
Name string `json:"name"`
Dimension string `json:"dimension"`
X float64 `json:"x"`
Y float64 `json:"y"`
Z float64 `json:"z"`
Yaw float32 `json:"yaw"`
Pitch float32 `json:"pitch"`
VelocityX float64 `json:"velocityX"`
VelocityY float64 `json:"velocityY"`
VelocityZ float64 `json:"velocityZ"`
Alive bool `json:"alive"`
Player bool `json:"player"`
Health *float32 `json:"health"`
MaxHealth *float32 `json:"maxHealth"`
}
type BlockSnapshot struct {
Dimension string `json:"dimension"`
X int `json:"x"`
Y int `json:"y"`
Z int `json:"z"`
Block string `json:"block"`
Properties map[string]string `json:"properties"`
}
type BlockReference struct {
Dimension string `json:"dimension"`
X int `json:"x"`
Y int `json:"y"`
Z int `json:"z"`
}
type ChatEvent struct {
PlayerUUID string `json:"playerUuid"`
PlayerName string `json:"playerName"`
Message string `json:"message"`
TimestampUnixMilli int64 `json:"timestampUnixMilli"`
}
type DeathEvent struct {
Entity EntitySnapshot `json:"entity"`
DamageType string `json:"damageType"`
AttackerUUID *string `json:"attackerUuid"`
TimestampUnixMilli int64 `json:"timestampUnixMilli"`
}
type SystemCallResult struct {
ID string `json:"id"`
Name string `json:"name"`
Success bool `json:"success"`
Data json.RawMessage `json:"data"`
Error string `json:"error"`
}
type DeinitEvent struct {
Reason string `json:"reason"`
}
type ActionRequest struct {
Type string `json:"type"`
Payload any `json:"payload"`
}
type SystemCallRequest struct {
ID string `json:"id"`
Name string `json:"name"`
Payload any `json:"payload"`
}
type SnapshotSubscription struct {
Entities bool `json:"entities"`
Blocks []BlockReference `json:"blocks"`
}
type LogEntry struct {
Stream string `json:"stream"`
Level string `json:"level"`
Message string `json:"message"`
TimestampUnixMilli int64 `json:"timestampUnixMilli"`
}
type response struct {
Status string `json:"status"`
Error string `json:"error,omitempty"`
Stack string `json:"stack,omitempty"`
Data any `json:"data,omitempty"`
Logs []LogEntry `json:"logs,omitempty"`
Actions []ActionRequest `json:"actions,omitempty"`
SystemCalls []SystemCallRequest `json:"systemCalls,omitempty"`
Snapshot *SnapshotSubscription `json:"snapshot,omitempty"`
}