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:
commit
a7bb9496ae
146 changed files with 217621 additions and 0 deletions
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal 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
63
README.md
Normal 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
60
cmd/regionio/main.go
Normal 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
3
go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module regionio
|
||||
|
||||
go 1.26.3
|
||||
229
internal/nbt/decode.go
Normal file
229
internal/nbt/decode.go
Normal 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
95
internal/nbt/encode.go
Normal 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
59
internal/nbt/mutf8.go
Normal 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
98
internal/nbt/nbt.go
Normal 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
119
internal/nbt/nbt_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
220
internal/network/configuration.go
Normal file
220
internal/network/configuration.go
Normal 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
89
internal/network/conn.go
Normal 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
133
internal/network/handler.go
Normal 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")
|
||||
}
|
||||
}
|
||||
64
internal/network/listener.go
Normal file
64
internal/network/listener.go
Normal 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
95
internal/network/login.go
Normal 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
404
internal/network/play.go
Normal 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
228
internal/protocol/buffer.go
Normal 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
139
internal/protocol/frame.go
Normal 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
114
internal/protocol/ids.go
Normal 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
137
internal/protocol/types.go
Normal 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
|
||||
}
|
||||
79
internal/registry/registry.go
Normal file
79
internal/registry/registry.go
Normal 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"}
|
||||
524
internal/registry/synced_registries.json
Normal file
524
internal/registry/synced_registries.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
BIN
internal/registry/synced_tags.bin
Normal file
BIN
internal/registry/synced_tags.bin
Normal file
Binary file not shown.
21
internal/server/profile.go
Normal file
21
internal/server/profile.go
Normal 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
98
internal/server/server.go
Normal 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)
|
||||
}
|
||||
79
internal/world/bench_test.go
Normal file
79
internal/world/bench_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
119
internal/world/biome_lookup.go
Normal file
119
internal/world/biome_lookup.go
Normal 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
|
||||
}
|
||||
73
internal/world/biome_lookup_test.go
Normal file
73
internal/world/biome_lookup_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
205021
internal/world/biome_parameters.json
Normal file
205021
internal/world/biome_parameters.json
Normal file
File diff suppressed because it is too large
Load diff
106
internal/world/cache.go
Normal file
106
internal/world/cache.go
Normal 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
23
internal/world/chunk.go
Normal 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
269
internal/world/encode.go
Normal 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))
|
||||
}
|
||||
154
internal/world/encode_test.go
Normal file
154
internal/world/encode_test.go
Normal 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]))
|
||||
}
|
||||
46
internal/world/golden_test.go
Normal file
46
internal/world/golden_test.go
Normal 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))
|
||||
}
|
||||
}
|
||||
1
internal/world/item_blocks.json
Normal file
1
internal/world/item_blocks.json
Normal file
File diff suppressed because one or more lines are too long
38
internal/world/items.go
Normal file
38
internal/world/items.go
Normal 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
45
internal/world/light.go
Normal 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
88
internal/world/terrain.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
9
internal/world/terrain_bench_test.go
Normal file
9
internal/world/terrain_bench_test.go
Normal 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) }
|
||||
}
|
||||
30
internal/world/terrain_debug_test.go
Normal file
30
internal/world/terrain_debug_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
BIN
internal/world/testdata/vanilla_flat_chunk.bin
vendored
Normal file
BIN
internal/world/testdata/vanilla_flat_chunk.bin
vendored
Normal file
Binary file not shown.
266
internal/world/vanilla.go
Normal file
266
internal/world/vanilla.go
Normal 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) }
|
||||
7
internal/world/vanilla_bench_test.go
Normal file
7
internal/world/vanilla_bench_test.go
Normal 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) }
|
||||
}
|
||||
45
internal/world/vanilla_parity_test.go
Normal file
45
internal/world/vanilla_parity_test.go
Normal 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
158
internal/worldgen/biome.go
Normal 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)
|
||||
}
|
||||
96
internal/worldgen/biome_test.go
Normal file
96
internal/worldgen/biome_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
102
internal/worldgen/blended.go
Normal file
102
internal/worldgen/blended.go
Normal 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)
|
||||
}
|
||||
29
internal/worldgen/blended_test.go
Normal file
29
internal/worldgen/blended_test.go
Normal 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)
|
||||
}
|
||||
16
internal/worldgen/blendedcompute_test.go
Normal file
16
internal/worldgen/blendedcompute_test.go
Normal 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) }
|
||||
}
|
||||
}
|
||||
37
internal/worldgen/climate_sampler.go
Normal file
37
internal/worldgen/climate_sampler.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
11
internal/worldgen/data/density_function/overworld/depth.json
Normal file
11
internal/worldgen/data/density_function/overworld/depth.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
890
internal/worldgen/data/density_function/overworld/factor.json
Normal file
890
internal/worldgen/data/density_function/overworld/factor.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1523
internal/worldgen/data/density_function/overworld/offset.json
Normal file
1523
internal/worldgen/data/density_function/overworld/offset.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
10
internal/worldgen/data/density_function/shift_x.json
Normal file
10
internal/worldgen/data/density_function/shift_x.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"type": "minecraft:flat_cache",
|
||||
"argument": {
|
||||
"type": "minecraft:cache_2d",
|
||||
"argument": {
|
||||
"type": "minecraft:shift_a",
|
||||
"argument": "minecraft:offset"
|
||||
}
|
||||
}
|
||||
}
|
||||
10
internal/worldgen/data/density_function/shift_z.json
Normal file
10
internal/worldgen/data/density_function/shift_z.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"type": "minecraft:flat_cache",
|
||||
"argument": {
|
||||
"type": "minecraft:cache_2d",
|
||||
"argument": {
|
||||
"type": "minecraft:shift_b",
|
||||
"argument": "minecraft:offset"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
internal/worldgen/data/density_function/y.json
Normal file
7
internal/worldgen/data/density_function/y.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "minecraft:y_clamped_gradient",
|
||||
"from_value": -4064.0,
|
||||
"from_y": -4064,
|
||||
"to_value": 4062.0,
|
||||
"to_y": 4062
|
||||
}
|
||||
1
internal/worldgen/data/density_function/zero.json
Normal file
1
internal/worldgen/data/density_function/zero.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.0
|
||||
6
internal/worldgen/data/noise/aquifer_barrier.json
Normal file
6
internal/worldgen/data/noise/aquifer_barrier.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -3
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -7
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -5
|
||||
}
|
||||
6
internal/worldgen/data/noise/aquifer_lava.json
Normal file
6
internal/worldgen/data/noise/aquifer_lava.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -1
|
||||
}
|
||||
9
internal/worldgen/data/noise/badlands_pillar.json
Normal file
9
internal/worldgen/data/noise/badlands_pillar.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -2
|
||||
}
|
||||
6
internal/worldgen/data/noise/badlands_pillar_roof.json
Normal file
6
internal/worldgen/data/noise/badlands_pillar_roof.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -8
|
||||
}
|
||||
8
internal/worldgen/data/noise/badlands_surface.json
Normal file
8
internal/worldgen/data/noise/badlands_surface.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -6
|
||||
}
|
||||
9
internal/worldgen/data/noise/calcite.json
Normal file
9
internal/worldgen/data/noise/calcite.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -9
|
||||
}
|
||||
14
internal/worldgen/data/noise/cave_cheese.json
Normal file
14
internal/worldgen/data/noise/cave_cheese.json
Normal 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
|
||||
}
|
||||
8
internal/worldgen/data/noise/cave_entrance.json
Normal file
8
internal/worldgen/data/noise/cave_entrance.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
0.4,
|
||||
0.5,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -7
|
||||
}
|
||||
6
internal/worldgen/data/noise/cave_layer.json
Normal file
6
internal/worldgen/data/noise/cave_layer.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -8
|
||||
}
|
||||
6
internal/worldgen/data/noise/clay_bands_offset.json
Normal file
6
internal/worldgen/data/noise/clay_bands_offset.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -8
|
||||
}
|
||||
14
internal/worldgen/data/noise/continentalness.json
Normal file
14
internal/worldgen/data/noise/continentalness.json
Normal 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
|
||||
}
|
||||
14
internal/worldgen/data/noise/continentalness_large.json
Normal file
14
internal/worldgen/data/noise/continentalness_large.json
Normal 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
|
||||
}
|
||||
10
internal/worldgen/data/noise/erosion.json
Normal file
10
internal/worldgen/data/noise/erosion.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -9
|
||||
}
|
||||
10
internal/worldgen/data/noise/erosion_large.json
Normal file
10
internal/worldgen/data/noise/erosion_large.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
0.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -11
|
||||
}
|
||||
9
internal/worldgen/data/noise/gravel.json
Normal file
9
internal/worldgen/data/noise/gravel.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -8
|
||||
}
|
||||
14
internal/worldgen/data/noise/gravel_layer.json
Normal file
14
internal/worldgen/data/noise/gravel_layer.json
Normal 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
|
||||
}
|
||||
9
internal/worldgen/data/noise/ice.json
Normal file
9
internal/worldgen/data/noise/ice.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -4
|
||||
}
|
||||
9
internal/worldgen/data/noise/iceberg_pillar.json
Normal file
9
internal/worldgen/data/noise/iceberg_pillar.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -6
|
||||
}
|
||||
6
internal/worldgen/data/noise/iceberg_pillar_roof.json
Normal file
6
internal/worldgen/data/noise/iceberg_pillar_roof.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -3
|
||||
}
|
||||
8
internal/worldgen/data/noise/iceberg_surface.json
Normal file
8
internal/worldgen/data/noise/iceberg_surface.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -6
|
||||
}
|
||||
21
internal/worldgen/data/noise/jagged.json
Normal file
21
internal/worldgen/data/noise/jagged.json
Normal 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
|
||||
}
|
||||
7
internal/worldgen/data/noise/nether/temperature.json
Normal file
7
internal/worldgen/data/noise/nether/temperature.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -7
|
||||
}
|
||||
7
internal/worldgen/data/noise/nether/vegetation.json
Normal file
7
internal/worldgen/data/noise/nether/vegetation.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -7
|
||||
}
|
||||
6
internal/worldgen/data/noise/nether_state_selector.json
Normal file
6
internal/worldgen/data/noise/nether_state_selector.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -4
|
||||
}
|
||||
9
internal/worldgen/data/noise/nether_wart.json
Normal file
9
internal/worldgen/data/noise/nether_wart.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.9
|
||||
],
|
||||
"firstOctave": -3
|
||||
}
|
||||
9
internal/worldgen/data/noise/netherrack.json
Normal file
9
internal/worldgen/data/noise/netherrack.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.35
|
||||
],
|
||||
"firstOctave": -3
|
||||
}
|
||||
6
internal/worldgen/data/noise/noodle.json
Normal file
6
internal/worldgen/data/noise/noodle.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -8
|
||||
}
|
||||
6
internal/worldgen/data/noise/noodle_ridge_a.json
Normal file
6
internal/worldgen/data/noise/noodle_ridge_a.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"amplitudes": [
|
||||
1.0
|
||||
],
|
||||
"firstOctave": -7
|
||||
}
|
||||
6
internal/worldgen/data/noise/noodle_ridge_b.json
Normal file
6
internal/worldgen/data/noise/noodle_ridge_b.json
Normal 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
Loading…
Add table
Add a link
Reference in a new issue