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.
This commit is contained in:
parent
7880531bdb
commit
9e91425c5d
3 changed files with 69 additions and 14 deletions
|
|
@ -391,12 +391,13 @@ func packHeightmap(h [256]uint16) []uint64 {
|
||||||
func (c *Chunk) writeSection(w *protocol.Writer, i int) {
|
func (c *Chunk) writeSection(w *protocol.Writer, i int) {
|
||||||
s := c.sections[i]
|
s := c.sections[i]
|
||||||
if s == nil {
|
if s == nil {
|
||||||
w.Uint16(0) // non-air block count
|
w.Uint16(0) // nonEmptyBlockCount
|
||||||
w.Uint16(0) // reserved 2-byte field (always 0 in vanilla)
|
w.Uint16(0) // fluidCount
|
||||||
writeSingleValued(w, uint32(StateAir))
|
writeSingleValued(w, uint32(StateAir))
|
||||||
} else {
|
} else {
|
||||||
w.Uint16(uint16(nonAirCount(s)))
|
nonEmpty, fluid := sectionCounts(s)
|
||||||
w.Uint16(0) // reserved 2-byte field
|
w.Uint16(nonEmpty)
|
||||||
|
w.Uint16(fluid)
|
||||||
writeBlockPalette(w, s)
|
writeBlockPalette(w, s)
|
||||||
}
|
}
|
||||||
// Biome container: per-cell palette when present, else the uniform fallback.
|
// 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 {
|
// sectionCounts is LevelChunkSection.recalcBlockCounts, restricted to the two
|
||||||
n := 0
|
// 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 {
|
for _, v := range s {
|
||||||
if v != StateAir {
|
if v == StateAir {
|
||||||
n++
|
continue
|
||||||
|
}
|
||||||
|
nonEmpty++
|
||||||
|
if stateFlags(v)&flagFluid != 0 {
|
||||||
|
fluid++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return n
|
return nonEmpty, fluid
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeSingleValued writes a bits-per-entry-0 paletted container (no data).
|
// writeSingleValued writes a bits-per-entry-0 paletted container (no data).
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,8 @@ func TestFlatChunkEncodesCleanly(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("section %d count: %v", s, err)
|
t.Fatalf("section %d count: %v", s, err)
|
||||||
}
|
}
|
||||||
if _, err := r.Uint16(); err != nil { // reserved 2-byte field
|
if _, err := r.Uint16(); err != nil { // fluidCount
|
||||||
t.Fatalf("section %d reserved: %v", s, err)
|
t.Fatalf("section %d fluid count: %v", s, err)
|
||||||
}
|
}
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
nonAirSections++
|
nonAirSections++
|
||||||
|
|
@ -125,10 +125,10 @@ func TestFlatChunkEncodesCleanly(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Light: four bitsets, then sky arrays, then block arrays.
|
// 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
|
expectedBlockArrays := parseBitSet(t, r) // block mask
|
||||||
parseBitSet(t, r) // empty sky mask
|
parseBitSet(t, r) // empty sky mask
|
||||||
parseBitSet(t, r) // empty block mask
|
parseBitSet(t, r) // empty block mask
|
||||||
skyArrays, err := r.VarInt()
|
skyArrays, err := r.VarInt()
|
||||||
if err != nil || skyArrays != int32(expectedSkyArrays) {
|
if err != nil || skyArrays != int32(expectedSkyArrays) {
|
||||||
t.Fatalf("sky arrays = %d (err %v), want %d", skyArrays, err, expectedSkyArrays)
|
t.Fatalf("sky arrays = %d (err %v), want %d", skyArrays, err, expectedSkyArrays)
|
||||||
|
|
|
||||||
|
|
@ -104,3 +104,45 @@ func TestBlockStatePredicates(t *testing.T) {
|
||||||
t.Error("dandelion blocks motion; it should not")
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue