diff options
Diffstat (limited to 'mcmap')
| -rw-r--r-- | mcmap/chunk.go | 26 | ||||
| -rw-r--r-- | mcmap/examples/emeraldfinder/.gitignore | 1 | ||||
| -rw-r--r-- | mcmap/examples/emeraldfinder/main.go | 6 | ||||
| -rw-r--r-- | mcmap/examples/replace/.gitignore | 1 | ||||
| -rw-r--r-- | mcmap/examples/replace/main.go | 66 | ||||
| -rw-r--r-- | mcmap/prechunk.go | 146 | ||||
| -rw-r--r-- | mcmap/region.go | 47 | ||||
| -rw-r--r-- | mcmap/regionfile.go | 66 | 
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 +} | 
