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