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

31
.gitignore vendored Normal file
View file

@ -0,0 +1,31 @@
# Compiled binaries
/regionio
/regionio.exe
*.exe
*.test
# Mojang server distribution (not redistributable; obtain separately)
/server.jar
/libraries/
/versions/
# Build output
/dist/
# Vanilla data-generator output (regenerated from server.jar --reports).
# All build-time embeds live under internal/; generated/ is not needed to build.
/generated/
# Logs and runtime artefacts
/logs/
*.log
# Editor / tool local config
/.claude/
/.vscode/
/.idea/
*.swp
# OS junk
.DS_Store
Thumbs.db

63
README.md Normal file
View file

@ -0,0 +1,63 @@
# RegionIO
A Minecraft Java Edition server core written in Go, targeting version
**26.1.2** (protocol **775**). RegionIO implements the connection lifecycle
(status → login → configuration → play), chunk streaming, block editing, and a
vanilla-faithful overworld generator built on the real `noise_router`
`final_density` tree.
## Status
- **Network**: full handshake/status/login (offline mode)/configuration/play
state machine with zlib compression, keep-alive, and chunk streaming.
- **Registries**: 28 synchronized registries + tags, captured verbatim from the
26.1.2 vanilla server and sent during configuration.
- **World**: in-memory chunk cache with memoized, compression-ready
`level_chunk_with_light` frames; paletted block containers; heightmaps.
- **Generation**: bit-faithful overworld terrain from the embedded datapack
(`ImprovedNoise`/`PerlinNoise`/`BlendedNoise`/`NormalNoise` + the density
function interpreter), plus multi-noise **biomes** (per-chunk, surface layer)
via the vanilla `Climate` finder over the official biome parameter table.
- **Gameplay**: creative block place/break, hotbar item→block mapping, chat,
teleport ack, and a randomized bedrock floor / beach & gravel surface pass.
## Build & run
```
go build ./...
go run ./cmd/regionio -seed 12345
```
The world seed defaults to `0`; override it with the `-seed` flag or the
`REGIONIO_SEED` environment variable. The server listens on `0.0.0.0:25565`.
## Testing
```
go test ./...
```
Parity tests (`internal/world/vanilla_parity_test.go`) compare generated surface
heights against captures from the official server and skip when no capture is
present.
## Project layout
```
cmd/regionio/ entry point (config, listener, graceful shutdown)
internal/
protocol/ wire primitives: VarInt, framing, compression, packet IDs
nbt/ NBT encoder/decoder (with modified UTF-8)
registry/ embedded synchronized registries + tags
world/ chunk model, level_chunk encoder, cache, generators, biomes
worldgen/ noise core + density-function interpreter + climate finder
network/ per-connection state machine (handler/conn/play/login/...)
server/ shared core: config, status response, profiles
```
## Notes
The vanilla `server.jar` and its unpacked `libraries/`/`versions/` are **not**
included (obtain them from Mojang). The embedded data under `internal/`
(registries, biome parameters, the overworld datapack) is derived from vanilla
reports and is all that is required to build and run.

60
cmd/regionio/main.go Normal file
View file

@ -0,0 +1,60 @@
// Command regionio starts a RegionIO Minecraft server core.
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"strconv"
"syscall"
"regionio/internal/network"
"regionio/internal/server"
)
func main() {
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
cfg := server.DefaultConfig()
// World seed: -seed flag takes precedence, then REGIONIO_SEED env, then the
// default (0). Accepted formats: decimal, or "0x" hex. An invalid value is
// fatal — a wrong seed silently generates a different world than intended.
seedFlag := flag.Int64("seed", parseSeedEnv(os.Getenv("REGIONIO_SEED"), cfg.WorldSeed, log),
"world seed (overrides REGIONIO_SEED)")
flag.Parse()
cfg.WorldSeed = *seedFlag
log.Info("using world seed", "seed", cfg.WorldSeed)
srv := server.New(cfg)
ln := network.NewListener(srv, log)
// Cancel the context on SIGINT/SIGTERM for a graceful shutdown.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := ln.ListenAndServe(ctx); err != nil {
log.Error("server stopped", "err", err)
os.Exit(1)
}
}
// parseSeedEnv parses the REGIONIO_SEED env var. It returns fallback when the
// variable is empty, and logs+returns fallback when parsing fails (so a typo
// does not silently change the world).
func parseSeedEnv(raw string, fallback int64, log *slog.Logger) int64 {
if raw == "" {
return fallback
}
// strconv.ParseInt with base 0 handles decimal and "0x" hex prefixes.
v, err := strconv.ParseInt(raw, 0, 64)
if err != nil {
log.Error("invalid REGIONIO_SEED, falling back to default", "raw", raw, "err", err)
return fallback
}
return v
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module regionio
go 1.26.3

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

View file

@ -0,0 +1,220 @@
package network
import (
"errors"
"regionio/internal/protocol"
"regionio/internal/registry"
)
// ClientSettings holds the subset of client_information we currently track.
type ClientSettings struct {
Locale string
ViewDistance int8
ChatMode int32
MainHand int32
}
// beginConfiguration is called on entering the configuration phase. It mirrors
// the vanilla opening sequence: server brand, enabled feature flags, then the
// known-packs negotiation. The client's known-packs reply triggers the registry
// data (see handleKnownPacks).
func (h *handler) beginConfiguration() error {
if err := h.sendBrand(); err != nil {
return err
}
if err := h.sendEnabledFeatures(); err != nil {
return err
}
return h.sendKnownPacks()
}
// sendBrand reports the server brand on the minecraft:brand plugin channel.
func (h *handler) sendBrand() error {
w := protocol.NewWriter(32)
w.String("minecraft:brand").String("RegionIO")
return h.conn.SendWriter(protocol.ConfigCustomPayloadCB, w)
}
// sendEnabledFeatures enables the vanilla feature flag set. The client requires
// this so that vanilla content (blocks, items, registries) is active.
func (h *handler) sendEnabledFeatures() error {
w := protocol.NewWriter(24)
w.VarInt(1)
w.String("minecraft:vanilla")
return h.conn.SendWriter(protocol.ConfigUpdateEnabledFeatures, w)
}
// handleConfiguration drives the configuration phase:
//
// S→C select_known_packs (on entry)
// C→S client_information → record settings
// C→S custom_payload (brand) → log the client brand
// C→S select_known_packs → send registry data, then finish_configuration
// C→S finish_configuration → switch to the Play phase
func (h *handler) handleConfiguration(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.ConfigClientInformation:
return h.handleClientInformation(pkt)
case protocol.ConfigCustomPayload:
return h.handleConfigCustomPayload(pkt)
case protocol.ConfigKnownPacksServer:
return h.handleKnownPacks(pkt)
case protocol.ConfigKeepAliveServer, protocol.ConfigPong,
protocol.ConfigResourcePackResp, protocol.ConfigCookieResponse:
h.log.Debug("configuration packet ignored", "id", pkt.ID)
return nil
case protocol.ConfigFinishServerbound:
h.conn.SetState(protocol.StatePlay)
h.log.Info("entered play phase", "name", h.conn.Profile.Name)
return h.beginPlay()
default:
h.log.Debug("unknown configuration packet", "id", pkt.ID)
return nil
}
}
// handleClientInformation records the client's settings (no longer the trigger
// to finish configuration; that is now driven by known-packs negotiation).
func (h *handler) handleClientInformation(pkt protocol.Packet) error {
r := pkt.Body()
locale, err := r.String()
if err != nil {
return err
}
vd, err := r.ReadByte()
if err != nil {
return err
}
chatMode, err := r.VarInt()
if err != nil {
return err
}
if _, err := r.Bool(); err != nil { // chat colors
return err
}
if _, err := r.ReadByte(); err != nil { // displayed skin parts
return err
}
mainHand, err := r.VarInt()
if err != nil {
return err
}
h.log.Info("client information",
"locale", locale, "view_distance", int8(vd),
"chat_mode", chatMode, "main_hand", mainHand)
return nil
}
// handleConfigCustomPayload logs the client brand and ignores other channels.
func (h *handler) handleConfigCustomPayload(pkt protocol.Packet) error {
r := pkt.Body()
channel, err := r.String()
if err != nil {
return err
}
if channel == "minecraft:brand" {
brand, err := r.String()
if err != nil {
return err
}
h.log.Info("client brand", "brand", brand)
} else {
h.log.Debug("plugin message", "channel", channel)
}
return nil
}
// handleKnownPacks reads the client's known packs and, once we know what it
// already has, sends the registry data followed by finish_configuration.
func (h *handler) handleKnownPacks(pkt protocol.Packet) error {
r := pkt.Body()
count, err := r.VarInt()
if err != nil {
return err
}
if count < 0 || count > 1024 {
return errors.New("implausible known-pack count")
}
hasCore := false
for i := int32(0); i < count; i++ {
ns, err := r.String()
if err != nil {
return err
}
id, err := r.String()
if err != nil {
return err
}
ver, err := r.String()
if err != nil {
return err
}
if ns == registry.CorePack.Namespace && id == registry.CorePack.ID &&
ver == registry.CorePack.Version {
hasCore = true
}
h.log.Debug("client known pack", "namespace", ns, "id", id, "version", ver)
}
if !hasCore {
// Without the matching pack the client cannot fill in registry data
// from its built-in copy; sending has_data=false would desync it.
h.log.Warn("client lacks matching core pack; registry data may desync",
"want", registry.CorePack.Version)
}
if err := h.sendRegistries(); err != nil {
return err
}
if err := h.sendUpdateTags(); err != nil {
return err
}
return h.sendFinishConfiguration()
}
// sendKnownPacks advertises the vanilla core pack to the client.
func (h *handler) sendKnownPacks() error {
p := registry.CorePack
w := protocol.NewWriter(32)
w.VarInt(1)
w.String(p.Namespace).String(p.ID).String(p.Version)
return h.conn.SendWriter(protocol.ConfigKnownPacksCB, w)
}
// sendRegistries sends one registry_data packet per synchronized registry. Each
// entry is sent with has_data=false; the client supplies the contents from its
// matching known pack.
func (h *handler) sendRegistries() error {
for _, reg := range registry.Synced() {
w := protocol.NewWriter(64 + len(reg.Entries)*24)
w.String(reg.Name)
w.VarInt(int32(len(reg.Entries)))
for _, entry := range reg.Entries {
w.String(entry)
w.Bool(false) // has_data: client uses its own copy
}
if err := h.conn.SendWriter(protocol.ConfigRegistryData, w); err != nil {
return err
}
}
h.log.Debug("sent registry data", "registries", len(registry.Synced()))
return nil
}
// sendUpdateTags sends the captured vanilla tag set. Tags map registry entries
// (by numeric index) into named groups the client and gameplay rely on.
func (h *handler) sendUpdateTags() error {
return h.conn.Send(protocol.ConfigUpdateTags, registry.Tags())
}
// sendFinishConfiguration signals configuration is complete; the client replies
// with its own finish_configuration to enter Play.
func (h *handler) sendFinishConfiguration() error {
return h.conn.Send(protocol.ConfigFinishClientbound, nil)
}

89
internal/network/conn.go Normal file
View file

@ -0,0 +1,89 @@
// Package network owns the TCP listener and per-connection lifecycle: framing,
// the handshake state machine, and dispatch to per-state handlers.
package network
import (
"bufio"
"net"
"sync"
"regionio/internal/protocol"
"regionio/internal/server"
)
// Conn wraps a TCP connection with buffered reads and tracks protocol state.
type Conn struct {
raw net.Conn
br *bufio.Reader
state protocol.State
// compressionThreshold is -1 until Set Compression is negotiated.
compressionThreshold int32
// Profile is populated once the client identifies during login.
Profile server.Profile
// writeMu serializes writes so a background sender (e.g. keep-alive) and
// the read-loop handler never interleave bytes on the wire.
writeMu sync.Mutex
}
// NewConn wraps a raw TCP connection.
func NewConn(raw net.Conn) *Conn {
return &Conn{
raw: raw,
br: bufio.NewReaderSize(raw, 4096),
state: protocol.StateHandshaking,
compressionThreshold: -1,
}
}
// State returns the current protocol state.
func (c *Conn) State() protocol.State { return c.state }
// SetState transitions to a new protocol state.
func (c *Conn) SetState(s protocol.State) { c.state = s }
// EnableCompression sets the compression threshold for all subsequent packets.
// The caller must send the Set Compression packet (uncompressed) first.
func (c *Conn) EnableCompression(threshold int32) { c.compressionThreshold = threshold }
// CompressionEnabled reports whether compression is active.
func (c *Conn) CompressionEnabled() bool { return c.compressionThreshold >= 0 }
// RemoteAddr returns the peer address for logging.
func (c *Conn) RemoteAddr() net.Addr { return c.raw.RemoteAddr() }
// ReadPacket reads the next frame using the current compression settings.
func (c *Conn) ReadPacket() (protocol.Packet, error) {
return protocol.ReadPacket(c.br, c.compressionThreshold)
}
// Send writes a packet with the given ID and pre-encoded body. Safe for
// concurrent use.
func (c *Conn) Send(id int32, body []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
return protocol.WritePacket(c.raw, c.compressionThreshold, id, body)
}
// SendWriter writes a packet whose body was built with a protocol.Writer.
func (c *Conn) SendWriter(id int32, w *protocol.Writer) error {
return c.Send(id, w.Bytes())
}
// SendFramed writes an already-framed packet (e.g. a cached chunk) verbatim.
// The frame must have been built for this connection's compression threshold.
// Safe for concurrent use.
func (c *Conn) SendFramed(frame []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
_, err := c.raw.Write(frame)
return err
}
// CompressionThreshold returns the active threshold (-1 if disabled).
func (c *Conn) CompressionThreshold() int32 { return c.compressionThreshold }
// Close closes the underlying connection.
func (c *Conn) Close() error { return c.raw.Close() }

133
internal/network/handler.go Normal file
View file

@ -0,0 +1,133 @@
package network
import (
"errors"
"io"
"log/slog"
"net"
"regionio/internal/protocol"
"regionio/internal/server"
)
// handler drives one connection through its state machine until it closes.
// It is owned by a single goroutine (the read loop), so the play fields below
// need no synchronization.
type handler struct {
conn *Conn
srv *server.Server
log *slog.Logger
// Play-phase chunk streaming state.
loaded map[[2]int32]bool // chunks currently sent to the client
centerX int32
centerZ int32
hasCenter bool
// Creative inventory state for block placement.
heldSlot int32 // selected hotbar index (0-8)
hotbar [9]int32 // item network IDs per hotbar slot (-1 = empty)
}
// serve runs the read/dispatch loop for a single connection.
func (h *handler) serve() {
defer h.conn.Close()
for {
pkt, err := h.conn.ReadPacket()
if err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) {
h.log.Debug("connection closed", "err", err)
}
return
}
if err := h.dispatch(pkt); err != nil {
h.log.Debug("dispatch error", "state", h.conn.State(), "id", pkt.ID, "err", err)
return
}
}
}
// dispatch routes a packet to the handler for the current state.
func (h *handler) dispatch(pkt protocol.Packet) error {
switch h.conn.State() {
case protocol.StateHandshaking:
return h.handleHandshake(pkt)
case protocol.StateStatus:
return h.handleStatus(pkt)
case protocol.StateLogin:
return h.handleLogin(pkt)
case protocol.StateConfiguration:
return h.handleConfiguration(pkt)
case protocol.StatePlay:
return h.handlePlay(pkt)
default:
return errors.New("no handler for state " + h.conn.State().String())
}
}
// handleHandshake reads the single handshake packet and transitions state.
func (h *handler) handleHandshake(pkt protocol.Packet) error {
if pkt.ID != protocol.HandshakeID {
return errors.New("unexpected packet in handshaking state")
}
r := pkt.Body()
protoVer, err := r.VarInt()
if err != nil {
return err
}
addr, err := r.String()
if err != nil {
return err
}
port, err := r.Uint16()
if err != nil {
return err
}
next, err := r.VarInt()
if err != nil {
return err
}
h.log.Debug("handshake",
"protocol", protoVer, "addr", addr, "port", port, "next", next)
switch next {
case protocol.NextStateStatus:
h.conn.SetState(protocol.StateStatus)
case protocol.NextStateLogin, protocol.NextStateTransfer:
h.conn.SetState(protocol.StateLogin)
default:
return errors.New("invalid next state in handshake")
}
return nil
}
// handleStatus answers the server-list ping: status request and ping/pong.
func (h *handler) handleStatus(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.StatusRequestID:
jsonBytes, err := h.srv.StatusJSON(0)
if err != nil {
return err
}
w := protocol.NewWriter(len(jsonBytes) + 4)
w.String(string(jsonBytes))
return h.conn.SendWriter(protocol.StatusResponseID, w)
case protocol.PingRequestID:
// Echo the client's payload back verbatim for latency measurement.
payload, err := pkt.Body().Int64()
if err != nil {
return err
}
w := protocol.NewWriter(8)
w.Int64(payload)
return h.conn.SendWriter(protocol.PongResponseID, w)
default:
return errors.New("unexpected packet in status state")
}
}

View file

@ -0,0 +1,64 @@
package network
import (
"context"
"fmt"
"log/slog"
"net"
"regionio/internal/server"
)
// Listener accepts TCP connections and serves each in its own goroutine.
type Listener struct {
srv *server.Server
log *slog.Logger
}
// NewListener constructs a Listener bound to srv.
func NewListener(srv *server.Server, log *slog.Logger) *Listener {
return &Listener{srv: srv, log: log}
}
// ListenAndServe binds the configured address and accepts connections until
// ctx is cancelled.
func (l *Listener) ListenAndServe(ctx context.Context) error {
cfg := l.srv.Config()
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var lc net.ListenConfig
ln, err := lc.Listen(ctx, "tcp", addr)
if err != nil {
return fmt.Errorf("listen on %s: %w", addr, err)
}
l.log.Info("RegionIO listening", "addr", addr, "version", "26.1.2")
// Close the listener when the context is cancelled to unblock Accept.
go func() {
<-ctx.Done()
_ = ln.Close()
}()
for {
raw, err := ln.Accept()
if err != nil {
if ctx.Err() != nil {
return nil // graceful shutdown
}
l.log.Warn("accept failed", "err", err)
continue
}
go l.serveConn(raw)
}
}
// serveConn wraps a raw connection and runs its state-machine handler.
func (l *Listener) serveConn(raw net.Conn) {
conn := NewConn(raw)
h := &handler{
conn: conn,
srv: l.srv,
log: l.log.With("peer", raw.RemoteAddr().String()),
}
h.serve()
}

95
internal/network/login.go Normal file
View file

@ -0,0 +1,95 @@
package network
import (
"errors"
"regionio/internal/protocol"
"regionio/internal/server"
)
// handleLogin drives the offline-mode login phase:
//
// C→S Login Start → derive offline profile
// S→C Set Compression → (optional) enable zlib for later packets
// S→C Login Success → confirm the profile
// C→S Login Acknowledged → switch to the Configuration phase
//
// Encryption (online mode) is intentionally not implemented here.
func (h *handler) handleLogin(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.LoginStartID:
return h.handleLoginStart(pkt)
case protocol.LoginAcknowledgedID:
// Client acknowledges Login Success; both sides enter Configuration.
h.conn.SetState(protocol.StateConfiguration)
h.log.Info("player logged in",
"name", h.conn.Profile.Name,
"uuid", uuidString(h.conn.Profile.UUID))
return h.beginConfiguration()
default:
return errors.New("unexpected packet in login state")
}
}
func (h *handler) handleLoginStart(pkt protocol.Packet) error {
r := pkt.Body()
name, err := r.String()
if err != nil {
return err
}
if name == "" || len(name) > 16 {
return errors.New("invalid login name")
}
// The client also sends a UUID, but in offline mode we derive our own so it
// is stable and independent of what the client claims.
if _, err := r.UUID(); err != nil {
return err
}
h.conn.Profile = server.Profile{
UUID: server.OfflineUUID(name),
Name: name,
}
// Negotiate compression before Login Success so that packet (and every
// later one) is sent in the compressed format.
if t := h.srv.Config().CompressionThreshold; t >= 0 {
w := protocol.NewWriter(protocol.VarIntLen(int32(t)))
w.VarInt(int32(t))
if err := h.conn.SendWriter(protocol.SetCompressionID, w); err != nil {
return err
}
h.conn.EnableCompression(int32(t))
}
return h.sendLoginSuccess()
}
// sendLoginSuccess writes the Login Success packet. For protocol 775 the body
// is: UUID, Username, then a VarInt-prefixed array of profile properties (none
// in offline mode).
func (h *handler) sendLoginSuccess() error {
p := h.conn.Profile
w := protocol.NewWriter(16 + len(p.Name) + 2)
w.UUID(p.UUID)
w.String(p.Name)
w.VarInt(0) // property count
return h.conn.SendWriter(protocol.LoginSuccessID, w)
}
// uuidString formats a UUID as the canonical 8-4-4-4-12 hex string.
func uuidString(u [16]byte) string {
const hexdigits = "0123456789abcdef"
var b [36]byte
j := 0
for i := 0; i < 16; i++ {
if i == 4 || i == 6 || i == 8 || i == 10 {
b[j] = '-'
j++
}
b[j] = hexdigits[u[i]>>4]
b[j+1] = hexdigits[u[i]&0x0f]
j += 2
}
return string(b[:j])
}

404
internal/network/play.go Normal file
View file

@ -0,0 +1,404 @@
package network
import (
"math"
"time"
"regionio/internal/nbt"
"regionio/internal/protocol"
"regionio/internal/registry"
"regionio/internal/world"
)
// Spawn coordinates. Y sits above the maximum terrain height so the player
// drops onto the generated surface rather than spawning inside it.
const (
spawnX = 8.5
spawnY = 200.0
spawnZ = 8.5
)
// chunkRadius is how many chunks around the player we send (a square of side
// 2*radius+1). Kept modest until streaming by view distance exists.
const chunkRadius = 4
// beginPlay sends the join sequence once the client enters the Play phase and
// starts the keep-alive loop. In milestone 4a no chunks are sent, so the client
// reaches the "loading terrain" screen and waits.
func (h *handler) beginPlay() error {
for i := range h.hotbar {
h.hotbar[i] = -1 // empty
}
if err := h.sendPlayLogin(); err != nil {
return err
}
// "Start waiting for level chunks": tells the client to show the loading
// screen until chunks arrive.
if err := h.sendGameEvent(protocol.GameEventStartWaitingChunks, 0); err != nil {
return err
}
if err := h.sendPlayerPosition(1); err != nil {
return err
}
if err := h.streamAround(0, 0); err != nil {
return err
}
go h.keepAliveLoop()
return nil
}
// streamAround recenters the client's chunk cache on (centerX, centerZ) and
// sends the chunks newly in range. Chunks that fall out of range are dropped by
// the client automatically once it receives the new cache center, so we only
// send the difference and track the currently-loaded set.
func (h *handler) streamAround(centerX, centerZ int32) error {
cc := protocol.NewWriter(8)
cc.VarInt(centerX)
cc.VarInt(centerZ)
if err := h.conn.SendWriter(protocol.PlayChunkCacheCenter, cc); err != nil {
return err
}
cache := h.srv.Chunks()
next := make(map[[2]int32]bool, (2*chunkRadius+1)*(2*chunkRadius+1))
sent := 0
for cx := centerX - chunkRadius; cx <= centerX+chunkRadius; cx++ {
for cz := centerZ - chunkRadius; cz <= centerZ+chunkRadius; cz++ {
key := [2]int32{cx, cz}
next[key] = true
if h.loaded[key] {
continue
}
if err := h.conn.SendFramed(cache.Frame(cx, cz)); err != nil {
return err
}
sent++
}
}
h.loaded = next
h.centerX, h.centerZ, h.hasCenter = centerX, centerZ, true
h.log.Debug("streamed chunks", "center_x", centerX, "center_z", centerZ, "new", sent)
return nil
}
// onPlayerMove recenters chunk streaming when the player crosses into a new
// chunk. X and Z are the player's block-precise coordinates.
func (h *handler) onPlayerMove(x, z float64) error {
cx := int32(int64(math.Floor(x)) >> 4)
cz := int32(int64(math.Floor(z)) >> 4)
if h.hasCenter && cx == h.centerX && cz == h.centerZ {
return nil
}
return h.streamAround(cx, cz)
}
// sendPlayLogin writes the clientbound play "login" packet. Field layout was
// confirmed against the 26.1.2 vanilla server capture.
func (h *handler) sendPlayLogin() error {
dimTypeIdx := registry.Index("minecraft:dimension_type", "minecraft:overworld")
if dimTypeIdx < 0 {
dimTypeIdx = 0
}
w := protocol.NewWriter(128)
w.Int32(1) // entity ID
w.Bool(false) // is hardcore
// Dimension names: the worlds available on this server.
dims := []string{"minecraft:overworld", "minecraft:the_end", "minecraft:the_nether"}
w.VarInt(int32(len(dims)))
for _, d := range dims {
w.String(d)
}
w.VarInt(int32(h.srv.Config().MaxPlayers)) // max players (legacy)
w.VarInt(10) // view distance
w.VarInt(10) // simulation distance
w.Bool(false) // reduced debug info
w.Bool(true) // enable respawn screen
w.Bool(false) // do limited crafting
w.VarInt(int32(dimTypeIdx)) // dimension type (registry index)
w.String("minecraft:overworld") // dimension name (this world)
w.Int64(0) // hashed seed
w.Byte(1) // game mode: creative (instant break, creative inventory)
w.Byte(0xFF) // previous game mode: -1 (none)
w.Bool(false) // is debug
w.Bool(false) // is flat
w.Bool(false) // has death location
w.VarInt(0) // portal cooldown
w.VarInt(63) // sea level (overworld)
w.Bool(false) // enforces secure chat
return h.conn.SendWriter(protocol.PlayLogin, w)
}
// sendGameEvent writes a game_event packet (event id + float value).
func (h *handler) sendGameEvent(event byte, value float32) error {
w := protocol.NewWriter(5)
w.Byte(event)
w.Float32(value)
return h.conn.SendWriter(protocol.PlayGameEvent, w)
}
// sendPlayerPosition teleports the player to spawn. The client must echo the
// teleport ID back via accept_teleportation.
func (h *handler) sendPlayerPosition(teleportID int32) error {
w := protocol.NewWriter(64)
w.VarInt(teleportID)
w.Float64(spawnX).Float64(spawnY).Float64(spawnZ) // position
w.Float64(0).Float64(0).Float64(0) // velocity
w.Float32(0) // yaw
w.Float32(0) // pitch
w.Int32(0) // relative flags
return h.conn.SendWriter(protocol.PlayPlayerPosition, w)
}
// keepAliveLoop sends a keep-alive every 15 seconds. It exits as soon as a send
// fails, which happens when the connection closes.
func (h *handler) keepAliveLoop() {
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()
for range ticker.C {
id := time.Now().UnixMilli()
w := protocol.NewWriter(8)
w.Int64(id)
if err := h.conn.SendWriter(protocol.PlayKeepAliveCB, w); err != nil {
return
}
}
}
// handlePlay dispatches serverbound play packets. Most are tolerated for now;
// teleport and keep-alive are acknowledged/logged.
func (h *handler) handlePlay(pkt protocol.Packet) error {
switch pkt.ID {
case protocol.PlayAcceptTeleport:
id, err := pkt.Body().VarInt()
if err != nil {
return err
}
h.log.Debug("teleport confirmed", "id", id)
return nil
case protocol.PlayKeepAliveServer:
// A response to our keep-alive; presence is enough for liveness.
h.log.Debug("keep-alive ack")
return nil
case protocol.PlayPlayerLoaded:
h.log.Info("player loaded into world", "name", h.conn.Profile.Name)
return nil
case protocol.PlayMovePos, protocol.PlayMovePosRot:
// Both packets begin with the absolute X, Y, Z position.
r := pkt.Body()
x, err := r.Float64()
if err != nil {
return err
}
if _, err := r.Float64(); err != nil { // feet Y, unused for streaming
return err
}
z, err := r.Float64()
if err != nil {
return err
}
return h.onPlayerMove(x, z)
case protocol.PlayPlayerAction:
return h.handlePlayerAction(pkt)
case protocol.PlayChatMessage:
return h.handleChat(pkt)
case protocol.PlayUseItemOn:
return h.handleUseItemOn(pkt)
case protocol.PlaySetCarriedItem:
slot, err := pkt.Body().Uint16()
if err != nil {
return err
}
if slot < 9 {
h.heldSlot = int32(slot)
}
return nil
case protocol.PlaySetCreativeSlot:
return h.handleCreativeSlot(pkt)
default:
h.log.Debug("play packet ignored", "id", pkt.ID)
return nil
}
}
// handleChat reads a chat message (only the leading text field is needed) and
// echoes it to the player as a system message prefixed with their name. Once a
// player registry exists this will broadcast to everyone.
func (h *handler) handleChat(pkt protocol.Packet) error {
msg, err := pkt.Body().String()
if err != nil {
return err
}
line := "<" + h.conn.Profile.Name + "> " + msg
h.log.Info("chat", "msg", line)
return h.sendSystemChat(line)
}
// sendSystemChat sends a plain-text system chat message. The text component is
// network NBT; a bare string tag is the shorthand for {"text": ...}.
func (h *handler) sendSystemChat(text string) error {
w := protocol.NewWriter(len(text) + 8)
w.Raw(nbt.Marshal(nbt.String(text)))
w.Bool(false) // not an action-bar overlay
return h.conn.SendWriter(protocol.PlaySystemChat, w)
}
// handlePlayerAction processes digging. In creative the client sends
// START_DESTROY_BLOCK (status 0) for an instant break; survival also sends
// STOP/FINISH (status 2). Either way we clear the block, push a block_update,
// and acknowledge the sequence so the client keeps its predicted change.
func (h *handler) handlePlayerAction(pkt protocol.Packet) error {
r := pkt.Body()
status, err := r.VarInt()
if err != nil {
return err
}
x, y, z, err := r.Position()
if err != nil {
return err
}
if _, err := r.ReadByte(); err != nil { // face
return err
}
seq, err := r.VarInt()
if err != nil {
return err
}
const startDig, finishDig = 0, 2
if status == startDig || status == finishDig {
if h.srv.Chunks().SetBlock(x, y, z, world.StateAir) {
if err := h.sendBlockUpdate(x, y, z, world.StateAir); err != nil {
return err
}
h.log.Debug("block broken", "x", x, "y", y, "z", z)
}
}
return h.sendBlockChangedAck(seq)
}
// hotbarInvStart is the inventory slot index of hotbar slot 0.
const hotbarInvStart = 36
// handleCreativeSlot records the item a creative player placed into a slot so we
// know what block to place. The packet is: Short slot, then an item stack
// (VarInt count; if non-empty, VarInt item id followed by components we ignore).
func (h *handler) handleCreativeSlot(pkt protocol.Packet) error {
r := pkt.Body()
slot, err := r.Uint16()
if err != nil {
return err
}
hotbarIdx := int(slot) - hotbarInvStart
if hotbarIdx < 0 || hotbarIdx >= len(h.hotbar) {
return nil // not a hotbar slot; ignored
}
count, err := r.VarInt()
if err != nil {
return err
}
if count <= 0 {
h.hotbar[hotbarIdx] = -1 // emptied
return nil
}
itemID, err := r.VarInt()
if err != nil {
return err
}
h.hotbar[hotbarIdx] = itemID // remaining component data is not needed
return nil
}
// faceOffsets maps a Direction (block face) to the unit offset of the block
// placed against it: DOWN, UP, NORTH, SOUTH, WEST, EAST.
var faceOffsets = [6][3]int{
{0, -1, 0}, {0, 1, 0}, {0, 0, -1}, {0, 0, 1}, {-1, 0, 0}, {1, 0, 0},
}
// handleUseItemOn places the held block against the clicked face. Layout
// (captured from the client): Hand, Position, Face, cursor XYZ floats,
// insideBlock bool, worldBorderHit bool, sequence.
func (h *handler) handleUseItemOn(pkt protocol.Packet) error {
r := pkt.Body()
if _, err := r.VarInt(); err != nil { // hand
return err
}
x, y, z, err := r.Position()
if err != nil {
return err
}
face, err := r.VarInt()
if err != nil {
return err
}
// Skip cursor (3 floats) + insideBlock + worldBorderHit, then read sequence.
for i := 0; i < 3; i++ {
if _, err := r.Float32(); err != nil {
return err
}
}
if _, err := r.Bool(); err != nil {
return err
}
if _, err := r.Bool(); err != nil {
return err
}
seq, err := r.VarInt()
if err != nil {
return err
}
if face >= 0 && int(face) < len(faceOffsets) {
if state, ok := h.heldBlock(); ok {
off := faceOffsets[face]
px, py, pz := x+off[0], y+off[1], z+off[2]
if h.srv.Chunks().SetBlock(px, py, pz, state) {
if err := h.sendBlockUpdate(px, py, pz, state); err != nil {
return err
}
h.log.Debug("block placed", "x", px, "y", py, "z", pz, "state", state)
}
}
}
return h.sendBlockChangedAck(seq)
}
// heldBlock returns the block state for the currently held item, if it is a
// placeable block.
func (h *handler) heldBlock() (uint16, bool) {
itemID := h.hotbar[h.heldSlot]
if itemID < 0 {
return 0, false
}
return world.ItemToBlock(itemID)
}
// sendBlockUpdate notifies the client of a single block change.
func (h *handler) sendBlockUpdate(x, y, z int, state uint16) error {
w := protocol.NewWriter(12)
w.Position(x, y, z)
w.VarInt(int32(state))
return h.conn.SendWriter(protocol.PlayBlockUpdate, w)
}
// sendBlockChangedAck confirms a block-action sequence so the client does not
// roll back its predicted change.
func (h *handler) sendBlockChangedAck(sequence int32) error {
w := protocol.NewWriter(4)
w.VarInt(sequence)
return h.conn.SendWriter(protocol.PlayBlockChangedAck, w)
}

228
internal/protocol/buffer.go Normal file
View file

@ -0,0 +1,228 @@
package protocol
import (
"encoding/binary"
"errors"
"io"
"math"
)
// ErrShortBuffer is returned when a read would exceed the buffer's contents.
var ErrShortBuffer = errors.New("protocol: unexpected end of buffer")
// Reader decodes typed protocol values from an in-memory packet body.
// It tracks a cursor and never reads past the underlying slice.
type Reader struct {
buf []byte
pos int
}
// NewReader returns a Reader over buf. The slice is not copied.
func NewReader(buf []byte) *Reader { return &Reader{buf: buf} }
// Remaining returns the number of unread bytes.
func (r *Reader) Remaining() int { return len(r.buf) - r.pos }
// ReadByte implements io.ByteReader so VarInt helpers can consume the Reader.
func (r *Reader) ReadByte() (byte, error) {
if r.pos >= len(r.buf) {
return 0, ErrShortBuffer
}
b := r.buf[r.pos]
r.pos++
return b, nil
}
// readN returns the next n bytes as a sub-slice of the underlying buffer.
func (r *Reader) readN(n int) ([]byte, error) {
if n < 0 || r.Remaining() < n {
return nil, ErrShortBuffer
}
b := r.buf[r.pos : r.pos+n]
r.pos += n
return b, nil
}
// VarInt reads a 32-bit VarInt.
func (r *Reader) VarInt() (int32, error) {
v, _, err := ReadVarInt(r)
return v, err
}
// VarLong reads a 64-bit VarLong.
func (r *Reader) VarLong() (int64, error) {
v, _, err := ReadVarLong(r)
return v, err
}
// Bool reads a single-byte boolean.
func (r *Reader) Bool() (bool, error) {
b, err := r.ReadByte()
return b != 0, err
}
// Uint16 reads a big-endian unsigned short.
func (r *Reader) Uint16() (uint16, error) {
b, err := r.readN(2)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint16(b), nil
}
// Int64 reads a big-endian signed long.
func (r *Reader) Int64() (int64, error) {
b, err := r.readN(8)
if err != nil {
return 0, err
}
return int64(binary.BigEndian.Uint64(b)), nil
}
// Float64 reads a big-endian IEEE-754 double.
func (r *Reader) Float64() (float64, error) {
b, err := r.readN(8)
if err != nil {
return 0, err
}
return math.Float64frombits(binary.BigEndian.Uint64(b)), nil
}
// Float32 reads a big-endian IEEE-754 float.
func (r *Reader) Float32() (float32, error) {
b, err := r.readN(4)
if err != nil {
return 0, err
}
return math.Float32frombits(binary.BigEndian.Uint32(b)), nil
}
// String reads a VarInt-length-prefixed UTF-8 string.
func (r *Reader) String() (string, error) {
n, err := r.VarInt()
if err != nil {
return "", err
}
if n < 0 || int(n) > MaxStringLen*3 {
return "", ErrStringTooLong
}
b, err := r.readN(int(n))
if err != nil {
return "", err
}
return string(b), nil
}
// Position reads a block position packed into a single long (x:26, z:26, y:12).
func (r *Reader) Position() (x, y, z int, err error) {
v, err := r.Int64()
if err != nil {
return 0, 0, 0, err
}
x = int(v >> 38) // top 26 bits, sign-extended
y = int(v << 52 >> 52) // low 12 bits, sign-extended
z = int(v << 26 >> 38) // middle 26 bits, sign-extended
return x, y, z, nil
}
// UUID reads a 128-bit UUID as two big-endian longs (16 bytes).
func (r *Reader) UUID() ([16]byte, error) {
var u [16]byte
b, err := r.readN(16)
if err != nil {
return u, err
}
copy(u[:], b)
return u, nil
}
// Writer accumulates typed protocol values into a byte buffer that becomes a
// packet body. The zero value is ready to use.
type Writer struct {
buf []byte
}
// NewWriter returns a Writer with an optional initial capacity hint.
func NewWriter(capacity int) *Writer {
return &Writer{buf: make([]byte, 0, capacity)}
}
// Bytes returns the accumulated body. The slice aliases internal storage.
func (w *Writer) Bytes() []byte { return w.buf }
// Len returns the current body length.
func (w *Writer) Len() int { return len(w.buf) }
// VarInt appends a 32-bit VarInt.
func (w *Writer) VarInt(v int32) *Writer { w.buf = AppendVarInt(w.buf, v); return w }
// VarLong appends a 64-bit VarLong.
func (w *Writer) VarLong(v int64) *Writer { w.buf = AppendVarLong(w.buf, v); return w }
// Bool appends a single-byte boolean.
func (w *Writer) Bool(v bool) *Writer {
if v {
w.buf = append(w.buf, 1)
} else {
w.buf = append(w.buf, 0)
}
return w
}
// Byte appends a raw byte.
func (w *Writer) Byte(v byte) *Writer { w.buf = append(w.buf, v); return w }
// Uint16 appends a big-endian unsigned short.
func (w *Writer) Uint16(v uint16) *Writer {
w.buf = binary.BigEndian.AppendUint16(w.buf, v)
return w
}
// Int32 appends a big-endian signed int.
func (w *Writer) Int32(v int32) *Writer {
w.buf = binary.BigEndian.AppendUint32(w.buf, uint32(v))
return w
}
// Int64 appends a big-endian signed long.
func (w *Writer) Int64(v int64) *Writer {
w.buf = binary.BigEndian.AppendUint64(w.buf, uint64(v))
return w
}
// Float32 appends a big-endian IEEE-754 float.
func (w *Writer) Float32(v float32) *Writer {
w.buf = binary.BigEndian.AppendUint32(w.buf, math.Float32bits(v))
return w
}
// Float64 appends a big-endian IEEE-754 double.
func (w *Writer) Float64(v float64) *Writer {
w.buf = binary.BigEndian.AppendUint64(w.buf, math.Float64bits(v))
return w
}
// String appends a VarInt-length-prefixed UTF-8 string.
func (w *Writer) String(s string) *Writer {
w.buf = AppendVarInt(w.buf, int32(len(s)))
w.buf = append(w.buf, s...)
return w
}
// Position appends a block position packed into a single long (x:26, z:26, y:12).
func (w *Writer) Position(x, y, z int) *Writer {
v := (int64(x)&0x3FFFFFF)<<38 | (int64(z)&0x3FFFFFF)<<12 | (int64(y) & 0xFFF)
return w.Int64(v)
}
// UUID appends a 128-bit UUID verbatim (16 bytes).
func (w *Writer) UUID(u [16]byte) *Writer { w.buf = append(w.buf, u[:]...); return w }
// Raw appends bytes verbatim.
func (w *Writer) Raw(b []byte) *Writer { w.buf = append(w.buf, b...); return w }
// WriteTo writes the accumulated body to dst.
func (w *Writer) WriteTo(dst io.Writer) (int64, error) {
n, err := dst.Write(w.buf)
return int64(n), err
}

139
internal/protocol/frame.go Normal file
View file

@ -0,0 +1,139 @@
package protocol
import (
"bufio"
"bytes"
"compress/zlib"
"io"
)
// Packet is a decoded frame: a packet ID plus its raw body bytes.
// The body excludes the ID and any length/compression prefixes.
type Packet struct {
ID int32
Data []byte
}
// Body returns a Reader positioned at the start of the packet body.
func (p Packet) Body() *Reader { return NewReader(p.Data) }
// ReadPacket reads one frame from br.
//
// When threshold < 0 the uncompressed format is used:
//
// VarInt length | VarInt packet ID | body
//
// When threshold >= 0 the compressed format is used:
//
// VarInt packet length | VarInt data length | (zlib or raw) packet ID + body
//
// A data length of 0 means the payload is stored uncompressed (its
// uncompressed size was below the threshold).
func ReadPacket(br *bufio.Reader, threshold int32) (Packet, error) {
length, _, err := ReadVarInt(br)
if err != nil {
return Packet{}, err
}
if length < 0 || int(length) > MaxPacketSize {
return Packet{}, ErrPacketTooLarge
}
frame := make([]byte, length)
if _, err := io.ReadFull(br, frame); err != nil {
return Packet{}, err
}
if threshold < 0 {
return parseIDBody(frame)
}
return parseCompressed(frame)
}
// parseCompressed handles a frame that begins with a Data Length VarInt.
func parseCompressed(frame []byte) (Packet, error) {
r := NewReader(frame)
dataLen, err := r.VarInt()
if err != nil {
return Packet{}, err
}
payload := frame[r.pos:]
if dataLen == 0 {
// Stored uncompressed.
return parseIDBody(payload)
}
if dataLen < 0 || int(dataLen) > MaxPacketSize {
return Packet{}, ErrPacketTooLarge
}
zr, err := zlib.NewReader(bytes.NewReader(payload))
if err != nil {
return Packet{}, err
}
defer zr.Close()
out := make([]byte, dataLen)
if _, err := io.ReadFull(zr, out); err != nil {
return Packet{}, err
}
return parseIDBody(out)
}
// parseIDBody splits a VarInt packet ID off the front of buf.
func parseIDBody(buf []byte) (Packet, error) {
r := NewReader(buf)
id, err := r.VarInt()
if err != nil {
return Packet{}, err
}
return Packet{ID: id, Data: buf[r.pos:]}, nil
}
// WritePacket writes one frame to w with the given ID and body, using the
// uncompressed format when threshold < 0 and the compressed format otherwise.
func WritePacket(w io.Writer, threshold int32, id int32, body []byte) error {
_, err := w.Write(AppendPacket(nil, threshold, id, body))
return err
}
// AppendPacket appends one fully-framed packet to dst and returns the result.
// The produced bytes are identical to what WritePacket would write, so callers
// may cache them and replay via a raw write.
func AppendPacket(dst []byte, threshold int32, id int32, body []byte) []byte {
if threshold < 0 {
return appendUncompressed(dst, id, body)
}
return appendCompressed(dst, threshold, id, body)
}
func appendUncompressed(dst []byte, id int32, body []byte) []byte {
total := VarIntLen(id) + len(body)
dst = AppendVarInt(dst, int32(total))
dst = AppendVarInt(dst, id)
return append(dst, body...)
}
func appendCompressed(dst []byte, threshold int32, id int32, body []byte) []byte {
// raw = packet ID + body, the unit that compression applies to.
raw := make([]byte, 0, VarIntLen(id)+len(body))
raw = AppendVarInt(raw, id)
raw = append(raw, body...)
var payload []byte
if len(raw) >= int(threshold) {
var buf bytes.Buffer
zw := zlib.NewWriter(&buf)
zw.Write(raw)
zw.Close()
// Data Length = uncompressed size, then the compressed bytes.
payload = AppendVarInt(make([]byte, 0, VarIntLen(int32(len(raw)))+buf.Len()), int32(len(raw)))
payload = append(payload, buf.Bytes()...)
} else {
// Below threshold: Data Length = 0, raw stored verbatim.
payload = AppendVarInt(make([]byte, 0, 1+len(raw)), 0)
payload = append(payload, raw...)
}
dst = AppendVarInt(dst, int32(len(payload)))
return append(dst, payload...)
}

114
internal/protocol/ids.go Normal file
View file

@ -0,0 +1,114 @@
package protocol
// Packet IDs, grouped by state and direction. Serverbound = client→server,
// Clientbound = server→client. Values are for protocol 775 (26.1.2).
// Handshaking, serverbound.
const (
HandshakeID = 0x00
)
// Status, serverbound.
const (
StatusRequestID = 0x00
PingRequestID = 0x01
)
// Status, clientbound.
const (
StatusResponseID = 0x00
PongResponseID = 0x01
)
// Login, serverbound.
const (
LoginStartID = 0x00
EncryptionResponse = 0x01
LoginPluginResponse = 0x02
LoginAcknowledgedID = 0x03
CookieResponseLogin = 0x04
)
// Login, clientbound.
const (
LoginDisconnectID = 0x00
EncryptionRequest = 0x01
LoginSuccessID = 0x02
SetCompressionID = 0x03
LoginPluginReq = 0x04
CookieRequestLogin = 0x05
)
// Configuration, serverbound.
const (
ConfigClientInformation = 0x00
ConfigCookieResponse = 0x01
ConfigCustomPayload = 0x02
ConfigFinishServerbound = 0x03
ConfigKeepAliveServer = 0x04
ConfigPong = 0x05
ConfigResourcePackResp = 0x06
ConfigKnownPacksServer = 0x07
)
// Configuration, clientbound.
const (
ConfigCookieRequest = 0x00
ConfigCustomPayloadCB = 0x01
ConfigDisconnect = 0x02
ConfigFinishClientbound = 0x03
ConfigKeepAliveCB = 0x04
ConfigPing = 0x05
ConfigRegistryData = 0x07
ConfigUpdateEnabledFeatures = 0x0c
ConfigUpdateTags = 0x0d
ConfigKnownPacksCB = 0x0e
)
// Play, clientbound (protocol 775).
const (
PlayLogin = 0x31
PlayGameEvent = 0x26
PlayKeepAliveCB = 0x2c
PlayPlayerPosition = 0x48
PlayDefaultSpawnPos = 0x61
PlayChunkCacheCenter = 0x5e
PlayLevelChunk = 0x2d
PlayAbilities = 0x40
PlaySetHeldSlot = 0x69
PlayDisconnect = 0x20
PlayBlockUpdate = 0x08
PlayBlockChangedAck = 0x04
PlaySystemChat = 0x79
)
// Play, serverbound (protocol 775).
const (
PlayAcceptTeleport = 0x00
PlayKeepAliveServer = 0x1c
PlayClientTickEnd = 0x0d
PlayClientInformation = 0x0e
PlayCustomPayload = 0x16
PlayMovePos = 0x1e
PlayMovePosRot = 0x1f
PlayMoveRot = 0x20
PlayMoveStatusOnly = 0x21
PlayPlayerLoaded = 0x2c
PlayPlayerAction = 0x29
PlayUseItemOn = 0x42
PlaySetCreativeSlot = 0x38
PlaySetCarriedItem = 0x35
PlayChatMessage = 0x09
)
// GameEvent sub-IDs carried by the clientbound game_event packet.
const (
GameEventStartWaitingChunks = 13
)
// NextState values carried by the handshake packet.
const (
NextStateStatus = 1
NextStateLogin = 2
NextStateTransfer = 3
)

137
internal/protocol/types.go Normal file
View file

@ -0,0 +1,137 @@
// Package protocol implements the wire-level primitives of the Minecraft
// Java Edition protocol (version 26.1.2, protocol 775).
//
// All multi-byte numeric fields are big-endian. Length-prefixed and
// frequently-used integers use the LEB128-style VarInt/VarLong encoding.
package protocol
import (
"errors"
"io"
)
// Protocol constants for the targeted Minecraft version.
const (
// ProtocolVersion is the handshake protocol number for 26.1.2.
ProtocolVersion = 775
// GameVersion is the human-readable version string.
GameVersion = "26.1.2"
)
// State is a connection state as negotiated during the handshake. The numeric
// values of Status/Login match the "next state" field of the handshake packet.
type State int
const (
StateHandshaking State = iota
StateStatus
StateLogin
StateConfiguration
StatePlay
)
func (s State) String() string {
switch s {
case StateHandshaking:
return "handshaking"
case StateStatus:
return "status"
case StateLogin:
return "login"
case StateConfiguration:
return "configuration"
case StatePlay:
return "play"
default:
return "unknown"
}
}
// Protocol limits guarding against malicious or malformed input.
const (
// MaxVarIntLen is the maximum number of bytes a 32-bit VarInt may occupy.
MaxVarIntLen = 5
// MaxVarLongLen is the maximum number of bytes a 64-bit VarLong may occupy.
MaxVarLongLen = 10
// MaxStringLen bounds decoded strings to the protocol's 32767-char limit.
MaxStringLen = 32767
// MaxPacketSize bounds a single uncompressed packet body.
MaxPacketSize = 2 * 1024 * 1024
)
var (
// ErrVarIntTooBig is returned when a VarInt/VarLong exceeds its byte limit.
ErrVarIntTooBig = errors.New("protocol: varint is too big")
// ErrStringTooLong is returned when a string exceeds MaxStringLen.
ErrStringTooLong = errors.New("protocol: string too long")
// ErrPacketTooLarge is returned when a packet length exceeds MaxPacketSize.
ErrPacketTooLarge = errors.New("protocol: packet too large")
)
// ReadVarInt reads a 32-bit VarInt from r, returning the value and the number
// of bytes consumed.
func ReadVarInt(r io.ByteReader) (value int32, n int, err error) {
var result uint32
for i := 0; i < MaxVarIntLen; i++ {
b, e := r.ReadByte()
if e != nil {
return 0, i, e
}
result |= uint32(b&0x7F) << (7 * i)
if b&0x80 == 0 {
return int32(result), i + 1, nil
}
}
return 0, MaxVarIntLen, ErrVarIntTooBig
}
// ReadVarLong reads a 64-bit VarLong from r.
func ReadVarLong(r io.ByteReader) (value int64, n int, err error) {
var result uint64
for i := 0; i < MaxVarLongLen; i++ {
b, e := r.ReadByte()
if e != nil {
return 0, i, e
}
result |= uint64(b&0x7F) << (7 * i)
if b&0x80 == 0 {
return int64(result), i + 1, nil
}
}
return 0, MaxVarLongLen, ErrVarIntTooBig
}
// AppendVarInt encodes v as a VarInt and appends it to dst.
func AppendVarInt(dst []byte, v int32) []byte {
u := uint32(v)
for {
if u&^0x7F == 0 {
return append(dst, byte(u))
}
dst = append(dst, byte(u&0x7F)|0x80)
u >>= 7
}
}
// AppendVarLong encodes v as a VarLong and appends it to dst.
func AppendVarLong(dst []byte, v int64) []byte {
u := uint64(v)
for {
if u&^uint64(0x7F) == 0 {
return append(dst, byte(u))
}
dst = append(dst, byte(u&0x7F)|0x80)
u >>= 7
}
}
// VarIntLen returns the number of bytes the VarInt encoding of v occupies.
func VarIntLen(v int32) int {
u := uint32(v)
n := 1
for u&^0x7F != 0 {
u >>= 7
n++
}
return n
}

View file

@ -0,0 +1,79 @@
// Package registry holds the set of synchronized registries the server sends
// to the client during the configuration phase.
//
// The data in synced_registries.json was captured verbatim from the official
// 26.1.2 server: 28 registries in exact send order, each entry flagged
// has_data=false. Because we advertise the same "minecraft:core" known pack
// that a matching client already has, the client fills in each entry's data
// from its built-in pack, so we transmit only the entry identifiers. Entry
// order is significant: it defines the network (numeric) IDs used later in the
// play phase (biome IDs in chunks, dimension-type indices, and so on).
package registry
import (
_ "embed"
"encoding/json"
"fmt"
)
//go:embed synced_registries.json
var syncedJSON []byte
// syncedTags is the verbatim body of the vanilla 26.1.2 update_tags packet,
// captured from the official server. Tags reference registry entries by their
// numeric (network) index, so this blob is only valid because our synchronized
// registries are sent in the exact same order vanilla uses.
//
//go:embed synced_tags.bin
var syncedTags []byte
// Tags returns the update_tags packet body. The slice is shared and must not
// be mutated.
func Tags() []byte { return syncedTags }
// Registry is one synchronized registry and its ordered entry identifiers.
type Registry struct {
Name string `json:"name"`
Entries []string `json:"entries"`
}
// synced is the parsed, ordered list loaded once at init.
var synced []Registry
func init() {
if err := json.Unmarshal(syncedJSON, &synced); err != nil {
panic(fmt.Sprintf("registry: parsing embedded synced_registries.json: %v", err))
}
}
// Synced returns the ordered synchronized registries. The slice is shared and
// must not be mutated.
func Synced() []Registry { return synced }
// Index returns the zero-based position of entry within the named registry,
// which is the numeric (network) ID the client assigns it. It returns -1 if
// the registry or entry is unknown.
func Index(registryName, entry string) int {
for _, reg := range synced {
if reg.Name != registryName {
continue
}
for i, e := range reg.Entries {
if e == entry {
return i
}
}
}
return -1
}
// KnownPack identifies a resource/data pack advertised via select_known_packs.
type KnownPack struct {
Namespace string
ID string
Version string
}
// CorePack is the vanilla built-in pack. Advertising it lets a matching client
// supply registry contents from its own copy.
var CorePack = KnownPack{Namespace: "minecraft", ID: "core", Version: "26.1.2"}

View file

@ -0,0 +1,524 @@
[
{
"name": "minecraft:worldgen/biome",
"entries": [
"minecraft:badlands",
"minecraft:bamboo_jungle",
"minecraft:basalt_deltas",
"minecraft:beach",
"minecraft:birch_forest",
"minecraft:cherry_grove",
"minecraft:cold_ocean",
"minecraft:crimson_forest",
"minecraft:dark_forest",
"minecraft:deep_cold_ocean",
"minecraft:deep_dark",
"minecraft:deep_frozen_ocean",
"minecraft:deep_lukewarm_ocean",
"minecraft:deep_ocean",
"minecraft:desert",
"minecraft:dripstone_caves",
"minecraft:end_barrens",
"minecraft:end_highlands",
"minecraft:end_midlands",
"minecraft:eroded_badlands",
"minecraft:flower_forest",
"minecraft:forest",
"minecraft:frozen_ocean",
"minecraft:frozen_peaks",
"minecraft:frozen_river",
"minecraft:grove",
"minecraft:ice_spikes",
"minecraft:jagged_peaks",
"minecraft:jungle",
"minecraft:lukewarm_ocean",
"minecraft:lush_caves",
"minecraft:mangrove_swamp",
"minecraft:meadow",
"minecraft:mushroom_fields",
"minecraft:nether_wastes",
"minecraft:ocean",
"minecraft:old_growth_birch_forest",
"minecraft:old_growth_pine_taiga",
"minecraft:old_growth_spruce_taiga",
"minecraft:pale_garden",
"minecraft:plains",
"minecraft:river",
"minecraft:savanna",
"minecraft:savanna_plateau",
"minecraft:small_end_islands",
"minecraft:snowy_beach",
"minecraft:snowy_plains",
"minecraft:snowy_slopes",
"minecraft:snowy_taiga",
"minecraft:soul_sand_valley",
"minecraft:sparse_jungle",
"minecraft:stony_peaks",
"minecraft:stony_shore",
"minecraft:sunflower_plains",
"minecraft:swamp",
"minecraft:taiga",
"minecraft:the_end",
"minecraft:the_void",
"minecraft:warm_ocean",
"minecraft:warped_forest",
"minecraft:windswept_forest",
"minecraft:windswept_gravelly_hills",
"minecraft:windswept_hills",
"minecraft:windswept_savanna",
"minecraft:wooded_badlands"
]
},
{
"name": "minecraft:chat_type",
"entries": [
"minecraft:chat",
"minecraft:emote_command",
"minecraft:msg_command_incoming",
"minecraft:msg_command_outgoing",
"minecraft:say_command",
"minecraft:team_msg_command_incoming",
"minecraft:team_msg_command_outgoing"
]
},
{
"name": "minecraft:trim_pattern",
"entries": [
"minecraft:bolt",
"minecraft:coast",
"minecraft:dune",
"minecraft:eye",
"minecraft:flow",
"minecraft:host",
"minecraft:raiser",
"minecraft:rib",
"minecraft:sentry",
"minecraft:shaper",
"minecraft:silence",
"minecraft:snout",
"minecraft:spire",
"minecraft:tide",
"minecraft:vex",
"minecraft:ward",
"minecraft:wayfinder",
"minecraft:wild"
]
},
{
"name": "minecraft:trim_material",
"entries": [
"minecraft:amethyst",
"minecraft:copper",
"minecraft:diamond",
"minecraft:emerald",
"minecraft:gold",
"minecraft:iron",
"minecraft:lapis",
"minecraft:netherite",
"minecraft:quartz",
"minecraft:redstone",
"minecraft:resin"
]
},
{
"name": "minecraft:wolf_variant",
"entries": [
"minecraft:ashen",
"minecraft:black",
"minecraft:chestnut",
"minecraft:pale",
"minecraft:rusty",
"minecraft:snowy",
"minecraft:spotted",
"minecraft:striped",
"minecraft:woods"
]
},
{
"name": "minecraft:wolf_sound_variant",
"entries": [
"minecraft:angry",
"minecraft:big",
"minecraft:classic",
"minecraft:cute",
"minecraft:grumpy",
"minecraft:puglin",
"minecraft:sad"
]
},
{
"name": "minecraft:pig_variant",
"entries": [
"minecraft:cold",
"minecraft:temperate",
"minecraft:warm"
]
},
{
"name": "minecraft:pig_sound_variant",
"entries": [
"minecraft:big",
"minecraft:classic",
"minecraft:mini"
]
},
{
"name": "minecraft:frog_variant",
"entries": [
"minecraft:cold",
"minecraft:temperate",
"minecraft:warm"
]
},
{
"name": "minecraft:cat_variant",
"entries": [
"minecraft:all_black",
"minecraft:black",
"minecraft:british_shorthair",
"minecraft:calico",
"minecraft:jellie",
"minecraft:persian",
"minecraft:ragdoll",
"minecraft:red",
"minecraft:siamese",
"minecraft:tabby",
"minecraft:white"
]
},
{
"name": "minecraft:cat_sound_variant",
"entries": [
"minecraft:classic",
"minecraft:royal"
]
},
{
"name": "minecraft:cow_sound_variant",
"entries": [
"minecraft:classic",
"minecraft:moody"
]
},
{
"name": "minecraft:cow_variant",
"entries": [
"minecraft:cold",
"minecraft:temperate",
"minecraft:warm"
]
},
{
"name": "minecraft:chicken_sound_variant",
"entries": [
"minecraft:classic",
"minecraft:picky"
]
},
{
"name": "minecraft:chicken_variant",
"entries": [
"minecraft:cold",
"minecraft:temperate",
"minecraft:warm"
]
},
{
"name": "minecraft:zombie_nautilus_variant",
"entries": [
"minecraft:temperate",
"minecraft:warm"
]
},
{
"name": "minecraft:painting_variant",
"entries": [
"minecraft:alban",
"minecraft:aztec",
"minecraft:aztec2",
"minecraft:backyard",
"minecraft:baroque",
"minecraft:bomb",
"minecraft:bouquet",
"minecraft:burning_skull",
"minecraft:bust",
"minecraft:cavebird",
"minecraft:changing",
"minecraft:cotan",
"minecraft:courbet",
"minecraft:creebet",
"minecraft:dennis",
"minecraft:donkey_kong",
"minecraft:earth",
"minecraft:endboss",
"minecraft:fern",
"minecraft:fighters",
"minecraft:finding",
"minecraft:fire",
"minecraft:graham",
"minecraft:humble",
"minecraft:kebab",
"minecraft:lowmist",
"minecraft:match",
"minecraft:meditative",
"minecraft:orb",
"minecraft:owlemons",
"minecraft:passage",
"minecraft:pigscene",
"minecraft:plant",
"minecraft:pointer",
"minecraft:pond",
"minecraft:pool",
"minecraft:prairie_ride",
"minecraft:sea",
"minecraft:skeleton",
"minecraft:skull_and_roses",
"minecraft:stage",
"minecraft:sunflowers",
"minecraft:sunset",
"minecraft:tides",
"minecraft:unpacked",
"minecraft:void",
"minecraft:wanderer",
"minecraft:wasteland",
"minecraft:water",
"minecraft:wind",
"minecraft:wither"
]
},
{
"name": "minecraft:dimension_type",
"entries": [
"minecraft:overworld",
"minecraft:overworld_caves",
"minecraft:the_end",
"minecraft:the_nether"
]
},
{
"name": "minecraft:damage_type",
"entries": [
"minecraft:arrow",
"minecraft:bad_respawn_point",
"minecraft:cactus",
"minecraft:campfire",
"minecraft:cramming",
"minecraft:dragon_breath",
"minecraft:drown",
"minecraft:dry_out",
"minecraft:ender_pearl",
"minecraft:explosion",
"minecraft:fall",
"minecraft:falling_anvil",
"minecraft:falling_block",
"minecraft:falling_stalactite",
"minecraft:fireball",
"minecraft:fireworks",
"minecraft:fly_into_wall",
"minecraft:freeze",
"minecraft:generic",
"minecraft:generic_kill",
"minecraft:hot_floor",
"minecraft:in_fire",
"minecraft:in_wall",
"minecraft:indirect_magic",
"minecraft:lava",
"minecraft:lightning_bolt",
"minecraft:mace_smash",
"minecraft:magic",
"minecraft:mob_attack",
"minecraft:mob_attack_no_aggro",
"minecraft:mob_projectile",
"minecraft:on_fire",
"minecraft:out_of_world",
"minecraft:outside_border",
"minecraft:player_attack",
"minecraft:player_explosion",
"minecraft:sonic_boom",
"minecraft:spear",
"minecraft:spit",
"minecraft:stalagmite",
"minecraft:starve",
"minecraft:sting",
"minecraft:sweet_berry_bush",
"minecraft:thorns",
"minecraft:thrown",
"minecraft:trident",
"minecraft:unattributed_fireball",
"minecraft:wind_charge",
"minecraft:wither",
"minecraft:wither_skull"
]
},
{
"name": "minecraft:banner_pattern",
"entries": [
"minecraft:base",
"minecraft:border",
"minecraft:bricks",
"minecraft:circle",
"minecraft:creeper",
"minecraft:cross",
"minecraft:curly_border",
"minecraft:diagonal_left",
"minecraft:diagonal_right",
"minecraft:diagonal_up_left",
"minecraft:diagonal_up_right",
"minecraft:flow",
"minecraft:flower",
"minecraft:globe",
"minecraft:gradient",
"minecraft:gradient_up",
"minecraft:guster",
"minecraft:half_horizontal",
"minecraft:half_horizontal_bottom",
"minecraft:half_vertical",
"minecraft:half_vertical_right",
"minecraft:mojang",
"minecraft:piglin",
"minecraft:rhombus",
"minecraft:skull",
"minecraft:small_stripes",
"minecraft:square_bottom_left",
"minecraft:square_bottom_right",
"minecraft:square_top_left",
"minecraft:square_top_right",
"minecraft:straight_cross",
"minecraft:stripe_bottom",
"minecraft:stripe_center",
"minecraft:stripe_downleft",
"minecraft:stripe_downright",
"minecraft:stripe_left",
"minecraft:stripe_middle",
"minecraft:stripe_right",
"minecraft:stripe_top",
"minecraft:triangle_bottom",
"minecraft:triangle_top",
"minecraft:triangles_bottom",
"minecraft:triangles_top"
]
},
{
"name": "minecraft:enchantment",
"entries": [
"minecraft:aqua_affinity",
"minecraft:bane_of_arthropods",
"minecraft:binding_curse",
"minecraft:blast_protection",
"minecraft:breach",
"minecraft:channeling",
"minecraft:density",
"minecraft:depth_strider",
"minecraft:efficiency",
"minecraft:feather_falling",
"minecraft:fire_aspect",
"minecraft:fire_protection",
"minecraft:flame",
"minecraft:fortune",
"minecraft:frost_walker",
"minecraft:impaling",
"minecraft:infinity",
"minecraft:knockback",
"minecraft:looting",
"minecraft:loyalty",
"minecraft:luck_of_the_sea",
"minecraft:lunge",
"minecraft:lure",
"minecraft:mending",
"minecraft:multishot",
"minecraft:piercing",
"minecraft:power",
"minecraft:projectile_protection",
"minecraft:protection",
"minecraft:punch",
"minecraft:quick_charge",
"minecraft:respiration",
"minecraft:riptide",
"minecraft:sharpness",
"minecraft:silk_touch",
"minecraft:smite",
"minecraft:soul_speed",
"minecraft:sweeping_edge",
"minecraft:swift_sneak",
"minecraft:thorns",
"minecraft:unbreaking",
"minecraft:vanishing_curse",
"minecraft:wind_burst"
]
},
{
"name": "minecraft:jukebox_song",
"entries": [
"minecraft:11",
"minecraft:13",
"minecraft:5",
"minecraft:blocks",
"minecraft:cat",
"minecraft:chirp",
"minecraft:creator",
"minecraft:creator_music_box",
"minecraft:far",
"minecraft:lava_chicken",
"minecraft:mall",
"minecraft:mellohi",
"minecraft:otherside",
"minecraft:pigstep",
"minecraft:precipice",
"minecraft:relic",
"minecraft:stal",
"minecraft:strad",
"minecraft:tears",
"minecraft:wait",
"minecraft:ward"
]
},
{
"name": "minecraft:instrument",
"entries": [
"minecraft:admire_goat_horn",
"minecraft:call_goat_horn",
"minecraft:dream_goat_horn",
"minecraft:feel_goat_horn",
"minecraft:ponder_goat_horn",
"minecraft:seek_goat_horn",
"minecraft:sing_goat_horn",
"minecraft:yearn_goat_horn"
]
},
{
"name": "minecraft:test_environment",
"entries": [
"minecraft:default"
]
},
{
"name": "minecraft:test_instance",
"entries": [
"minecraft:always_pass"
]
},
{
"name": "minecraft:dialog",
"entries": [
"minecraft:custom_options",
"minecraft:quick_actions",
"minecraft:server_links"
]
},
{
"name": "minecraft:world_clock",
"entries": [
"minecraft:overworld",
"minecraft:the_end"
]
},
{
"name": "minecraft:timeline",
"entries": [
"minecraft:day",
"minecraft:early_game",
"minecraft:moon",
"minecraft:villager_schedule"
]
}
]

Binary file not shown.

View file

@ -0,0 +1,21 @@
package server
import (
"crypto/md5"
)
// Profile is an authenticated (or, in offline mode, derived) player identity.
type Profile struct {
UUID [16]byte
Name string
}
// OfflineUUID derives the stable offline-mode UUID for a username, matching
// vanilla's java.util.UUID.nameUUIDFromBytes("OfflinePlayer:" + name): an
// MD5-based (version 3) UUID with the IETF variant bits set.
func OfflineUUID(name string) [16]byte {
sum := md5.Sum([]byte("OfflinePlayer:" + name))
sum[6] = (sum[6] & 0x0f) | 0x30 // version 3
sum[8] = (sum[8] & 0x3f) | 0x80 // IETF variant
return sum
}

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

@ -0,0 +1,98 @@
// Package server holds the RegionIO core: configuration, shared state, and the
// data shown to clients (status, MOTD).
package server
import (
"encoding/json"
"regionio/internal/protocol"
"regionio/internal/world"
)
// Config holds operator-tunable settings for a RegionIO instance.
type Config struct {
Host string
Port int
MOTD string
MaxPlayers int
// CompressionThreshold is the minimum uncompressed packet size (bytes) that
// triggers zlib compression. Negative disables compression entirely.
CompressionThreshold int
// WorldSeed seeds terrain generation.
WorldSeed int64
}
// DefaultConfig returns sensible defaults matching vanilla expectations.
func DefaultConfig() Config {
return Config{
Host: "0.0.0.0",
Port: 25565,
MOTD: "RegionIO — a Minecraft server in Go",
MaxPlayers: 20,
CompressionThreshold: 256,
// WorldSeed defaults to 0 for backward compatibility; operators override
// it via the REGIONIO_SEED env var or the -seed flag.
WorldSeed: 0,
}
}
// Server is the top-level core shared across all connections.
type Server struct {
cfg Config
chunks *world.Cache
}
// New constructs a Server from cfg.
func New(cfg Config) *Server {
return &Server{
cfg: cfg,
chunks: world.NewCache(int32(cfg.CompressionThreshold), world.NewVanillaGenerator(cfg.WorldSeed)),
}
}
// Config returns the active configuration.
func (s *Server) Config() Config { return s.cfg }
// Chunks returns the shared chunk cache.
func (s *Server) Chunks() *world.Cache { return s.chunks }
// statusResponse mirrors the JSON shape the client expects for the server-list
// ping. Field names and nesting are part of the protocol contract.
type statusResponse struct {
Version statusVersion `json:"version"`
Players statusPlayers `json:"players"`
Description statusText `json:"description"`
// EnforcesSecureChat is read by the client; false keeps unsigned chat working.
EnforcesSecureChat bool `json:"enforcesSecureChat"`
}
type statusVersion struct {
Name string `json:"name"`
Protocol int `json:"protocol"`
}
type statusPlayers struct {
Max int `json:"max"`
Online int `json:"online"`
}
type statusText struct {
Text string `json:"text"`
}
// StatusJSON returns the marshaled status response for the server-list ping.
func (s *Server) StatusJSON(onlinePlayers int) ([]byte, error) {
resp := statusResponse{
Version: statusVersion{
Name: protocol.GameVersion,
Protocol: protocol.ProtocolVersion,
},
Players: statusPlayers{
Max: s.cfg.MaxPlayers,
Online: onlinePlayers,
},
Description: statusText{Text: s.cfg.MOTD},
EnforcesSecureChat: false,
}
return json.Marshal(resp)
}

View file

@ -0,0 +1,79 @@
package world
import (
"bytes"
"compress/zlib"
"testing"
)
func BenchmarkGenerateFlat(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = GenerateFlat(int32(i), 0)
}
}
func BenchmarkEncode(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = GenerateFlat(int32(i), 0).Encode()
}
}
func BenchmarkEncodeAndCompress(b *testing.B) {
b.ReportAllocs()
var buf bytes.Buffer
for i := 0; i < b.N; i++ {
body := GenerateFlat(int32(i), 0).Encode()
buf.Reset()
zw := zlib.NewWriter(&buf)
zw.Write(body)
zw.Close()
}
}
// One join currently sends a (2*radius+1)^2 grid; benchmark that batch.
func BenchmarkJoinChunkBatch(b *testing.B) {
const radius = 4
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for cx := int32(-radius); cx <= radius; cx++ {
for cz := int32(-radius); cz <= radius; cz++ {
_ = GenerateFlat(cx, cz).Encode()
}
}
}
}
func BenchmarkCacheWarmJoin(b *testing.B) {
const radius = 4
c := NewCache(256, GenerateFlat)
// Warm the cache once (cold join).
for cx := int32(-radius); cx <= radius; cx++ {
for cz := int32(-radius); cz <= radius; cz++ {
c.Frame(cx, cz)
}
}
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
for cx := int32(-radius); cx <= radius; cx++ {
for cz := int32(-radius); cz <= radius; cz++ {
_ = c.Frame(cx, cz) // all hits
}
}
}
}
func BenchmarkCacheColdJoin(b *testing.B) {
const radius = 4
b.ReportAllocs()
for i := 0; i < b.N; i++ {
c := NewCache(256, GenerateFlat) // fresh cache each iter = all misses
for cx := int32(-radius); cx <= radius; cx++ {
for cz := int32(-radius); cz <= radius; cz++ {
_ = c.Frame(cx, cz)
}
}
}
}

View file

@ -0,0 +1,119 @@
package world
import (
_ "embed"
"encoding/json"
"fmt"
"sync"
"regionio/internal/registry"
"regionio/internal/worldgen"
)
//go:embed biome_parameters.json
var biomeParametersJSON []byte
// rawParameter mirrors one entry of biome_parameters.json: a biome name plus its
// climate ranges. Each axis value is a [min, max] array; depth is normally a
// scalar (0.0 surface / 1.0 underground) but a few cave entries carry a [min,
// max] array, so it is decoded loosely (see depthScalar).
type rawParameter struct {
Biome string `json:"biome"`
Param struct {
Temperature [2]float64 `json:"temperature"`
Humidity [2]float64 `json:"humidity"`
Continentalness [2]float64 `json:"continentalness"`
Erosion [2]float64 `json:"erosion"`
Weirdness [2]float64 `json:"weirdness"`
Depth any `json:"depth"`
Offset float64 `json:"offset"`
} `json:"parameters"`
}
// depthScalar extracts a scalar depth from a raw entry, accepting either a JSON
// number or a single-element [v] array. Arrays with a range are cave entries
// (non-surface) and return ok=false so the caller skips them.
func depthScalar(v any) (float64, bool) {
switch d := v.(type) {
case float64:
return d, true
case []any:
if len(d) == 1 {
if f, ok := d[0].(float64); ok {
return f, true
}
}
}
return 0, false
}
// surfaceTable is the biome parameter table filtered to depth=0 (surface layer),
// built once at init. Cave/underground entries (depth=1, or non-zero offset for
// lush/dripstone/deep_dark) are excluded until the per-cell milestone.
var (
surfaceTable *worldgen.ParameterTable
surfaceTableOnce sync.Once
)
// loadSurfaceTable parses the embedded biome parameters once and returns the
// surface-only ParameterTable. Panics on a parse error (a corrupt embedded
// table is a build-time bug, not a runtime condition).
func loadSurfaceTable() *worldgen.ParameterTable {
surfaceTableOnce.Do(func() {
var raw struct {
Biomes []rawParameter `json:"biomes"`
}
if err := json.Unmarshal(biomeParametersJSON, &raw); err != nil {
panic(fmt.Sprintf("world: parsing embedded biome_parameters.json: %v", err))
}
params := make([]worldgen.BiomeParameter, 0, len(raw.Biomes)/2)
for _, e := range raw.Biomes {
// Surface layer only: depth resolves to the scalar 0.0, and no cave
// offset. Range/array depths and non-zero offsets belong to cave
// biomes (lush/dripstone/deep_dark), deferred to the per-cell stage.
dp, ok := depthScalar(e.Param.Depth)
if !ok || dp != 0.0 || e.Param.Offset != 0.0 {
continue
}
params = append(params, makeBiomeParameter(e, dp))
}
surfaceTable = worldgen.NewParameterTable(params)
})
return surfaceTable
}
// makeBiomeParameter converts a raw JSON entry into a BiomeParameter, mapping
// the [min,max] ranges to quantized ClimateRanges. depth is a scalar in the
// source but a [depth, depth] band in the table (a single value).
func makeBiomeParameter(e rawParameter, depth float64) worldgen.BiomeParameter {
qr := func(a [2]float64) worldgen.ClimateRange {
return worldgen.ClimateRange{Min: worldgen.Quantize(a[0]), Max: worldgen.Quantize(a[1])}
}
dpQ := worldgen.Quantize(depth)
return worldgen.BiomeParameter{
Name: e.Biome,
Ranges: [worldgen.AxisCount]worldgen.ClimateRange{
qr(e.Param.Temperature),
qr(e.Param.Humidity),
qr(e.Param.Continentalness),
qr(e.Param.Erosion),
qr(e.Param.Weirdness),
{Min: dpQ, Max: dpQ + 1}, // half-open band covering exactly depth
},
Offset: worldgen.Quantize(e.Param.Offset),
}
}
// BiomeAt returns the network biome ID for the surface biome at block (wx, wz)
// given the loaded overworld density. It samples the climate axes at sea level,
// finds the matching biome in the parameter table, and resolves its name to a
// numeric ID via the synchronized biome registry. Unknown biomes fall back to
// plains so chunk encoding always gets a valid ID.
func BiomeAt(od *worldgen.OverworldDensity, wx, wz int) uint16 {
point := worldgen.SampleColumn(od, SeaLevel, wx, wz)
name := loadSurfaceTable().FindBiome(point)
if id := registry.Index("minecraft:worldgen/biome", name); id >= 0 {
return uint16(id)
}
return BiomePlains
}

View file

@ -0,0 +1,73 @@
package world
import (
"testing"
"regionio/internal/registry"
"regionio/internal/worldgen"
)
// TestBiomeAtDeterministic checks BiomeAt is stable for fixed seed/coords and
// resolves to a registry-known biome (not a fallback placeholder).
func TestBiomeAtDeterministic(t *testing.T) {
od, err := worldgen.LoadOverworldFinalDensity(12345)
if err != nil {
t.Fatalf("load: %v", err)
}
id1 := BiomeAt(od, 100, 200)
id2 := BiomeAt(od, 100, 200)
if id1 != id2 {
t.Fatalf("BiomeAt not deterministic: %d vs %d", id1, id2)
}
// The returned ID must be a valid registry biome, not the plains fallback by
// accident — resolve it back and confirm plains only when genuinely plains.
if int(id1) != registry.Index("minecraft:worldgen/biome", biomeName(od, 100, 200)) {
t.Errorf("BiomeAt id %d does not round-trip through registry", id1)
}
}
// biomeName is a test helper exposing the resolved biome name at (wx, wz).
func biomeName(od *worldgen.OverworldDensity, wx, wz int) string {
point := worldgen.SampleColumn(od, SeaLevel, wx, wz)
return loadSurfaceTable().FindBiome(point)
}
// TestBiomeAtVaryingAcrossWorld confirms different regions of the world map to
// different biomes — the whole point of multi-noise. If every sampled chunk
// resolved to the same biome, climate sampling or the finder would be broken.
func TestBiomeAtVaryingAcrossWorld(t *testing.T) {
od, err := worldgen.LoadOverworldFinalDensity(12345)
if err != nil {
t.Fatalf("load: %v", err)
}
seen := make(map[uint16]bool)
for cx := int32(0); cx < 16; cx++ {
for cz := int32(0); cz < 16; cz++ {
seen[BiomeAt(od, int(cx)*16+8, int(cz)*16+8)] = true
}
}
if len(seen) < 2 {
t.Fatalf("expected >=2 biomes across 16x16 chunks, got %d (%v)", len(seen), seen)
}
t.Logf("found %d distinct biomes across 16x16 chunks", len(seen))
}
// TestVanillaChunkHasBiome confirms generateVanilla threads the per-column biome
// into the chunk (regression guard for the NewChunk call site in vanilla.go).
func TestVanillaChunkHasBiome(t *testing.T) {
gen := NewVanillaGenerator(12345)
ch := gen(10, -3)
if ch == nil {
t.Fatal("nil chunk")
}
// biome is unexported; verify via the registry by re-deriving it. The chunk's
// biome must match what BiomeAt returns at the chunk centre.
od, err := worldgen.LoadOverworldFinalDensity(12345)
if err != nil {
t.Fatalf("load: %v", err)
}
want := BiomeAt(od, 10*16+8, -3*16+8)
if uint16(ch.biome) != want {
t.Errorf("chunk biome = %d, want %d", ch.biome, want)
}
}

File diff suppressed because it is too large Load diff

106
internal/world/cache.go Normal file
View file

@ -0,0 +1,106 @@
package world
import (
"sync"
"regionio/internal/protocol"
)
// Generator produces the chunk at the given coordinate.
type Generator func(cx, cz int32) *Chunk
// Cache is the live world: it owns the mutable chunk data and memoizes the
// framed, compression-ready level_chunk packet for each chunk. A block edit
// mutates the chunk and invalidates its cached frame so the next request
// re-encodes it.
//
// Frames are built for a fixed compression threshold shared by all play
// connections, so one frame is valid for every client.
//
// Generation can be expensive; it runs outside the lock to avoid blocking other
// chunk requests. An eviction policy belongs here once worlds stream far.
type Cache struct {
threshold int32
gen Generator
mu sync.Mutex
chunks map[[2]int32]*Chunk
frames map[[2]int32][]byte
}
// NewCache returns a world cache that frames packets at the given compression
// threshold using gen to produce missing chunks.
func NewCache(threshold int32, gen Generator) *Cache {
return &Cache{
threshold: threshold,
gen: gen,
chunks: make(map[[2]int32]*Chunk),
frames: make(map[[2]int32][]byte),
}
}
// chunkAt returns the chunk at (cx, cz), generating it on first access.
func (c *Cache) chunkAt(cx, cz int32) *Chunk {
key := [2]int32{cx, cz}
c.mu.Lock()
if ch, ok := c.chunks[key]; ok {
c.mu.Unlock()
return ch
}
c.mu.Unlock()
ch := c.gen(cx, cz) // generate outside the lock
c.mu.Lock()
defer c.mu.Unlock()
if existing, ok := c.chunks[key]; ok {
return existing // another goroutine won the race
}
c.chunks[key] = ch
return ch
}
// Frame returns the prebuilt level_chunk packet for (cx, cz), building it on
// first request and caching until the chunk is edited. The slice must not be
// mutated.
func (c *Cache) Frame(cx, cz int32) []byte {
key := [2]int32{cx, cz}
c.mu.Lock()
if f, ok := c.frames[key]; ok {
c.mu.Unlock()
return f
}
c.mu.Unlock()
ch := c.chunkAt(cx, cz)
frame := protocol.AppendPacket(nil, c.threshold, protocol.PlayLevelChunk, ch.Encode())
c.mu.Lock()
defer c.mu.Unlock()
if existing, ok := c.frames[key]; ok {
return existing
}
c.frames[key] = frame
return frame
}
// SetBlock changes the block at world coordinates (x, y, z), invalidating the
// affected chunk's cached frame. It reports whether a chunk was actually
// touched (false if y is out of range).
func (c *Cache) SetBlock(x, y, z int, state uint16) bool {
if y < MinY || y >= MinY+WorldHeight {
return false
}
cx := int32(x >> 4)
cz := int32(z >> 4)
ch := c.chunkAt(cx, cz)
ch.SetBlock(x, y, z, state)
c.mu.Lock()
delete(c.frames, [2]int32{cx, cz})
c.mu.Unlock()
return true
}

23
internal/world/chunk.go Normal file
View file

@ -0,0 +1,23 @@
// Package world provides chunk data for the play phase: an in-memory chunk
// representation, the level_chunk_with_light encoder, and (for now) a flat
// world generator. Real noise-based generation arrives in a later sub-milestone.
package world
// FlatSurfaceY is the Y of the topmost solid block (grass) in the flat world.
// A player spawns one block above it.
const FlatSurfaceY = -61
// GenerateFlat builds a Classic-Flat-style chunk at (cx, cz): bedrock at the
// world floor, two dirt layers, and a grass surface, all under a plains biome.
func GenerateFlat(cx, cz int32) *Chunk {
c := NewChunk(cx, cz, BiomePlains)
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
c.SetBlock(lx, MinY+0, lz, StateBedrock)
c.SetBlock(lx, MinY+1, lz, StateDirt)
c.SetBlock(lx, MinY+2, lz, StateDirt)
c.SetBlock(lx, FlatSurfaceY, lz, StateGrass)
}
}
return c
}

269
internal/world/encode.go Normal file
View file

@ -0,0 +1,269 @@
package world
import (
"math/bits"
"regionio/internal/protocol"
)
// World vertical geometry for the overworld dimension type.
const (
MinY = -64
WorldHeight = 384
SectionCount = WorldHeight / 16 // 24 sections
sectionVol = 16 * 16 * 16 // 4096 blocks
)
// Common block-state network IDs (from the generated block report).
const (
StateAir uint16 = 0
StateStone uint16 = 1
StateGrass uint16 = 9
StateDirt uint16 = 10
StateBedrock uint16 = 85
StateWater uint16 = 86
StateSand uint16 = 118
StateGravel uint16 = 124
StateOakLog uint16 = 137
StateOakLeaf uint16 = 279
)
// BiomePlains is the network ID (registry index) of minecraft:plains.
const BiomePlains uint16 = 40
// totalBlockStates is one past the largest block-state ID; it sets the
// direct-palette bit width.
const totalBlockStates = 29873
// Chunk is a 16xWorldHeightx16 column of block states with a single biome.
// A nil section is entirely air.
type Chunk struct {
X, Z int32
sections [SectionCount]*[sectionVol]uint16
biome uint16
}
// NewChunk returns an empty (all-air) chunk at (x, z) with the given biome.
func NewChunk(x, z int32, biome uint16) *Chunk {
return &Chunk{X: x, Z: z, biome: biome}
}
// blockIndex maps local coordinates to the YZX-ordered section array index.
func blockIndex(lx, ly, lz int) int { return (ly&15)<<8 | (lz&15)<<4 | (lx & 15) }
// section returns section i, allocating it on first write.
func (c *Chunk) section(i int) *[sectionVol]uint16 {
if c.sections[i] == nil {
c.sections[i] = new([sectionVol]uint16)
}
return c.sections[i]
}
// GetBlock returns the block state at local (lx, lz) and world height y, or
// StateAir if the section is empty or y is out of range.
func (c *Chunk) GetBlock(lx, y, lz int) uint16 {
si := (y - MinY) >> 4
if si < 0 || si >= SectionCount {
return StateAir
}
s := c.sections[si]
if s == nil {
return StateAir
}
return s[blockIndex(lx, y, lz)]
}
// SetBlock sets the block at local (lx, lz) and absolute world height y.
func (c *Chunk) SetBlock(lx, y, lz int, state uint16) {
si := (y - MinY) >> 4
if si < 0 || si >= SectionCount {
return
}
c.section(si)[blockIndex(lx, y, lz)] = state
}
// Encode serializes the level_chunk_with_light body for this chunk.
func (c *Chunk) Encode() []byte {
w := protocol.NewWriter(8192)
w.Int32(c.X).Int32(c.Z)
c.writeHeightmaps(w)
// Section data is length-prefixed.
sec := protocol.NewWriter(4096)
for i := 0; i < SectionCount; i++ {
c.writeSection(sec, i)
}
w.VarInt(int32(sec.Len()))
w.Raw(sec.Bytes())
w.VarInt(0) // block entity count
c.writeLight(w)
return w.Bytes()
}
// Heightmap.Types ordinals sent to the client.
const (
hmWorldSurface = 1
hmMotionBlocking = 4
hmMotionBlockingNoLeaves = 5
)
// writeHeightmaps emits the three client-relevant heightmaps. For our blocky
// terrain (no leaves/transparency) they share the same column heights.
func (c *Chunk) writeHeightmaps(w *protocol.Writer) {
heights := c.columnHeights()
packed := packHeightmap(heights)
w.VarInt(3)
for _, t := range []int32{hmMotionBlockingNoLeaves, hmMotionBlocking, hmWorldSurface} {
w.VarInt(t)
w.VarInt(int32(len(packed)))
for _, v := range packed {
w.Int64(int64(v))
}
}
}
// columnHeights returns, per column, (highestNonAirY + 1) - MinY, clamped to 0.
func (c *Chunk) columnHeights() [256]uint16 {
var h [256]uint16
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
height := 0
for y := MinY + WorldHeight - 1; y >= MinY; y-- {
si := (y - MinY) >> 4
s := c.sections[si]
if s != nil && s[blockIndex(lx, y, lz)] != StateAir {
height = y + 1 - MinY
break
}
}
h[lz*16+lx] = uint16(height)
}
}
return h
}
// packHeightmap packs 256 column heights at 9 bits each, 7 values per long,
// without spanning longs (37 longs).
func packHeightmap(h [256]uint16) []uint64 {
const bpe = 9
const perLong = 64 / bpe // 7
out := make([]uint64, (256+perLong-1)/perLong)
for i, v := range h {
out[i/perLong] |= uint64(v&0x1FF) << uint((i%perLong)*bpe)
}
return out
}
// writeSection emits one chunk section: block count, block paletted container,
// then the (single-value) biome paletted container.
func (c *Chunk) writeSection(w *protocol.Writer, i int) {
s := c.sections[i]
if s == nil {
w.Uint16(0) // non-air block count
w.Uint16(0) // reserved 2-byte field (always 0 in vanilla)
writeSingleValued(w, uint32(StateAir))
} else {
w.Uint16(uint16(nonAirCount(s)))
w.Uint16(0) // reserved 2-byte field
writeBlockPalette(w, s)
}
// Biomes: a single value covers the whole section for now.
writeSingleValued(w, uint32(c.biome))
}
func nonAirCount(s *[sectionVol]uint16) int {
n := 0
for _, v := range s {
if v != StateAir {
n++
}
}
return n
}
// writeSingleValued writes a bits-per-entry-0 paletted container (no data).
func writeSingleValued(w *protocol.Writer, value uint32) {
w.Byte(0)
w.VarInt(int32(value))
}
// writeBlockPalette writes a block-state paletted container, choosing the
// single-valued, indirect, or direct encoding as appropriate.
func writeBlockPalette(w *protocol.Writer, s *[sectionVol]uint16) {
palette, indexOf := buildPalette(s)
if len(palette) == 1 {
writeSingleValued(w, uint32(palette[0]))
return
}
bpe := bitsFor(len(palette))
if bpe < 4 {
bpe = 4 // minimum for the indirect block format
}
if bpe > 8 {
writeDirect(w, s)
return
}
w.Byte(byte(bpe))
w.VarInt(int32(len(palette)))
for _, st := range palette {
w.VarInt(int32(st))
}
writePackedIndices(w, bpe, sectionVol, func(i int) uint32 {
return uint32(indexOf[s[i]])
})
}
// writeDirect writes a direct (palette-less) container of global state IDs.
func writeDirect(w *protocol.Writer, s *[sectionVol]uint16) {
bpe := bitsFor(totalBlockStates)
w.Byte(byte(bpe))
writePackedIndices(w, bpe, sectionVol, func(i int) uint32 {
return uint32(s[i])
})
}
// writePackedIndices emits the long-array data: count entries of bpe bits each,
// packed perLong=64/bpe values per long, never spanning a long boundary. The
// long count is NOT length-prefixed; the client derives it from bpe.
func writePackedIndices(w *protocol.Writer, bpe, count int, value func(i int) uint32) {
perLong := 64 / bpe
numLongs := (count + perLong - 1) / perLong
mask := uint64(1)<<uint(bpe) - 1
for l := 0; l < numLongs; l++ {
var packed uint64
for j := 0; j < perLong; j++ {
idx := l*perLong + j
if idx >= count {
break
}
packed |= (uint64(value(idx)) & mask) << uint(j*bpe)
}
w.Int64(int64(packed))
}
}
// buildPalette returns the distinct block states in s and a value->index map.
func buildPalette(s *[sectionVol]uint16) ([]uint16, map[uint16]int) {
indexOf := make(map[uint16]int)
var palette []uint16
for _, v := range s {
if _, ok := indexOf[v]; !ok {
indexOf[v] = len(palette)
palette = append(palette, v)
}
}
return palette, indexOf
}
// bitsFor returns the bits needed to index n distinct values (min 1).
func bitsFor(n int) int {
if n <= 1 {
return 0
}
return bits.Len(uint(n - 1))
}

View file

@ -0,0 +1,154 @@
package world
import (
"testing"
"regionio/internal/protocol"
)
// parsePalettedContainer consumes one paletted container of entryCount entries.
// The long-array length is derived from bits-per-entry, not length-prefixed.
func parsePalettedContainer(t *testing.T, r *protocol.Reader, maxBits, entryCount int) {
t.Helper()
bpe, err := r.ReadByte()
if err != nil {
t.Fatalf("bpe: %v", err)
}
if bpe == 0 {
if _, err := r.VarInt(); err != nil { // single value
t.Fatalf("single value: %v", err)
}
return
}
if int(bpe) <= maxBits { // indirect: palette precedes data
n, err := r.VarInt()
if err != nil || n < 0 {
t.Fatalf("palette len: %v", err)
}
for i := int32(0); i < n; i++ {
if _, err := r.VarInt(); err != nil {
t.Fatalf("palette entry: %v", err)
}
}
}
perLong := 64 / int(bpe)
longs := (entryCount + perLong - 1) / perLong
for i := 0; i < longs; i++ {
if _, err := r.Int64(); err != nil {
t.Fatalf("data long: %v", err)
}
}
}
func skipBitSet(t *testing.T, r *protocol.Reader) {
t.Helper()
n, err := r.VarInt()
if err != nil || n < 0 {
t.Fatalf("bitset len: %v", err)
}
for i := int32(0); i < n; i++ {
if _, err := r.Int64(); err != nil {
t.Fatalf("bitset long: %v", err)
}
}
}
// TestFlatChunkEncodesCleanly fully parses an encoded flat chunk and asserts
// the byte stream is consumed exactly, with the expected high-level structure.
func TestFlatChunkEncodesCleanly(t *testing.T) {
body := GenerateFlat(2, -3).Encode()
// X and Z are plain big-endian ints.
if got := readInt32(t, body[0:4]); got != 2 {
t.Fatalf("chunkX = %d, want 2", got)
}
if got := readInt32(t, body[4:8]); got != -3 {
t.Fatalf("chunkZ = %d, want -3", got)
}
r := protocol.NewReader(body[8:])
// Heightmaps: 3 entries, each 37 longs of packed 9-bit heights.
hmCount, err := r.VarInt()
if err != nil || hmCount != 3 {
t.Fatalf("heightmap count = %d (err %v), want 3", hmCount, err)
}
for i := int32(0); i < hmCount; i++ {
if _, err := r.VarInt(); err != nil { // type
t.Fatalf("hm type: %v", err)
}
longs, err := r.VarInt()
if err != nil || longs != 37 {
t.Fatalf("hm longs = %d (err %v), want 37", longs, err)
}
for j := int32(0); j < longs; j++ {
if _, err := r.Int64(); err != nil {
t.Fatalf("hm long: %v", err)
}
}
}
// Section data block.
dataLen, err := r.VarInt()
if err != nil || dataLen <= 0 {
t.Fatalf("data len = %d (err %v)", dataLen, err)
}
nonAirSections := 0
for s := 0; s < SectionCount; s++ {
count, err := r.Uint16()
if err != nil {
t.Fatalf("section %d count: %v", s, err)
}
if _, err := r.Uint16(); err != nil { // reserved 2-byte field
t.Fatalf("section %d reserved: %v", s, err)
}
if count > 0 {
nonAirSections++
}
parsePalettedContainer(t, r, 8, 4096) // blocks
parsePalettedContainer(t, r, 3, 64) // biomes
}
if nonAirSections != 1 {
t.Fatalf("non-air sections = %d, want 1 (flat layers live in section 0)", nonAirSections)
}
// Block entities.
if be, err := r.VarInt(); err != nil || be != 0 {
t.Fatalf("block entities = %d (err %v), want 0", be, err)
}
// Light: four bitsets, then sky arrays, then block arrays.
skipBitSet(t, r) // sky mask
skipBitSet(t, r) // block mask
skipBitSet(t, r) // empty sky mask
skipBitSet(t, r) // empty block mask
skyArrays, err := r.VarInt()
if err != nil || skyArrays != lightSections {
t.Fatalf("sky arrays = %d (err %v), want %d", skyArrays, err, lightSections)
}
for i := int32(0); i < skyArrays; i++ {
n, err := r.VarInt()
if err != nil || n != 2048 {
t.Fatalf("sky array len = %d (err %v), want 2048", n, err)
}
for j := int32(0); j < n; j++ {
if _, err := r.ReadByte(); err != nil {
t.Fatalf("sky byte: %v", err)
}
}
}
if blockArrays, err := r.VarInt(); err != nil || blockArrays != 0 {
t.Fatalf("block arrays = %d (err %v), want 0", blockArrays, err)
}
if rem := r.Remaining(); rem != 0 {
t.Fatalf("trailing bytes after parse: %d", rem)
}
}
func readInt32(t *testing.T, b []byte) int32 {
t.Helper()
if len(b) < 4 {
t.Fatal("short int32")
}
return int32(uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]))
}

View file

@ -0,0 +1,46 @@
package world
import (
"bytes"
"os"
"testing"
"regionio/internal/protocol"
)
// extractSectionData returns the section-data byte slice of a level_chunk body
// (the bytes covered by the VarInt size that follows the heightmaps).
func extractSectionData(t *testing.T, body []byte) []byte {
t.Helper()
r := protocol.NewReader(body[8:]) // skip chunk X,Z
count, _ := r.VarInt()
for i := int32(0); i < count; i++ {
r.VarInt() // heightmap type
n, _ := r.VarInt() // long count
for j := int32(0); j < n; j++ {
r.Int64()
}
}
size, _ := r.VarInt()
consumed := len(body[8:]) - r.Remaining()
return body[8+consumed : 8+consumed+int(size)]
}
// TestGoldenAgainstVanilla asserts our flat-chunk section data is byte-for-byte
// identical to a chunk captured from the official 26.1.2 server (same world
// coordinate). This guards the paletted-container and heightmap encoding.
// Light is intentionally not compared (we send full-bright, which differs).
func TestGoldenAgainstVanilla(t *testing.T) {
vanilla, err := os.ReadFile("testdata/vanilla_flat_chunk.bin")
if err != nil {
t.Fatalf("read fixture: %v", err)
}
ours := GenerateFlat(0, -1).Encode() // fixture was captured at chunk (0, -1)
want := extractSectionData(t, vanilla)
got := extractSectionData(t, ours)
if !bytes.Equal(want, got) {
t.Fatalf("section data differs: vanilla=%d bytes, ours=%d bytes", len(want), len(got))
}
}

File diff suppressed because one or more lines are too long

38
internal/world/items.go Normal file
View file

@ -0,0 +1,38 @@
package world
import (
_ "embed"
"encoding/json"
"fmt"
"strconv"
)
//go:embed item_blocks.json
var itemBlocksJSON []byte
// itemToBlock maps an item's network ID to the default block state placed when
// that item is used. Built from the item registry crossed with block defaults
// (items whose name is a block). Items with no block (tools, food) are absent.
var itemToBlock map[int32]uint16
func init() {
raw := make(map[string]int)
if err := json.Unmarshal(itemBlocksJSON, &raw); err != nil {
panic(fmt.Sprintf("world: parsing item_blocks.json: %v", err))
}
itemToBlock = make(map[int32]uint16, len(raw))
for k, v := range raw {
id, err := strconv.Atoi(k)
if err != nil {
panic(fmt.Sprintf("world: bad item id %q: %v", k, err))
}
itemToBlock[int32(id)] = uint16(v)
}
}
// ItemToBlock returns the block state placed by an item, and whether the item
// is a placeable block.
func ItemToBlock(itemID int32) (uint16, bool) {
s, ok := itemToBlock[itemID]
return s, ok
}

45
internal/world/light.go Normal file
View file

@ -0,0 +1,45 @@
package world
import "regionio/internal/protocol"
// lightSections is the number of light subchunks: one below the world and one
// above, plus one per block section.
const lightSections = SectionCount + 2
// writeLight emits a fully-lit sky: every light section carries sky light 15,
// and block light is reported as uniformly empty. This avoids a black world
// without implementing real light propagation (deferred).
func (c *Chunk) writeLight(w *protocol.Writer) {
full := allSectionsMask()
writeBitSet(w, full) // sky light mask: all sections present
writeBitSet(w, nil) // block light mask: none present
writeBitSet(w, nil) // empty sky light mask: none empty
writeBitSet(w, full) // empty block light mask: all empty
// Sky light arrays: one 2048-byte (4096 nibbles) array of 0x0F per section.
bright := make([]byte, 2048)
for i := range bright {
bright[i] = 0xFF // two nibbles of 15
}
w.VarInt(lightSections)
for i := 0; i < lightSections; i++ {
w.VarInt(2048)
w.Raw(bright)
}
w.VarInt(0) // no block light arrays
}
// allSectionsMask returns a bitset (as longs) with the low lightSections bits set.
func allSectionsMask() []uint64 {
return []uint64{(uint64(1) << lightSections) - 1}
}
// writeBitSet emits a length-prefixed array of longs.
func writeBitSet(w *protocol.Writer, longs []uint64) {
w.VarInt(int32(len(longs)))
for _, v := range longs {
w.Int64(int64(v))
}
}

88
internal/world/terrain.go Normal file
View file

@ -0,0 +1,88 @@
package world
import (
"sync"
"regionio/internal/worldgen"
)
// SeaLevel is the water surface height for generated terrain.
const SeaLevel = 63
// NewTerrainGenerator returns a chunk generator backed by a density function.
// The density tree is built once (shared, read-only noise state) and sampled
// per block.
func NewTerrainGenerator(seed int64) Generator {
density := worldgen.SimpleTerrain(seed)
return func(cx, cz int32) *Chunk {
return generateFromDensity(density, cx, cz)
}
}
// generateFromDensity fills a chunk by sampling the density function (>0 is
// solid). The per-column sampling is the expensive part and is run in parallel
// across the 16 x-rows; chunk assembly is sequential to avoid racing on lazy
// section allocation. Sampling the density only reads shared noise state, so it
// is safe to run concurrently.
func generateFromDensity(d worldgen.DensityFunction, cx, cz int32) *Chunk {
c := NewChunk(cx, cz, BiomePlains)
var columns [16][16][WorldHeight]uint16
var wg sync.WaitGroup
for lx := 0; lx < 16; lx++ {
wg.Add(1)
go func(lx int) {
defer wg.Done()
for lz := 0; lz < 16; lz++ {
wx := float64(int(cx)*16 + lx)
wz := float64(int(cz)*16 + lz)
computeColumn(d, wx, wz, &columns[lx][lz])
}
}(lx)
}
wg.Wait()
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
col := &columns[lx][lz]
for i := 0; i < WorldHeight; i++ {
if s := col[i]; s != StateAir {
c.SetBlock(lx, MinY+i, lz, s)
}
}
}
}
return c
}
// computeColumn fills out with the block state for each Y in one column.
func computeColumn(d worldgen.DensityFunction, wx, wz float64, out *[WorldHeight]uint16) {
var solid [WorldHeight]bool
top := -1
for i := 0; i < WorldHeight; i++ {
y := MinY + i
if d.Compute(worldgen.FunctionContext{X: wx, Y: float64(y), Z: wz}) > 0 {
solid[i] = true
top = i
}
}
for i := 0; i < WorldHeight; i++ {
y := MinY + i
switch {
case y == MinY:
out[i] = StateBedrock
case solid[i]:
switch {
case i == top && y >= SeaLevel:
out[i] = StateGrass
case i > top-4:
out[i] = StateDirt
default:
out[i] = StateStone
}
case y < SeaLevel:
out[i] = StateWater
}
}
}

View file

@ -0,0 +1,9 @@
package world
import "testing"
func BenchmarkGenerateTerrain(b *testing.B) {
gen := NewTerrainGenerator(0)
b.ResetTimer(); b.ReportAllocs()
for i := 0; i < b.N; i++ { _ = gen(int32(i), 0) }
}

View file

@ -0,0 +1,30 @@
package world
import (
"fmt"
"testing"
"regionio/internal/worldgen"
)
func TestTerrainHeightProfile(t *testing.T) {
d := worldgen.SimpleTerrain(0)
minH, maxH := 1000, -1000
// surface height along z=8 for x in [-32,32]
line := ""
for x := -32; x <= 32; x += 4 {
top := MinY - 1
for y := MinY; y < MinY+WorldHeight; y++ {
if d.Compute(worldgen.FunctionContext{X: float64(x), Y: float64(y), Z: 8}) > 0 {
top = y
}
}
line += fmt.Sprintf("%d ", top)
if top < minH { minH = top }
if top > maxH { maxH = top }
}
t.Logf("surface heights (z=8): %s", line)
t.Logf("min=%d max=%d range=%d", minH, maxH, maxH-minH)
if minH < MinY || maxH > 120 {
t.Fatalf("implausible terrain heights")
}
}

Binary file not shown.

266
internal/world/vanilla.go Normal file
View file

@ -0,0 +1,266 @@
package world
import (
"sync"
"regionio/internal/worldgen"
)
// Noise cell dimensions for the overworld (size_horizontal=1 → 4 wide,
// size_vertical=2 → 8 tall). Only the Interpolated terrain noise is sampled on
// the cell-corner grid and trilinearly interpolated (as vanilla's NoiseChunk
// does); the rest of final_density — squeeze/min and the caves — is evaluated
// per block with those interpolated values substituted in.
const (
cellWidth = 4
cellHeight = 8
cellsXZ = 16 / cellWidth // 4
cellsY = WorldHeight / cellHeight // 48
)
type cornerGrid [cellsXZ + 1][cellsY + 1][cellsXZ + 1]float64
// NewVanillaGenerator returns a generator backed by the real overworld
// final_density tree for the given seed, plus a simplified cosmetic pass
// (beaches and trees) layered on the bit-accurate terrain.
func NewVanillaGenerator(seed int64) Generator {
od, err := worldgen.LoadOverworldFinalDensity(seed)
if err != nil {
panic("world: loading overworld density: " + err.Error())
}
return func(cx, cz int32) *Chunk {
return generateVanilla(od, seed, cx, cz)
}
}
func generateVanilla(od *worldgen.OverworldDensity, seed int64, cx, cz int32) *Chunk {
// Surface biome is sampled at the chunk centre column. Climate noises are
// 2D at this stage (depth fixed to surface), so one sample per chunk is
// representative; the per-cell milestone will sample the 4×4×4 grid.
biome := BiomeAt(od, int(cx)*16+8, int(cz)*16+8)
c := NewChunk(cx, cz, biome)
baseX, baseZ := int(cx)*16, int(cz)*16
grids := make([]cornerGrid, len(od.Interpolated))
var wg sync.WaitGroup
for ix := 0; ix <= cellsXZ; ix++ {
wg.Add(1)
go func(ix int) {
defer wg.Done()
wx := float64(baseX + ix*cellWidth)
for iy := 0; iy <= cellsY; iy++ {
wy := float64(MinY + iy*cellHeight)
for iz := 0; iz <= cellsXZ; iz++ {
ctx := worldgen.FunctionContext{X: wx, Y: wy, Z: float64(baseZ + iz*cellWidth)}
for n, node := range od.Interpolated {
grids[n][ix][iy][iz] = node.Inner.Compute(ctx)
}
}
}
}(ix)
}
wg.Wait()
var columns [16][16][WorldHeight]uint16
var surfTop [16][16]int // top solid index, -1 if none
var grass [16][16]bool // grassy land surface (tree-plantable)
for lx := 0; lx < 16; lx++ {
wg.Add(1)
go func(lx int) {
defer wg.Done()
interp := make([]float64, len(od.Interpolated))
for lz := 0; lz < 16; lz++ {
surfTop[lx][lz], grass[lx][lz] = fillVanillaColumn(od, grids, interp, &columns[lx][lz], baseX+lx, baseZ+lz, lx, lz, seed)
}
}(lx)
}
wg.Wait()
for lx := 0; lx < 16; lx++ {
for lz := 0; lz < 16; lz++ {
col := &columns[lx][lz]
for i := 0; i < WorldHeight; i++ {
if s := col[i]; s != StateAir {
c.SetBlock(lx, MinY+i, lz, s)
}
}
}
}
decorate(c, cx, cz, seed, &surfTop, &grass)
return c
}
// fillVanillaColumn lays the blocks for one column and returns the top solid
// index and whether the surface is grassy land (suitable for trees). Beaches
// (sand) form a narrow ring around the waterline; deep water floors use gravel;
// the bottom is a vanilla-style randomised bedrock layer.
func fillVanillaColumn(od *worldgen.OverworldDensity, grids []cornerGrid, interp []float64, out *[WorldHeight]uint16, wx, wz, lx, lz int, seed int64) (int, bool) {
cx0 := lx / cellWidth
cz0 := lz / cellWidth
fx := float64(lx%cellWidth) / cellWidth
fz := float64(lz%cellWidth) / cellWidth
var solid [WorldHeight]bool
top := -1
for i := 0; i < WorldHeight; i++ {
cy0 := i / cellHeight
fy := float64(i%cellHeight) / cellHeight
for n := range grids {
interp[n] = trilerp(&grids[n], cx0, cy0, cz0, fx, fy, fz)
}
ctx := worldgen.FunctionContext{X: float64(wx), Y: float64(MinY + i), Z: float64(wz)}.WithInterp(interp)
if od.Final.Compute(ctx) > 0 {
solid[i] = true
top = i
}
}
topY := MinY + top
// Beach: a narrow band straddling the waterline. Dry columns well above sea
// level stay grass; deep water floors become gravel, not sand.
const beachBand = 3
beach := top >= 0 && topY >= SeaLevel-beachBand && topY <= SeaLevel+1
deepWater := top >= 0 && topY < SeaLevel-beachBand
// Randomised bedrock floor: solid at MinY, decaying chance up to MinY+4, like
// the vanilla overworld floor (each layer drops the probability by ~1/4).
rng := newColumnRand(wx, wz, int(seed))
for i := 0; i < WorldHeight; i++ {
y := MinY + i
switch {
case y <= MinY:
out[i] = StateBedrock
case y <= MinY+4 && solid[i] && bedrockAt(rng, y-MinY):
out[i] = StateBedrock
case solid[i]:
switch {
case beach && i > top-4:
out[i] = StateSand
case deepWater && i == top:
out[i] = StateGravel
case i == top && y >= SeaLevel:
out[i] = StateGrass
case i > top-4:
out[i] = StateDirt
default:
out[i] = StateStone
}
case y < SeaLevel:
out[i] = StateWater
}
}
return top, top >= 0 && !beach && !deepWater && topY >= SeaLevel
}
// bedrockAt reports whether a block at layer d (1..4 above the floor) should be
// bedrock, consuming randomness from rng. Vanilla's floor has probability ~1 at
// the bottom layer dropping to 0 a few blocks up; we approximate the decay with
// a 1/4 chance per step up from the solid floor.
func bedrockAt(rng chunkRand, d int) bool {
// Probability per layer: d=1 → 50%, d=2 → 25%, d=3 → 12.5%, d=4 → 6.25%.
// Need (5-d) high bits from a 32-bit draw; compare against a per-step mask.
keep := 5 - d // 4..1
if keep <= 0 {
return false
}
// Each surviving bit roughly halves the chance; draw once and check `keep`
// of its low bits.
r := rng.next()
for b := 0; b < keep; b++ {
if (r>>uint(b))&1 == 0 {
return false
}
}
return true
}
// decorate places simple oak trees on grassy columns. Trunks are kept two
// blocks inside the chunk so the radius-2 canopy never crosses into a neighbour
// (avoiding cross-chunk coordination); placement is deterministic per chunk.
func decorate(c *Chunk, cx, cz int32, seed int64, surfTop *[16][16]int, grass *[16][16]bool) {
r := newChunkRand(cx, cz, seed)
const attempts = 8
for a := 0; a < attempts; a++ {
lx := 2 + int(r.next()%12)
lz := 2 + int(r.next()%12)
if !grass[lx][lz] {
continue
}
baseY := MinY + surfTop[lx][lz] + 1
placeOak(c, lx, baseY, lz, &r)
}
}
func placeOak(c *Chunk, lx, baseY, lz int, r *chunkRand) {
h := 4 + int(r.next()%3) // trunk height 4..6
for i := 0; i < h; i++ {
c.SetBlock(lx, baseY+i, lz, StateOakLog)
}
topY := baseY + h - 1
// Canopy: two wide layers around the top, then two narrow layers above.
layers := []struct {
dy, radius int
}{{-1, 2}, {0, 2}, {1, 1}, {2, 1}}
for _, ly := range layers {
y := topY + ly.dy
for dx := -ly.radius; dx <= ly.radius; dx++ {
for dz := -ly.radius; dz <= ly.radius; dz++ {
if ly.radius == 2 && abs(dx) == 2 && abs(dz) == 2 {
continue // trim the far corners for a rounder shape
}
if c.GetBlock(lx+dx, y, lz+dz) == StateAir {
c.SetBlock(lx+dx, y, lz+dz, StateOakLeaf)
}
}
}
}
}
func abs(v int) int {
if v < 0 {
return -v
}
return v
}
// chunkRand is a tiny deterministic PRNG (SplitMix64) seeded per chunk.
type chunkRand struct{ s uint64 }
func newChunkRand(cx, cz int32, seed int64) chunkRand {
h := uint64(seed)
h ^= uint64(uint32(cx)) * 0x9E3779B97F4A7C15
h ^= uint64(uint32(cz)) * 0xC2B2AE3D27D4EB4F
return chunkRand{s: h | 1}
}
// newColumnRand seeds a deterministic PRNG from a column's world coordinates so
// each (x,z) gets a stable but independent stream (used for the random bedrock
// layer). Mixing in the world seed keeps worlds with the same terrain shape but
// different seeds distinct at the floor.
func newColumnRand(wx, wz, seed int) chunkRand {
h := uint64(seed)
h ^= uint64(uint32(wx)) * 0x9E3779B97F4A7C15
h ^= uint64(uint32(wz)) * 0xC2B2AE3D27D4EB4F
return chunkRand{s: h | 1}
}
func (r *chunkRand) next() uint32 {
r.s += 0x9E3779B97F4A7C15
z := r.s
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9
z = (z ^ (z >> 27)) * 0x94D049BB133111EB
z = z ^ (z >> 31)
return uint32(z >> 32)
}
func trilerp(c *cornerGrid, x0, y0, z0 int, fx, fy, fz float64) float64 {
x1, y1, z1 := x0+1, y0+1, z0+1
c00 := lerpf(fx, c[x0][y0][z0], c[x1][y0][z0])
c10 := lerpf(fx, c[x0][y1][z0], c[x1][y1][z0])
c01 := lerpf(fx, c[x0][y0][z1], c[x1][y0][z1])
c11 := lerpf(fx, c[x0][y1][z1], c[x1][y1][z1])
return lerpf(fz, lerpf(fy, c00, c10), lerpf(fy, c01, c11))
}
func lerpf(t, a, b float64) float64 { return a + t*(b-a) }

View file

@ -0,0 +1,7 @@
package world
import "testing"
func BenchmarkGenerateVanilla(b *testing.B){
g:=NewVanillaGenerator(12345)
b.ResetTimer(); b.ReportAllocs()
for i:=0;i<b.N;i++{ _=g(int32(i),0) }
}

View file

@ -0,0 +1,45 @@
package world
import (
"encoding/json"
"math"
"os"
"strings"
"strconv"
"testing"
)
// TestVanillaParity compares our generated surface heights against heights
// captured from the official server (seed 12345, normal terrain). Requires
// /tmp/vanilla_ground.json from the capture step.
func TestVanillaParity(t *testing.T) {
raw, err := os.ReadFile("/tmp/vanilla_ground.json")
if err != nil {
t.Skip("no vanilla capture")
}
var van map[string][]int
json.Unmarshal(raw, &van)
gen := NewVanillaGenerator(12345)
var total, exact, within1, within3 int
var maxDiff int
for key, vh := range van {
parts := strings.Split(key, ",")
cx, _ := strconv.Atoi(parts[0])
cz, _ := strconv.Atoi(parts[1])
ch := gen(int32(cx), int32(cz))
oh := ch.columnHeights()
for idx := 0; idx < 256; idx++ {
ourY := int(oh[idx]) - 65
d := int(math.Abs(float64(ourY - vh[idx])))
total++
if d == 0 { exact++ }
if d <= 1 { within1++ }
if d <= 3 { within3++ }
if d > maxDiff { maxDiff = d }
}
}
pct := func(n int) float64 { return 100 * float64(n) / float64(total) }
t.Logf("columns=%d exact=%.1f%% within1=%.1f%% within3=%.1f%% maxDiff=%d",
total, pct(exact), pct(within1), pct(within3), maxDiff)
}

158
internal/worldgen/biome.go Normal file
View file

@ -0,0 +1,158 @@
package worldgen
import "math"
// This file reproduces net.minecraft.world.level.biome.Climate, the multi-noise
// biome selector. A point in climate space is six quantized coordinates
// (temperature, humidity, continentalness, erosion, weirdness, depth); the
// finder returns the biome whose parameter range is closest to the point by the
// vanilla fitDistance metric.
//
// Coordinates are quantized to long via Math.round(v * 10000.0) exactly as the
// vanilla Climate.quantizeCoord does, and fitDistance is the sum of squared
// coordinate differences (no per-axis weighting) — matching the vanilla
// TargetPoint/ParameterPoint fitness. Range membership uses the inclusive-lower
// / exclusive-upper half-open convention vanilla applies to each axis band.
// quantize converts a climate coordinate to its long representation. Vanilla's
// Climate.quantizeCoord is Math.round(v * 10000.0); Go's math.Round halves
// away from zero, matching Java for these inputs.
func quantize(v float64) int64 {
return int64(math.Round(v * 10000.0))
}
// Quantize is the exported form of quantize, for the biome table builder in the
// world package.
func Quantize(v float64) int64 { return quantize(v) }
// AxisCount is the number of climate coordinates (temperature, humidity,
// continentalness, erosion, weirdness, depth).
const AxisCount = 6
// TargetPoint is a fully-specified climate point: the value the biome finder
// tries to match against parameter ranges. Fields are pre-quantized longs.
type TargetPoint struct {
Temperature, Humidity, Continentalness, Erosion, Weirdness, Depth int64
}
// NewTargetPoint quantizes six float climate coordinates into a TargetPoint.
func NewTargetPoint(temp, humid, cont, ero, weird, depth float64) TargetPoint {
return TargetPoint{
Temperature: quantize(temp),
Humidity: quantize(humid),
Continentalness: quantize(cont),
Erosion: quantize(ero),
Weirdness: quantize(weird),
Depth: quantize(depth),
}
}
// fitDistance is the vanilla Climate.fitness metric: the sum of squared
// differences between two points across all six axes. The squared sum is the
// comparison key; smaller is a better match.
func fitDistance(a, b TargetPoint) int64 {
dx := a.Temperature - b.Temperature
dh := a.Humidity - b.Humidity
dc := a.Continentalness - b.Continentalness
de := a.Erosion - b.Erosion
dw := a.Weirdness - b.Weirdness
dd := a.Depth - b.Depth
return dx*dx + dh*dh + dc*dc + de*de + dw*dw + dd*dd
}
// ClimateRange is one axis's [min, max] half-open band on a biome parameter.
type ClimateRange struct {
Min, Max int64
}
// contains reports whether the quantized coordinate v falls in [min, max).
func (r ClimateRange) contains(v int64) bool { return v >= r.Min && v < r.Max }
// BiomeParameter is one biome entry's full climate signature plus its name.
// Each axis is a half-open range; offset is the extra depth offset (always 0 in
// the overworld surface table, but kept for parity/future cave biomes).
type BiomeParameter struct {
Name string
// ranges[0..5] = temperature, humidity, continentalness, erosion, weirdness, depth.
Ranges [AxisCount]ClimateRange
Offset int64
}
// paramCentre returns the centre of the entry's climate ranges as a TargetPoint
// (depth centre folded in). Pre-computing this once lets the finder compare by
// distance to the centre, then verify range membership — mirroring how the
// vanilla finder prunes by fitness then tests the band.
func (p *BiomeParameter) centre() TargetPoint {
mid := func(r ClimateRange) int64 { return (r.Min + r.Max) / 2 }
return TargetPoint{
Temperature: mid(p.Ranges[0]),
Humidity: mid(p.Ranges[1]),
Continentalness: mid(p.Ranges[2]),
Erosion: mid(p.Ranges[3]),
Weirdness: mid(p.Ranges[4]),
Depth: mid(p.Ranges[5]),
}
}
// ParameterTable is the set of biome parameters the finder searches.
type ParameterTable struct {
entries []tableEntry
}
// tableEntry pairs a parameter with its precomputed centre for fast pruning.
type tableEntry struct {
param BiomeParameter
centre TargetPoint
}
// NewParameterTable builds a searchable table from raw biome parameters.
func NewParameterTable(params []BiomeParameter) *ParameterTable {
t := &ParameterTable{entries: make([]tableEntry, len(params))}
for i, p := range params {
t.entries[i] = tableEntry{param: p, centre: p.centre()}
}
return t
}
// FindBiome returns the name of the biome whose range best matches point, by
// the vanilla fitDistance metric among entries whose ranges all contain point.
// If no entry's ranges contain point (should not happen for the overworld table,
// which tiles climate space), it falls back to the nearest centre.
func (t *ParameterTable) FindBiome(point TargetPoint) string {
var best string
bestDist := int64(math.MaxInt64)
var fallback string
fallbackDist := int64(math.MaxInt64)
for _, e := range t.entries {
// Distance to centre is the pruning key (precomputed). Track it always
// so we have a fallback if no range contains the point.
d := fitDistance(point, e.centre)
if d < fallbackDist {
fallbackDist = d
fallback = e.param.Name
}
// Only consider entries whose ranges actually contain the point.
if !containsAll(e.param.Ranges, point) {
continue
}
if d < bestDist {
bestDist = d
best = e.param.Name
}
}
if best != "" {
return best
}
return fallback
}
// containsAll reports whether every range contains its corresponding coordinate.
func containsAll(ranges [AxisCount]ClimateRange, p TargetPoint) bool {
return ranges[0].contains(p.Temperature) &&
ranges[1].contains(p.Humidity) &&
ranges[2].contains(p.Continentalness) &&
ranges[3].contains(p.Erosion) &&
ranges[4].contains(p.Weirdness) &&
ranges[5].contains(p.Depth)
}

View file

@ -0,0 +1,96 @@
package worldgen
import (
"testing"
)
func TestQuantize(t *testing.T) {
cases := []struct {
v float64
want int64
}{
{0.0, 0},
{0.5, 5000},
{-1.0, -10000},
{1.0, 10000},
{-0.15, -1500},
{0.55, 5500},
}
for _, c := range cases {
if got := quantize(c.v); got != c.want {
t.Errorf("quantize(%v) = %d, want %d", c.v, got, c.want)
}
}
}
// TestFitDistanceZero confirms identical points are zero-distance and distinct
// points are positive; the exact value is not asserted to stay robust to
// representation choices.
func TestFitDistance(t *testing.T) {
a := NewTargetPoint(0, 0, 0, 0, 0, 0)
if got := fitDistance(a, a); got != 0 {
t.Errorf("fitDistance(a,a) = %d, want 0", got)
}
b := NewTargetPoint(1, 0, 0, 0, 0, 0)
// 10000^2 per axis of difference.
if got := fitDistance(a, b); got != 10000*10000 {
t.Errorf("fitDistance for 1.0 temp diff = %d, want %d", got, int64(10000*10000))
}
}
// TestRangeContains checks the half-open [min, max) band used by the finder.
func TestRangeContains(t *testing.T) {
r := ClimateRange{Min: 0, Max: 100}
if !r.contains(0) {
t.Error("min should be inclusive")
}
if r.contains(100) {
t.Error("max should be exclusive")
}
if !r.contains(50) {
t.Error("interior should contain")
}
}
// TestSampleColumnDeterministic verifies the same seed/coords give the same
// biome and a different seed gives (almost certainly) a different one.
func TestSampleColumnDeterministic(t *testing.T) {
od1, err := LoadOverworldFinalDensity(1)
if err != nil {
t.Fatalf("load seed 1: %v", err)
}
od2, err := LoadOverworldFinalDensity(99999)
if err != nil {
t.Fatalf("load seed 99999: %v", err)
}
p1a := SampleColumn(od1, 63, 100, 200)
p1b := SampleColumn(od1, 63, 100, 200)
if p1a != p1b {
t.Error("same seed/coords should produce identical TargetPoint")
}
p2 := SampleColumn(od2, 63, 100, 200)
if p1a == p2 {
// Not a hard failure (collisions exist), but flag it for inspection.
t.Log("note: different seed produced identical climate point at (100,200)")
}
}
// TestClimateFieldsLoaded confirms the loader populates all six climate axes
// from the noise_router (regression guard for the loader change).
func TestClimateFieldsLoaded(t *testing.T) {
od, err := LoadOverworldFinalDensity(42)
if err != nil {
t.Fatalf("load: %v", err)
}
if od.Final == nil {
t.Fatal("Final density not loaded")
}
dfs := []DensityFunction{od.Temperature, od.Humidity, od.Continentalness, od.Erosion, od.Weirdness, od.Depth}
for i, df := range dfs {
if df == nil {
t.Errorf("climate axis %d not loaded", i)
}
}
}

View file

@ -0,0 +1,102 @@
package worldgen
// BlendedNoise is the old_blended_noise density function: the legacy 3D
// terrain noise built from min/max limit noises and a main noise. Transcribed
// from the official BlendedNoise; the building-block noises are validated
// bit-for-bit against captured reference values.
type BlendedNoise struct {
minLimit, maxLimit, main *PerlinNoise
xzScale, yScale, xzFactor, yFactor float64
smearScaleMultiplier float64
xzMultiplier, yMultiplier float64
maxValue float64
}
// NewBlendedNoise builds a BlendedNoise from r (legacy seeding: three Perlin
// stacks drawn sequentially) and the scale parameters.
func NewBlendedNoise(r RandomSource, xzScale, yScale, xzFactor, yFactor, smearScaleMultiplier float64) *BlendedNoise {
b := &BlendedNoise{
minLimit: legacyOctaves(r, -15, 0),
maxLimit: legacyOctaves(r, -15, 0),
main: legacyOctaves(r, -7, 0),
xzScale: xzScale,
yScale: yScale,
xzFactor: xzFactor,
yFactor: yFactor,
smearScaleMultiplier: smearScaleMultiplier,
}
b.xzMultiplier = 684.412 * xzScale
b.yMultiplier = 684.412 * yScale
b.maxValue = b.minLimit.MaxBrokenValue(b.yMultiplier)
return b
}
// legacyOctaves creates a legacy PerlinNoise over the inclusive octave range
// [firstOctave, lastOctave], all amplitudes 1 (PerlinNoise.makeAmplitudes).
func legacyOctaves(r RandomSource, firstOctave, lastOctave int) *PerlinNoise {
count := lastOctave - firstOctave + 1
amps := make([]float64, count)
for i := range amps {
amps[i] = 1.0
}
return NewLegacyPerlinNoise(r, firstOctave, amps)
}
// Compute samples the blended noise at (x, y, z).
func (b *BlendedNoise) Compute(c FunctionContext) float64 {
limitX := c.X * b.xzMultiplier
limitY := c.Y * b.yMultiplier
limitZ := c.Z * b.xzMultiplier
mainX := limitX / b.xzFactor
mainY := limitY / b.yFactor
mainZ := limitZ / b.xzFactor
limitSmear := b.yMultiplier * b.smearScaleMultiplier
mainSmear := limitSmear / b.yFactor
mainNoiseValue := 0.0
pow := 1.0
for i := 0; i < 8; i++ {
if oct := b.main.GetOctaveNoise(i); oct != nil {
mainNoiseValue += oct.NoiseY(wrap(mainX*pow), wrap(mainY*pow), wrap(mainZ*pow), mainSmear*pow, mainY*pow) / pow
}
pow /= 2.0
}
factor := (mainNoiseValue/10.0 + 1.0) / 2.0
isMax := factor >= 1.0
isMin := factor <= 0.0
blendMin, blendMax := 0.0, 0.0
pow = 1.0
for i := 0; i < 16; i++ {
wx := wrap(limitX * pow)
wy := wrap(limitY * pow)
wz := wrap(limitZ * pow)
yScalePow := limitSmear * pow
if !isMax {
if oct := b.minLimit.GetOctaveNoise(i); oct != nil {
blendMin += oct.NoiseY(wx, wy, wz, yScalePow, limitY*pow) / pow
}
}
if !isMin {
if oct := b.maxLimit.GetOctaveNoise(i); oct != nil {
blendMax += oct.NoiseY(wx, wy, wz, yScalePow, limitY*pow) / pow
}
}
pow /= 2.0
}
return clampedLerp(factor, blendMin/512.0, blendMax/512.0) / 128.0
}
// clampedLerp is Mth.clampedLerp(factor, min, max): min if factor<0, max if
// factor>1, otherwise linear interpolation.
func clampedLerp(factor, min, max float64) float64 {
if factor < 0 {
return min
}
if factor > 1 {
return max
}
return min + factor*(max-min)
}

View file

@ -0,0 +1,29 @@
package worldgen
import (
"math"
"testing"
)
func approx(t *testing.T, name string, got, want float64) {
t.Helper()
if math.Abs(got-want) > 1e-12 {
t.Fatalf("%s = %v, want %v", name, got, want)
}
}
func TestImprovedNoise5Arg(t *testing.T) {
n := NewImprovedNoise(NewXoroshiro(42))
approx(t, "imp5(1.5,2.5,3.5,0.1,0.2)", n.NoiseY(1.5, 2.5, 3.5, 0.1, 0.2), 0.33416541490816576)
approx(t, "imp5(100.1,64,-200.7,0.5,1.3)", n.NoiseY(100.1, 64.0, -200.7, 0.5, 1.3), -0.31688479572345046)
}
func TestLegacyPerlinNoise(t *testing.T) {
pn := legacyOctaves(NewXoroshiro(42), -15, 0) // PerlinNoise.createLegacyForBlendedNoise(-15..0)
approx(t, "octave0.xo", pn.GetOctaveNoise(0).Xo, 190.83062484342904)
approx(t, "octave15.xo", pn.GetOctaveNoise(15).Xo, 128.19773398126475)
approx(t, "maxBrokenValue(85.5515)", pn.MaxBrokenValue(85.5515), 87.55150000000002)
approx(t, "pn5(0.5,0.5,0.5,0.3,1.1)", pn.GetValueY(0.5, 0.5, 0.5, 0.3, 1.1), 0.03454101275150972)
approx(t, "pn5(12.3,45.6,-78.9,0.3,1.1)", pn.GetValueY(12.3, 45.6, -78.9, 0.3, 1.1), 0.03853671559715216)
}

View file

@ -0,0 +1,16 @@
package worldgen
import ("math";"testing")
func TestBlendedCompute(t *testing.T){
bn:=NewBlendedNoise(NewXoroshiro(42),0.25,0.125,80.0,160.0,8.0)
cases:=[]struct{x,y,z float64;want float64}{
{0,64,0,-0.012282880040235755},
{100,40,-200,0.007126388725845459},
{1234,80,-5678,-0.13408933571823986},
{-37,128,99,-0.0932958728408956},
{8,200,8,0.0075622598474688885},
}
for _,c:=range cases{
got:=bn.Compute(FunctionContext{X:c.x,Y:c.y,Z:c.z})
if math.Abs(got-c.want)>1e-12 { t.Fatalf("bn(%v,%v,%v)=%v want %v (diff %v)",c.x,c.y,c.z,got,c.want,got-c.want) }
}
}

View file

@ -0,0 +1,37 @@
package worldgen
// This file samples the climate density functions into a TargetPoint for the
// biome finder. The climate router keys are 2D (flat_cache + y_scale=0) except
// depth, which is 3D. For surface biome selection we fix depth to 0.0, matching
// the depth=0 (surface) entries of the biome parameter table; underground and
// cave biomes use depth=1.0 / non-zero offset and are a later milestone.
// SampleColumn evaluates the six climate parameters at block (wx, wz) using od
// and returns the TargetPoint for surface biome lookup. seaLevelY is the Y at
// which to sample the 2D climate noises (callers pass the world sea level).
func SampleColumn(od *OverworldDensity, seaLevelY int, wx, wz int) TargetPoint {
ctx := FunctionContext{X: float64(wx), Y: float64(seaLevelY), Z: float64(wz)}
temp := computeOrZero(od.Temperature, ctx)
humid := computeOrZero(od.Humidity, ctx)
cont := computeOrZero(od.Continentalness, ctx)
ero := computeOrZero(od.Erosion, ctx)
weird := computeOrZero(od.Weirdness, ctx)
// Surface layer: depth axis is fixed at 0.0 so only the depth=0 (surface)
// biome parameter entries match. The real 3D depth is consulted in the
// per-cell milestone.
const surfaceDepth = 0.0
return NewTargetPoint(temp, humid, cont, ero, weird, surfaceDepth)
}
// computeOrZero evaluates df at ctx, returning 0 when df is nil (a climate key
// absent from the router). This keeps sampling robust without special-casing
// each axis at the call site.
func computeOrZero(df DensityFunction, ctx FunctionContext) float64 {
if df == nil {
return 0
}
return df.Compute(ctx)
}

View file

@ -0,0 +1,8 @@
{
"type": "minecraft:old_blended_noise",
"smear_scale_multiplier": 8.0,
"xz_factor": 80.0,
"xz_scale": 0.25,
"y_factor": 160.0,
"y_scale": 0.125
}

View file

@ -0,0 +1,83 @@
{
"type": "minecraft:cache_once",
"argument": {
"type": "minecraft:min",
"argument1": {
"type": "minecraft:add",
"argument1": {
"type": "minecraft:add",
"argument1": 0.37,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:cave_entrance",
"xz_scale": 0.75,
"y_scale": 0.5
}
},
"argument2": {
"type": "minecraft:y_clamped_gradient",
"from_value": 0.3,
"from_y": -10,
"to_value": 0.0,
"to_y": 30
}
},
"argument2": {
"type": "minecraft:add",
"argument1": "minecraft:overworld/caves/spaghetti_roughness_function",
"argument2": {
"type": "minecraft:clamp",
"input": {
"type": "minecraft:add",
"argument1": {
"type": "minecraft:max",
"argument1": {
"type": "minecraft:weird_scaled_sampler",
"input": {
"type": "minecraft:cache_once",
"argument": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_3d_rarity",
"xz_scale": 2.0,
"y_scale": 1.0
}
},
"noise": "minecraft:spaghetti_3d_1",
"rarity_value_mapper": "type_1"
},
"argument2": {
"type": "minecraft:weird_scaled_sampler",
"input": {
"type": "minecraft:cache_once",
"argument": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_3d_rarity",
"xz_scale": 2.0,
"y_scale": 1.0
}
},
"noise": "minecraft:spaghetti_3d_2",
"rarity_value_mapper": "type_1"
}
},
"argument2": {
"type": "minecraft:add",
"argument1": -0.0765,
"argument2": {
"type": "minecraft:mul",
"argument1": -0.011499999999999996,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_3d_thickness",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
}
},
"max": 1.0,
"min": -1.0
}
}
}
}

View file

@ -0,0 +1,94 @@
{
"type": "minecraft:range_choice",
"input": {
"type": "minecraft:interpolated",
"argument": {
"type": "minecraft:range_choice",
"input": "minecraft:y",
"max_exclusive": 321.0,
"min_inclusive": -60.0,
"when_in_range": {
"type": "minecraft:noise",
"noise": "minecraft:noodle",
"xz_scale": 1.0,
"y_scale": 1.0
},
"when_out_of_range": -1.0
}
},
"max_exclusive": 0.0,
"min_inclusive": -1000000.0,
"when_in_range": 64.0,
"when_out_of_range": {
"type": "minecraft:add",
"argument1": {
"type": "minecraft:interpolated",
"argument": {
"type": "minecraft:range_choice",
"input": "minecraft:y",
"max_exclusive": 321.0,
"min_inclusive": -60.0,
"when_in_range": {
"type": "minecraft:add",
"argument1": -0.07500000000000001,
"argument2": {
"type": "minecraft:mul",
"argument1": -0.025,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:noodle_thickness",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
},
"when_out_of_range": 0.0
}
},
"argument2": {
"type": "minecraft:mul",
"argument1": 1.5,
"argument2": {
"type": "minecraft:max",
"argument1": {
"type": "minecraft:abs",
"argument": {
"type": "minecraft:interpolated",
"argument": {
"type": "minecraft:range_choice",
"input": "minecraft:y",
"max_exclusive": 321.0,
"min_inclusive": -60.0,
"when_in_range": {
"type": "minecraft:noise",
"noise": "minecraft:noodle_ridge_a",
"xz_scale": 2.6666666666666665,
"y_scale": 2.6666666666666665
},
"when_out_of_range": 0.0
}
}
},
"argument2": {
"type": "minecraft:abs",
"argument": {
"type": "minecraft:interpolated",
"argument": {
"type": "minecraft:range_choice",
"input": "minecraft:y",
"max_exclusive": 321.0,
"min_inclusive": -60.0,
"when_in_range": {
"type": "minecraft:noise",
"noise": "minecraft:noodle_ridge_b",
"xz_scale": 2.6666666666666665,
"y_scale": 2.6666666666666665
},
"when_out_of_range": 0.0
}
}
}
}
}
}
}

View file

@ -0,0 +1,50 @@
{
"type": "minecraft:cache_once",
"argument": {
"type": "minecraft:mul",
"argument1": {
"type": "minecraft:add",
"argument1": {
"type": "minecraft:mul",
"argument1": 2.0,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:pillar",
"xz_scale": 25.0,
"y_scale": 0.3
}
},
"argument2": {
"type": "minecraft:add",
"argument1": -1.0,
"argument2": {
"type": "minecraft:mul",
"argument1": -1.0,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:pillar_rareness",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
}
},
"argument2": {
"type": "minecraft:cube",
"argument": {
"type": "minecraft:add",
"argument1": 0.55,
"argument2": {
"type": "minecraft:mul",
"argument1": 0.55,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:pillar_thickness",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
}
}
}
}

View file

@ -0,0 +1,61 @@
{
"type": "minecraft:clamp",
"input": {
"type": "minecraft:max",
"argument1": {
"type": "minecraft:add",
"argument1": {
"type": "minecraft:weird_scaled_sampler",
"input": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_2d_modulator",
"xz_scale": 2.0,
"y_scale": 1.0
},
"noise": "minecraft:spaghetti_2d",
"rarity_value_mapper": "type_2"
},
"argument2": {
"type": "minecraft:mul",
"argument1": 0.083,
"argument2": "minecraft:overworld/caves/spaghetti_2d_thickness_modulator"
}
},
"argument2": {
"type": "minecraft:cube",
"argument": {
"type": "minecraft:add",
"argument1": {
"type": "minecraft:abs",
"argument": {
"type": "minecraft:add",
"argument1": {
"type": "minecraft:add",
"argument1": 0.0,
"argument2": {
"type": "minecraft:mul",
"argument1": 8.0,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_2d_elevation",
"xz_scale": 1.0,
"y_scale": 0.0
}
}
},
"argument2": {
"type": "minecraft:y_clamped_gradient",
"from_value": 8.0,
"from_y": -64,
"to_value": -40.0,
"to_y": 320
}
}
},
"argument2": "minecraft:overworld/caves/spaghetti_2d_thickness_modulator"
}
}
},
"max": 1.0,
"min": -1.0
}

View file

@ -0,0 +1,17 @@
{
"type": "minecraft:cache_once",
"argument": {
"type": "minecraft:add",
"argument1": -0.95,
"argument2": {
"type": "minecraft:mul",
"argument1": -0.35000000000000003,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_2d_thickness",
"xz_scale": 2.0,
"y_scale": 1.0
}
}
}
}

View file

@ -0,0 +1,33 @@
{
"type": "minecraft:cache_once",
"argument": {
"type": "minecraft:mul",
"argument1": {
"type": "minecraft:add",
"argument1": -0.05,
"argument2": {
"type": "minecraft:mul",
"argument1": -0.05,
"argument2": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_roughness_modulator",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
},
"argument2": {
"type": "minecraft:add",
"argument1": -0.4,
"argument2": {
"type": "minecraft:abs",
"argument": {
"type": "minecraft:noise",
"noise": "minecraft:spaghetti_roughness",
"xz_scale": 1.0,
"y_scale": 1.0
}
}
}
}
}

View file

@ -0,0 +1,12 @@
{
"type": "minecraft:flat_cache",
"argument": {
"type": "minecraft:shifted_noise",
"noise": "minecraft:continentalness",
"shift_x": "minecraft:shift_x",
"shift_y": 0.0,
"shift_z": "minecraft:shift_z",
"xz_scale": 0.25,
"y_scale": 0.0
}
}

View file

@ -0,0 +1,11 @@
{
"type": "minecraft:add",
"argument1": {
"type": "minecraft:y_clamped_gradient",
"from_value": 1.5,
"from_y": -64,
"to_value": -1.5,
"to_y": 320
},
"argument2": "minecraft:overworld/offset"
}

View file

@ -0,0 +1,12 @@
{
"type": "minecraft:flat_cache",
"argument": {
"type": "minecraft:shifted_noise",
"noise": "minecraft:erosion",
"shift_x": "minecraft:shift_x",
"shift_y": 0.0,
"shift_z": "minecraft:shift_z",
"xz_scale": 0.25,
"y_scale": 0.0
}
}

View file

@ -0,0 +1,890 @@
{
"type": "minecraft:flat_cache",
"argument": {
"type": "minecraft:cache_2d",
"argument": {
"type": "minecraft:add",
"argument1": 10.0,
"argument2": {
"type": "minecraft:mul",
"argument1": {
"type": "minecraft:blend_alpha"
},
"argument2": {
"type": "minecraft:add",
"argument1": -10.0,
"argument2": {
"type": "minecraft:spline",
"spline": {
"coordinate": "minecraft:overworld/continents",
"points": [
{
"derivative": 0.0,
"location": -0.19,
"value": 3.95
},
{
"derivative": 0.0,
"location": -0.15,
"value": {
"coordinate": "minecraft:overworld/erosion",
"points": [
{
"derivative": 0.0,
"location": -0.6,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 6.25
}
]
}
},
{
"derivative": 0.0,
"location": -0.5,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.05,
"value": 2.67
}
]
}
},
{
"derivative": 0.0,
"location": -0.35,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 6.25
}
]
}
},
{
"derivative": 0.0,
"location": -0.25,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 6.25
}
]
}
},
{
"derivative": 0.0,
"location": -0.1,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 2.67
},
{
"derivative": 0.0,
"location": 0.05,
"value": 6.3
}
]
}
},
{
"derivative": 0.0,
"location": 0.03,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 6.25
}
]
}
},
{
"derivative": 0.0,
"location": 0.35,
"value": 6.25
},
{
"derivative": 0.0,
"location": 0.45,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.9,
"value": 6.25
},
{
"derivative": 0.0,
"location": -0.69,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": 0.0,
"value": 6.25
},
{
"derivative": 0.0,
"location": 0.1,
"value": 0.625
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": 0.55,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.9,
"value": 6.25
},
{
"derivative": 0.0,
"location": -0.69,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": 0.0,
"value": 6.25
},
{
"derivative": 0.0,
"location": 0.1,
"value": 0.625
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": 0.62,
"value": 6.25
}
]
}
},
{
"derivative": 0.0,
"location": -0.1,
"value": {
"coordinate": "minecraft:overworld/erosion",
"points": [
{
"derivative": 0.0,
"location": -0.6,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.47
}
]
}
},
{
"derivative": 0.0,
"location": -0.5,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.05,
"value": 2.67
}
]
}
},
{
"derivative": 0.0,
"location": -0.35,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.47
}
]
}
},
{
"derivative": 0.0,
"location": -0.25,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.47
}
]
}
},
{
"derivative": 0.0,
"location": -0.1,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 2.67
},
{
"derivative": 0.0,
"location": 0.05,
"value": 6.3
}
]
}
},
{
"derivative": 0.0,
"location": 0.03,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.47
}
]
}
},
{
"derivative": 0.0,
"location": 0.35,
"value": 5.47
},
{
"derivative": 0.0,
"location": 0.45,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.9,
"value": 5.47
},
{
"derivative": 0.0,
"location": -0.69,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": 0.0,
"value": 5.47
},
{
"derivative": 0.0,
"location": 0.1,
"value": 0.625
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": 0.55,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.9,
"value": 5.47
},
{
"derivative": 0.0,
"location": -0.69,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": 0.0,
"value": 5.47
},
{
"derivative": 0.0,
"location": 0.1,
"value": 0.625
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": 0.62,
"value": 5.47
}
]
}
},
{
"derivative": 0.0,
"location": 0.03,
"value": {
"coordinate": "minecraft:overworld/erosion",
"points": [
{
"derivative": 0.0,
"location": -0.6,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.08
}
]
}
},
{
"derivative": 0.0,
"location": -0.5,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.05,
"value": 2.67
}
]
}
},
{
"derivative": 0.0,
"location": -0.35,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.08
}
]
}
},
{
"derivative": 0.0,
"location": -0.25,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.08
}
]
}
},
{
"derivative": 0.0,
"location": -0.1,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 2.67
},
{
"derivative": 0.0,
"location": 0.05,
"value": 6.3
}
]
}
},
{
"derivative": 0.0,
"location": 0.03,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 5.08
}
]
}
},
{
"derivative": 0.0,
"location": 0.35,
"value": 5.08
},
{
"derivative": 0.0,
"location": 0.45,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.9,
"value": 5.08
},
{
"derivative": 0.0,
"location": -0.69,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": 0.0,
"value": 5.08
},
{
"derivative": 0.0,
"location": 0.1,
"value": 0.625
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": 0.55,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.9,
"value": 5.08
},
{
"derivative": 0.0,
"location": -0.69,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": 0.0,
"value": 5.08
},
{
"derivative": 0.0,
"location": 0.1,
"value": 0.625
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": 0.62,
"value": 5.08
}
]
}
},
{
"derivative": 0.0,
"location": 0.06,
"value": {
"coordinate": "minecraft:overworld/erosion",
"points": [
{
"derivative": 0.0,
"location": -0.6,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": -0.5,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.05,
"value": 2.67
}
]
}
},
{
"derivative": 0.0,
"location": -0.35,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": -0.25,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": -0.1,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.05,
"value": 2.67
},
{
"derivative": 0.0,
"location": 0.05,
"value": 6.3
}
]
}
},
{
"derivative": 0.0,
"location": 0.03,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": 0.05,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.45,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": 0.7,
"value": 1.56
}
]
}
},
{
"derivative": 0.0,
"location": 0.4,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.45,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": 0.7,
"value": 1.56
}
]
}
},
{
"derivative": 0.0,
"location": 0.45,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.7,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": -0.15,
"value": 1.37
}
]
}
},
{
"derivative": 0.0,
"location": 0.55,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": -0.7,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.2,
"value": 6.3
},
{
"derivative": 0.0,
"location": 0.2,
"value": 4.69
}
]
}
},
{
"derivative": 0.0,
"location": -0.15,
"value": 1.37
}
]
}
},
{
"derivative": 0.0,
"location": 0.58,
"value": 4.69
}
]
}
}
]
}
}
}
}
}
}
}

View file

@ -0,0 +1,303 @@
{
"type": "minecraft:flat_cache",
"argument": {
"type": "minecraft:cache_2d",
"argument": {
"type": "minecraft:add",
"argument1": 0.0,
"argument2": {
"type": "minecraft:mul",
"argument1": {
"type": "minecraft:blend_alpha"
},
"argument2": {
"type": "minecraft:add",
"argument1": -0.0,
"argument2": {
"type": "minecraft:spline",
"spline": {
"coordinate": "minecraft:overworld/continents",
"points": [
{
"derivative": 0.0,
"location": -0.11,
"value": 0.0
},
{
"derivative": 0.0,
"location": 0.03,
"value": {
"coordinate": "minecraft:overworld/erosion",
"points": [
{
"derivative": 0.0,
"location": -1.0,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.19999999,
"value": 0.0
},
{
"derivative": 0.0,
"location": 0.44999996,
"value": 0.0
},
{
"derivative": 0.0,
"location": 1.0,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.01,
"value": 0.63
},
{
"derivative": 0.0,
"location": 0.01,
"value": 0.3
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": -0.78,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.19999999,
"value": 0.0
},
{
"derivative": 0.0,
"location": 0.44999996,
"value": 0.0
},
{
"derivative": 0.0,
"location": 1.0,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.01,
"value": 0.315
},
{
"derivative": 0.0,
"location": 0.01,
"value": 0.15
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": -0.5775,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.19999999,
"value": 0.0
},
{
"derivative": 0.0,
"location": 0.44999996,
"value": 0.0
},
{
"derivative": 0.0,
"location": 1.0,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.01,
"value": 0.315
},
{
"derivative": 0.0,
"location": 0.01,
"value": 0.15
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": -0.375,
"value": 0.0
}
]
}
},
{
"derivative": 0.0,
"location": 0.65,
"value": {
"coordinate": "minecraft:overworld/erosion",
"points": [
{
"derivative": 0.0,
"location": -1.0,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.19999999,
"value": 0.0
},
{
"derivative": 0.0,
"location": 0.44999996,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.01,
"value": 0.63
},
{
"derivative": 0.0,
"location": 0.01,
"value": 0.3
}
]
}
},
{
"derivative": 0.0,
"location": 1.0,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.01,
"value": 0.63
},
{
"derivative": 0.0,
"location": 0.01,
"value": 0.3
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": -0.78,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.19999999,
"value": 0.0
},
{
"derivative": 0.0,
"location": 0.44999996,
"value": 0.0
},
{
"derivative": 0.0,
"location": 1.0,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.01,
"value": 0.63
},
{
"derivative": 0.0,
"location": 0.01,
"value": 0.3
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": -0.5775,
"value": {
"coordinate": "minecraft:overworld/ridges_folded",
"points": [
{
"derivative": 0.0,
"location": 0.19999999,
"value": 0.0
},
{
"derivative": 0.0,
"location": 0.44999996,
"value": 0.0
},
{
"derivative": 0.0,
"location": 1.0,
"value": {
"coordinate": "minecraft:overworld/ridges",
"points": [
{
"derivative": 0.0,
"location": -0.01,
"value": 0.63
},
{
"derivative": 0.0,
"location": 0.01,
"value": 0.3
}
]
}
}
]
}
},
{
"derivative": 0.0,
"location": -0.375,
"value": 0.0
}
]
}
}
]
}
}
}
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
{
"type": "minecraft:flat_cache",
"argument": {
"type": "minecraft:shifted_noise",
"noise": "minecraft:ridge",
"shift_x": "minecraft:shift_x",
"shift_y": 0.0,
"shift_z": "minecraft:shift_z",
"xz_scale": 0.25,
"y_scale": 0.0
}
}

View file

@ -0,0 +1,19 @@
{
"type": "minecraft:mul",
"argument1": -3.0,
"argument2": {
"type": "minecraft:add",
"argument1": -0.3333333333333333,
"argument2": {
"type": "minecraft:abs",
"argument": {
"type": "minecraft:add",
"argument1": -0.6666666666666666,
"argument2": {
"type": "minecraft:abs",
"argument": "minecraft:overworld/ridges"
}
}
}
}
}

View file

@ -0,0 +1,32 @@
{
"type": "minecraft:add",
"argument1": {
"type": "minecraft:mul",
"argument1": 4.0,
"argument2": {
"type": "minecraft:quarter_negative",
"argument": {
"type": "minecraft:mul",
"argument1": {
"type": "minecraft:add",
"argument1": "minecraft:overworld/depth",
"argument2": {
"type": "minecraft:mul",
"argument1": "minecraft:overworld/jaggedness",
"argument2": {
"type": "minecraft:half_negative",
"argument": {
"type": "minecraft:noise",
"noise": "minecraft:jagged",
"xz_scale": 1500.0,
"y_scale": 0.0
}
}
}
},
"argument2": "minecraft:overworld/factor"
}
}
},
"argument2": "minecraft:overworld/base_3d_noise"
}

View file

@ -0,0 +1,10 @@
{
"type": "minecraft:flat_cache",
"argument": {
"type": "minecraft:cache_2d",
"argument": {
"type": "minecraft:shift_a",
"argument": "minecraft:offset"
}
}
}

View file

@ -0,0 +1,10 @@
{
"type": "minecraft:flat_cache",
"argument": {
"type": "minecraft:cache_2d",
"argument": {
"type": "minecraft:shift_b",
"argument": "minecraft:offset"
}
}
}

View file

@ -0,0 +1,7 @@
{
"type": "minecraft:y_clamped_gradient",
"from_value": -4064.0,
"from_y": -4064,
"to_value": 4062.0,
"to_y": 4062
}

View file

@ -0,0 +1 @@
0.0

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -3
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -7
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -5
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -1
}

View file

@ -0,0 +1,9 @@
{
"amplitudes": [
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -2
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -8
}

View file

@ -0,0 +1,8 @@
{
"amplitudes": [
1.0,
1.0,
1.0
],
"firstOctave": -6
}

View file

@ -0,0 +1,9 @@
{
"amplitudes": [
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -9
}

View file

@ -0,0 +1,14 @@
{
"amplitudes": [
0.5,
1.0,
2.0,
1.0,
2.0,
1.0,
0.0,
2.0,
0.0
],
"firstOctave": -8
}

View file

@ -0,0 +1,8 @@
{
"amplitudes": [
0.4,
0.5,
1.0
],
"firstOctave": -7
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -8
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -8
}

View file

@ -0,0 +1,14 @@
{
"amplitudes": [
1.0,
1.0,
2.0,
2.0,
2.0,
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -9
}

View file

@ -0,0 +1,14 @@
{
"amplitudes": [
1.0,
1.0,
2.0,
2.0,
2.0,
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -11
}

View file

@ -0,0 +1,10 @@
{
"amplitudes": [
1.0,
1.0,
0.0,
1.0,
1.0
],
"firstOctave": -9
}

View file

@ -0,0 +1,10 @@
{
"amplitudes": [
1.0,
1.0,
0.0,
1.0,
1.0
],
"firstOctave": -11
}

View file

@ -0,0 +1,9 @@
{
"amplitudes": [
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -8
}

View file

@ -0,0 +1,14 @@
{
"amplitudes": [
1.0,
1.0,
1.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.013333333333333334
],
"firstOctave": -8
}

View file

@ -0,0 +1,9 @@
{
"amplitudes": [
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -4
}

View file

@ -0,0 +1,9 @@
{
"amplitudes": [
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -6
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -3
}

View file

@ -0,0 +1,8 @@
{
"amplitudes": [
1.0,
1.0,
1.0
],
"firstOctave": -6
}

View file

@ -0,0 +1,21 @@
{
"amplitudes": [
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0
],
"firstOctave": -16
}

View file

@ -0,0 +1,7 @@
{
"amplitudes": [
1.0,
1.0
],
"firstOctave": -7
}

View file

@ -0,0 +1,7 @@
{
"amplitudes": [
1.0,
1.0
],
"firstOctave": -7
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -4
}

View file

@ -0,0 +1,9 @@
{
"amplitudes": [
1.0,
0.0,
0.0,
0.9
],
"firstOctave": -3
}

View file

@ -0,0 +1,9 @@
{
"amplitudes": [
1.0,
0.0,
0.0,
0.35
],
"firstOctave": -3
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -8
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -7
}

View file

@ -0,0 +1,6 @@
{
"amplitudes": [
1.0
],
"firstOctave": -7
}

Some files were not shown because too many files have changed in this diff Show more