From a44c4ad107a197a92e9ffdde38593fd7012a309e Mon Sep 17 00:00:00 2001 From: Kevin Chabowski Date: Mon, 12 Aug 2013 12:50:57 +0200 Subject: Writing maps implemented --- mcmap/chunk.go | 26 +++--- mcmap/examples/emeraldfinder/.gitignore | 1 + mcmap/examples/emeraldfinder/main.go | 6 +- mcmap/examples/replace/.gitignore | 1 + mcmap/examples/replace/main.go | 66 +++++++++++++++ mcmap/prechunk.go | 146 +++++++++++++++++++++++++++++++- mcmap/region.go | 47 +++++++--- mcmap/regionfile.go | 66 +++++++++++++++ 8 files changed, 332 insertions(+), 27 deletions(-) create mode 100644 mcmap/examples/emeraldfinder/.gitignore create mode 100644 mcmap/examples/replace/.gitignore create mode 100644 mcmap/examples/replace/main.go (limited to 'mcmap') 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 +} -- cgit v1.2.3-70-g09d2