Initial commit: RegionIO Minecraft server core (26.1.2/protocol 775)

Vanilla-faithful overworld generator (final_density + multi-noise biomes),
full connection lifecycle (status/login/configuration/play), chunk streaming,
creative block editing, and the protocol/nbt/registry infrastructure.
This commit is contained in:
Master290 2026-06-24 00:32:51 +03:00
commit a7bb9496ae
146 changed files with 217621 additions and 0 deletions

229
internal/nbt/decode.go Normal file
View file

@ -0,0 +1,229 @@
package nbt
import (
"encoding/binary"
"errors"
"math"
)
var (
errTruncated = errors.New("nbt: truncated input")
errBadTag = errors.New("nbt: unknown tag id")
errNegativeLen = errors.New("nbt: negative length")
)
// decoder walks a byte slice, tracking a cursor.
type decoder struct {
b []byte
pos int
}
// Unmarshal decodes a network-format payload (unnamed root) into a Tag.
func Unmarshal(b []byte) (Tag, error) {
d := &decoder{b: b}
id, err := d.u8()
if err != nil {
return nil, err
}
if id == TagEnd {
return nil, nil
}
return d.payload(id)
}
// UnmarshalNamed decodes a classic named-format payload, returning the root
// name and tag.
func UnmarshalNamed(b []byte) (string, Tag, error) {
d := &decoder{b: b}
id, err := d.u8()
if err != nil {
return "", nil, err
}
if id == TagEnd {
return "", nil, nil
}
name, err := d.str()
if err != nil {
return "", nil, err
}
t, err := d.payload(id)
return name, t, err
}
func (d *decoder) need(n int) error {
if n < 0 {
return errNegativeLen
}
if d.pos+n > len(d.b) {
return errTruncated
}
return nil
}
func (d *decoder) u8() (byte, error) {
if err := d.need(1); err != nil {
return 0, err
}
v := d.b[d.pos]
d.pos++
return v, nil
}
func (d *decoder) u16() (uint16, error) {
if err := d.need(2); err != nil {
return 0, err
}
v := binary.BigEndian.Uint16(d.b[d.pos:])
d.pos += 2
return v, nil
}
func (d *decoder) u32() (uint32, error) {
if err := d.need(4); err != nil {
return 0, err
}
v := binary.BigEndian.Uint32(d.b[d.pos:])
d.pos += 4
return v, nil
}
func (d *decoder) u64() (uint64, error) {
if err := d.need(8); err != nil {
return 0, err
}
v := binary.BigEndian.Uint64(d.b[d.pos:])
d.pos += 8
return v, nil
}
func (d *decoder) str() (string, error) {
n, err := d.u16()
if err != nil {
return "", err
}
if err := d.need(int(n)); err != nil {
return "", err
}
s, err := decodeModifiedUTF8(d.b[d.pos : d.pos+int(n)])
d.pos += int(n)
return s, err
}
// payload decodes a tag payload of the given type id.
func (d *decoder) payload(id byte) (Tag, error) {
switch id {
case TagByte:
v, err := d.u8()
return Byte(int8(v)), err
case TagShort:
v, err := d.u16()
return Short(int16(v)), err
case TagInt:
v, err := d.u32()
return Int(int32(v)), err
case TagLong:
v, err := d.u64()
return Long(int64(v)), err
case TagFloat:
v, err := d.u32()
return Float(math.Float32frombits(v)), err
case TagDouble:
v, err := d.u64()
return Double(math.Float64frombits(v)), err
case TagByteArray:
n, err := d.u32()
if err != nil {
return nil, err
}
if err := d.need(int(int32(n))); err != nil {
return nil, err
}
out := make(ByteArray, n)
copy(out, d.b[d.pos:d.pos+int(n)])
d.pos += int(n)
return out, nil
case TagString:
s, err := d.str()
return String(s), err
case TagIntArray:
n, err := d.u32()
if err != nil {
return nil, err
}
out := make(IntArray, int32(n))
for i := range out {
v, err := d.u32()
if err != nil {
return nil, err
}
out[i] = int32(v)
}
return out, nil
case TagLongArray:
n, err := d.u32()
if err != nil {
return nil, err
}
out := make(LongArray, int32(n))
for i := range out {
v, err := d.u64()
if err != nil {
return nil, err
}
out[i] = int64(v)
}
return out, nil
case TagList:
return d.list()
case TagCompound:
return d.compound()
default:
return nil, errBadTag
}
}
func (d *decoder) list() (Tag, error) {
elemID, err := d.u8()
if err != nil {
return nil, err
}
n, err := d.u32()
if err != nil {
return nil, err
}
count := int(int32(n))
if count < 0 {
return nil, errNegativeLen
}
l := List{ElemID: elemID, Elems: make([]Tag, 0, count)}
for i := 0; i < count; i++ {
t, err := d.payload(elemID)
if err != nil {
return nil, err
}
l.Elems = append(l.Elems, t)
}
return l, nil
}
func (d *decoder) compound() (Tag, error) {
c := NewCompound()
for {
id, err := d.u8()
if err != nil {
return nil, err
}
if id == TagEnd {
return c, nil
}
name, err := d.str()
if err != nil {
return nil, err
}
t, err := d.payload(id)
if err != nil {
return nil, err
}
c.Set(name, t)
}
}

95
internal/nbt/encode.go Normal file
View file

@ -0,0 +1,95 @@
package nbt
import (
"encoding/binary"
"fmt"
"math"
)
// Marshal encodes root in the network format: the root tag's type byte followed
// by its payload, with no root name (Minecraft 1.20.2+).
func Marshal(root Tag) []byte {
dst := []byte{root.ID()}
return appendPayload(dst, root)
}
// MarshalNamed encodes root in the classic named format: type byte, root name,
// then payload. Used for files and legacy framing.
func MarshalNamed(name string, root Tag) []byte {
dst := []byte{root.ID()}
dst = appendString(dst, name)
return appendPayload(dst, root)
}
// appendString writes a modified-UTF-8 string with an unsigned-short length.
func appendString(dst []byte, s string) []byte {
start := len(dst)
dst = append(dst, 0, 0) // length placeholder
dst = encodeModifiedUTF8(dst, s)
binary.BigEndian.PutUint16(dst[start:], uint16(len(dst)-start-2))
return dst
}
// appendPayload writes a tag's payload (no type byte, no name).
func appendPayload(dst []byte, t Tag) []byte {
switch v := t.(type) {
case Byte:
return append(dst, byte(v))
case Short:
return binary.BigEndian.AppendUint16(dst, uint16(v))
case Int:
return binary.BigEndian.AppendUint32(dst, uint32(v))
case Long:
return binary.BigEndian.AppendUint64(dst, uint64(v))
case Float:
return binary.BigEndian.AppendUint32(dst, math.Float32bits(float32(v)))
case Double:
return binary.BigEndian.AppendUint64(dst, math.Float64bits(float64(v)))
case ByteArray:
dst = binary.BigEndian.AppendUint32(dst, uint32(len(v)))
return append(dst, v...)
case String:
return appendString(dst, string(v))
case IntArray:
dst = binary.BigEndian.AppendUint32(dst, uint32(len(v)))
for _, n := range v {
dst = binary.BigEndian.AppendUint32(dst, uint32(n))
}
return dst
case LongArray:
dst = binary.BigEndian.AppendUint32(dst, uint32(len(v)))
for _, n := range v {
dst = binary.BigEndian.AppendUint64(dst, uint64(n))
}
return dst
case List:
return appendList(dst, v)
case *Compound:
return appendCompound(dst, v)
default:
panic(fmt.Sprintf("nbt: cannot encode %T", t))
}
}
func appendList(dst []byte, l List) []byte {
elemID := l.ElemID
if len(l.Elems) == 0 {
elemID = TagEnd // Java writes TagEnd for empty lists.
}
dst = append(dst, elemID)
dst = binary.BigEndian.AppendUint32(dst, uint32(len(l.Elems)))
for _, e := range l.Elems {
dst = appendPayload(dst, e)
}
return dst
}
func appendCompound(dst []byte, c *Compound) []byte {
for _, k := range c.keys {
t := c.m[k]
dst = append(dst, t.ID())
dst = appendString(dst, k)
dst = appendPayload(dst, t)
}
return append(dst, TagEnd)
}

59
internal/nbt/mutf8.go Normal file
View file

@ -0,0 +1,59 @@
package nbt
import (
"errors"
"unicode/utf16"
)
// errBadMUTF8 is returned when a modified-UTF-8 byte sequence is malformed.
var errBadMUTF8 = errors.New("nbt: invalid modified UTF-8")
// encodeModifiedUTF8 encodes s as Java modified UTF-8: ASCII stays one byte,
// NUL and code points up to U+07FF take two bytes, U+0800..U+FFFF take three,
// and supplementary code points are split into a surrogate pair (six bytes).
func encodeModifiedUTF8(dst []byte, s string) []byte {
for _, u := range utf16.Encode([]rune(s)) {
switch {
case u >= 0x0001 && u <= 0x007F:
dst = append(dst, byte(u))
case u == 0 || u <= 0x07FF:
dst = append(dst,
0xC0|byte(u>>6),
0x80|byte(u&0x3F))
default:
dst = append(dst,
0xE0|byte(u>>12),
0x80|byte((u>>6)&0x3F),
0x80|byte(u&0x3F))
}
}
return dst
}
// decodeModifiedUTF8 decodes b (length-delimited modified UTF-8) into a string.
func decodeModifiedUTF8(b []byte) (string, error) {
units := make([]uint16, 0, len(b))
for i := 0; i < len(b); {
c := b[i]
switch {
case c&0x80 == 0: // 0xxxxxxx
units = append(units, uint16(c))
i++
case c&0xE0 == 0xC0: // 110xxxxx 10xxxxxx
if i+1 >= len(b) || b[i+1]&0xC0 != 0x80 {
return "", errBadMUTF8
}
units = append(units, uint16(c&0x1F)<<6|uint16(b[i+1]&0x3F))
i += 2
case c&0xF0 == 0xE0: // 1110xxxx 10xxxxxx 10xxxxxx
if i+2 >= len(b) || b[i+1]&0xC0 != 0x80 || b[i+2]&0xC0 != 0x80 {
return "", errBadMUTF8
}
units = append(units, uint16(c&0x0F)<<12|uint16(b[i+1]&0x3F)<<6|uint16(b[i+2]&0x3F))
i += 3
default:
return "", errBadMUTF8
}
}
return string(utf16.Decode(units)), nil
}

98
internal/nbt/nbt.go Normal file
View file

@ -0,0 +1,98 @@
// Package nbt implements Minecraft's Named Binary Tag format, including the
// network variant used since 1.20.2 where the root tag carries no name.
//
// All numeric payloads are big-endian. Strings use Java's "modified UTF-8":
// an unsigned-short byte length followed by bytes where NUL and supplementary
// code points are escaped (see encode/decodeModifiedUTF8).
package nbt
// Tag type identifiers as defined by the NBT specification.
const (
TagEnd = 0x00
TagByte = 0x01
TagShort = 0x02
TagInt = 0x03
TagLong = 0x04
TagFloat = 0x05
TagDouble = 0x06
TagByteArray = 0x07
TagString = 0x08
TagList = 0x09
TagCompound = 0x0A
TagIntArray = 0x0B
TagLongArray = 0x0C
)
// Tag is any NBT value. ID returns its tag type identifier.
type Tag interface{ ID() byte }
// Primitive and array tag types map directly onto Go types.
type (
Byte int8
Short int16
Int int32
Long int64
Float float32
Double float64
ByteArray []byte
String string
IntArray []int32
LongArray []int64
)
func (Byte) ID() byte { return TagByte }
func (Short) ID() byte { return TagShort }
func (Int) ID() byte { return TagInt }
func (Long) ID() byte { return TagLong }
func (Float) ID() byte { return TagFloat }
func (Double) ID() byte { return TagDouble }
func (ByteArray) ID() byte { return TagByteArray }
func (String) ID() byte { return TagString }
func (IntArray) ID() byte { return TagIntArray }
func (LongArray) ID() byte { return TagLongArray }
// List is a homogeneous sequence of unnamed tags. ElemID must match the type
// of every element; for an empty list it is written as TagEnd, per Java.
type List struct {
ElemID byte
Elems []Tag
}
func (List) ID() byte { return TagList }
// Compound is an ordered set of named tags. Insertion order is preserved so
// that encoding is deterministic (the protocol does not require any order).
type Compound struct {
keys []string
m map[string]Tag
}
func (*Compound) ID() byte { return TagCompound }
// NewCompound returns an empty compound ready for Set.
func NewCompound() *Compound {
return &Compound{m: make(map[string]Tag)}
}
// Set inserts or replaces the tag for name, preserving first-insertion order,
// and returns the compound for chaining.
func (c *Compound) Set(name string, t Tag) *Compound {
if _, ok := c.m[name]; !ok {
c.keys = append(c.keys, name)
}
c.m[name] = t
return c
}
// Get returns the tag for name and whether it is present.
func (c *Compound) Get(name string) (Tag, bool) {
t, ok := c.m[name]
return t, ok
}
// Len returns the number of entries.
func (c *Compound) Len() int { return len(c.keys) }
// Keys returns the entry names in insertion order. The slice must not be
// mutated by callers.
func (c *Compound) Keys() []string { return c.keys }

119
internal/nbt/nbt_test.go Normal file
View file

@ -0,0 +1,119 @@
package nbt
import (
"bytes"
"reflect"
"testing"
)
// TestGoldenHelloWorld checks the canonical NBT example: a named-root compound
// {"hello world": {"name": "Bananrama"}}.
func TestGoldenHelloWorld(t *testing.T) {
root := NewCompound().Set("name", String("Bananrama"))
got := MarshalNamed("hello world", root)
want := []byte{
0x0a, 0x00, 0x0b, // TAG_Compound, name len 11
'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd',
0x08, 0x00, 0x04, 'n', 'a', 'm', 'e', // TAG_String, name len 4
0x00, 0x09, // string len 9
'B', 'a', 'n', 'a', 'n', 'r', 'a', 'm', 'a',
0x00, // TAG_End
}
if !bytes.Equal(got, want) {
t.Fatalf("golden mismatch:\n got=%x\nwant=%x", got, want)
}
}
// TestNetworkRootHasNoName verifies the 1.20.2+ network root omits the name.
func TestNetworkRootHasNoName(t *testing.T) {
got := Marshal(NewCompound().Set("a", Byte(1)))
want := []byte{
0x0a, // TAG_Compound (no name)
0x01, 0x00, 0x01, 'a', // TAG_Byte, name len 1
0x01, // value 1
0x00, // TAG_End
}
if !bytes.Equal(got, want) {
t.Fatalf("network root mismatch:\n got=%x\nwant=%x", got, want)
}
}
// TestRoundTrip encodes a richly-nested compound and decodes it back.
func TestRoundTrip(t *testing.T) {
root := NewCompound().
Set("b", Byte(-7)).
Set("s", Short(300)).
Set("i", Int(-123456)).
Set("l", Long(1<<40)).
Set("f", Float(3.5)).
Set("d", Double(-2.25)).
Set("str", String("héllo")).
Set("bytes", ByteArray{1, 2, 3}).
Set("ints", IntArray{10, -20, 30}).
Set("longs", LongArray{1, 2}).
Set("list", List{ElemID: TagInt, Elems: []Tag{Int(1), Int(2), Int(3)}}).
Set("empty", List{ElemID: TagString}).
Set("nested", NewCompound().Set("x", String("y")))
enc := Marshal(root)
dec, err := Unmarshal(enc)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
reenc := Marshal(dec)
if !bytes.Equal(enc, reenc) {
t.Fatalf("round-trip bytes differ:\n first=%x\nsecond=%x", enc, reenc)
}
}
// TestModifiedUTF8 round-trips strings including NUL and supplementary chars.
func TestModifiedUTF8(t *testing.T) {
cases := []string{
"",
"plain ascii",
"with\x00nul",
"café — français",
"emoji 😀 supplementary",
}
for _, s := range cases {
enc := encodeModifiedUTF8(nil, s)
// NUL must be escaped as 0xC0 0x80, never a literal zero byte.
if bytes.IndexByte(enc, 0) >= 0 {
t.Fatalf("encoding of %q contains a literal NUL byte", s)
}
dec, err := decodeModifiedUTF8(enc)
if err != nil {
t.Fatalf("decode %q: %v", s, err)
}
if dec != s {
t.Fatalf("round-trip mismatch: got %q want %q", dec, s)
}
}
}
// TestDecodeNamedRoundTrip checks the named-format decode path.
func TestDecodeNamedRoundTrip(t *testing.T) {
root := NewCompound().Set("k", Int(42))
enc := MarshalNamed("root", root)
name, dec, err := UnmarshalNamed(enc)
if err != nil {
t.Fatalf("unmarshal named: %v", err)
}
if name != "root" {
t.Fatalf("root name = %q, want %q", name, "root")
}
if !reflect.DeepEqual(Marshal(dec), Marshal(root)) {
t.Fatal("named decode produced different tree")
}
}
// TestTruncatedInput ensures the decoder rejects short buffers without panic.
func TestTruncatedInput(t *testing.T) {
enc := Marshal(NewCompound().Set("x", Long(1)))
for n := 0; n < len(enc); n++ {
if _, err := Unmarshal(enc[:n]); err == nil {
t.Fatalf("expected error decoding %d/%d bytes", n, len(enc))
}
}
}