diff --git a/save/region/mca.go b/save/region/mca.go index fa3295e..9ded58c 100644 --- a/save/region/mca.go +++ b/save/region/mca.go @@ -78,14 +78,19 @@ func Load(f io.ReadWriteSeeker) (r *Region, err error) { } // Create open .mca file with os.O_CREATE|os. O_EXCL, and init the region -func Create(name string) (r *Region, err error) { - r = new(Region) - r.sectors = make(map[int32]bool) - - r.f, err = os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0666) +func Create(name string) (*Region, error) { + f, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0666) if err != nil { return nil, err } + return CreateWriter(f) +} + +// CreateWriter init the region +func CreateWriter(f io.ReadWriteSeeker) (r *Region, err error) { + r = new(Region) + r.sectors = make(map[int32]bool) + r.f = f // write the offsets err = binary.Write(r.f, binary.BigEndian, &r.offsets) @@ -152,7 +157,7 @@ func (r *Region) ReadSector(x, z int) (data []byte, err error) { // WriteSector write Chunk data into region file func (r *Region) WriteSector(x, z int, data []byte) error { - need := int32(len(data)+4)/4096 + 1 + need := int32((len(data) + 4 + 4096 - 1) / 4096) n, now := sectorLoc(r.offsets[z][x]) // maximum chunk size is 1MB @@ -212,6 +217,23 @@ func (r *Region) ExistSector(x, z int) bool { return r.offsets[z][x] != 0 } +// PadToFullSector writes zeros to the end of the file to make size a multiple of 4096 +// Legacy versions of Minecraft require this +// Need to be called right before Close +func (r *Region) PadToFullSector() error { + size, err := r.f.Seek(0, io.SeekEnd) + if err != nil { + return err + } + if size%4096 != 0 { + _, err = r.f.Write(make([]byte, 4096-size%4096)) + if err != nil { + return err + } + } + return nil +} + func (r *Region) findSpace(need int32) (n int32) { for i := int32(0); i < need; i++ { if r.sectors[n+i] { diff --git a/save/region/mca_test.go b/save/region/mca_test.go index b7a78fb..33642bd 100644 --- a/save/region/mca_test.go +++ b/save/region/mca_test.go @@ -3,8 +3,12 @@ package region import ( "bytes" "compress/zlib" - "github.com/Tnze/go-mc/nbt" + "io" + "math/rand" + "os" "testing" + + "github.com/Tnze/go-mc/nbt" ) func TestIn(t *testing.T) { @@ -95,3 +99,73 @@ func TestCountChunks(t *testing.T) { } t.Logf("chunk count: %d", count) } + +func TestWriteSectors(t *testing.T) { + temp, err := os.CreateTemp("", "region*.mca") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := temp.Close(); err != nil { + t.Error(err) + } + if err := os.Remove(temp.Name()); err != nil { + t.Error(err) + } + }() + region, err := CreateWriter(temp) + if err != nil { + t.Fatal(err) + } + + expectedSectorsNum := 2 + for idx, test := range []struct{ size, sectors int }{ + {0, 1}, + {1000, 1}, + {4091, 1}, + {4092, 1}, + {4093, 2}, + {5000, 2}, + } { + expectedSectorsNum += test.sectors + + data := make([]byte, test.size) + rand.Read(data) + if err = region.WriteSector(idx, 0, data); err != nil { + t.Fatal("write sector", err) + } + if len(region.sectors) != expectedSectorsNum { + t.Errorf("wrong region sector count. Got: %d, Want: %d", len(region.sectors), expectedSectorsNum) + } + + if read, err := region.ReadSector(idx, 0); err != nil { + t.Fatal("read sector", err) + } else if !bytes.Equal(data, read) { + t.Fatal("read corrupted sector data") + } + } + + // reset file + if _, err = temp.Seek(0, io.SeekStart); err != nil { + t.Fatal(err) + } + + // Test load + region, err = Load(temp) + if err != nil { + t.Fatalf("load region: %v", err) + } + if len(region.sectors) != expectedSectorsNum { + t.Fatalf("read sector count missmatch. Got: %d, Want: %d", len(region.sectors), expectedSectorsNum) + } + + // Test padding + if err = region.PadToFullSector(); err != nil { + t.Fatal(err) + } + if stat, err := temp.Stat(); err != nil { + t.Fatal(err) + } else if stat.Size()%4096 != 0 { + t.Fatalf("wrong file size. Got %d, Want: %d", stat.Size(), stat.Size()+(4096-stat.Size()%4096)) + } +}