Add vanilla parity harness and harden server boundaries
This commit is contained in:
parent
1924cb5591
commit
ca019756ec
25 changed files with 1118 additions and 217 deletions
|
|
@ -16,7 +16,7 @@ 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).
|
||||
// max] array, so it is decoded loosely (see depthRange).
|
||||
type rawParameter struct {
|
||||
Biome string `json:"biome"`
|
||||
Param struct {
|
||||
|
|
@ -31,21 +31,20 @@ type rawParameter struct {
|
|||
}
|
||||
|
||||
// depthRange extracts a depth band from a raw entry. It accepts a JSON number
|
||||
// (mapped to the half-open band [v, v+1) so a scalar value matches exactly one
|
||||
// integer depth layer), a single-element [v] array (same as the scalar), or a
|
||||
// (mapped to the exact inclusive range [v,v]), a single-element [v] array, or a
|
||||
// two-element [min, max] range (used by cave biomes like lush/dripstone_caves
|
||||
// whose depth is [0.2, 0.9]). Returns ok=false only for malformed input.
|
||||
func depthRange(v any) (worldgen.ClimateRange, bool) {
|
||||
switch d := v.(type) {
|
||||
case float64:
|
||||
q := worldgen.Quantize(d)
|
||||
return worldgen.ClimateRange{Min: q, Max: q + 1}, true
|
||||
return worldgen.ClimateRange{Min: q, Max: q}, true
|
||||
case []any:
|
||||
switch len(d) {
|
||||
case 1:
|
||||
if f, ok := d[0].(float64); ok {
|
||||
q := worldgen.Quantize(f)
|
||||
return worldgen.ClimateRange{Min: q, Max: q + 1}, true
|
||||
return worldgen.ClimateRange{Min: q, Max: q}, true
|
||||
}
|
||||
case 2:
|
||||
lo, ok1 := d[0].(float64)
|
||||
|
|
@ -59,8 +58,7 @@ func depthRange(v any) (worldgen.ClimateRange, bool) {
|
|||
}
|
||||
|
||||
// biomeTable is the full biome parameter table (surface + underground twins +
|
||||
// cave biomes), built once at init. The finder's range-contains check on the
|
||||
// depth axis selects the correct layer per cell.
|
||||
// cave biomes), built once at init.
|
||||
var (
|
||||
biomeTable *worldgen.ParameterTable
|
||||
biomeTableOnce sync.Once
|
||||
|
|
@ -92,7 +90,7 @@ func loadBiomeTable() *worldgen.ParameterTable {
|
|||
|
||||
// makeBiomeParameter converts a raw JSON entry into a BiomeParameter, mapping
|
||||
// the [min,max] ranges to quantized ClimateRanges. depth is a ClimateRange
|
||||
// (half-open band for scalar depths, explicit range for cave biomes).
|
||||
// (exact range for scalar depths, explicit range for cave biomes).
|
||||
func makeBiomeParameter(e rawParameter, depth worldgen.ClimateRange) worldgen.BiomeParameter {
|
||||
qr := func(a [2]float64) worldgen.ClimateRange {
|
||||
return worldgen.ClimateRange{Min: worldgen.Quantize(a[0]), Max: worldgen.Quantize(a[1])}
|
||||
|
|
@ -105,7 +103,7 @@ func makeBiomeParameter(e rawParameter, depth worldgen.ClimateRange) worldgen.Bi
|
|||
qr(e.Param.Continentalness),
|
||||
qr(e.Param.Erosion),
|
||||
qr(e.Param.Weirdness),
|
||||
depth, // half-open band (scalar) or explicit range (cave biomes)
|
||||
depth,
|
||||
},
|
||||
Offset: worldgen.Quantize(e.Param.Offset),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,26 +147,9 @@ func (r *RegionFile) WriteChunk(localX, localZ int, nbt []byte) error {
|
|||
defer r.mu.Unlock()
|
||||
|
||||
idx := locationIndex(localX, localZ)
|
||||
old := r.offsets[idx]
|
||||
oldSectors := 0
|
||||
if old != 0 {
|
||||
oldSectors = int(old & 0xFF)
|
||||
}
|
||||
|
||||
// Decide where to write. Reuse the existing allocation if it still fits;
|
||||
// otherwise append at end-of-file.
|
||||
var offset int
|
||||
switch {
|
||||
case old != 0 && oldSectors == sectorsNeeded:
|
||||
offset = int(old >> 8)
|
||||
case old != 0 && oldSectors >= sectorsNeeded:
|
||||
// Keep the old offset but record the smaller count (the tail of the old
|
||||
// allocation becomes unreferenced dead space; acceptable for now).
|
||||
offset = int(old >> 8)
|
||||
default:
|
||||
// Append after the last used sector.
|
||||
offset = r.endSectorLocked()
|
||||
}
|
||||
// Always use copy-on-write. Reusing the published allocation would let a
|
||||
// crash during WriteAt corrupt the only readable copy of the chunk.
|
||||
offset := r.endSectorLocked()
|
||||
|
||||
// Build the on-disk record: length + compression byte + compressed data,
|
||||
// zero-padded to a sector boundary.
|
||||
|
|
@ -177,13 +160,23 @@ func (r *RegionFile) WriteChunk(localX, localZ int, nbt []byte) error {
|
|||
if _, err := r.f.WriteAt(rec, off); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the offset table and timestamp, then persist both tables.
|
||||
r.offsets[idx] = uint32(offset<<8) | uint32(sectorsNeeded)
|
||||
if err := r.writeTablesLocked(); err != nil {
|
||||
// Publish the new location only after the complete record is durable. A
|
||||
// crash before this sync leaves an unreachable tail and the old slot intact.
|
||||
if err := r.f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.f.Sync()
|
||||
|
||||
location := uint32(offset<<8) | uint32(sectorsNeeded)
|
||||
var locationBytes [4]byte
|
||||
binary.BigEndian.PutUint32(locationBytes[:], location)
|
||||
if _, err := r.f.WriteAt(locationBytes[:], int64(idx*4)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.f.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
r.offsets[idx] = location
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeTablesLocked writes the offset + timestamp tables back to the header.
|
||||
|
|
@ -205,6 +198,11 @@ func (r *RegionFile) writeTablesLocked() error {
|
|||
// i.e. where new chunk data can be appended. Caller holds r.mu.
|
||||
func (r *RegionFile) endSectorLocked() int {
|
||||
maxUsed := headerSectors
|
||||
if info, err := r.f.Stat(); err == nil {
|
||||
if sectors := int((info.Size() + sectorSize - 1) / sectorSize); sectors > maxUsed {
|
||||
maxUsed = sectors
|
||||
}
|
||||
}
|
||||
for _, loc := range r.offsets {
|
||||
if loc == 0 {
|
||||
continue
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package world
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
|
@ -32,7 +33,7 @@ const dataVersion26 = 4790
|
|||
// first time it ran: chunkAt prefers the store over the generator, so the
|
||||
// already-explored area around spawn keeps its old terrain and every later fix
|
||||
// looks like it did nothing in exactly the place you are standing.
|
||||
const generatorVersion = 11
|
||||
const generatorVersion = 12
|
||||
|
||||
// generatorVersionTag is the NBT key holding generatorVersion. It is namespaced
|
||||
// because it is ours, not part of the vanilla chunk format.
|
||||
|
|
@ -233,6 +234,17 @@ func (s *Store) regionFor(cx, cz int32) (*RegionFile, error) {
|
|||
// LoadChunk reads and decodes the chunk at (cx, cz). It returns ErrChunkNotFound
|
||||
// when the chunk is not stored.
|
||||
func (s *Store) LoadChunk(cx, cz int32) (*Chunk, error) {
|
||||
return s.loadChunk(cx, cz, true)
|
||||
}
|
||||
|
||||
// LoadVanillaChunk reads an official-server chunk without requiring RegionIO's
|
||||
// generator stamp. It exists for parity tooling; runtime world loading must use
|
||||
// LoadChunk so stale RegionIO terrain still regenerates.
|
||||
func (s *Store) LoadVanillaChunk(cx, cz int32) (*Chunk, error) {
|
||||
return s.loadChunk(cx, cz, false)
|
||||
}
|
||||
|
||||
func (s *Store) loadChunk(cx, cz int32, requireGeneratorVersion bool) (*Chunk, error) {
|
||||
rx, rz, lx, lz := regionIndex(cx, cz)
|
||||
rf, err := s.regionFor(cx, cz)
|
||||
if err != nil {
|
||||
|
|
@ -250,7 +262,7 @@ func (s *Store) LoadChunk(cx, cz int32) (*Chunk, error) {
|
|||
if !ok {
|
||||
return nil, fmt.Errorf("world: chunk (%d,%d) root is not a compound", cx, cz)
|
||||
}
|
||||
return nbtToChunk(root, rx, rz, lx, lz)
|
||||
return nbtToChunkVersioned(root, rx, rz, lx, lz, requireGeneratorVersion)
|
||||
}
|
||||
|
||||
// SaveChunk encodes the chunk and writes it to its region file.
|
||||
|
|
@ -483,13 +495,17 @@ func packIndices(ids []uint16, indexOf map[uint16]int, bits int) nbt.LongArray {
|
|||
// absolute coordinates are derived from the on-disk xPos/zPos (authoritative);
|
||||
// the region/local coords passed in are used only to validate.
|
||||
func nbtToChunk(root *nbt.Compound, regionX, regionZ, localX, localZ int) (*Chunk, error) {
|
||||
return nbtToChunkVersioned(root, regionX, regionZ, localX, localZ, true)
|
||||
}
|
||||
|
||||
func nbtToChunkVersioned(root *nbt.Compound, regionX, regionZ, localX, localZ int, requireGeneratorVersion bool) (*Chunk, error) {
|
||||
// Reject anything the current generator did not produce so the caller
|
||||
// regenerates instead of serving stale terrain. Chunks written before the
|
||||
// stamp existed have no tag and decode as 0, so they are invalidated too.
|
||||
// This is per-chunk on purpose: the world metadata file guards the seed,
|
||||
// which is a hard mismatch, while a generator change is routine and should
|
||||
// quietly regenerate rather than refuse to open the world.
|
||||
if v := nbtAsInt(root, generatorVersionTag); v != generatorVersion {
|
||||
if requireGeneratorVersion && nbtAsInt(root, generatorVersionTag) != generatorVersion {
|
||||
return nil, ErrChunkNotFound
|
||||
}
|
||||
|
||||
|
|
@ -508,27 +524,39 @@ func nbtToChunk(root *nbt.Compound, regionX, regionZ, localX, localZ int) (*Chun
|
|||
}
|
||||
}
|
||||
|
||||
// Sections.
|
||||
if secTag, ok := root.Get("sections"); ok {
|
||||
if secList, ok := secTag.(nbt.List); ok && secList.ElemID == nbt.TagCompound {
|
||||
for _, st := range secList.Elems {
|
||||
sc, ok := st.(*nbt.Compound)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
yIdx, ok := nbtAsSectionY(sc, "Y")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
si := yIdx - minYSection
|
||||
if si < 0 || si >= SectionCount {
|
||||
continue
|
||||
}
|
||||
readBlockStates(c, si, sc)
|
||||
readBiomes(c, si, sc)
|
||||
readLightSection(c, si, sc)
|
||||
}
|
||||
secTag, ok := root.Get("sections")
|
||||
if !ok {
|
||||
return nil, errors.New("world: chunk NBT missing sections")
|
||||
}
|
||||
secList, ok := secTag.(nbt.List)
|
||||
if !ok || secList.ElemID != nbt.TagCompound {
|
||||
return nil, errors.New("world: chunk sections is not a compound list")
|
||||
}
|
||||
seenSections := make(map[int]bool, len(secList.Elems))
|
||||
for index, st := range secList.Elems {
|
||||
sc, ok := st.(*nbt.Compound)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("world: section %d is not a compound", index)
|
||||
}
|
||||
yIdx, ok := nbtAsSectionY(sc, "Y")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("world: section %d has no valid Y", index)
|
||||
}
|
||||
si := yIdx - minYSection
|
||||
if si < 0 || si >= SectionCount {
|
||||
continue
|
||||
}
|
||||
if seenSections[si] {
|
||||
return nil, fmt.Errorf("world: duplicate section Y %d", yIdx)
|
||||
}
|
||||
seenSections[si] = true
|
||||
if err := readBlockStates(c, si, sc); err != nil {
|
||||
return nil, fmt.Errorf("world: section Y %d block states: %w", yIdx, err)
|
||||
}
|
||||
if err := readBiomes(c, si, sc); err != nil {
|
||||
return nil, fmt.Errorf("world: section Y %d biomes: %w", yIdx, err)
|
||||
}
|
||||
readLightSection(c, si, sc)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
|
@ -555,36 +583,48 @@ func readLightSection(c *Chunk, si int, sc *nbt.Compound) {
|
|||
// readBlockStates decodes a section's block_states {palette, data?} into the
|
||||
// chunk's section array. A palette of size 1 fills the whole section; otherwise
|
||||
// the packed data array is unpacked.
|
||||
func readBlockStates(c *Chunk, si int, sc *nbt.Compound) {
|
||||
func readBlockStates(c *Chunk, si int, sc *nbt.Compound) error {
|
||||
bsTag, ok := sc.Get("block_states")
|
||||
if !ok {
|
||||
return
|
||||
return errors.New("missing block_states")
|
||||
}
|
||||
bs, ok := bsTag.(*nbt.Compound)
|
||||
if !ok {
|
||||
return
|
||||
return errors.New("block_states is not a compound")
|
||||
}
|
||||
palTag, ok := bs.Get("palette")
|
||||
if !ok {
|
||||
return
|
||||
return errors.New("missing palette")
|
||||
}
|
||||
pal, ok := palTag.(nbt.List)
|
||||
if !ok || pal.ElemID != nbt.TagCompound {
|
||||
return
|
||||
return errors.New("palette is not a compound list")
|
||||
}
|
||||
if len(pal.Elems) == 0 || len(pal.Elems) > totalBlockStates {
|
||||
return fmt.Errorf("palette size %d out of range", len(pal.Elems))
|
||||
}
|
||||
// Decode palette entries to state IDs.
|
||||
ids := make([]uint16, len(pal.Elems))
|
||||
for i, e := range pal.Elems {
|
||||
ec, ok := e.(*nbt.Compound)
|
||||
if !ok {
|
||||
ids[i] = StateAir
|
||||
continue
|
||||
return fmt.Errorf("palette entry %d is not a compound", i)
|
||||
}
|
||||
name := string(nbtAsString(ec, "Name"))
|
||||
nameTag, ok := ec.Get("Name")
|
||||
if !ok {
|
||||
return fmt.Errorf("palette entry %d has no Name", i)
|
||||
}
|
||||
nameValue, ok := nameTag.(nbt.String)
|
||||
if !ok || nameValue == "" {
|
||||
return fmt.Errorf("palette entry %d has invalid Name", i)
|
||||
}
|
||||
name := string(nameValue)
|
||||
props := readProps(ec)
|
||||
// An unknown block name decodes to air rather than to a neighbour's
|
||||
// state; that loses the block but does not corrupt the column.
|
||||
ids[i], _ = nameToStateID(name, props)
|
||||
var resolved bool
|
||||
ids[i], resolved = nameToStateID(name, props)
|
||||
if !resolved {
|
||||
return fmt.Errorf("unknown block state %q", name)
|
||||
}
|
||||
}
|
||||
c.section(si) // ensure allocated
|
||||
s := c.sections[si]
|
||||
|
|
@ -594,36 +634,55 @@ func readBlockStates(c *Chunk, si int, sc *nbt.Compound) {
|
|||
fill[i] = ids[0]
|
||||
}
|
||||
c.sections[si] = &fill
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if dataTag, ok := bs.Get("data"); ok {
|
||||
if data, ok := dataTag.(nbt.LongArray); ok {
|
||||
unpackIndices(s[:], ids, data, blockStorageBits(len(ids)))
|
||||
}
|
||||
dataTag, ok := bs.Get("data")
|
||||
if !ok {
|
||||
return errors.New("multi-entry palette has no data")
|
||||
}
|
||||
data, ok := dataTag.(nbt.LongArray)
|
||||
if !ok {
|
||||
return errors.New("data is not a long array")
|
||||
}
|
||||
bits := blockStorageBits(len(ids))
|
||||
if err := validatePackedData(len(s), bits, data); err != nil {
|
||||
return err
|
||||
}
|
||||
return unpackIndices(s[:], ids, data, bits)
|
||||
}
|
||||
|
||||
// readBiomes decodes a section's biomes {palette, data?} into the per-cell array.
|
||||
func readBiomes(c *Chunk, si int, sc *nbt.Compound) {
|
||||
func readBiomes(c *Chunk, si int, sc *nbt.Compound) error {
|
||||
bTag, ok := sc.Get("biomes")
|
||||
if !ok {
|
||||
return
|
||||
return errors.New("missing biomes")
|
||||
}
|
||||
bc, ok := bTag.(*nbt.Compound)
|
||||
if !ok {
|
||||
return
|
||||
return errors.New("biomes is not a compound")
|
||||
}
|
||||
palTag, ok := bc.Get("palette")
|
||||
if !ok {
|
||||
return
|
||||
return errors.New("missing palette")
|
||||
}
|
||||
pal, ok := palTag.(nbt.List)
|
||||
if !ok || pal.ElemID != nbt.TagString {
|
||||
return
|
||||
return errors.New("palette is not a string list")
|
||||
}
|
||||
if len(pal.Elems) == 0 || len(pal.Elems) > totalBiomes {
|
||||
return fmt.Errorf("palette size %d out of range", len(pal.Elems))
|
||||
}
|
||||
ids := make([]uint16, len(pal.Elems))
|
||||
for i, e := range pal.Elems {
|
||||
ids[i] = biomeIDByName(string(e.(nbt.String)))
|
||||
name, ok := e.(nbt.String)
|
||||
if !ok {
|
||||
return fmt.Errorf("palette entry %d is not a string", i)
|
||||
}
|
||||
id := registry.Index("minecraft:worldgen/biome", string(name))
|
||||
if id < 0 {
|
||||
return fmt.Errorf("unknown biome %q", name)
|
||||
}
|
||||
ids[i] = uint16(id)
|
||||
}
|
||||
if len(ids) == 1 {
|
||||
cells := new([biomeCellsPerSection]uint16)
|
||||
|
|
@ -631,15 +690,26 @@ func readBiomes(c *Chunk, si int, sc *nbt.Compound) {
|
|||
cells[i] = ids[0]
|
||||
}
|
||||
c.biomes[si] = cells
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if dataTag, ok := bc.Get("data"); ok {
|
||||
if data, ok := dataTag.(nbt.LongArray); ok {
|
||||
cells := new([biomeCellsPerSection]uint16)
|
||||
unpackIndices(cells[:], ids, data, biomeStorageBits(len(ids)))
|
||||
c.biomes[si] = cells
|
||||
}
|
||||
dataTag, ok := bc.Get("data")
|
||||
if !ok {
|
||||
return errors.New("multi-entry palette has no data")
|
||||
}
|
||||
data, ok := dataTag.(nbt.LongArray)
|
||||
if !ok {
|
||||
return errors.New("data is not a long array")
|
||||
}
|
||||
bits := biomeStorageBits(len(ids))
|
||||
cells := new([biomeCellsPerSection]uint16)
|
||||
if err := validatePackedData(len(cells), bits, data); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := unpackIndices(cells[:], ids, data, bits); err != nil {
|
||||
return err
|
||||
}
|
||||
c.biomes[si] = cells
|
||||
return nil
|
||||
}
|
||||
|
||||
func readProps(c *nbt.Compound) map[string]string {
|
||||
|
|
@ -701,21 +771,32 @@ func nbtAsString(c *nbt.Compound, name string) nbt.String {
|
|||
|
||||
// unpackIndices reverses packIndices: fills dst with palette IDs using the
|
||||
// packed long array.
|
||||
func unpackIndices(dst []uint16, ids []uint16, data nbt.LongArray, bits int) {
|
||||
func validatePackedData(entries, bits int, data nbt.LongArray) error {
|
||||
if bits < 1 {
|
||||
return
|
||||
return errors.New("invalid zero-bit packed data")
|
||||
}
|
||||
perLong := 64 / bits
|
||||
want := (entries + perLong - 1) / perLong
|
||||
if len(data) != want {
|
||||
return fmt.Errorf("packed data has %d longs, want %d", len(data), want)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unpackIndices(dst []uint16, ids []uint16, data nbt.LongArray, bits int) error {
|
||||
if bits < 1 {
|
||||
return errors.New("invalid zero-bit packed data")
|
||||
}
|
||||
perLong := 64 / bits
|
||||
mask := int64(1)<<uint(bits) - 1
|
||||
for i := range dst {
|
||||
longIdx := i / perLong
|
||||
bitOff := (i % perLong) * bits
|
||||
if longIdx >= len(data) {
|
||||
break
|
||||
}
|
||||
idx := int((data[longIdx] >> uint(bitOff)) & mask)
|
||||
if idx >= 0 && idx < len(ids) {
|
||||
dst[i] = ids[idx]
|
||||
if idx < 0 || idx >= len(ids) {
|
||||
return fmt.Errorf("palette index %d out of range %d", idx, len(ids))
|
||||
}
|
||||
dst[i] = ids[idx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,6 +74,26 @@ func TestRegionFileOverwrite(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRegionFileOverwriteUsesCopyOnWrite(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
rf, err := OpenRegion(dir, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rf.Close()
|
||||
if err := rf.WriteChunk(1, 1, []byte("first")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := rf.offsets[locationIndex(1, 1)] >> 8
|
||||
if err := rf.WriteChunk(1, 1, []byte("second")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second := rf.offsets[locationIndex(1, 1)] >> 8
|
||||
if second <= first {
|
||||
t.Fatalf("overwrite reused published sector %d; new location is %d", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStoreChunkRoundTrip encodes a chunk to NBT, decodes it back, and confirms
|
||||
// the blocks/biomes match. This validates the chunkToNBT/nbtToChunk bridge.
|
||||
func TestStoreChunkRoundTrip(t *testing.T) {
|
||||
|
|
@ -126,6 +146,44 @@ func TestStoreChunkRoundTrip(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestChunkNBTRejectsMissingSections(t *testing.T) {
|
||||
root := nbt.NewCompound().
|
||||
Set(generatorVersionTag, nbt.Int(generatorVersion)).
|
||||
Set("xPos", nbt.Int(0)).
|
||||
Set("zPos", nbt.Int(0))
|
||||
if _, err := nbtToChunk(root, 0, 0, 0, 0); err == nil {
|
||||
t.Fatal("accepted chunk without sections")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkNBTRejectsMalformedPaletteData(t *testing.T) {
|
||||
root := chunkToNBT(GenerateFlat(0, 0))
|
||||
sectionsTag, _ := root.Get("sections")
|
||||
sections := sectionsTag.(nbt.List)
|
||||
section := sections.Elems[0].(*nbt.Compound)
|
||||
blocksTag, _ := section.Get("block_states")
|
||||
blocks := blocksTag.(*nbt.Compound)
|
||||
blocks.Set("data", nbt.LongArray{0})
|
||||
if _, err := nbtToChunk(root, 0, 0, 0, 0); err == nil {
|
||||
t.Fatal("accepted packed block data with the wrong length")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkNBTRejectsUnknownBlock(t *testing.T) {
|
||||
root := chunkToNBT(NewChunk(0, 0, BiomePlains))
|
||||
sectionsTag, _ := root.Get("sections")
|
||||
sections := sectionsTag.(nbt.List)
|
||||
section := sections.Elems[0].(*nbt.Compound)
|
||||
blocksTag, _ := section.Get("block_states")
|
||||
blocks := blocksTag.(*nbt.Compound)
|
||||
blocks.Set("palette", nbt.List{ElemID: nbt.TagCompound, Elems: []nbt.Tag{
|
||||
nbt.NewCompound().Set("Name", nbt.String("minecraft:not_a_block")),
|
||||
}})
|
||||
if _, err := nbtToChunk(root, 0, 0, 0, 0); err == nil {
|
||||
t.Fatal("accepted unknown block palette entry")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreLightRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
store, err := NewStore(dir)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
|
|
@ -9,6 +11,73 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
const vanillaParityFixture = "testdata/vanilla_overworld_12345.bin"
|
||||
|
||||
func TestVanillaBlockParity(t *testing.T) {
|
||||
f, err := os.Open(vanillaParityFixture)
|
||||
if err != nil {
|
||||
if os.Getenv("REGIONIO_REQUIRE_PARITY") == "1" {
|
||||
t.Fatalf("required parity fixture: %v", err)
|
||||
}
|
||||
t.Skip("vanilla block fixture not installed; run cmd/vanillacapture with Java 25")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var header [24]byte
|
||||
if _, err := io.ReadFull(f, header[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(header[:8]) != "RIOPAR01" {
|
||||
t.Fatalf("bad parity fixture magic %q", header[:8])
|
||||
}
|
||||
seed := int64(binary.BigEndian.Uint64(header[8:16]))
|
||||
count := int(binary.BigEndian.Uint32(header[16:20]))
|
||||
if seed != 12345 || count <= 0 {
|
||||
t.Fatalf("fixture seed=%d chunks=%d", seed, count)
|
||||
}
|
||||
gen := NewVanillaGenerator(seed)
|
||||
for chunkIndex := 0; chunkIndex < count; chunkIndex++ {
|
||||
var coords [8]byte
|
||||
if _, err := io.ReadFull(f, coords[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cx := int32(binary.BigEndian.Uint32(coords[:4]))
|
||||
cz := int32(binary.BigEndian.Uint32(coords[4:]))
|
||||
chunk := gen(cx, cz)
|
||||
var state [2]byte
|
||||
for y := MinY; y < MinY+WorldHeight; y++ {
|
||||
for z := 0; z < 16; z++ {
|
||||
for x := 0; x < 16; x++ {
|
||||
if _, err := io.ReadFull(f, state[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := binary.BigEndian.Uint16(state[:])
|
||||
if got := chunk.GetBlock(x, y, z); got != want {
|
||||
t.Fatalf("chunk (%d,%d) block (%d,%d,%d): got state %d want %d", cx, cz, x, y, z, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for y := MinY; y < MinY+WorldHeight; y += biomeCellSize {
|
||||
for z := 0; z < 16; z += biomeCellSize {
|
||||
for x := 0; x < 16; x += biomeCellSize {
|
||||
if _, err := io.ReadFull(f, state[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := binary.BigEndian.Uint16(state[:])
|
||||
if got := chunk.GetBiome(x, y, z); got != want {
|
||||
t.Fatalf("chunk (%d,%d) biome (%d,%d,%d): got %d want %d", cx, cz, x, y, z, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var trailing [1]byte
|
||||
if n, err := f.Read(trailing[:]); n != 0 || err != io.EOF {
|
||||
t.Fatalf("fixture has trailing data or read error: n=%d err=%v", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue