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

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

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

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

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

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

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

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

View file

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

View file

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

File diff suppressed because one or more lines are too long

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

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

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

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

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

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

View file

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

View file

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

Binary file not shown.

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

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

View file

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

View file

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