summaryrefslogtreecommitdiff
path: root/mcmap
diff options
context:
space:
mode:
Diffstat (limited to 'mcmap')
-rw-r--r--mcmap/chunk.go26
-rw-r--r--mcmap/examples/emeraldfinder/.gitignore1
-rw-r--r--mcmap/examples/emeraldfinder/main.go6
-rw-r--r--mcmap/examples/replace/.gitignore1
-rw-r--r--mcmap/examples/replace/main.go66
-rw-r--r--mcmap/prechunk.go146
-rw-r--r--mcmap/region.go47
-rw-r--r--mcmap/regionfile.go66
8 files changed, 332 insertions, 27 deletions
diff --git a/mcmap/chunk.go b/mcmap/chunk.go
index 22f9dfa..c504ae5 100644
--- a/mcmap/chunk.go
+++ b/mcmap/chunk.go
@@ -8,10 +8,17 @@ import (
func calcBlockOffset(x, y, z int) int {
if (x < 0) || (y < 0) || (z < 0) || (x >= 16) || (y >= 256) || (z >= 16) {
- panic(errors.New("Can't calculate Block offset, coordinates out of range."))
+ return -1
}
- return x + (z * 16) + (y * 256)
+ return x | (z << 4) | (y << 8)
+}
+
+func offsetToPos(off int) (x, y, z int) {
+ x = off & 0xf
+ z = (off >> 4) & 0xf
+ y = (off >> 8) & 0xff
+ return
}
// BlockToChunk calculates the chunk (cx, cz) and the block position in this chunk(rbx, rbz) of a block position given global coordinates.
@@ -36,17 +43,16 @@ type Chunk struct {
x, z int32
- lastUpdate int64
- populated bool
- inhabitatedTime int64
- ts time.Time
+ lastUpdate int64
+ populated bool
+ inhabitedTime int64
+ ts time.Time
- heightMap []int32 // Note: Ordered ZX
- blockLight, skyLight []byte // Note: Ordered YZX, only half-bytes
+ heightMap []int32 // Ordered ZX
modified bool
- blocks []Block // NOTE: Ordered YZX
- biomes []Biome // NOTE: Orderes XZ
+ blocks []Block // Ordered YZX
+ biomes []Biome // Ordered XZ
}
// MarkModified needs to be called, if some data of the chunk was modified.
diff --git a/mcmap/examples/emeraldfinder/.gitignore b/mcmap/examples/emeraldfinder/.gitignore
new file mode 100644
index 0000000..4cc4a2d
--- /dev/null
+++ b/mcmap/examples/emeraldfinder/.gitignore
@@ -0,0 +1 @@
+emeraldfinder
diff --git a/mcmap/examples/emeraldfinder/main.go b/mcmap/examples/emeraldfinder/main.go
index c437d41..56a1a26 100644
--- a/mcmap/examples/emeraldfinder/main.go
+++ b/mcmap/examples/emeraldfinder/main.go
@@ -16,9 +16,9 @@ func main() {
os.Exit(1)
}
- region, err := mcmap.OpenRegion(*path)
+ region, err := mcmap.OpenRegion(*path, true)
if err != nil {
- fmt.Fprintf(os.Stderr, "Could not open region: %s", err)
+ fmt.Fprintf(os.Stderr, "Could not open region: %s\n", err)
os.Exit(1)
}
@@ -31,7 +31,7 @@ chunkLoop:
case mcmap.NotAvailable:
continue chunkLoop
default:
- fmt.Fprintf(os.Stderr, "Error while getting chunk (%d, %d): %s", cx, cz, err)
+ fmt.Fprintf(os.Stderr, "Error while getting chunk (%d, %d): %s\n", cx, cz, err)
os.Exit(1)
}
diff --git a/mcmap/examples/replace/.gitignore b/mcmap/examples/replace/.gitignore
new file mode 100644
index 0000000..6e8b374
--- /dev/null
+++ b/mcmap/examples/replace/.gitignore
@@ -0,0 +1 @@
+replace
diff --git a/mcmap/examples/replace/main.go b/mcmap/examples/replace/main.go
new file mode 100644
index 0000000..f7bad96
--- /dev/null
+++ b/mcmap/examples/replace/main.go
@@ -0,0 +1,66 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "github.com/kch42/gomcmap/mcmap"
+ "os"
+)
+
+func main() {
+ path := flag.String("path", "", "Path to region directory")
+ flag.Parse()
+
+ if *path == "" {
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ region, err := mcmap.OpenRegion(*path, true)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not open region: %s\n", err)
+ os.Exit(1)
+ }
+
+chunkLoop:
+ for chunkPos := range region.AllChunks() {
+ cx, cz := chunkPos.X, chunkPos.Z
+ chunk, err := region.Chunk(cx, cz)
+ switch err {
+ case nil:
+ case mcmap.NotAvailable:
+ continue chunkLoop
+ default:
+ fmt.Fprintf(os.Stderr, "Error while getting chunk (%d, %d): %s\n", cx, cz, err)
+ os.Exit(1)
+ }
+
+ modified := false
+ for y := 0; y < 256; y++ {
+ for x := 0; x < 16; x++ {
+ for z := 0; z < 16; z++ {
+ blk := chunk.Block(x, y, z)
+ if blk.ID == mcmap.BlkBlockOfIron {
+ blk.ID = mcmap.BlkBlockOfDiamond
+ modified = true
+ }
+ }
+ }
+ }
+
+ if modified {
+ fmt.Printf("Modified chunk %d, %d.\n", cx, cz)
+ chunk.MarkModified()
+ }
+
+ if err := region.UnloadChunk(cx, cz); err != nil {
+ fmt.Fprintf(os.Stderr, "Error while unloading chunk %d, %d: %s\n", cx, cz, err)
+ os.Exit(1)
+ }
+ }
+
+ if err := region.Save(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error while saving: %s\n", err)
+ os.Exit(1)
+ }
+}
diff --git a/mcmap/prechunk.go b/mcmap/prechunk.go
index aff3548..89febe7 100644
--- a/mcmap/prechunk.go
+++ b/mcmap/prechunk.go
@@ -31,6 +31,15 @@ func halfbyte(b []byte, i int) byte {
return (b[i/2] >> 4) & 0x0f
}
+func setHalfbyte(b []byte, i int, v byte) {
+ v &= 0xf
+ if i%2 == 0 {
+ b[i/2] |= v
+ } else {
+ b[i/2] |= v << 4
+ }
+}
+
func extractCoord(tc nbt.TagCompound) (x, y, z int, err error) {
var _x, _y, _z int32
if _x, err = tc.GetInt("x"); err != nil {
@@ -106,11 +115,11 @@ func (pc *preChunk) toChunk() (*Chunk, error) {
}
c.populated = (populated == 1)
- c.inhabitatedTime, err = lvl.GetLong("InhabitedTime")
+ c.inhabitedTime, err = lvl.GetLong("InhabitedTime")
switch err {
case nil:
case nbt.NotFound:
- c.inhabitatedTime = 0
+ c.inhabitedTime = 0
default:
return nil, fmt.Errorf("Could not read InhabitatedTime tag: %s", err)
}
@@ -255,3 +264,136 @@ func (pc *preChunk) toChunk() (*Chunk, error) {
return &c, nil
}
+
+func (c *Chunk) toPreChunk() (*preChunk, error) {
+ terraPopulated := byte(0)
+ if c.populated {
+ terraPopulated = 1
+ }
+ lvl := nbt.TagCompound{
+ "xPos": nbt.NewIntTag(c.x),
+ "zPos": nbt.NewIntTag(c.z),
+ "LastUpdate": nbt.NewLongTag(c.lastUpdate),
+ "TerrainPopulated": nbt.NewByteTag(terraPopulated),
+ "InhabitedTime": nbt.NewLongTag(c.inhabitedTime),
+ "HeightMap": nbt.NewIntArrayTag(c.heightMap),
+ "Entities": nbt.Tag{nbt.TAG_Compound, c.Entities},
+ }
+
+ hasBiomes := false
+ biomes := make([]byte, 16*16)
+ for i, bio := range c.biomes {
+ if bio != BioUncalculated {
+ hasBiomes = true
+ break
+ }
+ biomes[i] = byte(bio)
+ }
+ if hasBiomes {
+ lvl["Biomes"] = nbt.NewByteArrayTag(biomes)
+ }
+
+ sections := make([]nbt.TagCompound, 0)
+ tileEnts := make([]nbt.TagCompound, 0)
+ tileTicks := make([]nbt.TagCompound, 0)
+
+ for subchunk := 0; subchunk < 16; subchunk++ {
+ off := subchunk * 4096
+
+ blocks := make([]byte, 4096)
+ add := make([]byte, 2048)
+ data := make([]byte, 2048)
+ blockLight := make([]byte, 2048)
+ skyLight := make([]byte, 2048)
+
+ allAir, addEmpty := true, true
+ for i := 0; i < 4096; i++ {
+ blk := c.blocks[i+off]
+ id := blk.ID
+ if id != BlkAir {
+ allAir = false
+ }
+
+ blocks[i] = byte(id & 0xff)
+ idH := byte(id >> 8)
+ if idH != 0 {
+ addEmpty = false
+ }
+ setHalfbyte(add, i, idH)
+
+ setHalfbyte(data, i, blk.Data)
+ setHalfbyte(blockLight, i, blk.BlockLight)
+ setHalfbyte(skyLight, i, blk.SkyLight)
+
+ x, y, z := offsetToPos(i + off)
+ x, z = ChunkToBlock(int(c.x), int(c.z), x, z)
+
+ if (blk.TileEntity != nil) && (len(blk.TileEntity) > 0) {
+ // Fix coords
+ blk.TileEntity["x"] = nbt.NewIntTag(int32(x))
+ blk.TileEntity["y"] = nbt.NewIntTag(int32(y))
+ blk.TileEntity["z"] = nbt.NewIntTag(int32(z))
+ tileEnts = append(tileEnts, blk.TileEntity)
+ }
+
+ if blk.Tick != nil {
+ tileTick := nbt.TagCompound{
+ "x": nbt.NewIntTag(int32(x)),
+ "y": nbt.NewIntTag(int32(y)),
+ "z": nbt.NewIntTag(int32(z)),
+ "i": nbt.NewIntTag(blk.Tick.i),
+ "t": nbt.NewIntTag(blk.Tick.t),
+ }
+ if blk.Tick.hasP {
+ tileTick["p"] = nbt.NewIntTag(blk.Tick.p)
+ }
+ tileTicks = append(tileTicks, tileTick)
+ }
+ }
+
+ if !allAir {
+ comp := nbt.TagCompound{
+ "Y": nbt.NewByteTag(byte(subchunk)),
+ "Blocks": nbt.NewByteArrayTag(blocks),
+ "Data": nbt.NewByteArrayTag(data),
+ "BlockLight": nbt.NewByteArrayTag(blockLight),
+ "SkyLight": nbt.NewByteArrayTag(skyLight),
+ }
+ if !addEmpty {
+ comp["Add"] = nbt.NewByteArrayTag(add)
+ }
+ sections = append(sections, comp)
+ }
+ }
+
+ lvl["Sections"] = nbt.NewListTag(nbt.TAG_Compound, sections)
+
+ if len(c.Entities) > 0 {
+ lvl["Entities"] = nbt.NewListTag(nbt.TAG_Compound, c.Entities)
+ } else {
+ lvl["Entities"] = nbt.NewListTag(nbt.TAG_Byte, []byte{})
+ }
+ if len(tileEnts) > 0 {
+ lvl["TileEntities"] = nbt.NewListTag(nbt.TAG_Compound, tileEnts)
+ } else {
+ lvl["TileEntities"] = nbt.NewListTag(nbt.TAG_Byte, []byte{})
+ }
+ if len(tileTicks) > 0 {
+ lvl["TileTicks"] = nbt.NewListTag(nbt.TAG_Compound, tileTicks)
+ }
+
+ root := nbt.Tag{nbt.TAG_Compound, nbt.TagCompound{
+ "Level": nbt.Tag{nbt.TAG_Compound, lvl},
+ }}
+
+ buf := new(bytes.Buffer)
+ if err := nbt.WriteZlibdNamedTag(buf, "", root); err != nil {
+ return nil, err
+ }
+
+ return &preChunk{
+ ts: c.ts,
+ data: buf.Bytes(),
+ compression: compressZlib,
+ }, nil
+}
diff --git a/mcmap/region.go b/mcmap/region.go
index cbb9f58..8467bbf 100644
--- a/mcmap/region.go
+++ b/mcmap/region.go
@@ -21,15 +21,15 @@ type superchunk struct {
type Region struct {
path string
+ autosave bool
superchunksAvail map[XZPos]bool
-
- superchunks map[XZPos]*superchunk
+ superchunks map[XZPos]*superchunk
}
var mcaRegex = regexp.MustCompile(`^r\.([0-9-]+)\.([0-9-]+)\.mca$`)
-// OpenRegion opens a region directory.
-func OpenRegion(path string) (*Region, error) {
+// OpenRegion opens a region directory. If autosave is true, mcmap will save modified and unloaded chunks automatically to reduce memory usage. You still have to call Save at the end.
+func OpenRegion(path string, autosave bool) (*Region, error) {
rv := &Region{
path: path,
superchunksAvail: make(map[XZPos]bool),
@@ -142,16 +142,28 @@ func (reg *Region) loadSuperchunk(pos XZPos) error {
return nil
}
-func (reg *Region) cleanSuperchunks() error {
+func (reg *Region) cleanSuperchunks(forceSave bool) error {
del := make(map[XZPos]bool)
for scPos, sc := range reg.superchunks {
if len(sc.chunks) > 0 {
- return nil
+ continue
}
if sc.modified {
- // TODO: Save superchunk to region file
+ if !(reg.autosave || forceSave) {
+ continue
+ }
+ fn := fmt.Sprintf("%s%cr.%d.%d.mca", reg.path, os.PathSeparator, scPos.X, scPos.Z)
+ f, err := os.Create(fn)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ if err := writeRegionFile(f, sc.preChunks); err != nil {
+ return err
+ }
}
del[scPos] = true
@@ -191,7 +203,7 @@ func (reg *Region) Chunk(x, z int) (*Chunk, error) {
sc.chunks[cPos] = chunk
}
- if err := reg.cleanSuperchunks(); err != nil {
+ if err := reg.cleanSuperchunks(false); err != nil {
return nil, err
}
@@ -199,29 +211,35 @@ func (reg *Region) Chunk(x, z int) (*Chunk, error) {
}
// UnloadChunk marks a chunk as unused. If all chunks of a superchunk are marked as unused, the superchunk will be unloaded and saved (if needed).
-func (reg *Region) UnloadChunk(x, z int) {
+func (reg *Region) UnloadChunk(x, z int) error {
scx, scz, cx, cz := chunkToSuperchunk(x, z)
scPos := XZPos{scx, scz}
cPos := XZPos{cx, cz}
sc, ok := reg.superchunks[scPos]
if !ok {
- return
+ return nil
}
chunk, ok := sc.chunks[cPos]
if !ok {
- return
+ return nil
}
if chunk.modified {
- // TODO: Save to prechunks
+ pc, err := chunk.toPreChunk()
+ if err != nil {
+ return err
+ }
+ sc.preChunks[cPos] = pc
chunk.modified = false
sc.modified = true
}
delete(sc.chunks, cPos)
+
+ return nil
}
// AllChunks returns a channel that will give you the positions of all possibly available chunks in an efficient order.
@@ -244,3 +262,8 @@ func (reg *Region) AllChunks() <-chan XZPos {
return ch
}
+
+// Save saves modified and unloaded chunks.
+func (reg *Region) Save() error {
+ return reg.cleanSuperchunks(true)
+}
diff --git a/mcmap/regionfile.go b/mcmap/regionfile.go
index 1be08ea..ff9794e 100644
--- a/mcmap/regionfile.go
+++ b/mcmap/regionfile.go
@@ -15,6 +15,10 @@ type chunkOffTs struct {
ts time.Time
}
+func (co chunkOffTs) calcLocationEntry() uint32 {
+ return uint32((co.size>>12)&0xff) | (uint32(co.offset>>12) << 8)
+}
+
func (cOff chunkOffTs) readPreChunk(r io.ReadSeeker) (*preChunk, error) {
pc := preChunk{ts: cOff.ts}
@@ -99,3 +103,65 @@ func readRegionFile(r io.ReadSeeker) (map[XZPos]*preChunk, error) {
return preChunks, nil
}
+
+func (pc *preChunk) writePreChunk(w io.Writer) error {
+ length := uint32(len(pc.data) + 1)
+ if err := binary.Write(w, binary.BigEndian, length); err != nil {
+ return err
+ }
+ if _, err := w.Write([]byte{pc.compression}); err != nil {
+ return err
+ }
+ _, err := w.Write(pc.data)
+ return err
+}
+
+func writeRegionFile(w io.Writer, pcs map[XZPos]*preChunk) error {
+ offs := make(map[XZPos]chunkOffTs)
+ buf := new(bytes.Buffer)
+ pw := kagus.NewPaddedWriter(buf, 4096)
+
+ for pos, pc := range pcs {
+ off := buf.Len()
+ if err := pc.writePreChunk(pw); err != nil {
+ return err
+ }
+ if err := pw.Pad(); err != nil {
+ return err
+ }
+ offs[pos] = chunkOffTs{
+ offset: int64(8192 + off),
+ size: int64(buf.Len() - off),
+ ts: pc.ts,
+ }
+ }
+
+ for z := 0; z < 32; z++ {
+ for x := 0; x < 32; x++ {
+ off := uint32(0)
+ if cOff, ok := offs[XZPos{x, z}]; ok {
+ off = cOff.calcLocationEntry()
+ }
+
+ if err := binary.Write(w, binary.BigEndian, off); err != nil {
+ return err
+ }
+ }
+ }
+
+ for z := 0; z < 32; z++ {
+ for x := 0; x < 32; x++ {
+ ts := int32(0)
+ if cOff, ok := offs[XZPos{x, z}]; ok {
+ ts = int32(cOff.ts.Unix())
+ }
+
+ if err := binary.Write(w, binary.BigEndian, ts); err != nil {
+ return err
+ }
+ }
+ }
+
+ _, err := io.Copy(w, buf)
+ return err
+}