Adding client-side and server-side interactions. Also adds purpur/paper support

This commit is contained in:
Dmitri Shimanski
2026-07-20 05:41:57 +03:00
parent fbe4d786ca
commit ce93e99962
71 changed files with 5068 additions and 279 deletions
+27 -1
View File
@@ -15,6 +15,7 @@ type Context struct {
snapshot *SnapshotSubscription
}
// Broadcast queues a message to be sent to all players.
func (context *Context) Broadcast(message string) {
context.actions = append(context.actions, ActionRequest{
Type: "minecraft:chat.broadcast",
@@ -24,6 +25,7 @@ func (context *Context) Broadcast(message string) {
})
}
// SendMessage queues a message to be sent to a player.
func (context *Context) SendMessage(playerUUID, message string) {
context.actions = append(context.actions, ActionRequest{
Type: "minecraft:chat.player",
@@ -34,7 +36,29 @@ func (context *Context) SendMessage(playerUUID, message string) {
})
}
func (context *Context) SystemCall(name string, payload any) string {
// DisplayClientMessage appends a local-only message to the Minecraft client
// chat. Client runtimes reject server action types such as SendMessage.
func (context *Context) DisplayClientMessage(message string) {
context.actions = append(context.actions, ActionRequest{
Type: "minecraft:client.chat.display",
Payload: map[string]any{
"message": message,
},
})
}
// SystemCall queues one of the system calls built into the bridge.
func (context *Context) SystemCall(callType SystemCallType, payload any) string {
return context.queueSystemCall(string(callType), payload)
}
// CustomSystemCall queues a system call registered by another mod.
func (context *Context) CustomSystemCall(name string, payload any) string {
return context.queueSystemCall(name, payload)
}
// queueSystemCall queues a system call to be executed by the bridge.
func (context *Context) queueSystemCall(name string, payload any) string {
id := fmt.Sprintf("call-%d", callSequence.Add(1))
context.systemCalls = append(context.systemCalls, SystemCallRequest{
ID: id,
@@ -44,6 +68,7 @@ func (context *Context) SystemCall(name string, payload any) string {
return id
}
// SubscribeSnapshot queues a snapshot subscription to be executed by the bridge.
func (context *Context) SubscribeSnapshot(entities bool, blocks ...BlockReference) {
context.snapshot = &SnapshotSubscription{
Entities: entities,
@@ -51,6 +76,7 @@ func (context *Context) SubscribeSnapshot(entities bool, blocks ...BlockReferenc
}
}
// Log queues a log message to be sent to the server.
func (context *Context) Log(level, message string) {
context.logs = append(context.logs, LogEntry{
Stream: "sdk",
+31
View File
@@ -0,0 +1,31 @@
package sdk
import "testing"
func TestSystemCallUsesBuiltInType(t *testing.T) {
context := &Context{}
runtimeID := 42
id := context.SystemCall(SystemCallGetEntity, GetEntityRequest{RuntimeID: &runtimeID})
if id == "" {
t.Fatal("SystemCall returned an empty id")
}
if len(context.systemCalls) != 1 {
t.Fatalf("got %d queued calls, want 1", len(context.systemCalls))
}
if got := context.systemCalls[0].Name; got != "minecraft:get_entity" {
t.Fatalf("queued call name = %q, want %q", got, "minecraft:get_entity")
}
}
func TestCustomSystemCallKeepsExtensionPoint(t *testing.T) {
context := &Context{}
context.CustomSystemCall("example:claim.owner", nil)
if len(context.systemCalls) != 1 {
t.Fatalf("got %d queued calls, want 1", len(context.systemCalls))
}
if got := context.systemCalls[0].Name; got != "example:claim.owner" {
t.Fatalf("queued call name = %q, want %q", got, "example:claim.owner")
}
}
+95
View File
@@ -0,0 +1,95 @@
package sdk
import (
"fmt"
flat "github.com/yawaflua/GoMinecraftBridge/sdk/internal/fbs/gmb"
)
func decodeTickSnapshot(input []byte) (snapshot ServerSnapshot, err error) {
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("sdk: invalid FlatBuffers tick snapshot: %v", recovered)
}
}()
if len(input) < 8 || !flat.ServerSnapshotBufferHasIdentifier(input) {
return snapshot, fmt.Errorf("sdk: tick snapshot is not a GMBS FlatBuffer")
}
root := flat.GetRootAsServerSnapshot(input, 0)
snapshot.Tick = root.Tick()
snapshot.TimestampUnixMilli = root.TimestampUnixMilli()
snapshot.Levels = make([]LevelSnapshot, root.LevelsLength())
var level flat.LevelSnapshot
for index := range snapshot.Levels {
if !root.Levels(&level, index) {
return ServerSnapshot{}, fmt.Errorf("sdk: missing level %d", index)
}
snapshot.Levels[index] = LevelSnapshot{
Dimension: string(level.Dimension()),
GameTime: level.GameTime(),
DayTime: level.DayTime(),
Raining: level.Raining(),
Thundering: level.Thundering(),
}
}
snapshot.Entities = make([]EntitySnapshot, root.EntitiesLength())
var entity flat.EntitySnapshot
for index := range snapshot.Entities {
if !root.Entities(&entity, index) {
return ServerSnapshot{}, fmt.Errorf("sdk: missing entity %d", index)
}
converted := EntitySnapshot{
RuntimeID: int(entity.RuntimeId()),
UUID: string(entity.Uuid()),
Type: string(entity.Type()),
Name: string(entity.Name()),
Dimension: string(entity.Dimension()),
X: entity.X(),
Y: entity.Y(),
Z: entity.Z(),
Yaw: entity.Yaw(),
Pitch: entity.Pitch(),
VelocityX: entity.VelocityX(),
VelocityY: entity.VelocityY(),
VelocityZ: entity.VelocityZ(),
Alive: entity.Alive(),
Player: entity.Player(),
}
if entity.HasHealth() {
health := entity.Health()
maxHealth := entity.MaxHealth()
converted.Health = &health
converted.MaxHealth = &maxHealth
}
snapshot.Entities[index] = converted
}
snapshot.Blocks = make([]BlockSnapshot, root.BlocksLength())
var block flat.BlockSnapshot
var property flat.BlockProperty
for index := range snapshot.Blocks {
if !root.Blocks(&block, index) {
return ServerSnapshot{}, fmt.Errorf("sdk: missing block %d", index)
}
properties := make(map[string]string, block.PropertiesLength())
for propertyIndex := 0; propertyIndex < block.PropertiesLength(); propertyIndex++ {
if block.Properties(&property, propertyIndex) {
properties[string(property.Key())] = string(property.Value())
}
}
snapshot.Blocks[index] = BlockSnapshot{
Dimension: string(block.Dimension()),
X: int(block.X()),
Y: int(block.Y()),
Z: int(block.Z()),
Block: string(block.Block()),
Properties: properties,
}
}
return snapshot, nil
}
+103
View File
@@ -0,0 +1,103 @@
package sdk
import (
"testing"
flatbuffers "github.com/google/flatbuffers/go"
flat "github.com/yawaflua/GoMinecraftBridge/sdk/internal/fbs/gmb"
)
func TestDecodeTickSnapshot(t *testing.T) {
want := benchmarkSnapshot(3)
encoded := encodeBenchmarkFlatBuffer(want)
got, err := decodeTickSnapshot(encoded)
if err != nil {
t.Fatal(err)
}
if got.Tick != want.Tick || len(got.Entities) != 3 || got.Entities[2].UUID != want.Entities[2].UUID {
t.Fatalf("unexpected decoded snapshot: %#v", got)
}
}
func BenchmarkFlatBuffersSnapshot1000Entities(b *testing.B) {
encoded := encodeBenchmarkFlatBuffer(benchmarkSnapshot(1000))
b.ReportAllocs()
for b.Loop() {
if _, err := decodeTickSnapshot(encoded); err != nil {
b.Fatal(err)
}
}
b.ReportMetric(float64(len(encoded)), "bytes/snapshot")
}
func encodeBenchmarkFlatBuffer(snapshot ServerSnapshot) []byte {
builder := flatbuffers.NewBuilder(16 * 1024)
levels := make([]flatbuffers.UOffsetT, len(snapshot.Levels))
for index, level := range snapshot.Levels {
dimension := builder.CreateString(level.Dimension)
flat.LevelSnapshotStart(builder)
flat.LevelSnapshotAddDimension(builder, dimension)
flat.LevelSnapshotAddGameTime(builder, level.GameTime)
flat.LevelSnapshotAddDayTime(builder, level.DayTime)
flat.LevelSnapshotAddRaining(builder, level.Raining)
flat.LevelSnapshotAddThundering(builder, level.Thundering)
levels[index] = flat.LevelSnapshotEnd(builder)
}
entities := make([]flatbuffers.UOffsetT, len(snapshot.Entities))
for index, entity := range snapshot.Entities {
uuid := builder.CreateString(entity.UUID)
entityType := builder.CreateString(entity.Type)
name := builder.CreateString(entity.Name)
dimension := builder.CreateString(entity.Dimension)
hasHealth := entity.Health != nil && entity.MaxHealth != nil
var health, maxHealth float32
if hasHealth {
health = *entity.Health
maxHealth = *entity.MaxHealth
}
flat.EntitySnapshotStart(builder)
flat.EntitySnapshotAddRuntimeId(builder, int32(entity.RuntimeID))
flat.EntitySnapshotAddUuid(builder, uuid)
flat.EntitySnapshotAddType(builder, entityType)
flat.EntitySnapshotAddName(builder, name)
flat.EntitySnapshotAddDimension(builder, dimension)
flat.EntitySnapshotAddX(builder, entity.X)
flat.EntitySnapshotAddY(builder, entity.Y)
flat.EntitySnapshotAddZ(builder, entity.Z)
flat.EntitySnapshotAddYaw(builder, entity.Yaw)
flat.EntitySnapshotAddPitch(builder, entity.Pitch)
flat.EntitySnapshotAddVelocityX(builder, entity.VelocityX)
flat.EntitySnapshotAddVelocityY(builder, entity.VelocityY)
flat.EntitySnapshotAddVelocityZ(builder, entity.VelocityZ)
flat.EntitySnapshotAddAlive(builder, entity.Alive)
flat.EntitySnapshotAddPlayer(builder, entity.Player)
flat.EntitySnapshotAddHasHealth(builder, hasHealth)
flat.EntitySnapshotAddHealth(builder, health)
flat.EntitySnapshotAddMaxHealth(builder, maxHealth)
entities[index] = flat.EntitySnapshotEnd(builder)
}
flat.ServerSnapshotStartLevelsVector(builder, len(levels))
for index := len(levels) - 1; index >= 0; index-- {
builder.PrependUOffsetT(levels[index])
}
levelVector := builder.EndVector(len(levels))
flat.ServerSnapshotStartEntitiesVector(builder, len(entities))
for index := len(entities) - 1; index >= 0; index-- {
builder.PrependUOffsetT(entities[index])
}
entityVector := builder.EndVector(len(entities))
flat.ServerSnapshotStartBlocksVector(builder, 0)
blockVector := builder.EndVector(0)
flat.ServerSnapshotStart(builder)
flat.ServerSnapshotAddTick(builder, snapshot.Tick)
flat.ServerSnapshotAddTimestampUnixMilli(builder, snapshot.TimestampUnixMilli)
flat.ServerSnapshotAddLevels(builder, levelVector)
flat.ServerSnapshotAddEntities(builder, entityVector)
flat.ServerSnapshotAddBlocks(builder, blockVector)
root := flat.ServerSnapshotEnd(builder)
flat.FinishServerSnapshotBuffer(builder, root)
return builder.FinishedBytes()
}
+2
View File
@@ -1,3 +1,5 @@
module github.com/yawaflua/GoMinecraftBridge/sdk
go 1.24.0
require github.com/google/flatbuffers v25.2.10+incompatible
+71
View File
@@ -0,0 +1,71 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package gmb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type BlockProperty struct {
_tab flatbuffers.Table
}
func GetRootAsBlockProperty(buf []byte, offset flatbuffers.UOffsetT) *BlockProperty {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &BlockProperty{}
x.Init(buf, n+offset)
return x
}
func FinishBlockPropertyBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsBlockProperty(buf []byte, offset flatbuffers.UOffsetT) *BlockProperty {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &BlockProperty{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedBlockPropertyBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *BlockProperty) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *BlockProperty) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *BlockProperty) Key() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *BlockProperty) Value() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func BlockPropertyStart(builder *flatbuffers.Builder) {
builder.StartObject(2)
}
func BlockPropertyAddKey(builder *flatbuffers.Builder, key flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(key), 0)
}
func BlockPropertyAddValue(builder *flatbuffers.Builder, value flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(value), 0)
}
func BlockPropertyEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+142
View File
@@ -0,0 +1,142 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package gmb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type BlockSnapshot struct {
_tab flatbuffers.Table
}
func GetRootAsBlockSnapshot(buf []byte, offset flatbuffers.UOffsetT) *BlockSnapshot {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &BlockSnapshot{}
x.Init(buf, n+offset)
return x
}
func FinishBlockSnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsBlockSnapshot(buf []byte, offset flatbuffers.UOffsetT) *BlockSnapshot {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &BlockSnapshot{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedBlockSnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *BlockSnapshot) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *BlockSnapshot) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *BlockSnapshot) Dimension() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *BlockSnapshot) X() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *BlockSnapshot) MutateX(n int32) bool {
return rcv._tab.MutateInt32Slot(6, n)
}
func (rcv *BlockSnapshot) Y() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *BlockSnapshot) MutateY(n int32) bool {
return rcv._tab.MutateInt32Slot(8, n)
}
func (rcv *BlockSnapshot) Z() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *BlockSnapshot) MutateZ(n int32) bool {
return rcv._tab.MutateInt32Slot(10, n)
}
func (rcv *BlockSnapshot) Block() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *BlockSnapshot) Properties(obj *BlockProperty, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *BlockSnapshot) PropertiesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func BlockSnapshotStart(builder *flatbuffers.Builder) {
builder.StartObject(6)
}
func BlockSnapshotAddDimension(builder *flatbuffers.Builder, dimension flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(dimension), 0)
}
func BlockSnapshotAddX(builder *flatbuffers.Builder, x int32) {
builder.PrependInt32Slot(1, x, 0)
}
func BlockSnapshotAddY(builder *flatbuffers.Builder, y int32) {
builder.PrependInt32Slot(2, y, 0)
}
func BlockSnapshotAddZ(builder *flatbuffers.Builder, z int32) {
builder.PrependInt32Slot(3, z, 0)
}
func BlockSnapshotAddBlock(builder *flatbuffers.Builder, block flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(block), 0)
}
func BlockSnapshotAddProperties(builder *flatbuffers.Builder, properties flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(5, flatbuffers.UOffsetT(properties), 0)
}
func BlockSnapshotStartPropertiesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func BlockSnapshotEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+303
View File
@@ -0,0 +1,303 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package gmb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type EntitySnapshot struct {
_tab flatbuffers.Table
}
func GetRootAsEntitySnapshot(buf []byte, offset flatbuffers.UOffsetT) *EntitySnapshot {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &EntitySnapshot{}
x.Init(buf, n+offset)
return x
}
func FinishEntitySnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsEntitySnapshot(buf []byte, offset flatbuffers.UOffsetT) *EntitySnapshot {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &EntitySnapshot{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedEntitySnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *EntitySnapshot) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *EntitySnapshot) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *EntitySnapshot) RuntimeId() int32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetInt32(o + rcv._tab.Pos)
}
return 0
}
func (rcv *EntitySnapshot) MutateRuntimeId(n int32) bool {
return rcv._tab.MutateInt32Slot(4, n)
}
func (rcv *EntitySnapshot) Uuid() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *EntitySnapshot) Type() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *EntitySnapshot) Name() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *EntitySnapshot) Dimension() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *EntitySnapshot) X() float64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(14))
if o != 0 {
return rcv._tab.GetFloat64(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateX(n float64) bool {
return rcv._tab.MutateFloat64Slot(14, n)
}
func (rcv *EntitySnapshot) Y() float64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(16))
if o != 0 {
return rcv._tab.GetFloat64(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateY(n float64) bool {
return rcv._tab.MutateFloat64Slot(16, n)
}
func (rcv *EntitySnapshot) Z() float64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(18))
if o != 0 {
return rcv._tab.GetFloat64(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateZ(n float64) bool {
return rcv._tab.MutateFloat64Slot(18, n)
}
func (rcv *EntitySnapshot) Yaw() float32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(20))
if o != 0 {
return rcv._tab.GetFloat32(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateYaw(n float32) bool {
return rcv._tab.MutateFloat32Slot(20, n)
}
func (rcv *EntitySnapshot) Pitch() float32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(22))
if o != 0 {
return rcv._tab.GetFloat32(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutatePitch(n float32) bool {
return rcv._tab.MutateFloat32Slot(22, n)
}
func (rcv *EntitySnapshot) VelocityX() float64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(24))
if o != 0 {
return rcv._tab.GetFloat64(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateVelocityX(n float64) bool {
return rcv._tab.MutateFloat64Slot(24, n)
}
func (rcv *EntitySnapshot) VelocityY() float64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(26))
if o != 0 {
return rcv._tab.GetFloat64(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateVelocityY(n float64) bool {
return rcv._tab.MutateFloat64Slot(26, n)
}
func (rcv *EntitySnapshot) VelocityZ() float64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(28))
if o != 0 {
return rcv._tab.GetFloat64(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateVelocityZ(n float64) bool {
return rcv._tab.MutateFloat64Slot(28, n)
}
func (rcv *EntitySnapshot) Alive() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(30))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *EntitySnapshot) MutateAlive(n bool) bool {
return rcv._tab.MutateBoolSlot(30, n)
}
func (rcv *EntitySnapshot) Player() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(32))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *EntitySnapshot) MutatePlayer(n bool) bool {
return rcv._tab.MutateBoolSlot(32, n)
}
func (rcv *EntitySnapshot) HasHealth() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(34))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *EntitySnapshot) MutateHasHealth(n bool) bool {
return rcv._tab.MutateBoolSlot(34, n)
}
func (rcv *EntitySnapshot) Health() float32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(36))
if o != 0 {
return rcv._tab.GetFloat32(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateHealth(n float32) bool {
return rcv._tab.MutateFloat32Slot(36, n)
}
func (rcv *EntitySnapshot) MaxHealth() float32 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(38))
if o != 0 {
return rcv._tab.GetFloat32(o + rcv._tab.Pos)
}
return 0.0
}
func (rcv *EntitySnapshot) MutateMaxHealth(n float32) bool {
return rcv._tab.MutateFloat32Slot(38, n)
}
func EntitySnapshotStart(builder *flatbuffers.Builder) {
builder.StartObject(18)
}
func EntitySnapshotAddRuntimeId(builder *flatbuffers.Builder, runtimeId int32) {
builder.PrependInt32Slot(0, runtimeId, 0)
}
func EntitySnapshotAddUuid(builder *flatbuffers.Builder, uuid flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(uuid), 0)
}
func EntitySnapshotAddType(builder *flatbuffers.Builder, type_ flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(type_), 0)
}
func EntitySnapshotAddName(builder *flatbuffers.Builder, name flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(name), 0)
}
func EntitySnapshotAddDimension(builder *flatbuffers.Builder, dimension flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(dimension), 0)
}
func EntitySnapshotAddX(builder *flatbuffers.Builder, x float64) {
builder.PrependFloat64Slot(5, x, 0.0)
}
func EntitySnapshotAddY(builder *flatbuffers.Builder, y float64) {
builder.PrependFloat64Slot(6, y, 0.0)
}
func EntitySnapshotAddZ(builder *flatbuffers.Builder, z float64) {
builder.PrependFloat64Slot(7, z, 0.0)
}
func EntitySnapshotAddYaw(builder *flatbuffers.Builder, yaw float32) {
builder.PrependFloat32Slot(8, yaw, 0.0)
}
func EntitySnapshotAddPitch(builder *flatbuffers.Builder, pitch float32) {
builder.PrependFloat32Slot(9, pitch, 0.0)
}
func EntitySnapshotAddVelocityX(builder *flatbuffers.Builder, velocityX float64) {
builder.PrependFloat64Slot(10, velocityX, 0.0)
}
func EntitySnapshotAddVelocityY(builder *flatbuffers.Builder, velocityY float64) {
builder.PrependFloat64Slot(11, velocityY, 0.0)
}
func EntitySnapshotAddVelocityZ(builder *flatbuffers.Builder, velocityZ float64) {
builder.PrependFloat64Slot(12, velocityZ, 0.0)
}
func EntitySnapshotAddAlive(builder *flatbuffers.Builder, alive bool) {
builder.PrependBoolSlot(13, alive, false)
}
func EntitySnapshotAddPlayer(builder *flatbuffers.Builder, player bool) {
builder.PrependBoolSlot(14, player, false)
}
func EntitySnapshotAddHasHealth(builder *flatbuffers.Builder, hasHealth bool) {
builder.PrependBoolSlot(15, hasHealth, false)
}
func EntitySnapshotAddHealth(builder *flatbuffers.Builder, health float32) {
builder.PrependFloat32Slot(16, health, 0.0)
}
func EntitySnapshotAddMaxHealth(builder *flatbuffers.Builder, maxHealth float32) {
builder.PrependFloat32Slot(17, maxHealth, 0.0)
}
func EntitySnapshotEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+120
View File
@@ -0,0 +1,120 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package gmb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type LevelSnapshot struct {
_tab flatbuffers.Table
}
func GetRootAsLevelSnapshot(buf []byte, offset flatbuffers.UOffsetT) *LevelSnapshot {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &LevelSnapshot{}
x.Init(buf, n+offset)
return x
}
func FinishLevelSnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsLevelSnapshot(buf []byte, offset flatbuffers.UOffsetT) *LevelSnapshot {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &LevelSnapshot{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedLevelSnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *LevelSnapshot) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *LevelSnapshot) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *LevelSnapshot) Dimension() []byte {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.ByteVector(o + rcv._tab.Pos)
}
return nil
}
func (rcv *LevelSnapshot) GameTime() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *LevelSnapshot) MutateGameTime(n int64) bool {
return rcv._tab.MutateInt64Slot(6, n)
}
func (rcv *LevelSnapshot) DayTime() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *LevelSnapshot) MutateDayTime(n int64) bool {
return rcv._tab.MutateInt64Slot(8, n)
}
func (rcv *LevelSnapshot) Raining() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *LevelSnapshot) MutateRaining(n bool) bool {
return rcv._tab.MutateBoolSlot(10, n)
}
func (rcv *LevelSnapshot) Thundering() bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.GetBool(o + rcv._tab.Pos)
}
return false
}
func (rcv *LevelSnapshot) MutateThundering(n bool) bool {
return rcv._tab.MutateBoolSlot(12, n)
}
func LevelSnapshotStart(builder *flatbuffers.Builder) {
builder.StartObject(5)
}
func LevelSnapshotAddDimension(builder *flatbuffers.Builder, dimension flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(dimension), 0)
}
func LevelSnapshotAddGameTime(builder *flatbuffers.Builder, gameTime int64) {
builder.PrependInt64Slot(1, gameTime, 0)
}
func LevelSnapshotAddDayTime(builder *flatbuffers.Builder, dayTime int64) {
builder.PrependInt64Slot(2, dayTime, 0)
}
func LevelSnapshotAddRaining(builder *flatbuffers.Builder, raining bool) {
builder.PrependBoolSlot(3, raining, false)
}
func LevelSnapshotAddThundering(builder *flatbuffers.Builder, thundering bool) {
builder.PrependBoolSlot(4, thundering, false)
}
func LevelSnapshotEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+169
View File
@@ -0,0 +1,169 @@
// Code generated by the FlatBuffers compiler. DO NOT EDIT.
package gmb
import (
flatbuffers "github.com/google/flatbuffers/go"
)
type ServerSnapshot struct {
_tab flatbuffers.Table
}
const ServerSnapshotIdentifier = "GMBS"
func GetRootAsServerSnapshot(buf []byte, offset flatbuffers.UOffsetT) *ServerSnapshot {
n := flatbuffers.GetUOffsetT(buf[offset:])
x := &ServerSnapshot{}
x.Init(buf, n+offset)
return x
}
func FinishServerSnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
identifierBytes := []byte(ServerSnapshotIdentifier)
builder.FinishWithFileIdentifier(offset, identifierBytes)
}
func ServerSnapshotBufferHasIdentifier(buf []byte) bool {
return flatbuffers.BufferHasIdentifier(buf, ServerSnapshotIdentifier)
}
func GetSizePrefixedRootAsServerSnapshot(buf []byte, offset flatbuffers.UOffsetT) *ServerSnapshot {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ServerSnapshot{}
x.Init(buf, n+offset+flatbuffers.SizeUint32)
return x
}
func FinishSizePrefixedServerSnapshotBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
identifierBytes := []byte(ServerSnapshotIdentifier)
builder.FinishSizePrefixedWithFileIdentifier(offset, identifierBytes)
}
func SizePrefixedServerSnapshotBufferHasIdentifier(buf []byte) bool {
return flatbuffers.SizePrefixedBufferHasIdentifier(buf, ServerSnapshotIdentifier)
}
func (rcv *ServerSnapshot) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf
rcv._tab.Pos = i
}
func (rcv *ServerSnapshot) Table() flatbuffers.Table {
return rcv._tab
}
func (rcv *ServerSnapshot) Tick() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(4))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *ServerSnapshot) MutateTick(n int64) bool {
return rcv._tab.MutateInt64Slot(4, n)
}
func (rcv *ServerSnapshot) TimestampUnixMilli() int64 {
o := flatbuffers.UOffsetT(rcv._tab.Offset(6))
if o != 0 {
return rcv._tab.GetInt64(o + rcv._tab.Pos)
}
return 0
}
func (rcv *ServerSnapshot) MutateTimestampUnixMilli(n int64) bool {
return rcv._tab.MutateInt64Slot(6, n)
}
func (rcv *ServerSnapshot) Levels(obj *LevelSnapshot, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *ServerSnapshot) LevelsLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(8))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func (rcv *ServerSnapshot) Entities(obj *EntitySnapshot, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *ServerSnapshot) EntitiesLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(10))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func (rcv *ServerSnapshot) Blocks(obj *BlockSnapshot, j int) bool {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
x := rcv._tab.Vector(o)
x += flatbuffers.UOffsetT(j) * 4
x = rcv._tab.Indirect(x)
obj.Init(rcv._tab.Bytes, x)
return true
}
return false
}
func (rcv *ServerSnapshot) BlocksLength() int {
o := flatbuffers.UOffsetT(rcv._tab.Offset(12))
if o != 0 {
return rcv._tab.VectorLen(o)
}
return 0
}
func ServerSnapshotStart(builder *flatbuffers.Builder) {
builder.StartObject(5)
}
func ServerSnapshotAddTick(builder *flatbuffers.Builder, tick int64) {
builder.PrependInt64Slot(0, tick, 0)
}
func ServerSnapshotAddTimestampUnixMilli(builder *flatbuffers.Builder, timestampUnixMilli int64) {
builder.PrependInt64Slot(1, timestampUnixMilli, 0)
}
func ServerSnapshotAddLevels(builder *flatbuffers.Builder, levels flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(2, flatbuffers.UOffsetT(levels), 0)
}
func ServerSnapshotStartLevelsVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func ServerSnapshotAddEntities(builder *flatbuffers.Builder, entities flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(3, flatbuffers.UOffsetT(entities), 0)
}
func ServerSnapshotStartEntitiesVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func ServerSnapshotAddBlocks(builder *flatbuffers.Builder, blocks flatbuffers.UOffsetT) {
builder.PrependUOffsetTSlot(4, flatbuffers.UOffsetT(blocks), 0)
}
func ServerSnapshotStartBlocksVector(builder *flatbuffers.Builder, numElems int) flatbuffers.UOffsetT {
return builder.StartVector(4, numElems, 4)
}
func ServerSnapshotEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT {
return builder.EndObject()
}
+6
View File
@@ -12,6 +12,12 @@ type TickHandler interface {
Tick(context *Context, snapshot ServerSnapshot) error
}
// ClientTickHandler is invoked only by a client native runtime. A plugin with
// environment "both" may implement TickHandler, ClientTickHandler, or both.
type ClientTickHandler interface {
ClientTick(context *Context, event ClientTickEvent) error
}
type ChatHandler interface {
Chat(context *Context, event ChatEvent) error
}
+14 -1
View File
@@ -13,6 +13,7 @@ var (
registeredPlugin Plugin
)
// Register registers a plugin with the server.
func Register(plugin Plugin) {
if plugin == nil {
panic("sdk: cannot register a nil plugin")
@@ -27,6 +28,7 @@ func Register(plugin Plugin) {
enableOutputCapture()
}
// Dispatch dispatches a plugin operation to the registered plugin.
func Dispatch(operation int, input []byte) (output []byte) {
context := &Context{}
result := response{Status: "ok"}
@@ -61,6 +63,9 @@ func Dispatch(operation int, input []byte) (output []byte) {
if metadata.APIVersion == 0 {
metadata.APIVersion = ABIVersion
}
if metadata.Environment == "" {
metadata.Environment = PluginEnvironmentServer
}
result.Data = metadata
case OperationInit:
var event InitEvent
@@ -72,7 +77,7 @@ func Dispatch(operation int, input []byte) (output []byte) {
}
case OperationTick:
var snapshot ServerSnapshot
err = decode(input, &snapshot)
snapshot, err = decodeTickSnapshot(input)
if err == nil {
if handler, ok := plugin.(TickHandler); ok {
err = handler.Tick(context, snapshot)
@@ -110,6 +115,14 @@ func Dispatch(operation int, input []byte) (output []byte) {
err = handler.Deinit(context, event)
}
}
case OperationClientTick:
var event ClientTickEvent
err = decode(input, &event)
if err == nil {
if handler, ok := plugin.(ClientTickHandler); ok {
err = handler.ClientTick(context, event)
}
}
default:
err = fmt.Errorf("sdk: unknown operation %d", operation)
}
+41
View File
@@ -23,6 +23,13 @@ func (testPlugin) Chat(context *Context, event ChatEvent) error {
return nil
}
func (testPlugin) ClientTick(context *Context, event ClientTickEvent) error {
if event.Connected {
context.DisplayClientMessage(event.PlayerName)
}
return nil
}
func TestDispatch(t *testing.T) {
pluginMu.Lock()
registeredPlugin = testPlugin{}
@@ -38,6 +45,40 @@ func TestDispatch(t *testing.T) {
}
}
func TestMetadataDefaultsToServerEnvironment(t *testing.T) {
pluginMu.Lock()
registeredPlugin = testPlugin{}
pluginMu.Unlock()
var got struct {
Data Metadata `json:"data"`
}
if err := json.Unmarshal(Dispatch(OperationMetadata, nil), &got); err != nil {
t.Fatal(err)
}
if got.Data.Environment != PluginEnvironmentServer {
t.Fatalf("environment = %q, want %q", got.Data.Environment, PluginEnvironmentServer)
}
}
func TestDispatchClientTick(t *testing.T) {
pluginMu.Lock()
registeredPlugin = testPlugin{}
pluginMu.Unlock()
input, _ := json.Marshal(ClientTickEvent{Connected: true, PlayerName: "Client player"})
var got response
if err := json.Unmarshal(Dispatch(OperationClientTick, input), &got); err != nil {
t.Fatal(err)
}
if got.Status != "ok" || len(got.Actions) != 1 {
t.Fatalf("unexpected client tick response: %#v", got)
}
if got.Actions[0].Type != "minecraft:client.chat.display" {
t.Fatalf("action type = %q", got.Actions[0].Type)
}
}
func TestDispatchRecoversPanic(t *testing.T) {
pluginMu.Lock()
registeredPlugin = testPlugin{}
+54 -12
View File
@@ -2,7 +2,7 @@ package sdk
import "encoding/json"
const ABIVersion = 1
const ABIVersion = 2
const (
OperationMetadata = 1
@@ -12,23 +12,47 @@ const (
OperationDeath = 5
OperationSystemCallResult = 6
OperationDeinit = 7
OperationClientTick = 8
)
// PluginEnvironment declares which Minecraft process may execute a plugin.
type PluginEnvironment string
const (
PluginEnvironmentServer PluginEnvironment = "server"
PluginEnvironmentClient PluginEnvironment = "client"
PluginEnvironmentBoth PluginEnvironment = "both"
)
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"`
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"`
Environment PluginEnvironment `json:"environment"`
}
type InitEvent struct {
MinecraftVersion string `json:"minecraftVersion"`
Dedicated bool `json:"dedicated"`
DataDirectory string `json:"dataDirectory"`
MinecraftVersion string `json:"minecraftVersion"`
Dedicated bool `json:"dedicated"`
DataDirectory string `json:"dataDirectory"`
RuntimeEnvironment PluginEnvironment `json:"runtimeEnvironment"`
}
// ClientTickEvent contains client-local state. Pointer-like values are empty
// when the client is at the title screen or is not connected to a world.
type ClientTickEvent struct {
Tick int64 `json:"tick"`
TimestampUnixMilli int64 `json:"timestampUnixMilli"`
Connected bool `json:"connected"`
ServerAddress string `json:"serverAddress,omitempty"`
PlayerUUID string `json:"playerUuid,omitempty"`
PlayerName string `json:"playerName,omitempty"`
Dimension string `json:"dimension,omitempty"`
}
type ServerSnapshot struct {
@@ -105,6 +129,24 @@ type SystemCallResult struct {
Error string `json:"error"`
}
// SystemCallType identifies a system call provided by the bridge itself.
// Use Context.CustomSystemCall for calls registered by another mod.
type SystemCallType string
const (
SystemCallServerInfo SystemCallType = "minecraft:server.info"
SystemCallPlayerGet SystemCallType = "minecraft:player.get"
SystemCallBlockGet SystemCallType = "minecraft:block.get"
SystemCallGetEntity SystemCallType = "minecraft:get_entity"
)
// GetEntityRequest selects an entity by UUID or by its runtime ID.
// Exactly one field must be set.
type GetEntityRequest struct {
UUID string `json:"uuid,omitempty"`
RuntimeID *int `json:"runtimeId,omitempty"`
}
type DeinitEvent struct {
Reason string `json:"reason"`
}