RegionIO/internal/world/encode_test.go
Master290 9e91425c5d Write the section fluid count instead of a hardcoded zero
LevelChunkSection puts two shorts in front of every section: nonEmptyBlockCount
and fluidCount. We wrote the first and then a literal 0 for the second, under a
comment claiming it was a reserved field that vanilla always leaves at zero.
It is not reserved and vanilla does not.

So every client was told every section is fluid-free, in a world where the
aquifer now fills oceans, lakes and flooded caves. The golden test did not catch
it because its fixture is a superflat chunk whose real fluid count is zero.

The count is per block state, not per block: a waterlogged stair holds a fluid
while a dry one does not, and the flag for that comes from the block-state dump
added with the heightmaps.
2026-07-27 03:28:49 +03:00

268 lines
7.8 KiB
Go

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 parseBitSet(t *testing.T, r *protocol.Reader) int {
t.Helper()
n, err := r.VarInt()
if err != nil || n < 0 {
t.Fatalf("bitset len: %v", err)
}
bits := 0
for i := int32(0); i < n; i++ {
val, err := r.Int64()
if err != nil {
t.Fatalf("bitset long: %v", err)
}
// Count set bits
for val > 0 {
bits += int(val & 1)
val >>= 1
}
}
return bits
}
// 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 { // fluidCount
t.Fatalf("section %d fluid count: %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.
expectedSkyArrays := parseBitSet(t, r) // sky mask
expectedBlockArrays := parseBitSet(t, r) // block mask
parseBitSet(t, r) // empty sky mask
parseBitSet(t, r) // empty block mask
skyArrays, err := r.VarInt()
if err != nil || skyArrays != int32(expectedSkyArrays) {
t.Fatalf("sky arrays = %d (err %v), want %d", skyArrays, err, expectedSkyArrays)
}
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)
}
}
}
blockArrays, err := r.VarInt()
if err != nil || blockArrays != int32(expectedBlockArrays) {
t.Fatalf("block arrays = %d (err %v), want %d", blockArrays, err, expectedBlockArrays)
}
for i := int32(0); i < blockArrays; i++ {
n, err := r.VarInt()
if err != nil || n != 2048 {
t.Fatalf("block array len = %d (err %v), want 2048", n, err)
}
for j := int32(0); j < n; j++ {
if _, err := r.ReadByte(); err != nil {
t.Fatalf("block byte: %v", 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]))
}
// readBiomeContainerAsClient consumes one biome paletted container exactly the
// way the vanilla client does, and returns the number of bytes it used.
//
// The client picks the palette form from the bits-per-entry byte alone, using
// the SECTION_BIOMES strategy: `tableswitch {0..3}` where 0 is single-valued,
// 1-3 are linear (palette prefix present), and every other value falls through
// to the global palette — no palette prefix, and the data re-read at the
// registry's own bit width regardless of the byte we sent. This differs from
// block states, which additionally have a hashmap tier for 5-8 bits.
func readBiomeContainerAsClient(t *testing.T, buf []byte) int {
t.Helper()
r := protocol.NewReader(buf)
bpe, err := r.ReadByte()
if err != nil {
t.Fatalf("bits per entry: %v", err)
}
if bpe == 0 {
if _, err := r.VarInt(); err != nil {
t.Fatalf("single value: %v", err)
}
return len(buf) - r.Remaining()
}
dataBits := int(bpe)
if dataBits <= maxBiomeLinearBits {
n, err := r.VarInt()
if err != nil || n < 0 {
t.Fatalf("palette length: %v", err)
}
for i := int32(0); i < n; i++ {
if _, err := r.VarInt(); err != nil {
t.Fatalf("palette entry %d: %v", i, err)
}
}
} else {
dataBits = bitsFor(totalBiomes)
}
perLong := 64 / dataBits
longs := (biomeCellsPerSection + perLong - 1) / perLong
for i := 0; i < longs; i++ {
if _, err := r.Int64(); err != nil {
t.Fatalf("data long %d: %v", i, err)
}
}
return len(buf) - r.Remaining()
}
// TestBiomePaletteFormMatchesVanillaThresholds pins the SECTION_BIOMES palette
// contract at the linear/global boundary.
//
// A section holding 9 or more distinct biomes needs 4 bits per entry. Written as
// a linear palette, the client reads it as global instead: it consumes no
// palette prefix and re-reads the long array at 7 bits, so it walks off the end
// of the container and every following field in the chunk payload is
// misaligned. Sections straddling the surface and the cave biomes really do
// carry that many, so this is reachable in ordinary terrain.
//
// The assertion is the desync itself: decode each container the way the client
// would and require it to consume exactly the bytes we produced.
func TestBiomePaletteFormMatchesVanillaThresholds(t *testing.T) {
globalBits := bitsFor(totalBiomes)
for _, tc := range []struct {
name string
distinct int
wantBits int
}{
{"one biome stays single-valued", 1, 0},
{"two biomes", 2, 1},
{"eight biomes is the widest linear palette", 8, 3},
{"nine biomes must switch to global", 9, globalBits},
{"twenty biomes", 20, globalBits},
{"every registry biome", totalBiomes, globalBits},
} {
t.Run(tc.name, func(t *testing.T) {
var cells [biomeCellsPerSection]uint16
for i := range cells {
cells[i] = uint16(i % tc.distinct)
}
w := protocol.NewWriter(128)
writeBiomePalette(w, &cells)
got := w.Bytes()
if int(got[0]) != tc.wantBits {
t.Errorf("bits per entry = %d, want %d", got[0], tc.wantBits)
}
if used := readBiomeContainerAsClient(t, got); used != len(got) {
t.Errorf("client consumed %d of %d bytes; container is misframed by %d",
used, len(got), len(got)-used)
}
})
}
}