diff --git a/internal/world/encode.go b/internal/world/encode.go index 2da7c38..717663f 100644 --- a/internal/world/encode.go +++ b/internal/world/encode.go @@ -391,12 +391,13 @@ func packHeightmap(h [256]uint16) []uint64 { 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) + w.Uint16(0) // nonEmptyBlockCount + w.Uint16(0) // fluidCount writeSingleValued(w, uint32(StateAir)) } else { - w.Uint16(uint16(nonAirCount(s))) - w.Uint16(0) // reserved 2-byte field + nonEmpty, fluid := sectionCounts(s) + w.Uint16(nonEmpty) + w.Uint16(fluid) writeBlockPalette(w, s) } // Biome container: per-cell palette when present, else the uniform fallback. @@ -407,14 +408,26 @@ func (c *Chunk) writeSection(w *protocol.Writer, i int) { } } -func nonAirCount(s *[sectionVol]uint16) int { - n := 0 +// sectionCounts is LevelChunkSection.recalcBlockCounts, restricted to the two +// counters that go on the wire: how many blocks are not air, and how many hold +// a fluid. +// +// The second one used to be written as a constant zero, with a comment calling +// it a reserved field. It is not reserved — a client told a section has no +// fluid skips that section when it looks for water to swim in or lava to burn +// on, and the aquifer means almost every section below sea level has some. +// Waterlogged blocks count, which is why the flag is per block state. +func sectionCounts(s *[sectionVol]uint16) (nonEmpty, fluid uint16) { for _, v := range s { - if v != StateAir { - n++ + if v == StateAir { + continue + } + nonEmpty++ + if stateFlags(v)&flagFluid != 0 { + fluid++ } } - return n + return nonEmpty, fluid } // writeSingleValued writes a bits-per-entry-0 paletted container (no data). diff --git a/internal/world/encode_test.go b/internal/world/encode_test.go index be5cf11..db8066c 100644 --- a/internal/world/encode_test.go +++ b/internal/world/encode_test.go @@ -106,8 +106,8 @@ func TestFlatChunkEncodesCleanly(t *testing.T) { 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 _, err := r.Uint16(); err != nil { // fluidCount + t.Fatalf("section %d fluid count: %v", s, err) } if count > 0 { nonAirSections++ @@ -125,10 +125,10 @@ func TestFlatChunkEncodesCleanly(t *testing.T) { } // Light: four bitsets, then sky arrays, then block arrays. - expectedSkyArrays := parseBitSet(t, r) // sky mask + expectedSkyArrays := parseBitSet(t, r) // sky mask expectedBlockArrays := parseBitSet(t, r) // block mask - parseBitSet(t, r) // empty sky mask - parseBitSet(t, r) // empty 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) diff --git a/internal/world/heightmap_test.go b/internal/world/heightmap_test.go index 3e97ef2..4c57aaa 100644 --- a/internal/world/heightmap_test.go +++ b/internal/world/heightmap_test.go @@ -104,3 +104,45 @@ func TestBlockStatePredicates(t *testing.T) { t.Error("dandelion blocks motion; it should not") } } + +// TestSectionFluidCount checks the second short of a chunk section. It was +// written as a constant zero under a comment calling it reserved, so every +// client was told every section is fluid-free. +func TestSectionFluidCount(t *testing.T) { + c := NewChunk(0, 0, BiomePlains) + const y = 20 + // One section: stone floor, water above it, and one waterlogged block — + // which counts as fluid even though it is not a fluid block. + stairs := nameToStateID("minecraft:oak_stairs", map[string]string{ + "facing": "north", "half": "bottom", "shape": "straight", "waterlogged": "true", + }) + if stairs == StateAir { + t.Fatal("waterlogged oak stairs are missing from the block table") + } + if stateFlags(stairs)&flagFluid == 0 { + t.Fatal("waterlogged stairs do not carry the fluid flag; the dump is wrong") + } + for lx := 0; lx < 16; lx++ { + for lz := 0; lz < 16; lz++ { + c.SetBlock(lx, y, lz, StateStone) + c.SetBlock(lx, y+1, lz, StateWater) + } + } + c.SetBlock(0, y+2, 0, stairs) + + si := (y - MinY) >> 4 + nonEmpty, fluid := sectionCounts(c.sections[si]) + if want := uint16(16*16*2 + 1); nonEmpty != want { + t.Errorf("nonEmptyBlockCount = %d, want %d", nonEmpty, want) + } + if want := uint16(16*16 + 1); fluid != want { + t.Errorf("fluidCount = %d, want %d (256 water + 1 waterlogged)", fluid, want) + } + + // A section of dry stone still reports zero, and an absent section too. + dry := NewChunk(0, 0, BiomePlains) + dry.SetBlock(0, y, 0, StateStone) + if _, fluid := sectionCounts(dry.sections[si]); fluid != 0 { + t.Errorf("dry section fluidCount = %d, want 0", fluid) + } +}