├── cmd ├── mcobj │ ├── .gitignore │ ├── version.go │ ├── usage_linux.go │ ├── usage_windows.go │ ├── usage_darwin.go │ ├── enclosedChunk.go │ ├── sides.go │ ├── memorywriterpool.go │ ├── sideCache.go │ ├── mtl.go │ ├── blocktypes.go │ ├── prt.go │ ├── obj.go │ └── mcobj.go └── map2d │ └── map2d.go ├── nbt ├── block.go ├── level.go ├── level_test.go ├── explain.go ├── chunk.go └── nbt.go ├── .gitignore ├── update-version.sh ├── mcworld ├── chunkmasks.go ├── world.go ├── alphaworld.go └── betaworld.go ├── LICENSE ├── commandline ├── commandlineSplit_test.go └── commandline.go ├── TODO ├── README.md └── blocks.json /cmd/mcobj/.gitignore: -------------------------------------------------------------------------------- 1 | mcobj 2 | mcobj.exe 3 | -------------------------------------------------------------------------------- /nbt/block.go: -------------------------------------------------------------------------------- 1 | package nbt 2 | 3 | type Block uint16 4 | -------------------------------------------------------------------------------- /cmd/mcobj/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const version = "0.14" 4 | -------------------------------------------------------------------------------- /cmd/mcobj/usage_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | ExampleWorldPath = "~/.minecraft/saves/World1" 5 | ) 6 | -------------------------------------------------------------------------------- /cmd/mcobj/usage_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | ExampleWorldPath = "%AppData%\\.minecraft\\saves\\World1" 5 | ) 6 | -------------------------------------------------------------------------------- /cmd/mcobj/usage_darwin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const ( 4 | ExampleWorldPath = "~/Library/Application\\ Support/minecraft/saves/World1" 5 | ) 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _* 2 | *.out 3 | *.[568] 4 | *.a 5 | *.exe 6 | *.obj 7 | *.obj.f 8 | *.obj.v 9 | *.mtl 10 | *.prt 11 | *.zip 12 | *.7z 13 | .*.swp 14 | -------------------------------------------------------------------------------- /update-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ( 4 | echo "package main" 5 | echo 6 | echo "const version = \""$(git describe || echo "unknown")"\"" 7 | ) > cmd/mcobj/version.go 8 | -------------------------------------------------------------------------------- /mcworld/chunkmasks.go: -------------------------------------------------------------------------------- 1 | package mcworld 2 | 3 | type ChunkMask interface { 4 | IsMasked(x, z int) bool 5 | } 6 | 7 | type RectangleChunkMask struct { 8 | X0, Z0, X1, Z1 int 9 | } 10 | 11 | func (m *RectangleChunkMask) IsMasked(x, z int) bool { 12 | return x < m.X0 || x >= m.X1 || z < m.Z0 || z >= m.Z1 13 | } 14 | 15 | type AllChunksMask struct{} 16 | 17 | func (m *AllChunksMask) IsMasked(x, z int) bool { 18 | return false 19 | } 20 | -------------------------------------------------------------------------------- /cmd/mcobj/enclosedChunk.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quag/mcobj/nbt" 5 | ) 6 | 7 | type EnclosingSides [4]ChunkSide 8 | type EnclosedChunk struct { 9 | xPos, zPos int 10 | blocks Blocks 11 | enclosing EnclosingSides 12 | } 13 | 14 | func (s *EnclosingSides) side(i int) ChunkSide { 15 | return (*s)[i] 16 | } 17 | 18 | func (e *EnclosedChunk) Get(x, y, z int) (blockId nbt.Block) { 19 | switch { 20 | case y < 0 && hideBottom: 21 | blockId = 7 // Bedrock 22 | case y < 0 && !hideBottom: 23 | case y >= e.blocks.height: 24 | blockId = 0 25 | case x == -1: 26 | blockId = e.enclosing.side(0).BlockId(z, y) 27 | case x == 16: 28 | blockId = e.enclosing.side(1).BlockId(z, y) 29 | case z == -1: 30 | blockId = e.enclosing.side(2).BlockId(x, y) 31 | case z == 16: 32 | blockId = e.enclosing.side(3).BlockId(x, y) 33 | default: 34 | blockId = e.blocks.Get(x, y, z) 35 | } 36 | 37 | return 38 | } 39 | 40 | func (e *EnclosedChunk) height() int { 41 | return e.blocks.height 42 | } 43 | -------------------------------------------------------------------------------- /cmd/mcobj/sides.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quag/mcobj/nbt" 5 | ) 6 | 7 | var ( 8 | emptySide = &FixedChunkSide{0} 9 | solidSide = &FixedChunkSide{1} 10 | defaultSide = solidSide 11 | ) 12 | 13 | type ChunkSide interface { 14 | BlockId(x, y int) nbt.Block 15 | } 16 | 17 | type FixedChunkSide struct { 18 | blockId nbt.Block 19 | } 20 | 21 | func (s *FixedChunkSide) BlockId(x, y int) nbt.Block { 22 | return s.blockId 23 | } 24 | 25 | func NewChunkSide(height int) *ChunkSideData { 26 | return &ChunkSideData{make([]nbt.Block, height*16)} 27 | } 28 | 29 | type ChunkSideData struct { 30 | data []nbt.Block 31 | } 32 | 33 | type ChunkSidesData [4]*ChunkSideData 34 | 35 | func (s *ChunkSideData) index(x, y int) int { 36 | return y + (x * s.height()) 37 | } 38 | 39 | func (s *ChunkSideData) BlockId(x, y int) nbt.Block { 40 | return s.data[s.index(x, y)] 41 | } 42 | 43 | func (s *ChunkSideData) Column(x int) BlockColumn { 44 | var i = s.height() * x 45 | return BlockColumn(s.data[i : i+s.height()]) 46 | } 47 | 48 | func (s *ChunkSideData) height() int { 49 | return len(s.data) / 16 50 | } 51 | -------------------------------------------------------------------------------- /cmd/mcobj/memorywriterpool.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type MemoryWriter struct { 4 | buf []byte 5 | } 6 | 7 | func (m *MemoryWriter) Clean() { 8 | if m.buf != nil { 9 | m.buf = m.buf[:0] 10 | } 11 | } 12 | 13 | func (m *MemoryWriter) Write(p []byte) (n int, err error) { 14 | m.buf = append(m.buf, p...) 15 | return len(p), nil 16 | } 17 | 18 | type MemoryWriterPool struct { 19 | freelist chan *MemoryWriter 20 | initialBufferSize int 21 | } 22 | 23 | func NewMemoryWriterPool(capacity int, initialBufferSize int) *MemoryWriterPool { 24 | return &MemoryWriterPool{make(chan *MemoryWriter, capacity), initialBufferSize} 25 | } 26 | 27 | func (p *MemoryWriterPool) GetWriter() *MemoryWriter { 28 | var b *MemoryWriter 29 | select { 30 | case b = <-p.freelist: 31 | // Got a buffer 32 | default: 33 | b = &MemoryWriter{make([]byte, 0, p.initialBufferSize)} 34 | } 35 | return b 36 | } 37 | 38 | func (p *MemoryWriterPool) ReuseWriter(b *MemoryWriter) { 39 | b.Clean() 40 | select { 41 | case p.freelist <- b: 42 | // buffer added to free list 43 | default: 44 | // free list is full, discard the buffer 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jonathan Wright 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /nbt/level.go: -------------------------------------------------------------------------------- 1 | package nbt 2 | 3 | import ( 4 | "compress/gzip" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | var ( 10 | DataStructNotFound = errors.New("'Data' struct not found") 11 | SpawnIntNotFound = errors.New("SpawnX/SpawnY/SpawnZ int32s not found") 12 | ) 13 | 14 | type Level struct { 15 | SpawnX, SpawnY, SpawnZ int 16 | } 17 | 18 | func ReadLevelDat(reader io.Reader) (*Level, error) { 19 | r, err := gzip.NewReader(reader) 20 | defer r.Close() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return ReadLevelNbt(r) 26 | } 27 | 28 | func ReadLevelNbt(reader io.Reader) (*Level, error) { 29 | root, err := Parse(reader) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | dataValue := root["Data"] 35 | if dataValue == nil { 36 | return nil, DataStructNotFound 37 | } 38 | 39 | data, ok := dataValue.(map[string]interface{}) 40 | if !ok { 41 | return nil, DataStructNotFound 42 | } 43 | 44 | xval, xok := data["SpawnX"] 45 | yval, yok := data["SpawnY"] 46 | zval, zok := data["SpawnZ"] 47 | 48 | if !xok || !yok || !zok { 49 | return nil, SpawnIntNotFound 50 | } 51 | 52 | level := new(Level) 53 | level.SpawnX, xok = xval.(int) 54 | level.SpawnY, yok = yval.(int) 55 | level.SpawnZ, zok = zval.(int) 56 | 57 | if !xok || !yok || !zok { 58 | return nil, SpawnIntNotFound 59 | } 60 | 61 | return level, nil 62 | } 63 | -------------------------------------------------------------------------------- /mcworld/world.go: -------------------------------------------------------------------------------- 1 | package mcworld 2 | 3 | import ( 4 | "io" 5 | "math" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type ChunkOpener interface { 11 | OpenChunk(x, z int) (io.ReadCloser, error) 12 | } 13 | 14 | type ChunkPooler interface { 15 | ChunkPool(mask ChunkMask) (ChunkPool, error) 16 | } 17 | 18 | type World interface { 19 | ChunkOpener 20 | ChunkPooler 21 | } 22 | 23 | type ChunkPool interface { 24 | Pop(x, z int) bool 25 | Remaining() int 26 | BoundingBox() *BoundingBox 27 | } 28 | 29 | type BoundingBox struct { 30 | X0, Z0, X1, Z1 int 31 | } 32 | 33 | func OpenWorld(worldDir string) World { 34 | var _, err = os.Stat(filepath.Join(worldDir, "region")) 35 | if err != nil { 36 | return &AlphaWorld{worldDir} 37 | } 38 | return &BetaWorld{worldDir} 39 | } 40 | 41 | type ReadCloserPair struct { 42 | reader io.ReadCloser 43 | closer io.Closer 44 | } 45 | 46 | func (r *ReadCloserPair) Read(p []byte) (int, error) { 47 | return r.reader.Read(p) 48 | } 49 | 50 | func (r *ReadCloserPair) Close() error { 51 | var ( 52 | readerErr = r.reader.Close() 53 | closerErr = r.closer.Close() 54 | ) 55 | 56 | if closerErr != nil { 57 | return closerErr 58 | } 59 | return readerErr 60 | } 61 | 62 | func EmptyBoundingBox() *BoundingBox { 63 | return &BoundingBox{math.MaxInt32, math.MaxInt32, math.MinInt32, math.MinInt32} 64 | } 65 | 66 | func (b *BoundingBox) Union(x, z int) { 67 | if x < b.X0 { 68 | b.X0 = x 69 | } else if x > b.X1 { 70 | b.X1 = x 71 | } 72 | 73 | if z < b.Z0 { 74 | b.Z0 = z 75 | } else if z > b.Z1 { 76 | b.Z1 = z 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /cmd/mcobj/sideCache.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quag/mcobj/nbt" 5 | ) 6 | 7 | type SideCache struct { 8 | chunks map[uint64]*ChunkSidesData 9 | } 10 | 11 | func (s *SideCache) Clear() { 12 | s.chunks = nil 13 | } 14 | 15 | func (s *SideCache) AddChunk(chunk *nbt.Chunk) { 16 | if s.HasSide(chunk.XPos, chunk.ZPos) { 17 | return 18 | } 19 | 20 | if s.chunks == nil { 21 | s.chunks = make(map[uint64]*ChunkSidesData) 22 | } 23 | 24 | s.chunks[s.key(chunk.XPos, chunk.ZPos)] = calculateSides(wrapBlockData(chunk.Blocks)) 25 | } 26 | 27 | func wrapBlockData(data []nbt.Block) Blocks { 28 | return Blocks{data, len(data) / (16 * 16)} 29 | } 30 | 31 | func (s *SideCache) HasSide(x, z int) bool { 32 | if s.chunks == nil { 33 | return false 34 | } 35 | var _, present = s.chunks[s.key(x, z)] 36 | return present 37 | } 38 | 39 | func (s *SideCache) EncloseChunk(chunk *nbt.Chunk) *EnclosedChunk { 40 | return &EnclosedChunk{ 41 | chunk.XPos, 42 | chunk.ZPos, 43 | wrapBlockData(chunk.Blocks), 44 | EnclosingSides{ 45 | s.getSide(chunk.XPos-1, chunk.ZPos, 1), 46 | s.getSide(chunk.XPos+1, chunk.ZPos, 0), 47 | s.getSide(chunk.XPos, chunk.ZPos-1, 3), 48 | s.getSide(chunk.XPos, chunk.ZPos+1, 2), 49 | }, 50 | } 51 | } 52 | 53 | func calculateSides(blocks Blocks) *ChunkSidesData { 54 | var sides = &ChunkSidesData{NewChunkSide(blocks.height), NewChunkSide(blocks.height), NewChunkSide(blocks.height), NewChunkSide(blocks.height)} 55 | for i := 0; i < 16; i++ { 56 | copy(sides[0].Column(i), blocks.Column(0, i)) 57 | copy(sides[1].Column(i), blocks.Column(15, i)) 58 | copy(sides[2].Column(i), blocks.Column(i, 0)) 59 | copy(sides[3].Column(i), blocks.Column(i, 15)) 60 | } 61 | 62 | return sides 63 | } 64 | 65 | func (s *SideCache) getSide(x, z int, side int) ChunkSide { 66 | if s.chunks == nil { 67 | return defaultSide 68 | } 69 | var chunk, present = s.chunks[s.key(x, z)] 70 | if !present { 71 | return defaultSide 72 | } 73 | 74 | var chunkSide = chunk[side] 75 | 76 | chunk[side] = nil 77 | 78 | if chunk[0] == nil && chunk[1] == nil && chunk[2] == nil && chunk[3] == nil { 79 | delete(s.chunks, s.key(x, z)) 80 | } 81 | 82 | return chunkSide 83 | } 84 | 85 | func (s *SideCache) key(x, z int) uint64 { 86 | return (uint64(x) << 32) + uint64(z) 87 | } 88 | -------------------------------------------------------------------------------- /mcworld/alphaworld.go: -------------------------------------------------------------------------------- 1 | package mcworld 2 | 3 | import ( 4 | "compress/gzip" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | type AlphaWorld struct { 13 | worldDir string 14 | } 15 | 16 | func (w *AlphaWorld) OpenChunk(x, z int) (io.ReadCloser, error) { 17 | var file, fileErr = os.Open(chunkPath(w.worldDir, x, z)) 18 | if fileErr != nil { 19 | return nil, fileErr 20 | } 21 | var decompressor, gzipErr = gzip.NewReader(file) 22 | if gzipErr != nil { 23 | file.Close() 24 | return nil, gzipErr 25 | } 26 | return &ReadCloserPair{decompressor, file}, nil 27 | } 28 | 29 | type AlphaChunkPool struct { 30 | chunkMap map[string]bool 31 | box *BoundingBox 32 | worldDir string 33 | } 34 | 35 | func (p *AlphaChunkPool) Pop(x, z int) bool { 36 | var chunkFilename = chunkPath(p.worldDir, x, z) 37 | var _, exists = p.chunkMap[chunkFilename] 38 | delete(p.chunkMap, chunkFilename) 39 | return exists 40 | } 41 | 42 | func (p *AlphaChunkPool) BoundingBox() *BoundingBox { 43 | return p.box 44 | } 45 | 46 | func (p *AlphaChunkPool) Remaining() int { 47 | return len(p.chunkMap) 48 | } 49 | 50 | func (w *AlphaWorld) ChunkPool(mask ChunkMask) (ChunkPool, error) { 51 | chunks := make(map[string]bool) 52 | box := EmptyBoundingBox() 53 | 54 | err := filepath.Walk(w.worldDir, func(path string, info os.FileInfo, err error) error { 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if !info.IsDir() { 60 | var match, err = filepath.Match("c.*.*.dat", filepath.Base(path)) 61 | if match && err == nil { 62 | var ( 63 | s = strings.SplitN(filepath.Base(path), ".", 4) 64 | x, xErr = strconv.ParseInt(s[1], 36, 64) 65 | z, zErr = strconv.ParseInt(s[2], 36, 64) 66 | ) 67 | if xErr == nil && zErr == nil && !mask.IsMasked(int(x), int(z)) { 68 | chunks[path] = true 69 | box.Union(int(x), int(z)) 70 | } 71 | } 72 | } 73 | 74 | return nil 75 | }) 76 | return &AlphaChunkPool{chunks, box, w.worldDir}, err 77 | } 78 | 79 | func chunkPath(world string, x, z int) string { 80 | return filepath.Join(world, encodeFolder(x), encodeFolder(z), "c."+base36(x)+"."+base36(z)+".dat") 81 | } 82 | 83 | func base36(i int) string { 84 | return strconv.FormatInt(int64(i), 36) 85 | } 86 | 87 | func encodeFolder(i int) string { 88 | return base36(((i % 64) + 64) % 64) 89 | } 90 | -------------------------------------------------------------------------------- /commandline/commandlineSplit_test.go: -------------------------------------------------------------------------------- 1 | package commandline 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSingleArg(t *testing.T) { 8 | args := SplitCommandLine("abc") 9 | checkArgs(t, args, "abc") 10 | } 11 | 12 | func TestMultipleArgs(t *testing.T) { 13 | args := SplitCommandLine("abc def ghi jkl mno") 14 | checkArgs(t, args, "abc", "def", "ghi", "jkl", "mno") 15 | } 16 | 17 | func TestMultipleSpacesBetweenArgs(t *testing.T) { 18 | args := SplitCommandLine(" abc def ghi ") 19 | checkArgs(t, args, "abc", "def", "ghi") 20 | } 21 | 22 | func TestQuotedSpaces(t *testing.T) { 23 | checkArgs(t, SplitCommandLine("\"a b c\""), "a b c") 24 | checkArgs(t, SplitCommandLine("'a b c'"), "a b c") 25 | } 26 | 27 | func TestQuoteWithinArg(t *testing.T) { 28 | checkArgs(t, SplitCommandLine("abc\"def\"hij"), "abcdefhij") 29 | checkArgs(t, SplitCommandLine("abc'def'hij"), "abcdefhij") 30 | } 31 | 32 | func TestEmptyQuoteArg(t *testing.T) { 33 | checkArgs(t, SplitCommandLine("\"\""), "") 34 | checkArgs(t, SplitCommandLine("''"), "") 35 | } 36 | 37 | func TestUnterminatedQuote(t *testing.T) { 38 | checkArgs(t, SplitCommandLine("\""), "") 39 | checkArgs(t, SplitCommandLine("'"), "") 40 | } 41 | 42 | func TestSlashSpace(t *testing.T) { 43 | checkArgs(t, SplitCommandLine("a\\ b"), "a b") 44 | } 45 | 46 | func TestSlashQuote(t *testing.T) { 47 | checkArgs(t, SplitCommandLine("a\\\"b"), "a\\\"b") 48 | checkArgs(t, SplitCommandLine("a\\\"b"), "a\\\"b") 49 | } 50 | 51 | func TestSlashQuoteInQuote(t *testing.T) { 52 | checkArgs(t, SplitCommandLine("\"a\\\"b\""), "a\\\"b") 53 | checkArgs(t, SplitCommandLine("'a\\\"b'"), "a\\\"b") 54 | checkArgs(t, SplitCommandLine("\"a\\'b\""), "a\\'b") 55 | checkArgs(t, SplitCommandLine("'a\\'b'"), "a\\'b") 56 | } 57 | 58 | func TestSlashSpaceInQuote(t *testing.T) { 59 | checkArgs(t, SplitCommandLine("\"a\\ b\""), "a b") 60 | checkArgs(t, SplitCommandLine("'a\\ b'"), "a b") 61 | } 62 | 63 | // TODO: correct for spaces in paths 64 | 65 | func checkArgs(t *testing.T, args []string, expectedArgs ...string) { 66 | t.Logf("args: %q expected: %q", args, expectedArgs) 67 | checkArgCount(t, args, len(expectedArgs)) 68 | for i, value := range expectedArgs { 69 | checkArg(t, args, i, value) 70 | } 71 | } 72 | 73 | func checkArgCount(t *testing.T, args []string, n int) { 74 | if len(args) != n { 75 | t.Errorf("Arg count %d instead of %d", len(args), n) 76 | } 77 | } 78 | 79 | func checkArg(t *testing.T, args []string, i int, value string) { 80 | if i >= len(args) { 81 | t.Errorf("args[%d] missing (should have been %q)", i, value) 82 | } else if args[i] != value { 83 | t.Errorf("args[%d] %q instead of %q", i, args[i], value) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /nbt/level_test.go: -------------------------------------------------------------------------------- 1 | package nbt 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io" 7 | "testing" 8 | ) 9 | 10 | func TestReadSpawnXYZ(t *testing.T) { 11 | level, err := readLevelBytes(10, 0, 0, 10, 0, 4, 'D', 'a', 't', 'a', 3, 0, 6, 'S', 'p', 'a', 'w', 'n', 'X', 0, 0, 0, 13, 3, 0, 6, 'S', 'p', 'a', 'w', 'n', 'Y', 0, 0, 0, 14, 3, 0, 6, 'S', 'p', 'a', 'w', 'n', 'Z', 0, 0, 0, 15, 0, 0) 12 | 13 | checkError(t, err, nil) 14 | 15 | if level == nil { 16 | t.Error("Level is nil") 17 | } else { 18 | if level.SpawnX != 13 { 19 | t.Errorf("SpawnX %d not 13", level.SpawnX) 20 | } 21 | 22 | if level.SpawnY != 14 { 23 | t.Errorf("SpawnY %d not 14", level.SpawnY) 24 | } 25 | 26 | if level.SpawnZ != 15 { 27 | t.Errorf("SpawnZ %d not 15", level.SpawnZ) 28 | } 29 | } 30 | } 31 | 32 | func TestLevelParseError(t *testing.T) { 33 | checkLevelReadError(t, io.EOF, 0xff) 34 | } 35 | 36 | func TestLevelDataNotFound(t *testing.T) { 37 | checkLevelReadError(t, DataStructNotFound, 10, 0, 0, 10, 0, 4, 'Z', 'Z', 'Z', 'Z', 0, 0) 38 | } 39 | 40 | func TestLevelDataNotStruct(t *testing.T) { 41 | checkLevelReadError(t, DataStructNotFound, 10, 0, 0, 3, 0, 4, 'D', 'a', 't', 'a', 1, 2, 3, 4, 0, 0) 42 | } 43 | 44 | func TestSpawnNotFound(t *testing.T) { 45 | checkLevelReadError(t, SpawnIntNotFound, 10, 0, 0, 10, 0, 4, 'D', 'a', 't', 'a', 3, 0, 6, 'S', 'p', 'a', 'w', 'n', 'A', 0, 0, 0, 13, 3, 0, 6, 'S', 'p', 'a', 'w', 'n', 'B', 0, 0, 0, 14, 3, 0, 6, 'S', 'p', 'a', 'w', 'n', 'C', 0, 0, 0, 15, 0, 0) 46 | } 47 | 48 | func TestSpawnNotInt(t *testing.T) { 49 | checkLevelReadError(t, SpawnIntNotFound, 10, 0, 0, 10, 0, 4, 'D', 'a', 't', 'a', 10, 0, 6, 'S', 'p', 'a', 'w', 'n', 'X', 0, 10, 0, 6, 'S', 'p', 'a', 'w', 'n', 'Y', 0, 10, 0, 6, 'S', 'p', 'a', 'w', 'n', 'Z', 0, 0, 0) 50 | } 51 | 52 | // TODO: Data/Player/Pos 53 | 54 | func readLevelBytes(b ...byte) (*Level, error) { 55 | r, err := gzipBytesReader(b) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return ReadLevelDat(r) 60 | } 61 | 62 | func gzipBytesReader(b []byte) (io.Reader, error) { 63 | buffer := new(bytes.Buffer) 64 | gw := gzip.NewWriter(buffer) 65 | gw.Write(b) 66 | gw.Close() 67 | return buffer, nil 68 | } 69 | 70 | func checkLevelReadError(t *testing.T, expectedErr error, bytes ...byte) { 71 | level, err := readLevelBytes(bytes...) 72 | checkError(t, err, expectedErr) 73 | checkLevelNil(t, level) 74 | } 75 | 76 | func checkError(t *testing.T, err, expectedErr error) { 77 | if err != expectedErr { 78 | t.Errorf("Error was %q not %q", err, expectedErr) 79 | } 80 | } 81 | 82 | func checkLevelNil(t *testing.T, level *Level) { 83 | if level != nil { 84 | t.Errorf("Level %v is not nil", level) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /cmd/mcobj/mtl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/quag/mcobj/nbt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | func printMtl(w io.Writer, blockId nbt.Block) { 11 | if !noColor { 12 | fmt.Fprintln(w, "usemtl", MaterialNamer.NameBlockId(blockId)) 13 | } 14 | } 15 | 16 | func writeMtlFile(filename string) error { 17 | if noColor { 18 | return nil 19 | } 20 | 21 | var outFile, outErr = os.Create(filename) 22 | if outErr != nil { 23 | return outErr 24 | } 25 | defer outFile.Close() 26 | 27 | for _, color := range colors { 28 | color.Print(outFile) 29 | } 30 | 31 | return nil 32 | } 33 | 34 | type MTL struct { 35 | blockId byte 36 | metadata byte 37 | color uint32 38 | name string 39 | } 40 | 41 | func (mtl *MTL) Print(w io.Writer) { 42 | var ( 43 | r = mtl.color >> 24 44 | g = mtl.color >> 16 & 0xff 45 | b = mtl.color >> 8 & 0xff 46 | a = mtl.color & 0xff 47 | ) 48 | 49 | fmt.Fprintf(w, "# %s\nnewmtl %s\nKd %.4f %.4f %.4f\nd %.4f\nillum 1\n\n", mtl.name, MaterialNamer.NameBlockId(nbt.Block(mtl.blockId)+nbt.Block(mtl.metadata)*256), float64(r)/255, float64(g)/255, float64(b)/255, float64(a)/255) 50 | } 51 | 52 | func (mtl *MTL) colorId() nbt.Block { 53 | var id = nbt.Block(mtl.blockId) 54 | if mtl.metadata != 255 { 55 | id += nbt.Block(mtl.metadata) << 8 56 | } 57 | return id 58 | } 59 | 60 | func init() { 61 | colors = make([]MTL, 256) 62 | for i, _ := range colors { 63 | colors[i] = MTL{byte(i), 255, 0x800000ff, fmt.Sprintf("Unknown.%d", i)} 64 | } 65 | 66 | extraData = make(map[byte]bool) 67 | } 68 | 69 | var ( 70 | extraData map[byte]bool 71 | 72 | colors []MTL 73 | 74 | MaterialNamer BlockIdNamer 75 | ) 76 | 77 | type BlockIdNamer interface { 78 | NameBlockId(blockId nbt.Block) string 79 | } 80 | 81 | type NumberBlockIdNamer struct{} 82 | 83 | func (n *NumberBlockIdNamer) NameBlockId(blockId nbt.Block) (name string) { 84 | var idByte = byte(blockId & 0xff) 85 | var extraValue, extraPresent = extraData[idByte] 86 | if extraValue && extraPresent { 87 | name = fmt.Sprintf("%d_%d", idByte, blockId>>8) 88 | } else { 89 | name = fmt.Sprintf("%d", idByte) 90 | } 91 | return 92 | } 93 | 94 | type NameBlockIdNamer struct{} 95 | 96 | func (n *NameBlockIdNamer) NameBlockId(blockId nbt.Block) (name string) { 97 | var idByte = byte(blockId & 0xff) 98 | var extraValue, extraPresent = extraData[idByte] 99 | if extraValue && extraPresent { 100 | for _, color := range colors { 101 | if color.blockId == idByte && color.metadata == uint8(blockId>>8) { 102 | return color.name 103 | } 104 | } 105 | } else { 106 | for _, color := range colors { 107 | if color.blockId == idByte { 108 | return color.name 109 | } 110 | } 111 | } 112 | return 113 | } 114 | -------------------------------------------------------------------------------- /cmd/mcobj/blocktypes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/quag/mcobj/nbt" 5 | ) 6 | 7 | type BoundaryLocator struct { 8 | describer BlockDescriber 9 | } 10 | 11 | func (b *BoundaryLocator) Init() { 12 | var describer = new(Describer) 13 | describer.Init() 14 | b.describer = describer 15 | } 16 | 17 | func (b *BoundaryLocator) IsBoundary(blockId, otherBlockId nbt.Block) bool { 18 | var ( 19 | block = b.describer.BlockInfo(byte(blockId & 0xff)) 20 | other = b.describer.BlockInfo(byte(otherBlockId & 0xff)) 21 | ) 22 | 23 | if !block.IsEmpty() { 24 | if other.IsEmpty() { 25 | return true 26 | } 27 | 28 | if block.IsItem() { 29 | return true 30 | } 31 | 32 | if other.IsTransparent() && (other.IsItem() || blockId&0xff != otherBlockId&0xff) { 33 | return true 34 | } 35 | } 36 | return false 37 | } 38 | 39 | type Describer struct { 40 | unknown BlockInfo 41 | cache map[byte]BlockInfoByte 42 | } 43 | 44 | func (d *Describer) Init() { 45 | d.cache = make(map[byte]BlockInfoByte) 46 | for i := 0; i < 256; i++ { 47 | var blockId = byte(i) 48 | var blockType, hasType = blockTypeMap[blockId] 49 | var value byte 50 | if hasType { 51 | if blockType.mass == Mass { 52 | value += 1 53 | } 54 | if blockType.transparency == Transparent { 55 | value += 2 56 | } 57 | if blockType.empty { 58 | value += 4 59 | } 60 | } else { 61 | value = 1 62 | } 63 | var infoByte = BlockInfoByte(value) 64 | d.cache[blockId] = infoByte 65 | } 66 | } 67 | 68 | func (d *Describer) BlockInfo(blockId byte) BlockInfo { 69 | var info, _ = d.cache[blockId] 70 | return info 71 | } 72 | 73 | type BlockDescriber interface { 74 | BlockInfo(blockId byte) BlockInfo 75 | } 76 | 77 | type BlockInfo interface { 78 | IsItem() bool 79 | IsMass() bool 80 | IsOpaque() bool 81 | IsTransparent() bool 82 | IsEmpty() bool 83 | } 84 | 85 | type BlockType struct { 86 | blockId byte 87 | mass SingularOrAggregate 88 | transparency Transparency 89 | empty bool 90 | } 91 | 92 | type Transparency bool 93 | type SingularOrAggregate bool 94 | 95 | const ( 96 | Transparent Transparency = true 97 | Opaque Transparency = false 98 | 99 | Item SingularOrAggregate = true 100 | Mass SingularOrAggregate = false 101 | ) 102 | 103 | type BlockInfoByte byte 104 | 105 | func (b BlockInfoByte) IsItem() bool { 106 | return b&1 == 0 107 | } 108 | 109 | func (b BlockInfoByte) IsMass() bool { 110 | return b&1 != 0 111 | } 112 | 113 | func (b BlockInfoByte) IsOpaque() bool { 114 | return b&2 == 0 115 | } 116 | 117 | func (b BlockInfoByte) IsTransparent() bool { 118 | return b&2 != 0 119 | } 120 | 121 | func (b BlockInfoByte) IsEmpty() bool { 122 | return b&4 != 0 123 | } 124 | 125 | func init() { 126 | blockTypeMap = make(map[byte]*BlockType) 127 | } 128 | 129 | var ( 130 | blockTypeMap map[byte]*BlockType 131 | ) 132 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | - support naming worlds (and reading from default directory) rather than providing full path 2 | - list worlds (when no world provided) 3 | - if no world selected, default to picking first world in save directory 4 | 5 | - center on player, not chunk (0,0) or spawn 6 | 7 | - off-by-one errors with chunk selection (-cx 1 -cz -10) 8 | 9 | - paintings (Jaz/WormSlayer) 10 | - players 11 | - zombies/skellies/... 12 | - pigs/cows/... 13 | - items 14 | 15 | - customizable prt output (.json file) 16 | - column names 17 | - color 18 | 19 | - support users who don't know how to use command line programs 20 | 1. prompt for options if not provided 21 | 2. have configuration file for controling the export 22 | 3. provide a UI to use 23 | 24 | - Point Cloud file formats 25 | - xyz (emails: henryspam) 26 | 27 | - make processBlock() return the data rather than calling the process methods itself. Rename current method. 28 | - unit tests 29 | - godoc 30 | - refactor: introduce a 'preferences' type 31 | - refactor: tell the ntb file parser what it should extract and have it return that data (easy to pull out say, the spawn coords) 32 | - add flag to output all water faces / blocks (deep water darker) 33 | - add flag to output all blocks (particularly for prt mode) 34 | - transparent blocks (torches, glass, redstone paths...) aren't part of the 'surface' and must include the blocks under them 35 | - write out txt file with mcobjBlockIds, blockIds, data values, and interpretation of those values IOT make it easy for prt users to create meshes 36 | - 3DSMax .obj support 37 | - update build system to handle the three different platforms automatically 38 | - instead of showing 'cubes' for flat files (tracks, pressure plates, ...) change the face they sit on. (Same for buttons and torches on walls) 39 | - add FAQ (and try to make those questions not happen) 40 | - luxrender output 41 | - only write out the materials actually used 42 | - If no arguments supplied, read args from stdin 43 | - black glass (try removing glass faces adjacent with other faces) 44 | - compile as separate units (private stuff can only be seen when compiled at the same time) 45 | - Non-cubed blocks 46 | - crosses for saplings, sugar cane, fire, red/yellow flowers, red/brown shrooms, wheat 47 | - snow blocks are 1/8th height (Full height and 1/8th height snow blocks have the same ID) 48 | - calculate water/lava heights 49 | - farm land is 7/8th height 50 | - cake, half-blocks, pressure plates (stone, wood), fence posts, doors (wood, metal), ladder, tracks, redstone wire, signs, wall signs, steps, buttons, beds, redstone repeaters 51 | - Calculate data for grass, fenses, restone wire, chests, water/lava, portals 52 | - 3DSMax output or 3DSMax fixer 53 | - Center on spawn location 54 | - Center on player location 55 | - 'chunk slice' renders 56 | - add FBX output format (http://usa.autodesk.com/adsk/servlet/pc/index?id=6837478&siteID=123112) 57 | - add player and mob meshes 58 | - clean up error handling 59 | - blocks.json is not located relative to exe when exe is on the $PATH. Provide multiple ways to locate the blocks.json file 60 | - add flag for -sealevel=64 61 | -------------------------------------------------------------------------------- /commandline/commandline.go: -------------------------------------------------------------------------------- 1 | package commandline 2 | 3 | import ( 4 | "strings" 5 | "unicode/utf8" 6 | ) 7 | 8 | const eof = -1 9 | 10 | type stateFn func(*lexer) stateFn 11 | 12 | type lexer struct { 13 | input string 14 | state stateFn 15 | pos int 16 | start int 17 | width int 18 | args []string 19 | frags []string 20 | } 21 | 22 | func (l *lexer) next() (r rune) { 23 | if l.pos >= len(l.input) { 24 | l.width = 0 25 | return eof 26 | } 27 | r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) 28 | l.pos += l.width 29 | return r 30 | } 31 | 32 | func (l *lexer) peek() (r rune) { 33 | r = l.next() 34 | l.backup() 35 | return r 36 | } 37 | 38 | func (l *lexer) backup() { 39 | l.pos -= l.width 40 | } 41 | 42 | func (l *lexer) emit() { 43 | l.frags = append(l.frags, l.input[l.start:l.pos]) 44 | l.start = l.pos 45 | } 46 | 47 | func (l *lexer) emitArg() { 48 | l.args = append(l.args, strings.Join(l.frags, "")) 49 | l.frags = l.frags[:0] 50 | } 51 | 52 | func (l *lexer) ignore() { 53 | l.start = l.pos 54 | } 55 | 56 | func (l *lexer) accept(valid string) bool { 57 | if strings.IndexRune(valid, l.next()) >= 0 { 58 | return true 59 | } 60 | l.backup() 61 | return false 62 | } 63 | 64 | func (l *lexer) acceptRun(valid string) { 65 | for strings.IndexRune(valid, l.next()) >= 0 { 66 | } 67 | l.backup() 68 | } 69 | 70 | func lex(input string) *lexer { 71 | return &lexer{ 72 | input: input, 73 | state: lexBetweenArg, 74 | args: make([]string, 0), 75 | frags: make([]string, 0), 76 | } 77 | } 78 | 79 | func lexBetweenArg(l *lexer) stateFn { 80 | l.acceptRun(" ") 81 | l.ignore() 82 | if l.next() != eof { 83 | l.backup() 84 | return lexArg 85 | } 86 | return nil 87 | } 88 | 89 | func lexArg(l *lexer) stateFn { 90 | next := stateFn(nil) 91 | endArg := false 92 | 93 | Loop: 94 | for { 95 | switch l.next() { 96 | case '"': 97 | next = lexDblQuoteArg 98 | case '\'': 99 | next = lexSglQuoteArg 100 | case ' ': 101 | next = lexBetweenArg 102 | endArg = true 103 | case '\\': 104 | start := l.pos - l.width 105 | switch l.next() { 106 | case ' ': 107 | tmp := l.pos 108 | l.pos = start 109 | if l.pos > l.start { 110 | l.emit() 111 | } 112 | l.start = tmp - l.width 113 | l.pos = tmp 114 | continue 115 | case '\'': 116 | continue 117 | case '"': 118 | continue 119 | case eof: 120 | // TODO: Error, incomplete escape 121 | continue 122 | default: 123 | // TODO: Error, unknown escape 124 | continue 125 | } 126 | case eof: 127 | next = nil 128 | endArg = true 129 | default: 130 | continue 131 | } 132 | break Loop 133 | } 134 | l.backup() 135 | if l.pos > l.start { 136 | l.emit() 137 | } 138 | if endArg { 139 | l.emitArg() 140 | } 141 | return next 142 | } 143 | 144 | func lexDblQuoteArg(l *lexer) stateFn { 145 | return lexQuoteArg(l, '"') 146 | } 147 | 148 | func lexSglQuoteArg(l *lexer) stateFn { 149 | return lexQuoteArg(l, '\'') 150 | } 151 | 152 | func lexQuoteArg(l *lexer, quote rune) stateFn { 153 | l.next() 154 | l.ignore() 155 | Loop: 156 | for { 157 | switch l.next() { 158 | case '\\': 159 | start := l.pos - l.width 160 | switch l.next() { 161 | case ' ': 162 | tmp := l.pos 163 | l.pos = start 164 | if l.pos > l.start { 165 | l.emit() 166 | } 167 | l.start = tmp - l.width 168 | l.pos = tmp 169 | continue 170 | case '\'': 171 | case '"': 172 | case eof: 173 | // TODO: Error, incomplete escape 174 | default: 175 | // TODO: Error, unknown escape 176 | } 177 | case eof: 178 | // error unterminated quote 179 | return lexArg 180 | case quote: 181 | break Loop 182 | } 183 | } 184 | l.backup() 185 | l.emit() 186 | l.next() 187 | l.ignore() 188 | return lexArg 189 | } 190 | 191 | func SplitCommandLine(commandLine string) []string { 192 | l := lex(commandLine) 193 | for l.state != nil { 194 | l.state = l.state(l) 195 | } 196 | return l.args 197 | } 198 | -------------------------------------------------------------------------------- /nbt/explain.go: -------------------------------------------------------------------------------- 1 | package nbt 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | ) 8 | 9 | func Explain(r io.Reader, w io.Writer) error { 10 | e := &explainer{w, pathStack{make([]string, 0, 8)}} 11 | 12 | nr := NewReader(r) 13 | for { 14 | err := e.parseStruct(nr, false) 15 | if err == io.EOF { 16 | break 17 | } else if err != nil { 18 | return err 19 | } 20 | } 21 | 22 | return nil 23 | } 24 | 25 | type explainer struct { 26 | w io.Writer 27 | stack pathStack 28 | } 29 | 30 | func (e *explainer) parseStruct(nr *Reader, listStruct bool) error { 31 | structDepth := 0 32 | if listStruct { 33 | structDepth = 1 34 | } 35 | 36 | for { 37 | typeId, name, err := nr.ReadTag() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if typeId != TagStructEnd { 43 | e.stack.push(name) 44 | } 45 | 46 | e.RecordTag(typeId, name) 47 | switch typeId { 48 | case TagStruct: 49 | e.RecordNoValue() 50 | structDepth++ 51 | case TagStructEnd: 52 | e.RecordNoValue() 53 | structDepth-- 54 | if structDepth == 0 { 55 | return nil 56 | } 57 | case TagByteArray: 58 | e.RecordValue(nr.ReadBytes()) 59 | case TagInt8: 60 | e.RecordValue(nr.ReadInt8()) 61 | case TagInt16: 62 | e.RecordValue(nr.ReadInt16()) 63 | case TagInt32: 64 | e.RecordValue(nr.ReadInt32()) 65 | case TagInt64: 66 | e.RecordValue(nr.ReadInt64()) 67 | case TagFloat32: 68 | e.RecordValue(nr.ReadFloat32()) 69 | case TagFloat64: 70 | e.RecordValue(nr.ReadFloat64()) 71 | case TagString: 72 | e.RecordValue(nr.ReadString()) 73 | case TagList: 74 | itemTypeId, length, err := nr.ReadListHeader() 75 | if err != nil { 76 | return err 77 | } 78 | e.RecordList(itemTypeId, length) 79 | switch TypeId(itemTypeId) { 80 | case TagInt8: 81 | list := make([]int, length) 82 | for i := 0; i < length; i++ { 83 | x, err := nr.ReadInt8() 84 | list[i] = x 85 | if err != nil { 86 | return err 87 | } 88 | } 89 | e.RecordValue(list, nil) 90 | case TagFloat32: 91 | list := make([]float32, length) 92 | for i := 0; i < length; i++ { 93 | x, err := nr.ReadFloat32() 94 | list[i] = x 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | e.RecordValue(list, nil) 100 | case TagFloat64: 101 | list := make([]float64, length) 102 | for i := 0; i < length; i++ { 103 | x, err := nr.ReadFloat64() 104 | list[i] = x 105 | if err != nil { 106 | return err 107 | } 108 | } 109 | e.RecordValue(list, nil) 110 | case TagStruct: 111 | e.RecordNoValue() 112 | 113 | for i := 0; i < length; i++ { 114 | e.stack.push(fmt.Sprintf("[%d]", i)) 115 | 116 | err := e.parseStruct(nr, true) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | e.stack.pop() 122 | } 123 | default: 124 | return errors.New(fmt.Sprintf("reading lists of typeId %d not supported. length:%d", itemTypeId, length)) 125 | } 126 | } 127 | 128 | if typeId != TagStruct { 129 | e.stack.pop() 130 | } 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func (e *explainer) RecordTag(typeId TypeId, name string) { 137 | fmt.Fprintf(e.w, "[%2d %-20s] ", typeId, name) 138 | for i, name := range e.stack.path { 139 | if i != 0 { 140 | fmt.Fprintf(e.w, ".") 141 | } 142 | fmt.Fprintf(e.w, "%s", name) 143 | } 144 | 145 | if len(e.stack.path) != 0 { 146 | fmt.Fprintf(e.w, " ") 147 | } 148 | } 149 | 150 | func (e *explainer) RecordList(itemTypeId TypeId, length int) { 151 | fmt.Fprintf(e.w, " (%2d, %d) ", itemTypeId, length) 152 | } 153 | 154 | func (e *explainer) RecordValue(value interface{}, err error) { 155 | if err != nil { 156 | fmt.Fprintln(e.w, err) 157 | } 158 | fmt.Fprintf(e.w, "'%v'\n", value) 159 | //fmt.Fprintln(e.w) 160 | } 161 | 162 | func (e *explainer) RecordNoValue() { 163 | fmt.Fprintln(e.w) 164 | } 165 | 166 | type pathStack struct { 167 | path []string 168 | } 169 | 170 | func (ps *pathStack) push(name string) { 171 | ps.path = append(ps.path, name) 172 | } 173 | 174 | func (ps *pathStack) pop() { 175 | if len(ps.path) != 0 { 176 | ps.path = ps.path[0 : len(ps.path)-1] 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /mcworld/betaworld.go: -------------------------------------------------------------------------------- 1 | package mcworld 2 | 3 | import ( 4 | "compress/zlib" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | var ( 16 | ChunkNotFoundError = errors.New("Chunk Missing") 17 | ) 18 | 19 | type BetaWorld struct { 20 | worldDir string 21 | } 22 | 23 | type McrFile struct { 24 | *os.File 25 | } 26 | 27 | func (w *BetaWorld) OpenChunk(x, z int) (io.ReadCloser, error) { 28 | mcaName := fmt.Sprintf("r.%v.%v.mca", x>>5, z>>5) 29 | mcaPath := filepath.Join(w.worldDir, "region", mcaName) 30 | 31 | mcrName := fmt.Sprintf("r.%v.%v.mcr", x>>5, z>>5) 32 | mcrPath := filepath.Join(w.worldDir, "region", mcrName) 33 | 34 | var path string 35 | if _, err := os.Stat(mcaPath); err == nil { 36 | path = mcaPath 37 | } else { 38 | path = mcrPath 39 | } 40 | 41 | file, openErr := os.Open(path) 42 | if openErr != nil { 43 | return nil, openErr 44 | } 45 | defer func() { 46 | if file != nil { 47 | file.Close() 48 | } 49 | }() 50 | 51 | var mcr = &McrFile{file} 52 | var loc, readLocErr = mcr.ReadLocation(x, z) 53 | if readLocErr != nil { 54 | return nil, readLocErr 55 | } 56 | 57 | if loc == 0 { 58 | return nil, errors.New(fmt.Sprintf("Chunk missing: %v,%v in %v. %v", x, z, mcaName, (x&31)+(z&31)*32)) 59 | } 60 | 61 | var ( 62 | length uint32 63 | compressionType byte 64 | ) 65 | 66 | var _, seekErr = mcr.Seek(int64(loc.Offset()), 0) 67 | if seekErr != nil { 68 | return nil, seekErr 69 | } 70 | 71 | var lengthReadErr = binary.Read(mcr, binary.BigEndian, &length) 72 | if lengthReadErr != nil { 73 | return nil, lengthReadErr 74 | } 75 | 76 | var compressionTypeErr = binary.Read(mcr, binary.BigEndian, &compressionType) 77 | if compressionTypeErr != nil { 78 | return nil, compressionTypeErr 79 | } 80 | 81 | var r, zlibNewErr = zlib.NewReader(mcr) 82 | if zlibNewErr != nil { 83 | return nil, zlibNewErr 84 | } 85 | 86 | var pair = &ReadCloserPair{r, file} 87 | file = nil 88 | return pair, nil 89 | } 90 | 91 | func (r McrFile) ReadLocation(x, z int) (ChunkLocation, error) { 92 | var _, seekErr = r.Seek(int64(4*((x&31)+(z&31)*32)), 0) 93 | if seekErr != nil { 94 | return ChunkLocation(0), seekErr 95 | } 96 | var location uint32 97 | var readErr = binary.Read(r, binary.BigEndian, &location) 98 | if readErr != nil { 99 | return ChunkLocation(0), readErr 100 | } 101 | return ChunkLocation(location), nil 102 | } 103 | 104 | type ChunkLocation uint32 105 | 106 | func (cl ChunkLocation) Offset() int { 107 | return 4096 * (int(cl) >> 8) 108 | } 109 | 110 | func (cl ChunkLocation) Sectors() int { 111 | return (int(cl) & 0xff) 112 | } 113 | 114 | func (w *BetaWorld) ChunkPool(mask ChunkMask) (ChunkPool, error) { 115 | var regionDirname = filepath.Join(w.worldDir, "region") 116 | var dir, dirOpenErr = os.Open(regionDirname) 117 | if dirOpenErr != nil { 118 | return nil, dirOpenErr 119 | } 120 | defer dir.Close() 121 | 122 | var pool = &BetaChunkPool{make(map[uint64]bool), EmptyBoundingBox()} 123 | 124 | for { 125 | var filenames, readErr = dir.Readdirnames(1) 126 | if readErr == io.EOF || len(filenames) == 0 { 127 | break 128 | } 129 | if readErr != nil { 130 | return nil, readErr 131 | } 132 | 133 | var fields = strings.FieldsFunc(filenames[0], func(c rune) bool { return c == '.' }) 134 | 135 | if len(fields) == 4 { 136 | var ( 137 | rx, rxErr = strconv.Atoi(fields[1]) 138 | rz, ryErr = strconv.Atoi(fields[2]) 139 | ) 140 | 141 | if rxErr == nil && ryErr == nil { 142 | var regionFilename = filepath.Join(regionDirname, filenames[0]) 143 | var mcrErr = w.poolMcrChunks(regionFilename, mask, pool, rx, rz) 144 | if mcrErr != nil { 145 | return nil, mcrErr 146 | } 147 | } 148 | } 149 | } 150 | 151 | return pool, nil 152 | } 153 | 154 | func (w *BetaWorld) poolMcrChunks(regionFilename string, mask ChunkMask, pool *BetaChunkPool, rx, rz int) error { 155 | var region, regionOpenErr = os.Open(regionFilename) 156 | if regionOpenErr != nil { 157 | return regionOpenErr 158 | } 159 | defer region.Close() 160 | 161 | for cz := 0; cz < 32; cz++ { 162 | for cx := 0; cx < 32; cx++ { 163 | var location uint32 164 | var readErr = binary.Read(region, binary.BigEndian, &location) 165 | if readErr == io.EOF { 166 | continue 167 | } 168 | if readErr != nil { 169 | return readErr 170 | } 171 | if location != 0 { 172 | var ( 173 | x = rx*32 + cx 174 | z = rz*32 + cz 175 | ) 176 | 177 | if !mask.IsMasked(x, z) { 178 | pool.chunkMap[betaChunkPoolKey(x, z)] = true 179 | pool.box.Union(x, z) 180 | } 181 | } 182 | } 183 | } 184 | 185 | return nil 186 | } 187 | 188 | type BetaChunkPool struct { 189 | chunkMap map[uint64]bool 190 | box *BoundingBox 191 | } 192 | 193 | func (p *BetaChunkPool) Pop(x, z int) bool { 194 | var key = betaChunkPoolKey(x, z) 195 | var _, exists = p.chunkMap[key] 196 | delete(p.chunkMap, key) 197 | return exists 198 | } 199 | 200 | func (p *BetaChunkPool) Remaining() int { 201 | return len(p.chunkMap) 202 | } 203 | 204 | func (p *BetaChunkPool) BoundingBox() *BoundingBox { 205 | return p.box 206 | } 207 | 208 | func betaChunkPoolKey(x, z int) uint64 { 209 | return uint64(x)<<32 + uint64(z) 210 | } 211 | -------------------------------------------------------------------------------- /cmd/mcobj/prt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "compress/zlib" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | type PrtGenerator struct { 13 | enclosedsChan chan *EnclosedChunkJob 14 | completeChan chan bool 15 | 16 | outFile *os.File 17 | w *bufio.Writer 18 | zw io.WriteCloser 19 | 20 | particleCount int64 21 | total int 22 | boundary *BoundaryLocator 23 | } 24 | 25 | func (o *PrtGenerator) Start(outFilename string, total int, maxProcs int, boundary *BoundaryLocator) error { 26 | o.enclosedsChan = make(chan *EnclosedChunkJob, maxProcs*2) 27 | o.completeChan = make(chan bool) 28 | o.total = total 29 | o.boundary = boundary 30 | 31 | maxProcs = 1 32 | for i := 0; i < maxProcs; i++ { 33 | go o.chunkProcessor() 34 | } 35 | 36 | var openErr error 37 | 38 | o.outFile, openErr = os.Create(outFilename) 39 | if openErr != nil { 40 | return openErr 41 | } 42 | 43 | o.w = bufio.NewWriter(o.outFile) 44 | WriteHeader(o.w, -1, []ChannelDefinition{{"Position", 4, 3, 0}, {"BlockID", 1, 1, 12}}) 45 | 46 | var zErr error 47 | o.zw, zErr = zlib.NewWriterLevel(o.w, zlib.NoCompression) 48 | if zErr != nil { 49 | return zErr 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (o *PrtGenerator) chunkProcessor() { 56 | var chunkCount = 0 57 | for { 58 | var job = <-o.enclosedsChan 59 | 60 | var e = job.enclosed 61 | 62 | height := e.blocks.height 63 | for i := 0; i < len(e.blocks.data); i += height { 64 | var x, z = (i / height) / 16, (i / height) % 16 65 | 66 | var column = BlockColumn(e.blocks.data[i : i+height]) 67 | for y, blockId := range column { 68 | if y < yMin { 69 | continue 70 | } 71 | 72 | switch { 73 | case o.boundary.IsBoundary(blockId, e.Get(x, y-1, z)): 74 | fallthrough 75 | case o.boundary.IsBoundary(blockId, e.Get(x, y+1, z)): 76 | fallthrough 77 | case o.boundary.IsBoundary(blockId, e.Get(x-1, y, z)): 78 | fallthrough 79 | case o.boundary.IsBoundary(blockId, e.Get(x+1, y, z)): 80 | fallthrough 81 | case o.boundary.IsBoundary(blockId, e.Get(x, y, z-1)): 82 | fallthrough 83 | case o.boundary.IsBoundary(blockId, e.Get(x, y, z+1)): 84 | o.particleCount++ 85 | var ( 86 | xa = x + e.xPos*16 87 | ya = y - 64 88 | za = -(z + e.zPos*16) 89 | ) 90 | binary.Write(o.zw, binary.LittleEndian, float32(xa)) 91 | binary.Write(o.zw, binary.LittleEndian, float32(za)) 92 | binary.Write(o.zw, binary.LittleEndian, float32(ya)) 93 | binary.Write(o.zw, binary.LittleEndian, int32(blockId)) 94 | } 95 | } 96 | } 97 | 98 | chunkCount++ 99 | fmt.Printf("%4v/%-4v (%3v,%3v) Particles: %d\n", chunkCount, o.total, job.enclosed.xPos, job.enclosed.zPos, o.particleCount) 100 | 101 | if job.last { 102 | o.completeChan <- true 103 | break 104 | } 105 | } 106 | } 107 | 108 | func (o *PrtGenerator) Close() error { 109 | o.zw.Close() 110 | o.w.Flush() 111 | UpdateParticleCount(o.outFile, o.particleCount) 112 | o.outFile.Close() 113 | return nil 114 | } 115 | 116 | func (o *PrtGenerator) GetEnclosedJobsChan() chan *EnclosedChunkJob { 117 | return o.enclosedsChan 118 | } 119 | 120 | func (o *PrtGenerator) GetCompleteChan() chan bool { 121 | return o.completeChan 122 | } 123 | 124 | type ChannelDefinition struct { 125 | Name string // max of 31 characters and must meet the regex [a-zA-Z_][0-9a-zA-Z_]* 126 | DataType, Arity, Offset int32 127 | } 128 | 129 | // http://software.primefocusworld.com/software/support/krakatoa/prt_file_format.php 130 | // http://www.thinkboxsoftware.com/krak-prt-file-format/ 131 | func WriteHeader(w io.Writer, particleCount int64, channels []ChannelDefinition) { 132 | // Header (56 bytes) 133 | var magic = []byte{192, 'P', 'R', 'T', '\r', '\n', 26, '\n'} 134 | w.Write(magic) 135 | 136 | var headerLength = uint32(56) 137 | binary.Write(w, binary.LittleEndian, headerLength) 138 | 139 | var signature = make([]byte, 32) 140 | copy(signature, []byte("Extensible Particle Format")) 141 | w.Write(signature) 142 | 143 | var version = uint32(1) 144 | binary.Write(w, binary.LittleEndian, version) 145 | 146 | binary.Write(w, binary.LittleEndian, particleCount) 147 | 148 | // Reserved bytes (4 bytes) 149 | var reserved = int32(4) 150 | binary.Write(w, binary.LittleEndian, reserved) 151 | 152 | // Channel definition header (8 bytes) 153 | binary.Write(w, binary.LittleEndian, int32(len(channels))) 154 | 155 | var channelDefinitionSize = int32(44) 156 | binary.Write(w, binary.LittleEndian, channelDefinitionSize) 157 | 158 | for _, channel := range channels { 159 | var nameBytes = make([]byte, 32) 160 | copy(nameBytes, []byte(channel.Name)) 161 | w.Write(nameBytes) 162 | 163 | binary.Write(w, binary.LittleEndian, channel.DataType) 164 | binary.Write(w, binary.LittleEndian, channel.Arity) 165 | binary.Write(w, binary.LittleEndian, channel.Offset) 166 | } 167 | } 168 | 169 | func UpdateParticleCount(file *os.File, particleCount int64) error { 170 | var storedOffset, err = file.Seek(0, 1) 171 | if err != nil { 172 | return err 173 | } 174 | _, err = file.Seek(0x30, 0) 175 | if err != nil { 176 | return err 177 | } 178 | err = binary.Write(file, binary.LittleEndian, particleCount) 179 | if err != nil { 180 | return err 181 | } 182 | _, err = file.Seek(storedOffset, 0) 183 | return err 184 | } 185 | -------------------------------------------------------------------------------- /nbt/chunk.go: -------------------------------------------------------------------------------- 1 | package nbt 2 | 3 | import ( 4 | "compress/gzip" 5 | "errors" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | type Chunk struct { 11 | XPos, ZPos int 12 | Blocks []Block 13 | } 14 | 15 | var ( 16 | ErrListUnknown = errors.New("Lists of unknown type aren't supported") 17 | ) 18 | 19 | func ReadChunkDat(reader io.Reader) (*Chunk, error) { 20 | r, err := gzip.NewReader(reader) 21 | defer r.Close() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return ReadChunkNbt(r) 27 | } 28 | 29 | func ReadChunkNbt(reader io.Reader) (*Chunk, error) { 30 | chunkData := new(chunkData) 31 | chunkData.sections = make([]*sectionData, 0) 32 | if err := chunkData.parse(NewReader(reader), false); err != nil { 33 | return nil, err 34 | } 35 | 36 | chunk := &Chunk{chunkData.xPos, chunkData.zPos, nil} 37 | 38 | if len(chunkData.sections) != 0 { 39 | chunk.Blocks = make([]Block, 256*16*16) // Hard coded height for now. TODO: Make variable height chunks. 40 | for _, section := range chunkData.sections { 41 | for i, blockId := range section.blocks { 42 | var metadata byte 43 | if i&1 == 1 { 44 | metadata = section.data[i/2] >> 4 45 | } else { 46 | metadata = section.data[i/2] & 0xf 47 | } 48 | // Note that the old format is XZY and the new format is YZX 49 | x, z, y := indexToCoords(i, 16, 16) 50 | chunk.Blocks[coordsToIndex(x, z, y+16*section.y, 16, 256)] = Block(blockId) + (Block(metadata) << 8) 51 | } 52 | } 53 | } else { 54 | if chunkData.blocks != nil && chunkData.data != nil { 55 | chunk.Blocks = make([]Block, len(chunkData.blocks)) 56 | for i, blockId := range chunkData.blocks { 57 | var metadata byte 58 | if i&1 == 1 { 59 | metadata = chunkData.data[i/2] >> 4 60 | } else { 61 | metadata = chunkData.data[i/2] & 0xf 62 | } 63 | chunk.Blocks[i] = Block(blockId) + (Block(metadata) << 8) 64 | } 65 | } 66 | } 67 | 68 | return chunk, nil 69 | } 70 | 71 | func indexToCoords(i, aMax, bMax int) (a, b, c int) { 72 | a = i % aMax 73 | b = (i / aMax) % bMax 74 | c = i / (aMax * bMax) 75 | return 76 | } 77 | 78 | func coordsToIndex(a, b, c, bMax, cMax int) int { 79 | return c + cMax*(b+bMax*a) 80 | } 81 | 82 | func yzxToXzy(yzx, xMax, zMax, yMax int) int { 83 | x := yzx % xMax 84 | z := (yzx / xMax) % zMax 85 | y := (yzx / (xMax * zMax)) % yMax 86 | 87 | // yzx := x + xMax*(z + zMax*y) 88 | xzy := y + yMax*(z+zMax*x) 89 | return xzy 90 | } 91 | 92 | type chunkData struct { 93 | xPos, zPos int 94 | blocks []byte 95 | data []byte 96 | section *sectionData 97 | sections []*sectionData 98 | } 99 | 100 | type sectionData struct { 101 | y int 102 | blocks []byte 103 | data []byte 104 | } 105 | 106 | func (chunk *chunkData) parse(r *Reader, listStruct bool) error { 107 | structDepth := 0 108 | if listStruct { 109 | structDepth++ 110 | } 111 | 112 | for { 113 | typeId, name, err := r.ReadTag() 114 | if err != nil { 115 | if err == io.EOF { 116 | break 117 | } 118 | return err 119 | } 120 | 121 | switch typeId { 122 | case TagStruct: 123 | structDepth++ 124 | case TagStructEnd: 125 | structDepth-- 126 | if structDepth == 0 { 127 | return nil 128 | } 129 | case TagByteArray: 130 | bytes, err := r.ReadBytes() 131 | if err != nil { 132 | return err 133 | } 134 | if name == "Blocks" { 135 | if chunk.section != nil { 136 | chunk.section.blocks = bytes 137 | } else { 138 | chunk.blocks = bytes 139 | } 140 | } else if name == "Data" { 141 | if chunk.section != nil { 142 | chunk.section.data = bytes 143 | } else { 144 | chunk.data = bytes 145 | } 146 | } 147 | case TagIntArray: 148 | _, err := r.ReadInts() 149 | if err != nil { 150 | return err 151 | } 152 | case TagInt8: 153 | number, err := r.ReadInt8() 154 | if err != nil { 155 | return err 156 | } 157 | if name == "Y" { 158 | chunk.section.y = int(number) 159 | } 160 | case TagInt16: 161 | _, err := r.ReadInt16() 162 | if err != nil { 163 | return err 164 | } 165 | case TagInt32: 166 | number, err := r.ReadInt32() 167 | if err != nil { 168 | return err 169 | } 170 | 171 | if name == "xPos" { 172 | chunk.xPos = number 173 | } 174 | if name == "zPos" { 175 | chunk.zPos = number 176 | } 177 | case TagInt64: 178 | _, err := r.ReadInt64() 179 | if err != nil { 180 | return err 181 | } 182 | case TagFloat32: 183 | _, err := r.ReadFloat32() 184 | if err != nil { 185 | return err 186 | } 187 | case TagFloat64: 188 | _, err := r.ReadFloat64() 189 | if err != nil { 190 | return err 191 | } 192 | case TagString: 193 | _, err := r.ReadString() 194 | if err != nil { 195 | return err 196 | } 197 | case TagList: 198 | itemTypeId, length, err := r.ReadListHeader() 199 | if err != nil { 200 | return err 201 | } 202 | switch itemTypeId { 203 | case TagInt8: 204 | for i := 0; i < length; i++ { 205 | _, err := r.ReadInt8() 206 | if err != nil { 207 | return err 208 | } 209 | } 210 | case TagFloat32: 211 | for i := 0; i < length; i++ { 212 | _, err := r.ReadFloat32() 213 | if err != nil { 214 | return err 215 | } 216 | } 217 | case TagFloat64: 218 | for i := 0; i < length; i++ { 219 | _, err := r.ReadFloat64() 220 | if err != nil { 221 | return err 222 | } 223 | } 224 | case TagStruct: 225 | for i := 0; i < length; i++ { 226 | if name == "Sections" { 227 | chunk.section = new(sectionData) 228 | chunk.sections = append(chunk.sections, chunk.section) 229 | } 230 | err := chunk.parse(r, true) 231 | if err != nil { 232 | return err 233 | } 234 | } 235 | default: 236 | fmt.Printf("# %s list todo(%v) %v\n", name, itemTypeId, length) 237 | return ErrListUnknown 238 | } 239 | default: 240 | fmt.Printf("# %s todo(%d)\n", name, typeId) 241 | } 242 | } 243 | 244 | return nil 245 | } 246 | -------------------------------------------------------------------------------- /cmd/map2d/map2d.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/quag/mcobj/mcworld" 6 | "github.com/quag/mcobj/nbt" 7 | "image" 8 | "image/png" 9 | "io" 10 | "os" 11 | ) 12 | 13 | func main() { 14 | //dir := "/Users/jonathan/Library/Application Support/minecraft/saves/World1" 15 | dir := "/Users/jonathan/Library/Application Support/minecraft/saves/New World" 16 | //dir := "/Users/jonathan/Library/Application Support/minecraft/saves/1.8.1" 17 | //dir := "../../../world" 18 | //mask := &mcworld.AllChunksMask{} 19 | mask := &mcworld.RectangleChunkMask{-100, -100, 100, 100} 20 | 21 | world := mcworld.OpenWorld(dir) 22 | chunks, box, err := ZigZagChunks(world, mask) 23 | if err != nil { 24 | fmt.Println("ZigZagChunks:", err) 25 | return 26 | } 27 | 28 | width, height := 16*(box.X1-box.X0), 16*(box.Z1-box.Z0) 29 | xoffset, zoffset := -16*box.X0, -16*box.Z0 30 | 31 | fmt.Println(box, width, height) 32 | 33 | img := image.NewNRGBA(image.Rect(0, 0, width, height)) 34 | 35 | for chunk := range chunks { 36 | //fmt.Println(chunk.X, chunk.Z, xoffset, zoffset) 37 | err := useChunk(chunk, img, xoffset+16*chunk.X, zoffset+16*chunk.Z) 38 | if err != nil { 39 | fmt.Println(err) 40 | } 41 | fmt.Printf(".") 42 | } 43 | 44 | pngFile, err := os.Create("map.png") 45 | if err != nil { 46 | fmt.Println(err) 47 | return 48 | } 49 | defer pngFile.Close() 50 | png.Encode(pngFile, img) 51 | } 52 | 53 | func useChunk(chunk *Chunk, img *image.NRGBA, xoffset, zoffset int) error { 54 | var r, openErr = chunk.Open() 55 | if openErr != nil { 56 | return openErr 57 | } 58 | defer r.Close() 59 | 60 | var c, nbtErr = nbt.ReadChunkNbt(r) 61 | if nbtErr != nil { 62 | return nbtErr 63 | } 64 | 65 | blocks := Blocks(c.Blocks) 66 | 67 | for x := 0; x < 16; x++ { 68 | for z := 0; z < 16; z++ { 69 | column := blocks.Column(x, z) 70 | v := uint16(0) 71 | for y := 127; y > 0; y-- { 72 | if column[y] != 0 { 73 | v = column[y] 74 | break 75 | } 76 | } 77 | //fmt.Printf("%7x", color[v&0xff]) 78 | img.Set(xoffset+x, zoffset+z, rgb(color[v&0xff])) 79 | //fmt.Printf("%7x", img.At(x, z)) 80 | } 81 | //fmt.Println() 82 | } 83 | 84 | //fmt.Println() 85 | return nil 86 | } 87 | 88 | type Chunk struct { 89 | opener mcworld.ChunkOpener 90 | X, Z int 91 | } 92 | 93 | func (c *Chunk) Open() (io.ReadCloser, error) { 94 | return c.opener.OpenChunk(c.X, c.Z) 95 | } 96 | 97 | func zigzag(n int) int { 98 | return (n << 1) ^ (n >> 31) 99 | } 100 | 101 | func unzigzag(n int) int { 102 | return (n >> 1) ^ (-(n & 1)) 103 | } 104 | 105 | func ZigZagChunks(world mcworld.World, mask mcworld.ChunkMask) (chan *Chunk, *mcworld.BoundingBox, error) { 106 | pool, err := world.ChunkPool(mask) 107 | if err != nil { 108 | return nil, &mcworld.BoundingBox{}, err 109 | } 110 | 111 | c := make(chan *Chunk) 112 | 113 | go func() { 114 | defer close(c) 115 | for i := 0; ; i++ { 116 | for x := 0; x < i; x++ { 117 | for z := 0; z < i; z++ { 118 | if pool.Remaining() == 0 { 119 | return 120 | } 121 | ax, az := unzigzag(x), unzigzag(z) 122 | if pool.Pop(ax, az) { 123 | c <- &Chunk{world, ax, az} 124 | } 125 | } 126 | } 127 | } 128 | }() 129 | 130 | return c, pool.BoundingBox(), nil 131 | } 132 | 133 | type Blocks []uint16 134 | 135 | type BlockColumn []uint16 136 | 137 | func (b *Blocks) Get(x, y, z int) uint16 { 138 | return (*b)[y+(z*128+(x*128*16))] 139 | } 140 | 141 | func (b *Blocks) Column(x, z int) BlockColumn { 142 | var i = 128 * (z + x*16) 143 | return BlockColumn((*b)[i : i+128]) 144 | } 145 | 146 | type rgb uint32 147 | 148 | func (c rgb) RGBA() (r, g, b, a uint32) { 149 | r = (uint32(c) >> 16) << 8 150 | g = (uint32(c) >> 8 & 0xff) << 8 151 | b = (uint32(c) >> 0 & 0xff) << 8 152 | a = 0xff << 8 153 | return 154 | } 155 | 156 | var ( 157 | color []uint32 158 | ) 159 | 160 | func init() { 161 | color = make([]uint32, 256) 162 | color[0] = 0xfefeff 163 | color[1] = 0x7d7d7d 164 | color[2] = 0x52732c 165 | color[3] = 0x866043 166 | color[4] = 0x757575 167 | color[5] = 0x9d804f 168 | color[6] = 0x5d7e1e 169 | color[7] = 0x545454 170 | color[8] = 0x009aff 171 | color[9] = 0x009aff 172 | color[10] = 0xf54200 173 | color[11] = 0xf54200 174 | color[12] = 0xdad29e 175 | color[13] = 0x887f7e 176 | color[14] = 0x908c7d 177 | color[15] = 0x88837f 178 | color[16] = 0x737373 179 | color[17] = 0x665132 180 | color[18] = 0x1c4705 181 | color[19] = 0xb7b739 182 | color[20] = 0xffffff 183 | color[21] = 0x667087 184 | color[22] = 0x1d47a6 185 | color[23] = 0x6c6c6c 186 | color[24] = 0xd5cd94 187 | color[25] = 0x654433 188 | color[26] = 0x8f1717 189 | color[26] = 0xaf7475 190 | color[27] = 0x87714e 191 | color[28] = 0x766251 192 | color[30] = 0xdadada 193 | color[31] = 0x7c4f19 194 | color[32] = 0x7c4f19 195 | color[35] = 0xdedede 196 | color[37] = 0xc1c702 197 | color[38] = 0xcb060a 198 | color[39] = 0x967158 199 | color[40] = 0xc53c3f 200 | color[41] = 0xfaec4e 201 | color[42] = 0xe6e6e6 202 | color[43] = 0xa7a7a7 203 | color[44] = 0xa7a7a7 204 | color[45] = 0x9c6e62 205 | color[46] = 0xa6553f 206 | color[47] = 0x6c583a 207 | color[48] = 0x5b6c5b 208 | color[49] = 0x14121e 209 | color[50] = 0xffda66 210 | color[51] = 0xff7700 211 | color[52] = 0x1d4f72 212 | color[53] = 0x9d804f 213 | color[54] = 0x835e25 214 | color[55] = 0xcb0000 215 | color[56] = 0x828c8f 216 | color[57] = 0x64dcd6 217 | color[58] = 0x6b472b 218 | color[59] = 0x83c144 219 | color[60] = 0x4b290e 220 | color[61] = 0x4e4e4e 221 | color[62] = 0x7d6655 222 | color[63] = 0x9d804f 223 | color[64] = 0x9d804f 224 | color[65] = 0x9d804f 225 | color[66] = 0x75664c 226 | color[67] = 0x757575 227 | color[68] = 0x9d804f 228 | color[69] = 0x9d804f 229 | color[70] = 0x7d7d7d 230 | color[71] = 0xb2b2b2 231 | color[72] = 0x9d804f 232 | color[73] = 0x856b6b 233 | color[74] = 0xbd6b6b 234 | color[75] = 0x440000 235 | color[76] = 0xfe0000 236 | color[77] = 0x7d7d7d 237 | color[78] = 0xf0fbfb 238 | color[79] = 0x7daeff 239 | color[80] = 0xf0fbfb 240 | color[81] = 0x0d6418 241 | color[82] = 0x9fa5b1 242 | color[83] = 0x83c447 243 | color[84] = 0x6b4937 244 | color[85] = 0x9d804f 245 | color[86] = 0xc57918 246 | color[87] = 0x6e3533 247 | color[88] = 0x554134 248 | color[89] = 0x897141 249 | color[90] = 0x381d55 250 | color[91] = 0xb9861d 251 | color[92] = 0xe5cecf 252 | color[93] = 0x989494 253 | color[94] = 0xa19494 254 | color[95] = 0x835e25 255 | color[96] = 0x81602f 256 | } 257 | -------------------------------------------------------------------------------- /nbt/nbt.go: -------------------------------------------------------------------------------- 1 | package nbt 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "math" 9 | ) 10 | 11 | type TypeId byte 12 | 13 | const ( 14 | TagStructEnd TypeId = 0 // No name. Single zero byte. 15 | TagInt8 TypeId = 1 // A single signed byte (8 bits) 16 | TagInt16 TypeId = 2 // A signed short (16 bits, big endian) 17 | TagInt32 TypeId = 3 // A signed int (32 bits, big endian) 18 | TagInt64 TypeId = 4 // A signed long (64 bits, big endian) 19 | TagFloat32 TypeId = 5 // A floating point value (32 bits, big endian, IEEE 754-2008, binary32) 20 | TagFloat64 TypeId = 6 // A floating point value (64 bits, big endian, IEEE 754-2008, binary64) 21 | TagByteArray TypeId = 7 // { TAG_Int length; An array of bytes of unspecified format. The length of this array is bytes } 22 | TagString TypeId = 8 // { TAG_Short length; An array of bytes defining a string in UTF-8 format. The length of this array is bytes } 23 | TagList TypeId = 9 // { TAG_Byte tagId; TAG_Int length; A sequential list of Tags (not Named Tags), of type . The length of this array is Tags. } Notes: All tags share the same type. 24 | TagStruct TypeId = 10 // { A sequential list of Named Tags. This array keeps going until a TAG_End is found.; TAG_End end } Notes: If there's a nested TAG_Compound within this tag, that one will also have a TAG_End, so simply reading until the next TAG_End will not work. The names of the named tags have to be unique within each TAG_Compound The order of the tags is not guaranteed. 25 | TagIntArray TypeId = 11 // { TAG_Int length; An array of ints. The length of this array is ints } 26 | ) 27 | 28 | type Reader struct { 29 | r *bufio.Reader 30 | } 31 | 32 | func Parse(r io.Reader) (map[string]interface{}, error) { 33 | nr := NewReader(r) 34 | typeId, _, err := nr.ReadTag() 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | value, err := nr.ReadValue(typeId) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return value.(map[string]interface{}), nil 44 | } 45 | 46 | func NewReader(r io.Reader) *Reader { 47 | return &Reader{bufio.NewReader(r)} 48 | } 49 | 50 | func (r *Reader) ReadTag() (typeId TypeId, name string, err error) { 51 | typeId, err = r.readTypeId() 52 | if err != nil || typeId == 0 { 53 | return typeId, "", err 54 | } 55 | 56 | name, err = r.ReadString() 57 | if err != nil { 58 | return typeId, name, err 59 | } 60 | 61 | return typeId, name, nil 62 | } 63 | 64 | func (r *Reader) ReadListHeader() (itemTypeId TypeId, length int, err error) { 65 | length = 0 66 | 67 | itemTypeId, err = r.readTypeId() 68 | if err == nil { 69 | length, err = r.ReadInt32() 70 | } 71 | 72 | return 73 | } 74 | 75 | func (r *Reader) ReadString() (string, error) { 76 | var length, err1 = r.ReadInt16() 77 | if err1 != nil { 78 | return "", err1 79 | } 80 | 81 | var bytes = make([]byte, length) 82 | var _, err = io.ReadFull(r.r, bytes) 83 | return string(bytes), err 84 | } 85 | 86 | func (r *Reader) ReadBytes() ([]byte, error) { 87 | var length, err1 = r.ReadInt32() 88 | if err1 != nil { 89 | return nil, err1 90 | } 91 | 92 | var bytes = make([]byte, length) 93 | var _, err = io.ReadFull(r.r, bytes) 94 | return bytes, err 95 | } 96 | 97 | func (r *Reader) ReadInts() ([]int, error) { 98 | length, err := r.ReadInt32() 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | ints := make([]int, length) 104 | for i := 0; i < length; i++ { 105 | ints[i], err = r.ReadInt32() 106 | if err != nil { 107 | return nil, err 108 | } 109 | } 110 | return ints, nil 111 | } 112 | 113 | func (r *Reader) ReadInt8() (int, error) { 114 | return r.readIntN(1) 115 | } 116 | 117 | func (r *Reader) ReadInt16() (int, error) { 118 | return r.readIntN(2) 119 | } 120 | 121 | func (r *Reader) ReadInt32() (int, error) { 122 | return r.readIntN(4) 123 | } 124 | 125 | func (r *Reader) ReadInt64() (int, error) { 126 | return r.readIntN(8) 127 | } 128 | 129 | func (r *Reader) ReadFloat32() (float32, error) { 130 | x, err := r.readUintN(4) 131 | return math.Float32frombits(uint32(x)), err 132 | } 133 | 134 | func (r *Reader) ReadFloat64() (float64, error) { 135 | x, err := r.readUintN(8) 136 | return math.Float64frombits(x), err 137 | } 138 | 139 | func (r *Reader) readTypeId() (TypeId, error) { 140 | id, err := r.r.ReadByte() 141 | return TypeId(id), err 142 | } 143 | 144 | func (r *Reader) readIntN(n int) (int, error) { 145 | var a int = 0 146 | 147 | for i := 0; i < n; i++ { 148 | var b, err = r.r.ReadByte() 149 | if err != nil { 150 | return a, err 151 | } 152 | a = a<<8 + int(b) 153 | } 154 | 155 | return a, nil 156 | } 157 | 158 | func (r *Reader) readUintN(n int) (uint64, error) { 159 | var a uint64 = 0 160 | 161 | for i := 0; i < n; i++ { 162 | var b, err = r.r.ReadByte() 163 | if err != nil { 164 | return a, err 165 | } 166 | a = a<<8 + uint64(b) 167 | } 168 | 169 | return a, nil 170 | } 171 | 172 | func (r *Reader) ReadStruct() (map[string]interface{}, error) { 173 | s := make(map[string]interface{}) 174 | for { 175 | typeId, name, err := r.ReadTag() 176 | if err != nil { 177 | return s, err 178 | } 179 | if typeId == TagStructEnd { 180 | break 181 | } 182 | x, err := r.ReadValue(typeId) 183 | s[name] = x 184 | if err != nil { 185 | return s, err 186 | } 187 | } 188 | return s, nil 189 | } 190 | 191 | func (r *Reader) ReadValue(typeId TypeId) (interface{}, error) { 192 | switch typeId { 193 | case TagStruct: 194 | return r.ReadStruct() 195 | case TagStructEnd: 196 | return nil, nil 197 | case TagByteArray: 198 | return r.ReadBytes() 199 | case TagInt8: 200 | return r.ReadInt8() 201 | case TagInt16: 202 | return r.ReadInt16() 203 | case TagInt32: 204 | return r.ReadInt32() 205 | case TagInt64: 206 | return r.ReadInt64() 207 | case TagFloat32: 208 | return r.ReadFloat32() 209 | case TagFloat64: 210 | return r.ReadFloat64() 211 | case TagString: 212 | return r.ReadString() 213 | case TagList: 214 | itemTypeId, length, err := r.ReadListHeader() 215 | if err != nil { 216 | return nil, err 217 | } 218 | switch TypeId(itemTypeId) { 219 | case TagInt8: 220 | list := make([]int, length) 221 | for i := 0; i < length; i++ { 222 | x, err := r.ReadInt8() 223 | list[i] = x 224 | if err != nil { 225 | return list, err 226 | } 227 | } 228 | return list, nil 229 | case TagFloat32: 230 | list := make([]float32, length) 231 | for i := 0; i < length; i++ { 232 | x, err := r.ReadFloat32() 233 | list[i] = x 234 | if err != nil { 235 | return list, err 236 | } 237 | } 238 | return list, nil 239 | case TagFloat64: 240 | list := make([]float64, length) 241 | for i := 0; i < length; i++ { 242 | x, err := r.ReadFloat64() 243 | list[i] = x 244 | if err != nil { 245 | return list, err 246 | } 247 | } 248 | return list, nil 249 | case TagStruct: 250 | list := make([]interface{}, length) 251 | for i := 0; i < length; i++ { 252 | s := make(map[string]interface{}) 253 | s, err := r.ReadStruct() 254 | list[i] = s 255 | if err != nil { 256 | return list, err 257 | } 258 | } 259 | return list, nil 260 | default: 261 | return nil, errors.New(fmt.Sprintf("reading lists of typeId %d not supported. length:%d", itemTypeId, length)) 262 | } 263 | } 264 | 265 | return nil, errors.New(fmt.Sprintf("reading typeId %d not supported", typeId)) 266 | } 267 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mcobj: Minecraft to OBJ (and PRT) converter 2 | =========================================== 3 | 4 | ![](http://github.com/downloads/quag/mcobj/splash1.jpg) 5 | ![](http://github.com/downloads/quag/mcobj/splash2.jpg) 6 | 7 | For example renders see [http://quag.imgur.com/minecraft__blender](http://quag.imgur.com/minecraft__blender). 8 | 9 | If you'd like to show me renders, report problems, suggest improvements, would like builds for other platforms or to ask for help, please email me. 10 | 11 | The [http://reddit.com/r/mcobj](r/mcobj) sub-reddit been setup for showing off renders, discussing how to achieve nice effects and news about updates. Feel free to contribute and if it is quiet or empty to stir it up with some posts. 12 | 13 | As I'd love to see renders, please email me copies of images you create. 14 | 15 | Usage 16 | ----- 17 | 18 | On Windows: 19 | 20 | mcobj -cpu 4 -s 20 -o world1.obj %AppData%\.minecraft\saves\World1 21 | 22 | On OSX: 23 | 24 | mcobj -cpu 4 -s 20 -o world1.obj ~/Library/Application\ Support/minecraft/saves/World1 25 | 26 | On Linux: 27 | 28 | mcobj -cpu 4 -s 20 -o world1.obj ~/.minecraft/saves/World1 29 | 30 | Flags: 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
-cpu 4How many cores to use while processing. Defaults to 1. Set to the number of cpu's in the machine.
-o a.objName for the obj file to write to. Defaults to a.obj
-hHelp
-prtOutput a PRT file instead of OBJ
-3dsmax=falseOutput an obj file that is incompatible with 3dsMax. Typically is faster, uses less memory and results in a smaller .obj files
39 | 40 | Chunk Selection: 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
-x -8.4 -z 272.8Center the output to chunk x=-1 and z=17. Defaults to chunk 0,0
-cx 10 -cz -23Center the output to chunk x=10 and z=23. Defaults to chunk 0,0. To calculate the chunk coords, divide the values given in Minecraft's F3 screen by 16
-s 20Output a sized square of chunks centered on -cx -cz. -s 20 will output 20x20 area around 0,0
-rx 2 -rx 8Output a sized rectangle of chunks centered on -cx -cz. -rx 2 -rx 8 will output a 2x8 area around 0,0
49 | 50 | Limit the output: 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
-fk 300Limit the face count (in thousands of faces)
-y 63Omit all blocks below this height. Use 63 for sea level
-hbHide the bottom of the world
-gGray; omit materials
-bfDon't combine adjacent faces of the same block within a column
-sidesOutput sides of chunks at the edges of selection. Sides are usually omitted
60 | 61 | Change Log 62 | --------- 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | 92 | 93 | 94 | 110 | 111 | 112 | 113 | 114 | 127 | 128 | 129 | 130 | 131 | 140 | 141 | 142 | 143 | 144 | 151 | 152 | 153 | 154 | 155 | 160 | 161 | 162 | 163 | 164 | 170 | 171 | 172 | 173 | 174 | 179 | 180 | 181 | 182 | 183 | 188 | 189 | 190 | 191 | 192 | 197 | 198 | 199 | 200 | 201 | 206 | 207 | 208 | 209 | 210 | 215 | 216 | 217 | 218 | 219 | 225 | 226 | 227 | 228 | 229 | 235 | 236 | 237 | 238 | 239 | 245 | 246 | 247 | 248 | 249 | 250 | 251 |
Change Log
2012-04-030.14 74 |
    75 |
  • Support the Anvil world format
  • 76 |
  • Update blocks to 1.2.4
  • 77 |
  • Add variable height support
  • 78 |
79 |
2011-09-120.13.1 85 |
    86 |
  • Fix build to generate .exe on Windows executable's name
  • 87 |
  • Disable: Generate .obj files that can be read without any special flags in 3dsmax
  • 88 |
89 |
2011-09-110.13 95 |
    96 |
  • Add blocks for 1.6 and 1.7
  • 97 |
  • Center on spawn point by default
  • 98 |
  • Usability: Read commandline from settings.txt file beside mcobj.exe
  • 99 |
  • Usability: Prompt for command line when someone double clicks on the exe (no more fighting with Windows cmd or OSX Terminal)
  • 100 |
  • Usability: Correct for unquoted spaces in paths to worlds
  • 101 |
  • Fix bug: EOF error on empty mcr files in beta worlds
  • 102 |
  • Fix bug: crash when -o refers to a missing directory
  • 103 |
  • Fix bug: "panic: runtime error: slice bounds out of range" on some chunks
  • 104 |
  • Switch to goinstall based building (requires a weekly build newer than go-r57)
  • 105 |
  • PRT: invert x and z
  • 106 |
  • PRT: remove 2x scaling
  • 107 |
  • Generate .obj files that can be read without any special flags in 3dsmax
  • 108 |
109 |
2011-05-150.12 115 |
    116 |
  • Add -x and -z flags for centering using Minecraft's F3 coordinates
  • 117 |
  • Change to material names instead of numbers
  • 118 |
  • Update blocks.json to handle Minecraft 1.5 blocks
  • 119 |
  • Fix bug: materials with extra data, e.g., wool colors, were always grey
  • 120 |
  • Fix bug: some leaves showed up as grey blocks
  • 121 |
  • Fix bug: -s square not centered on -cx -cz (off-by-one)
  • 122 |
  • Add arrays of block data to blocks.json -- beds and saplings
  • 123 |
  • Reworked build and release scripts
  • 124 |
  • Update source to compile with go release.r57.1
  • 125 |
126 |
2011-04-170.11 132 |
    133 |
  • Add -solid flag for putting sides on the area
  • 134 |
  • Add -rx 2 -rz 8 for selecting a rectangular area
  • 135 |
  • Slight improvement to glass color (less likely to be black)
  • 136 |
  • Read block data from blocks.json instead of being hard coded
  • 137 |
  • Remove -hs (hide stone) flag. Now add "empty":true to stone in blocks.json
  • 138 |
139 |
2011-03-170.10.2 145 |
    146 |
  • Add beds and redstone repeaters
  • 147 |
  • Update colors from WormSlayer
  • 148 |
  • Only invert x-axis for PRT (not both x and z)
  • 149 |
150 |
2011-03-140.10.1 156 |
    157 |
  • Bug fix: colors missing from blocks with extra data (except for wool) in OBJ
  • 158 |
159 |
2011-03-130.10 165 |
    166 |
  • Flip both x and z axis for PRT output
  • 167 |
  • Write out blocks around transparent blocks. For example, the block under a torch or the blocks around glass
  • 168 |
169 |
2011-03-060.9.2 175 |
    176 |
  • Rename PRT channel from MtlIndex to BlockId
  • 177 |
178 |
2011-03-060.9.1 184 |
    185 |
  • Fix bug (when PRT support was added) that made OBJ files empty
  • 186 |
187 |
2011-03-050.9 193 |
    194 |
  • Fix PRT export bug where all blocks appeared in a 16x16x128 space
  • 195 |
196 |
2011-03-050.8 202 |
    203 |
  • Add alternative output format: PRT (use -prt flag to enable)
  • 204 |
205 |
2011-03-050.7 211 |
    212 |
  • Add support for the beta world format
  • 213 |
214 |
2011-02-190.6 220 |
    221 |
  • Further tweaks to colors
  • 222 |
  • Add color wool blocks
  • 223 |
224 |
2011-02-170.5 230 |
    231 |
  • Switch to WormSlayer's block colors
  • 232 |
  • Reverting to relative vertex numbers as abs numbers didn't help with the 3dsmax loading
  • 233 |
234 |
2011-02-170.4 240 |
    241 |
  • Use absolute vertex numbers instead of relative (to support 3dsmax)
  • 242 |
  • No longer leak file handles and gzip streams
  • 243 |
244 |
2011-02-140.3 
252 | 253 | Limitations 254 | ----------- 255 | 256 | Lots! 257 | 258 | Only understands cube blocks (no torch meashes, door meshes and so on) 259 | No textures. Just solid colors per block 260 | Torches and lava don't emit light 261 | ... 262 | The no-mesh and no-texture limitations are delibarate. They keep the obj file size and face counts down allowing dumps of large parts of the world without blowing out Blender's memory. 263 | 264 | Blender 265 | ------- 266 | 267 | Requires Blender 2.5. 268 | 269 | Steps: 270 | 271 | 1. Start Blender 272 | 2. Delete the standard cube (right-click and press delete) 273 | 3. Start the obj importer: (hit space, and type, "import obj" and hit return) 274 | 4. Select the generated obj file and wait for it to import 275 | 5. Enable Ambient Occlusion (switch to Blender's 'World' planel and click the 'Ambient Occlusion' checkbox) 276 | 6. Start a render (hit F12) 277 | 278 | Example Run 279 | ----------- 280 | 281 | C:\>mcobj -cpu 4 -s 4 -o world1.obj %AppData%\.minecraft\saves\World1 282 | mcobj dev-7-g1591dec (cpu: 4) 283 | 1/5590 ( 0, 0) Faces: 889 Size: 0.0MB 284 | 2/5590 ( 0, -1) Faces: 880 Size: 0.1MB 285 | 3/5590 ( -1, 0) Faces: 850 Size: 0.1MB 286 | 4/5590 ( -1, -1) Faces: 849 Size: 0.1MB 287 | 5/5590 ( 0, 1) Faces: 1042 Size: 0.2MB 288 | 6/5590 ( -1, 1) Faces: 888 Size: 0.2MB 289 | 7/5590 ( 1, 0) Faces: 918 Size: 0.3MB 290 | 8/5590 ( 1, -1) Faces: 860 Size: 0.3MB 291 | 9/5590 ( 1, 1) Faces: 1678 Size: 0.4MB 292 | 10/5590 ( 0, -2) Faces: 906 Size: 0.4MB 293 | 11/5590 ( -1, -2) Faces: 850 Size: 0.4MB 294 | 12/5590 ( 1, -2) Faces: 836 Size: 0.5MB 295 | 13/5590 ( -2, 0) Faces: 863 Size: 0.5MB 296 | 14/5590 ( -2, -1) Faces: 851 Size: 0.5MB 297 | 15/5590 ( -2, 1) Faces: 909 Size: 0.6MB 298 | 16/5590 ( -2, -2) Faces: 869 Size: 0.6MB 299 | 300 | ![](http://github.com/downloads/quag/mcobj/4x4.jpg) 301 | -------------------------------------------------------------------------------- /cmd/mcobj/obj.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/quag/mcobj/nbt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | ) 11 | 12 | type ObjGenerator struct { 13 | enclosedsChan chan *EnclosedChunkJob 14 | writeFacesChan chan *WriteFacesJob 15 | completeChan chan bool 16 | 17 | freelist chan *MemoryWriter 18 | memoryWriterPool *MemoryWriterPool 19 | 20 | total int 21 | 22 | outFile, voutFile, foutFile *os.File 23 | out, vout, fout *bufio.Writer 24 | 25 | outFilename, vFilename, fFilename string 26 | } 27 | 28 | func (o *ObjGenerator) Start(outFilename string, total int, maxProcs int, boundary *BoundaryLocator) error { 29 | o.enclosedsChan = make(chan *EnclosedChunkJob, maxProcs*2) 30 | o.writeFacesChan = make(chan *WriteFacesJob, maxProcs*2) 31 | o.completeChan = make(chan bool) 32 | 33 | o.memoryWriterPool = NewMemoryWriterPool(maxProcs*2, 128*1024) 34 | o.total = total 35 | 36 | for i := 0; i < maxProcs; i++ { 37 | go func() { 38 | var faces Faces 39 | faces.boundary = boundary 40 | for { 41 | var job = <-o.enclosedsChan 42 | 43 | var b = o.memoryWriterPool.GetWriter() 44 | var vb = o.memoryWriterPool.GetWriter() 45 | 46 | var faceCount, vertexCount, mtls = faces.ProcessChunk(job.enclosed, b, vb) 47 | 48 | o.writeFacesChan <- &WriteFacesJob{job.enclosed.xPos, job.enclosed.zPos, faceCount, vertexCount, mtls, b, vb, job.last} 49 | } 50 | }() 51 | } 52 | 53 | go func() { 54 | var chunkCount = 0 55 | var size = 0 56 | var vertexBase = 0 57 | for { 58 | var job = <-o.writeFacesChan 59 | chunkCount++ 60 | o.out.Write(job.b.buf) 61 | o.out.Flush() 62 | 63 | if obj3dsmax { 64 | o.vout.Write(job.vb.buf) 65 | o.vout.Flush() 66 | 67 | for _, mtl := range job.mtls { 68 | printMtl(o.fout, mtl.blockId) 69 | for _, face := range mtl.faces { 70 | printFaceLine(o.fout, face, vertexBase) 71 | } 72 | } 73 | o.fout.Flush() 74 | vertexBase += job.vertexCount 75 | } 76 | 77 | size += len(job.b.buf) 78 | fmt.Printf("%4v/%-4v (%3v,%3v) Faces: %4d Size: %4.1fMB\n", chunkCount, o.total, job.xPos, job.zPos, job.faceCount, float64(size)/1024/1024) 79 | 80 | o.memoryWriterPool.ReuseWriter(job.b) 81 | o.memoryWriterPool.ReuseWriter(job.vb) 82 | 83 | if job.last { 84 | o.completeChan <- true 85 | } 86 | } 87 | }() 88 | 89 | var mtlFilename = fmt.Sprintf("%s.mtl", outFilename[:len(outFilename)-len(filepath.Ext(outFilename))]) 90 | var mtlErr = writeMtlFile(mtlFilename) 91 | if mtlErr != nil { 92 | return mtlErr 93 | } 94 | 95 | o.outFilename = outFilename 96 | o.vFilename = outFilename + ".v" 97 | o.fFilename = outFilename + ".f" 98 | 99 | var outFile, voutFile, foutFile *os.File 100 | var outErr error 101 | outFile, outErr = os.Create(o.outFilename) 102 | if outErr != nil { 103 | return outErr 104 | } 105 | defer func() { 106 | if outFile != nil { 107 | outFile.Close() 108 | } 109 | }() 110 | 111 | o.out = bufio.NewWriterSize(outFile, 1024*1024) 112 | 113 | if obj3dsmax { 114 | voutFile, outErr = os.Create(o.vFilename) 115 | if outErr != nil { 116 | return outErr 117 | } 118 | defer func() { 119 | if voutFile != nil { 120 | voutFile.Close() 121 | } 122 | }() 123 | 124 | foutFile, outErr = os.Create(o.fFilename) 125 | if outErr != nil { 126 | return outErr 127 | } 128 | defer func() { 129 | if foutFile != nil { 130 | foutFile.Close() 131 | } 132 | }() 133 | 134 | o.vout = bufio.NewWriterSize(voutFile, 1024*1024) 135 | o.fout = bufio.NewWriterSize(foutFile, 1024*1024) 136 | } 137 | 138 | var mw io.Writer 139 | if obj3dsmax { 140 | mw = io.MultiWriter(o.out, o.vout) 141 | } else { 142 | mw = o.out 143 | } 144 | fmt.Fprintln(mw, "mtllib", filepath.Base(mtlFilename)) 145 | 146 | o.outFile, outFile = outFile, nil 147 | o.voutFile, voutFile = voutFile, nil 148 | o.foutFile, foutFile = foutFile, nil 149 | 150 | return nil 151 | } 152 | 153 | func (o *ObjGenerator) GetEnclosedJobsChan() chan *EnclosedChunkJob { 154 | return o.enclosedsChan 155 | } 156 | 157 | func (o *ObjGenerator) GetCompleteChan() chan bool { 158 | return o.completeChan 159 | } 160 | 161 | func (o *ObjGenerator) Close() error { 162 | o.out.Flush() 163 | o.outFile.Close() 164 | 165 | if obj3dsmax { 166 | o.vout.Flush() 167 | o.fout.Flush() 168 | 169 | o.voutFile.Close() 170 | o.foutFile.Close() 171 | 172 | var tFilename = o.outFilename + ".tmp" 173 | 174 | toutFile, outErr := os.Create(tFilename) 175 | if outErr != nil { 176 | return outErr 177 | } 178 | defer toutFile.Close() 179 | 180 | err := copyFile(toutFile, o.vFilename) 181 | if err != nil { 182 | return err 183 | } 184 | err = copyFile(toutFile, o.fFilename) 185 | if err != nil { 186 | return err 187 | } 188 | err = toutFile.Close() 189 | if err != nil { 190 | return err 191 | } 192 | err = os.Rename(tFilename, o.outFilename) 193 | if err != nil { 194 | return err 195 | } 196 | err = os.Remove(o.vFilename) 197 | if err != nil { 198 | return err 199 | } 200 | err = os.Remove(o.fFilename) 201 | if err != nil { 202 | return err 203 | } 204 | } 205 | 206 | return nil 207 | } 208 | 209 | func copyFile(w io.Writer, filename string) error { 210 | file, err := os.Open(filename) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | _, err = io.Copy(w, file) 216 | return err 217 | } 218 | 219 | type WriteFacesJob struct { 220 | xPos, zPos, faceCount, vertexCount int 221 | mtls []*MtlFaces 222 | b, vb *MemoryWriter 223 | last bool 224 | } 225 | 226 | type Faces struct { 227 | xPos, zPos int 228 | count int 229 | 230 | vertexes Vertexes 231 | faces []IndexFace 232 | boundary *BoundaryLocator 233 | } 234 | 235 | func (fs *Faces) ProcessChunk(enclosed *EnclosedChunk, w io.Writer, vw io.Writer) (faceCount, vertexCount int, mtls []*MtlFaces) { 236 | fs.clean(enclosed.xPos, enclosed.zPos, enclosed.height()) 237 | fs.processBlocks(enclosed) 238 | vertexCount, mtls = fs.Write(w, vw) 239 | return len(fs.faces), vertexCount, mtls 240 | } 241 | 242 | func (fs *Faces) clean(xPos, zPos int, height int) { 243 | fs.xPos = xPos 244 | fs.zPos = zPos 245 | 246 | if fs.vertexes.data == nil { 247 | fs.vertexes.data = make([]int16, (height+1)*(16+1)*(16+1)) 248 | fs.vertexes.height = height 249 | } else { 250 | fs.vertexes.Clear() 251 | } 252 | 253 | if fs.faces == nil { 254 | fs.faces = make([]IndexFace, 0, 8192) 255 | } else { 256 | fs.faces = fs.faces[:0] 257 | } 258 | } 259 | 260 | type IndexFace struct { 261 | blockId nbt.Block 262 | indexes [4]int 263 | } 264 | 265 | type VertexNumFace [4]int 266 | 267 | type MtlFaces struct { 268 | blockId nbt.Block 269 | faces []*VertexNumFace 270 | } 271 | 272 | func (fs *Faces) AddFace(blockId nbt.Block, v1, v2, v3, v4 Vertex) { 273 | var face = IndexFace{blockId, [4]int{fs.vertexes.Use(v1), fs.vertexes.Use(v2), fs.vertexes.Use(v3), fs.vertexes.Use(v4)}} 274 | fs.faces = append(fs.faces, face) 275 | } 276 | 277 | func (fs *Faces) Write(w io.Writer, vw io.Writer) (vertexCount int, mtls []*MtlFaces) { 278 | fs.vertexes.Number() 279 | var vc = int16(fs.vertexes.Print(io.MultiWriter(w, vw), fs.xPos, fs.zPos)) 280 | 281 | var blockIds = make([]nbt.Block, 0, 16) 282 | for _, face := range fs.faces { 283 | var found = false 284 | for _, id := range blockIds { 285 | if id == face.blockId { 286 | found = true 287 | break 288 | } 289 | } 290 | 291 | if !found { 292 | blockIds = append(blockIds, face.blockId) 293 | } 294 | } 295 | 296 | var mfs = make([]*MtlFaces, 0, len(blockIds)) 297 | 298 | for _, blockId := range blockIds { 299 | printMtl(w, blockId) 300 | var mf = &MtlFaces{blockId, make([]*VertexNumFace, 0, len(fs.faces))} 301 | mfs = append(mfs, mf) 302 | for _, face := range fs.faces { 303 | if face.blockId == blockId { 304 | var vf = face.VertexNumFace(fs.vertexes) 305 | printFaceLine(w, vf, -int(vc+1)) 306 | mf.faces = append(mf.faces, vf) 307 | faceCount++ 308 | } 309 | } 310 | } 311 | 312 | return int(vc), mfs 313 | } 314 | 315 | func printFaceLine(w io.Writer, f *VertexNumFace, offset int) { 316 | fmt.Fprintln(w, "f", f[0]+offset, f[1]+offset, f[2]+offset, f[3]+offset) 317 | } 318 | 319 | type Vertex struct { 320 | x, y, z int 321 | } 322 | 323 | type Vertexes struct { 324 | data []int16 325 | height int 326 | } 327 | 328 | func (vs *Vertexes) Index(x, y, z int) int { 329 | return y + (z*(vs.height+1) + (x * (vs.height + 1) * 17)) 330 | } 331 | 332 | func (vs *Vertexes) Use(v Vertex) int { 333 | var i = vs.Index(v.x, v.y, v.z) 334 | vs.data[i]++ 335 | return i 336 | } 337 | 338 | func (vs *Vertexes) Release(v Vertex) int { 339 | var i = vs.Index(v.x, v.y, v.z) 340 | vs.data[i]-- 341 | return i 342 | } 343 | 344 | func (vs *Vertexes) Get(i int) int16 { 345 | return vs.data[i] 346 | } 347 | 348 | func (face *IndexFace) VertexNumFace(vs Vertexes) *VertexNumFace { 349 | return &VertexNumFace{ 350 | int(vs.Get(face.indexes[0])), 351 | int(vs.Get(face.indexes[1])), 352 | int(vs.Get(face.indexes[2])), 353 | int(vs.Get(face.indexes[3]))} 354 | } 355 | 356 | func (vs *Vertexes) Clear() { 357 | for i, _ := range vs.data { 358 | vs.data[i] = 0 359 | } 360 | } 361 | 362 | func (vs *Vertexes) Number() { 363 | var count int16 = 0 364 | for i, references := range vs.data { 365 | if references != 0 { 366 | count++ 367 | vs.data[i] = count 368 | } else { 369 | vs.data[i] = -1 370 | } 371 | } 372 | } 373 | 374 | func (vs *Vertexes) Print(w io.Writer, xPos, zPos int) (count int) { 375 | var buf = make([]byte, 64) 376 | copy(buf[0:2], "v ") 377 | 378 | count = 0 379 | for i := 0; i < len(vs.data); i += (vs.height + 1) { 380 | var x, z = (i / (vs.height + 1)) / 17, (i / (vs.height + 1)) % 17 381 | 382 | var column = vs.data[i : i+(vs.height+1)] 383 | for y, offset := range column { 384 | if offset != -1 { 385 | 386 | count++ 387 | 388 | var ( 389 | xa = x + xPos*16 390 | ya = y - 64 391 | za = z + zPos*16 392 | ) 393 | 394 | buf = buf[:2] 395 | buf = appendCoord(buf, xa) 396 | buf = append(buf, ' ') 397 | buf = appendCoord(buf, ya) 398 | buf = append(buf, ' ') 399 | buf = appendCoord(buf, za) 400 | buf = append(buf, '\n') 401 | 402 | w.Write(buf) 403 | } 404 | } 405 | } 406 | return 407 | } 408 | 409 | func appendCoord(buf []byte, x int) []byte { 410 | var b [64]byte 411 | var j = len(b) 412 | 413 | var neg = x < 0 414 | if neg { 415 | x = -x 416 | } 417 | 418 | var ( 419 | high = x / 20 420 | low = (x % 20) * 5 421 | numbers = "0123456789" 422 | ) 423 | 424 | for i := 0; i < 2; i++ { 425 | j-- 426 | b[j] = numbers[low%10] 427 | low /= 10 428 | } 429 | 430 | j-- 431 | b[j] = '.' 432 | 433 | if high == 0 { 434 | j-- 435 | b[j] = '0' 436 | } else { 437 | for high > 0 { 438 | j-- 439 | b[j] = numbers[high%10] 440 | high /= 10 441 | } 442 | } 443 | 444 | if neg { 445 | j-- 446 | b[j] = '-' 447 | } 448 | 449 | var end = len(buf) + len(b) - j 450 | var d = buf[len(buf):end] 451 | copy(d, b[j:]) 452 | return buf[:end] 453 | } 454 | 455 | func (fs *Faces) processBlocks(enclosedChunk *EnclosedChunk) { 456 | type blockRun struct { 457 | blockId nbt.Block 458 | v1, v2, v3, v4 Vertex 459 | dirty bool 460 | } 461 | 462 | var finishRun = func(r *blockRun) { 463 | if r.dirty { 464 | fs.AddFace(r.blockId, r.v1, r.v2, r.v3, r.v4) 465 | r.dirty = false 466 | } 467 | } 468 | 469 | var updateBlockRun func(rp **blockRun, nr *blockRun, flag bool) 470 | if !blockFaces { 471 | updateBlockRun = func(rp **blockRun, nr *blockRun, flag bool) { 472 | var r = *rp 473 | if r.dirty { 474 | if nr.blockId == r.blockId { 475 | if flag { 476 | r.v3 = nr.v3 477 | r.v4 = nr.v4 478 | } else { 479 | r.v2 = nr.v2 480 | r.v3 = nr.v3 481 | } 482 | } else { 483 | finishRun(r) 484 | *rp = nr 485 | } 486 | } else { 487 | *rp = nr 488 | } 489 | } 490 | } else { 491 | updateBlockRun = func(rp **blockRun, nr *blockRun, flag bool) { 492 | finishRun(nr) 493 | } 494 | } 495 | 496 | height := enclosedChunk.blocks.height 497 | 498 | for i := 0; i < len(enclosedChunk.blocks.data); i += height { 499 | var x, z = (i / height) / 16, (i / height) % 16 500 | 501 | var ( 502 | r1 = new(blockRun) 503 | r2 = new(blockRun) 504 | r3 = new(blockRun) 505 | r4 = new(blockRun) 506 | ) 507 | 508 | var column = BlockColumn(enclosedChunk.blocks.data[i : i+height]) 509 | for y, blockId := range column { 510 | if y < yMin { 511 | continue 512 | } 513 | 514 | if fs.boundary.IsBoundary(blockId, enclosedChunk.Get(x, y-1, z)) { 515 | fs.AddFace(blockId, Vertex{x, y, z}, Vertex{x + 1, y, z}, Vertex{x + 1, y, z + 1}, Vertex{x, y, z + 1}) 516 | } 517 | 518 | if fs.boundary.IsBoundary(blockId, enclosedChunk.Get(x, y+1, z)) { 519 | fs.AddFace(blockId, Vertex{x, y + 1, z}, Vertex{x, y + 1, z + 1}, Vertex{x + 1, y + 1, z + 1}, Vertex{x + 1, y + 1, z}) 520 | } 521 | 522 | if fs.boundary.IsBoundary(blockId, enclosedChunk.Get(x-1, y, z)) { 523 | updateBlockRun(&r1, &blockRun{blockId, Vertex{x, y, z}, Vertex{x, y, z + 1}, Vertex{x, y + 1, z + 1}, Vertex{x, y + 1, z}, true}, true) 524 | } else { 525 | finishRun(r1) 526 | } 527 | 528 | if fs.boundary.IsBoundary(blockId, enclosedChunk.Get(x+1, y, z)) { 529 | updateBlockRun(&r2, &blockRun{blockId, Vertex{x + 1, y, z}, Vertex{x + 1, y + 1, z}, Vertex{x + 1, y + 1, z + 1}, Vertex{x + 1, y, z + 1}, true}, false) 530 | } else { 531 | finishRun(r2) 532 | } 533 | 534 | if fs.boundary.IsBoundary(blockId, enclosedChunk.Get(x, y, z-1)) { 535 | updateBlockRun(&r3, &blockRun{blockId, Vertex{x, y, z}, Vertex{x, y + 1, z}, Vertex{x + 1, y + 1, z}, Vertex{x + 1, y, z}, true}, false) 536 | } else { 537 | finishRun(r3) 538 | } 539 | 540 | if fs.boundary.IsBoundary(blockId, enclosedChunk.Get(x, y, z+1)) { 541 | updateBlockRun(&r4, &blockRun{blockId, Vertex{x, y, z + 1}, Vertex{x + 1, y, z + 1}, Vertex{x + 1, y + 1, z + 1}, Vertex{x, y + 1, z + 1}, true}, true) 542 | } else { 543 | finishRun(r4) 544 | } 545 | } 546 | 547 | finishRun(r1) 548 | finishRun(r2) 549 | finishRun(r3) 550 | finishRun(r4) 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /cmd/mcobj/mcobj.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "github.com/quag/mcobj/commandline" 9 | "github.com/quag/mcobj/mcworld" 10 | "github.com/quag/mcobj/nbt" 11 | "io" 12 | "io/ioutil" 13 | "math" 14 | "os" 15 | "path/filepath" 16 | "runtime" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | var ( 22 | out *bufio.Writer 23 | yMin int 24 | blockFaces bool 25 | hideBottom bool 26 | noColor bool 27 | 28 | faceCount int 29 | faceLimit int 30 | 31 | chunkCount int 32 | 33 | obj3dsmax bool 34 | ) 35 | 36 | func main() { 37 | var bx, bz float64 38 | var cx, cz int 39 | var square int 40 | var rectx, rectz int 41 | var maxProcs = runtime.GOMAXPROCS(0) 42 | var prt bool 43 | var solidSides bool 44 | var mtlNumber bool 45 | 46 | var defaultObjOutFilename = "a.obj" 47 | var defaultPrtOutFilename = "a.prt" 48 | 49 | commandLine := flag.NewFlagSet(os.Args[0], flag.ExitOnError) 50 | 51 | var outFilename string 52 | commandLine.IntVar(&maxProcs, "cpu", maxProcs, "Number of cores to use") 53 | commandLine.StringVar(&outFilename, "o", defaultObjOutFilename, "Name for output file") 54 | commandLine.IntVar(&yMin, "y", 0, "Omit all blocks below this height. 63 is sea level") 55 | commandLine.BoolVar(&solidSides, "sides", false, "Solid sides, rather than showing underground") 56 | commandLine.BoolVar(&blockFaces, "bf", false, "Don't combine adjacent faces of the same block within a column") 57 | commandLine.BoolVar(&hideBottom, "hb", false, "Hide bottom of world") 58 | commandLine.BoolVar(&noColor, "g", false, "Omit materials") 59 | commandLine.Float64Var(&bx, "x", 0, "Center x coordinate in blocks") 60 | commandLine.Float64Var(&bz, "z", 0, "Center z coordinate in blocks") 61 | commandLine.IntVar(&cx, "cx", 0, "Center x coordinate in chunks") 62 | commandLine.IntVar(&cz, "cz", 0, "Center z coordinate in chunks") 63 | commandLine.IntVar(&square, "s", math.MaxInt32, "Chunk square size") 64 | commandLine.IntVar(&rectx, "rx", math.MaxInt32, "Width(x) of rectangle size") 65 | commandLine.IntVar(&rectz, "rz", math.MaxInt32, "Height(z) of rectangle size") 66 | commandLine.IntVar(&faceLimit, "fk", math.MaxInt32, "Face limit (thousands of faces)") 67 | commandLine.BoolVar(&prt, "prt", false, "Write out PRT file instead of Obj file") 68 | commandLine.BoolVar(&obj3dsmax, "3dsmax", false, "Create .obj file compatible with 3dsMax") 69 | commandLine.BoolVar(&mtlNumber, "mtlnum", false, "Number materials instead of using names") 70 | var showHelp = commandLine.Bool("h", false, "Show Help") 71 | commandLine.Parse(os.Args[1:]) 72 | 73 | runtime.GOMAXPROCS(maxProcs) 74 | fmt.Printf("mcobj %v (cpu: %d) Copyright (c) 2011-2012 Jonathan Wright\n", version, runtime.GOMAXPROCS(0)) 75 | 76 | exeDir, _ := filepath.Split(strings.Replace(os.Args[0], "\\", "/", -1)) 77 | 78 | if *showHelp || commandLine.NArg() == 0 { 79 | settingsPath := filepath.Join(exeDir, "settings.txt") 80 | fi, err := os.Stat(settingsPath) 81 | if err == nil && (!fi.IsDir() || fi.Mode()&os.ModeSymlink != 0) { 82 | line, err := ioutil.ReadFile(settingsPath) 83 | if err != nil { 84 | fmt.Fprintln(os.Stderr, "ioutil.ReadFile:", err) 85 | } else { 86 | parseFakeCommandLine(commandLine, line) 87 | } 88 | } 89 | 90 | if commandLine.NArg() == 0 { 91 | fmt.Fprintln(os.Stderr) 92 | fmt.Fprintln(os.Stderr, "Usage: mcobj -cpu 4 -s 20 -o world1.obj", ExampleWorldPath) 93 | fmt.Fprintln(os.Stderr) 94 | commandLine.PrintDefaults() 95 | 96 | fmt.Println() 97 | stdin := bufio.NewReader(os.Stdin) 98 | 99 | for commandLine.NArg() == 0 { 100 | fmt.Printf("command line: ") 101 | line, _, err := stdin.ReadLine() 102 | if err == io.EOF { 103 | fmt.Println() 104 | return 105 | } else if err != nil { 106 | fmt.Fprintln(os.Stderr, "stdin.ReadLine:", err) 107 | return 108 | } 109 | 110 | parseFakeCommandLine(commandLine, line) 111 | fmt.Println() 112 | } 113 | } 114 | } 115 | 116 | manualCenter := false 117 | commandLine.Visit(func(f *flag.Flag) { 118 | switch f.Name { 119 | case "x": 120 | fallthrough 121 | case "z": 122 | fallthrough 123 | case "cx": 124 | fallthrough 125 | case "cz": 126 | manualCenter = true 127 | } 128 | }) 129 | 130 | if faceLimit != math.MaxInt32 { 131 | faceLimit *= 1000 132 | } 133 | 134 | if mtlNumber { 135 | MaterialNamer = new(NumberBlockIdNamer) 136 | } else { 137 | MaterialNamer = new(NameBlockIdNamer) 138 | } 139 | 140 | switch { 141 | case bx == 0 && cx == 0: 142 | cx = 0 143 | case cx == 0: 144 | cx = int(math.Floor(bx / 16)) 145 | } 146 | 147 | switch { 148 | case bz == 0 && cz == 0: 149 | cz = 0 150 | case cz == 0: 151 | cz = int(math.Floor(bz / 16)) 152 | } 153 | 154 | if prt && outFilename == defaultObjOutFilename { 155 | outFilename = defaultPrtOutFilename 156 | } 157 | 158 | if solidSides { 159 | defaultSide = emptySide 160 | } 161 | 162 | { 163 | var jsonError = loadBlockTypesJson(filepath.Join(exeDir, "blocks.json")) 164 | if jsonError != nil { 165 | fmt.Fprintln(os.Stderr, "blocks.json error:", jsonError) 166 | return 167 | } 168 | } 169 | 170 | settings := &ProcessingSettings{ 171 | Prt: prt, 172 | OutFilename: outFilename, 173 | MaxProcs: maxProcs, 174 | ManualCenter: manualCenter, 175 | Cx: cx, 176 | Cz: cz, 177 | Square: square, 178 | Rectx: rectx, 179 | Rectz: rectz, 180 | } 181 | 182 | validPath := false 183 | for _, dirpath := range commandLine.Args() { 184 | var fi, err = os.Stat(dirpath) 185 | validPath = validPath || (err == nil && !fi.IsDir()) 186 | } 187 | 188 | if validPath { 189 | for _, dirpath := range commandLine.Args() { 190 | processWorldDir(dirpath, settings) 191 | } 192 | } else { 193 | processWorldDir(strings.Join(commandLine.Args(), " "), settings) 194 | } 195 | } 196 | 197 | func parseFakeCommandLine(commandLine *flag.FlagSet, line []byte) { 198 | args := commandline.SplitCommandLine(strings.Trim(string(line), " \r\n")) 199 | if len(args) >= 1 && args[0] == "mcobj" { 200 | args = args[1:] 201 | } 202 | for i, arg := range args { 203 | if strings.HasPrefix(arg, "~/") { 204 | args[i] = filepath.Join(os.Getenv("HOME"), arg[2:]) 205 | } else if strings.HasPrefix(strings.ToUpper(arg), "%APPDATA%\\") || strings.HasPrefix(strings.ToUpper(arg), "%APPDATA%/") { 206 | args[i] = filepath.Join(os.Getenv("APPDATA"), arg[len("%APPDATA%/"):]) 207 | } 208 | } 209 | commandLine.Parse(args) 210 | } 211 | 212 | type ProcessingSettings struct { 213 | Prt bool 214 | OutFilename string 215 | MaxProcs int 216 | ManualCenter bool 217 | Cx, Cz int 218 | Square int 219 | Rectx, Rectz int 220 | } 221 | 222 | func processWorldDir(dirpath string, settings *ProcessingSettings) { 223 | var fi, err = os.Stat(dirpath) 224 | if err != nil { 225 | fmt.Fprintln(os.Stderr, "World error:", err) 226 | return 227 | } else if !fi.IsDir() { 228 | fmt.Fprintln(os.Stderr, dirpath, "is not a directory") 229 | } 230 | 231 | // Pick cx, cz 232 | var cx, cz int 233 | if settings.ManualCenter { 234 | cx, cz = settings.Cx, settings.Cz 235 | } else { 236 | var file, fileErr = os.Open(filepath.Join(dirpath, "level.dat")) 237 | defer file.Close() 238 | if fileErr != nil { 239 | fmt.Fprintln(os.Stderr, "os.Open", fileErr) 240 | return 241 | } 242 | level, err := nbt.ReadLevelDat(file) 243 | if err != nil { 244 | fmt.Fprintln(os.Stderr, "nbt.ReadLevelDat", err) 245 | return 246 | } 247 | file.Close() 248 | cx, cz = level.SpawnX/16, level.SpawnZ/16 249 | } 250 | 251 | // Create ChunkMask 252 | var ( 253 | chunkMask mcworld.ChunkMask 254 | chunkLimit int 255 | ) 256 | if settings.Square != math.MaxInt32 { 257 | chunkLimit = settings.Square * settings.Square 258 | var h = settings.Square / 2 259 | chunkMask = &mcworld.RectangleChunkMask{cx - h, cz - h, cx - h + settings.Square, cz - h + settings.Square} 260 | } else if settings.Rectx != math.MaxInt32 || settings.Rectz != math.MaxInt32 { 261 | switch { 262 | case settings.Rectx != math.MaxInt32 && settings.Rectz != math.MaxInt32: 263 | chunkLimit = settings.Rectx * settings.Rectz 264 | var ( 265 | hx = settings.Rectx / 2 266 | hz = settings.Rectz / 2 267 | ) 268 | chunkMask = &mcworld.RectangleChunkMask{cx - hx, cz - hz, cx - hx + settings.Rectx, cz - hz + settings.Rectz} 269 | case settings.Rectx != math.MaxInt32: 270 | chunkLimit = math.MaxInt32 271 | var hx = settings.Rectx / 2 272 | chunkMask = &mcworld.RectangleChunkMask{cx - hx, math.MinInt32, cx - hx + settings.Rectx, math.MaxInt32} 273 | case settings.Rectz != math.MaxInt32: 274 | chunkLimit = math.MaxInt32 275 | var hz = settings.Rectz / 2 276 | chunkMask = &mcworld.RectangleChunkMask{math.MinInt32, cz - hz, math.MaxInt32, cz - hz + settings.Rectz} 277 | } 278 | } else { 279 | chunkLimit = math.MaxInt32 280 | chunkMask = &mcworld.AllChunksMask{} 281 | } 282 | 283 | var world = mcworld.OpenWorld(dirpath) 284 | var pool, poolErr = world.ChunkPool(chunkMask) 285 | if poolErr != nil { 286 | fmt.Fprintln(os.Stderr, "Chunk pool error:", poolErr) 287 | return 288 | } 289 | 290 | var generator OutputGenerator 291 | if settings.Prt { 292 | generator = new(PrtGenerator) 293 | } else { 294 | generator = new(ObjGenerator) 295 | } 296 | var boundary = new(BoundaryLocator) 297 | boundary.Init() 298 | var startErr = generator.Start(settings.OutFilename, pool.Remaining(), settings.MaxProcs, boundary) 299 | if startErr != nil { 300 | fmt.Fprintln(os.Stderr, "Generator start error:", startErr) 301 | return 302 | } 303 | 304 | if walkEnclosedChunks(pool, world, chunkMask, chunkLimit, cx, cz, generator.GetEnclosedJobsChan()) { 305 | <-generator.GetCompleteChan() 306 | } 307 | 308 | var closeErr = generator.Close() 309 | if closeErr != nil { 310 | fmt.Fprintln(os.Stderr, "Generator close error:", closeErr) 311 | return 312 | } 313 | } 314 | 315 | type OutputGenerator interface { 316 | Start(outFilename string, total int, maxProcs int, boundary *BoundaryLocator) error 317 | GetEnclosedJobsChan() chan *EnclosedChunkJob 318 | GetCompleteChan() chan bool 319 | Close() error 320 | } 321 | 322 | type EnclosedChunkJob struct { 323 | last bool 324 | enclosed *EnclosedChunk 325 | } 326 | 327 | func walkEnclosedChunks(pool mcworld.ChunkPool, opener mcworld.ChunkOpener, chunkMask mcworld.ChunkMask, chunkLimit int, cx, cz int, enclosedsChan chan *EnclosedChunkJob) bool { 328 | var ( 329 | sideCache = new(SideCache) 330 | started = false 331 | ) 332 | 333 | for i := 0; moreChunks(pool.Remaining(), chunkLimit); i++ { 334 | for x := 0; x < i && moreChunks(pool.Remaining(), chunkLimit); x++ { 335 | for z := 0; z < i && moreChunks(pool.Remaining(), chunkLimit); z++ { 336 | var ( 337 | ax = cx + unzigzag(x) 338 | az = cz + unzigzag(z) 339 | ) 340 | 341 | if pool.Pop(ax, az) { 342 | loadSide(sideCache, opener, chunkMask, ax-1, az) 343 | loadSide(sideCache, opener, chunkMask, ax+1, az) 344 | loadSide(sideCache, opener, chunkMask, ax, az-1) 345 | loadSide(sideCache, opener, chunkMask, ax, az+1) 346 | 347 | var chunk, loadErr = loadChunk2(opener, ax, az) 348 | if loadErr != nil { 349 | fmt.Println(loadErr) 350 | } else { 351 | var enclosed = sideCache.EncloseChunk(chunk) 352 | sideCache.AddChunk(chunk) 353 | chunkCount++ 354 | enclosedsChan <- &EnclosedChunkJob{!moreChunks(pool.Remaining(), chunkLimit), enclosed} 355 | started = true 356 | } 357 | } 358 | } 359 | } 360 | } 361 | 362 | return started 363 | } 364 | 365 | type Blocks struct { 366 | data []nbt.Block 367 | height int 368 | } 369 | 370 | type BlockColumn []nbt.Block 371 | 372 | func (b *Blocks) Get(x, y, z int) nbt.Block { 373 | return b.data[y+b.height*(z+16*x)] 374 | } 375 | 376 | func (b *Blocks) Column(x, z int) BlockColumn { 377 | var i = b.height * (z + x*16) 378 | return BlockColumn(b.data[i : i+b.height]) 379 | } 380 | 381 | func zigzag(n int) int { 382 | return (n << 1) ^ (n >> 31) 383 | } 384 | 385 | func unzigzag(n int) int { 386 | return (n >> 1) ^ (-(n & 1)) 387 | } 388 | 389 | func moreChunks(unprocessedCount, chunkLimit int) bool { 390 | return unprocessedCount > 0 && faceCount < faceLimit && chunkCount < chunkLimit 391 | } 392 | 393 | func loadChunk(filename string) (*nbt.Chunk, error) { 394 | var file, fileErr = os.Open(filename) 395 | defer file.Close() 396 | if fileErr != nil { 397 | return nil, fileErr 398 | } 399 | var chunk, err = nbt.ReadChunkDat(file) 400 | if err == io.EOF { 401 | err = nil 402 | } 403 | return chunk, err 404 | } 405 | 406 | func loadChunk2(opener mcworld.ChunkOpener, x, z int) (*nbt.Chunk, error) { 407 | var r, openErr = opener.OpenChunk(x, z) 408 | if openErr != nil { 409 | return nil, openErr 410 | } 411 | defer r.Close() 412 | 413 | var chunk, nbtErr = nbt.ReadChunkNbt(r) 414 | if nbtErr != nil { 415 | return nil, nbtErr 416 | } 417 | return chunk, nil 418 | } 419 | 420 | func loadSide(sideCache *SideCache, opener mcworld.ChunkOpener, chunkMask mcworld.ChunkMask, x, z int) { 421 | if !sideCache.HasSide(x, z) && !chunkMask.IsMasked(x, z) { 422 | var chunk, loadErr = loadChunk2(opener, x, z) 423 | if loadErr != nil { 424 | fmt.Println(loadErr) 425 | } else { 426 | sideCache.AddChunk(chunk) 427 | } 428 | } 429 | } 430 | 431 | func loadBlockTypesJson(filename string) error { 432 | var jsonBytes, jsonIoError = ioutil.ReadFile(filename) 433 | 434 | if jsonIoError != nil { 435 | return jsonIoError 436 | } 437 | 438 | var f interface{} 439 | var unmarshalError = json.Unmarshal(jsonBytes, &f) 440 | if unmarshalError != nil { 441 | return unmarshalError 442 | } 443 | 444 | var lines, linesOk = f.([]interface{}) 445 | if linesOk { 446 | for _, line := range lines { 447 | var fields, fieldsOk = line.(map[string]interface{}) 448 | if fieldsOk { 449 | var ( 450 | blockId byte 451 | data byte = 255 452 | dataArray []byte 453 | name string 454 | mass SingularOrAggregate = Mass 455 | transparency Transparency = Opaque 456 | empty bool = false 457 | color uint32 458 | ) 459 | for k, v := range fields { 460 | switch k { 461 | case "name": 462 | name = v.(string) 463 | case "color": 464 | switch len(v.(string)) { 465 | case 7: 466 | var n, numErr = strconv.ParseUint(v.(string)[1:], 16, 64) 467 | if numErr == nil { 468 | color = uint32(n*0x100 + 0xff) 469 | } 470 | case 9: 471 | var n, numErr = strconv.ParseUint(v.(string)[1:], 16, 64) 472 | if numErr == nil { 473 | color = uint32(n) 474 | } 475 | } 476 | case "blockId": 477 | blockId = byte(v.(float64)) 478 | case "data": 479 | switch d := v.(type) { 480 | case float64: 481 | data = byte(d) 482 | case []interface{}: 483 | dataArray = make([]byte, len(d)) 484 | for i, value := range d { 485 | dataArray[i] = byte(value.(float64)) 486 | } 487 | } 488 | case "item": 489 | if v.(bool) { 490 | mass = Item 491 | transparency = Transparent 492 | } else { 493 | mass = Mass 494 | transparency = Opaque 495 | } 496 | case "transparent": 497 | if v.(bool) { 498 | transparency = Transparent 499 | } else { 500 | transparency = Opaque 501 | } 502 | case "empty": 503 | if v.(bool) { 504 | empty = true 505 | transparency = Transparent 506 | mass = Mass 507 | } else { 508 | empty = false 509 | } 510 | } 511 | } 512 | 513 | blockTypeMap[blockId] = &BlockType{blockId, mass, transparency, empty} 514 | if dataArray == nil { 515 | if data != 255 { 516 | extraData[blockId] = true 517 | colors = append(colors, MTL{blockId, data, color, name}) 518 | } else { 519 | colors[blockId] = MTL{blockId, data, color, name} 520 | } 521 | } else { 522 | extraData[blockId] = true 523 | for _, data = range dataArray { 524 | colors = append(colors, MTL{blockId, data, color, fmt.Sprintf("%s_%d", name, data)}) 525 | } 526 | } 527 | } 528 | } 529 | } 530 | 531 | return nil 532 | } 533 | -------------------------------------------------------------------------------- /blocks.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"blockId": 0, "name": "Air", "color": "#fefeff01", "empty": true }, 3 | {"blockId": 1, "name": "Stone", "color": "#7d7d7d" }, 4 | {"blockId": 2, "name": "Grass", "color": "#52732c" }, 5 | {"blockId": 3, "name": "Dirt", "color": "#866043" }, 6 | {"blockId": 4, "name": "Cobblestone", "color": "#757575" }, 7 | {"blockId": 5, "data": 0, "name": "WoodenPlank0", "color": "#9d804f" }, 8 | {"blockId": 5, "data": 1, "name": "WoodenPlank1", "color": "#4e3a23" }, 9 | {"blockId": 5, "data": 2, "name": "WoodenPlank2", "color": "#998b60" }, 10 | {"blockId": 5, "data": 3, "name": "WoodenPlank3", "color": "#7b583d" }, 11 | {"blockId": 6, "data": [0,4,8,12], "name": "Sapling.Default", "color": "#5d7e1e", "item": true }, 12 | {"blockId": 6, "data": [1,5,9,13], "name": "Sapling.Spruce", "color": "#779656", "item": true }, 13 | {"blockId": 6, "data": [2,6,10,14], "name": "Sapling.Birch", "color": "#30341e", "item": true }, 14 | {"blockId": 6, "data": [3,7,11,15], "name": "Sapling.Jungle", "color": "#30341e", "item": true }, 15 | {"blockId": 7, "name": "Bedrock", "color": "#545454" }, 16 | {"blockId": 8, "name": "Water", "color": "#009aff50", "transparent": true}, 17 | {"blockId": 9, "name": "WaterStationary", "color": "#009aff50", "transparent": true}, 18 | {"blockId": 10, "name": "Lava", "color": "#f54200", "transparent": true}, 19 | {"blockId": 11, "name": "LavaStationary", "color": "#f54200", "transparent": true}, 20 | {"blockId": 12, "name": "Sand", "color": "#dad29e" }, 21 | {"blockId": 13, "name": "Gravel", "color": "#887f7e" }, 22 | {"blockId": 14, "name": "GoldOre", "color": "#908c7d" }, 23 | {"blockId": 15, "name": "IronOre", "color": "#88837f" }, 24 | {"blockId": 16, "name": "CoalOre", "color": "#737373" }, 25 | {"blockId": 17, "data": 0, "name": "Wood.Oak", "color": "#635234" }, 26 | {"blockId": 17, "data": 1, "name": "Wood.Spruce", "color": "#2e1d0c" }, 27 | {"blockId": 17, "data": 2, "name": "Wood.Birch", "color": "#cfcec9" }, 28 | {"blockId": 17, "data": 3, "name": "Wood.Jungle", "color": "#55451f" }, 29 | {"blockId": 18, "data": [0,8], "name": "Leaves.Default", "color": "#1c4705", "transparent": true}, 30 | {"blockId": 18, "data": [1,9], "name": "Leaves.Spruce", "color": "#2a432a", "transparent": true}, 31 | {"blockId": 18, "data": [2,10], "name": "Leaves.Birch", "color": "#41542c", "transparent": true}, 32 | {"blockId": 18, "data": [3,11], "name": "Leaves.Jungle", "color": "#41542c", "transparent": true}, 33 | {"blockId": 19, "name": "Sponge", "color": "#b7b739", "item": true }, 34 | {"blockId": 20, "name": "Glass", "color": "#ffffff33", "transparent": true}, 35 | {"blockId": 21, "name": "LapisLazuliOre", "color": "#667087" }, 36 | {"blockId": 22, "name": "LapisLazuliBlock", "color": "#1d47a6" }, 37 | {"blockId": 23, "name": "Dispenser", "color": "#6c6c6c" }, 38 | {"blockId": 24, "data": 0, "name": "Sandstone", "color": "#d5cd94" }, 39 | {"blockId": 24, "data": 1, "name": "Sandstone.Glyph", "color": "#d5cd94" }, 40 | {"blockId": 24, "data": 2, "name": "Sandstone.Smooth", "color": "#d5cd94" }, 41 | {"blockId": 25, "name": "NoteBlock", "color": "#654433" }, 42 | {"blockId": 26, "data": [0, 1, 2, 3], "name": "Bed.Foot", "color": "#8f1717", "item": true }, 43 | {"blockId": 26, "data": [8, 9, 10, 11], "name": "Bed.Head", "color": "#af7475", "item": true }, 44 | {"blockId": 27, "data": 0, "name": "PoweredRail.Off", "color": "#87714e", "item": true }, 45 | {"blockId": 27, "data": 1, "name": "PoweredRail.On", "color": "#956746", "item": true }, 46 | {"blockId": 28, "name": "DetectorRail", "color": "#766251", "item": true }, 47 | {"blockId": 29, "name": "StickyPiston", "color": "#6b665f", "item": true }, 48 | {"blockId": 30, "name": "Web", "color": "#dadada99", "item": true }, 49 | {"blockId": 31, "data": 0, "name": "Dead Shrub", "color": "#7c4f19", "item": true }, 50 | {"blockId": 31, "data": 1, "name": "Tall Grass", "color": "#52732c", "item": true }, 51 | {"blockId": 31, "data": 2, "name": "Live Shrub", "color": "#497328", "item": true }, 52 | {"blockId": 32, "name": "Dead Shrub", "color": "#7c4f19", "item": true }, 53 | {"blockId": 33, "name": "Piston", "color": "#6b665f", "item": true }, 54 | {"blockId": 34, "name": "Piston.Ext", "color": "#9a825a", "item": true }, 55 | {"blockId": 35, "data": 0, "name": "Wool.White", "color": "#dedede" }, 56 | {"blockId": 35, "data": 1, "name": "Wool.Orange", "color": "#ea8037" }, 57 | {"blockId": 35, "data": 2, "name": "Wool.Magenta", "color": "#bf4cc9" }, 58 | {"blockId": 35, "data": 3, "name": "Wool.Light Blue", "color": "#688bd4" }, 59 | {"blockId": 35, "data": 4, "name": "Wool.Yellow", "color": "#c2b51c" }, 60 | {"blockId": 35, "data": 5, "name": "Wool.Light Green", "color": "#3bbd30" }, 61 | {"blockId": 35, "data": 6, "name": "Wool.Pink", "color": "#d9849b" }, 62 | {"blockId": 35, "data": 7, "name": "Wool.Gray", "color": "#434343" }, 63 | {"blockId": 35, "data": 8, "name": "Wool.Light Gray", "color": "#9ea6a6" }, 64 | {"blockId": 35, "data": 9, "name": "Wool.Cyan", "color": "#277596" }, 65 | {"blockId": 35, "data": 10, "name": "Wool.Purple", "color": "#8136c4" }, 66 | {"blockId": 35, "data": 11, "name": "Wool.Blue", "color": "#27339a" }, 67 | {"blockId": 35, "data": 12, "name": "Wool.Brown", "color": "#56331c" }, 68 | {"blockId": 35, "data": 13, "name": "Wool.Dark Green", "color": "#384d18" }, 69 | {"blockId": 35, "data": 14, "name": "Wool.Red", "color": "#a42d29" }, 70 | {"blockId": 35, "data": 15, "name": "Wool.Black", "color": "#1b1717" }, 71 | {"blockId": 37, "name": "FlowerYellow", "color": "#c1c702", "item": true }, 72 | {"blockId": 38, "name": "FlowerRed", "color": "#cb060a", "item": true }, 73 | {"blockId": 39, "name": "MushroomBrown", "color": "#967158", "item": true }, 74 | {"blockId": 40, "name": "MushroomRed", "color": "#c53c3f", "item": true }, 75 | {"blockId": 41, "name": "GoldBlock", "color": "#faec4e" }, 76 | {"blockId": 42, "name": "IronBlock", "color": "#e6e6e6" }, 77 | {"blockId": 43, "data": 0, "name": "DoubleSlab.Stone", "color": "#a7a7a7" }, 78 | {"blockId": 43, "data": 1, "name": "DoubleSlab.SandStone","color": "#d5cd94" }, 79 | {"blockId": 43, "data": 2, "name": "DoubleSlab.Wooden", "color": "#9d804f" }, 80 | {"blockId": 43, "data": 3, "name": "DoubleSlab.Cobblestone","color": "#757575" }, 81 | {"blockId": 43, "data": 4, "name": "DoubleSlab.Brick", "color": "#9c6e62" }, 82 | {"blockId": 43, "data": 5, "name": "DoubleSlab.StoneBrick","color": "#777777" }, 83 | {"blockId": 44, "data": 0, "name": "Slab.Stone", "color": "#a7a7a7", "item": true }, 84 | {"blockId": 44, "data": 1, "name": "Slab.SandStone", "color": "#d5cd94", "item": true }, 85 | {"blockId": 44, "data": 2, "name": "Slab.Wooden", "color": "#9d804f", "item": true }, 86 | {"blockId": 44, "data": 3, "name": "Slab.Cobblestone", "color": "#757575", "item": true }, 87 | {"blockId": 44, "data": 4, "name": "Slab.Brick", "color": "#9c6e62", "item": true }, 88 | {"blockId": 44, "data": 5, "name": "Slab.StoneBrick", "color": "#777777", "item": true }, 89 | {"blockId": 45, "name": "Brick", "color": "#926457" }, 90 | {"blockId": 46, "name": "TNT", "color": "#a6553f" }, 91 | {"blockId": 47, "name": "Bookshelf", "color": "#6c583a" }, 92 | {"blockId": 48, "name": "StoneMoss", "color": "#5b6c5b" }, 93 | {"blockId": 49, "name": "Obsidian", "color": "#14121e" }, 94 | {"blockId": 50, "name": "Torch", "color": "#ffda6699", "item": true }, 95 | {"blockId": 51, "name": "Fire", "color": "#ff770099", "item": true }, 96 | {"blockId": 52, "name": "MonsterSpawner", "color": "#1d4f72", "item": true }, 97 | {"blockId": 53, "name": "StairsWooden", "color": "#9d804f", "item": true }, 98 | {"blockId": 54, "name": "Chest", "color": "#835e25" }, 99 | {"blockId": 55, "name": "RedstoneWire", "color": "#cb0000", "item": true }, 100 | {"blockId": 56, "name": "DiamondOre", "color": "#828c8f" }, 101 | {"blockId": 57, "name": "DiamondBlock", "color": "#64dcd6" }, 102 | {"blockId": 58, "name": "Workbench", "color": "#6b472b" }, 103 | {"blockId": 59, "name": "Crops", "color": "#83c144", "item": true }, 104 | {"blockId": 60, "name": "Soil", "color": "#4b290e" }, 105 | {"blockId": 61, "name": "Furnace", "color": "#4e4e4e" }, 106 | {"blockId": 62, "name": "FurnaceBurning", "color": "#7d6655" }, 107 | {"blockId": 63, "name": "SignPost", "color": "#9d804f", "item": true }, 108 | {"blockId": 64, "name": "DoorWooden", "color": "#9d804f", "item": true }, 109 | {"blockId": 65, "name": "Ladder", "color": "#9d804f", "item": true }, 110 | {"blockId": 66, "name": "MinecartTracks", "color": "#75664c", "item": true }, 111 | {"blockId": 67, "name": "StairsCobblestone", "color": "#757575", "item": true }, 112 | {"blockId": 68, "name": "SignWall", "color": "#9d804f", "item": true }, 113 | {"blockId": 69, "name": "Lever", "color": "#9d804f", "item": true }, 114 | {"blockId": 70, "name": "PressurePlateStone", "color": "#7d7d7d", "item": true }, 115 | {"blockId": 71, "name": "DoorIron", "color": "#b2b2b2", "item": true }, 116 | {"blockId": 72, "name": "PressurePlateWooden", "color": "#9d804f", "item": true }, 117 | {"blockId": 73, "name": "RedstoneOre", "color": "#856b6b" }, 118 | {"blockId": 74, "name": "RedstoneOreGlowing", "color": "#bd6b6b" }, 119 | {"blockId": 75, "name": "RedstoneTorch.Off", "color": "#44000099", "item": true }, 120 | {"blockId": 76, "name": "RedstoneTorch.On", "color": "#fe000099", "item": true }, 121 | {"blockId": 77, "name": "ButtonStone", "color": "#7d7d7d", "item": true }, 122 | {"blockId": 78, "name": "Snow", "color": "#f0fbfb", "item": true }, 123 | {"blockId": 79, "name": "Ice", "color": "#7daeff77", "transparent": true}, 124 | {"blockId": 80, "name": "SnowBlock", "color": "#f0fbfb" }, 125 | {"blockId": 81, "name": "Cactus", "color": "#0d6418", "item": true }, 126 | {"blockId": 82, "name": "Clay", "color": "#9fa5b1" }, 127 | {"blockId": 83, "name": "SugarCane", "color": "#83c447", "item": true }, 128 | {"blockId": 84, "name": "Jukebox", "color": "#6b4937" }, 129 | {"blockId": 85, "name": "Fence", "color": "#9d804f", "item": true }, 130 | {"blockId": 86, "name": "Pumpkin", "color": "#c57918" }, 131 | {"blockId": 87, "name": "Netherrack", "color": "#6e3533" }, 132 | {"blockId": 88, "name": "SoulSand", "color": "#554134" }, 133 | {"blockId": 89, "name": "Glowstone", "color": "#897141" }, 134 | {"blockId": 90, "name": "Portal", "color": "#381d55bb", "transparent": true}, 135 | {"blockId": 91, "name": "JackOLantern", "color": "#b9861d" }, 136 | {"blockId": 92, "name": "CakeBlock", "color": "#e5cecf", "item": true }, 137 | {"blockId": 93, "name": "RedstoneRepeater.Off","color": "#989494", "item": true }, 138 | {"blockId": 94, "name": "RedstoneRepeater.On", "color": "#a19494", "item": true }, 139 | {"blockId": 95, "name": "LockedChest", "color": "#835e25" }, 140 | {"blockId": 96, "name": "Trapdoor", "color": "#81602f", "item": true }, 141 | {"blockId": 97, "name": "HiddenSilverfish", "color": "#7d7d7d" }, 142 | {"blockId": 98, "data": 0, "name": "StoneBrick", "color": "#777777" }, 143 | {"blockId": 98, "data": 1, "name": "StoneBrick.Mossy", "color": "#73776a" }, 144 | {"blockId": 98, "data": 2, "name": "StoneBrick.Cracked", "color": "#767676" }, 145 | {"blockId": 98, "data": 3, "name": "StoneBrick.Circle", "color": "#767676" }, 146 | {"blockId": 99, "data":[0,1,2,3,4,5,6,7,8,9],"name": "HugeBrownMushroom.Cap", "color": "#b72624" }, 147 | {"blockId": 99, "data": 10, "name": "HugeBrownMushroom.Stem", "color": "#d0ccc2" }, 148 | {"blockId":100, "data":[0,1,2,3,4,5,6,7,8,9],"name": "HugeRedMushroom.Cap", "color": "#8e6b53" }, 149 | {"blockId":100, "data": 10, "name": "HugeRedMushroom.Stem", "color": "#cbab79" }, 150 | {"blockId":101, "name": "IronBars", "color": "#696866", "transparent": true}, 151 | {"blockId":102, "name": "GlassPane", "color": "#ffffff33", "transparent": true}, 152 | {"blockId":103, "name": "Melon", "color": "#8d9224", "item": true }, 153 | {"blockId":104, "data": [0,1,2,3,4,5,6],"name": "PumpkinStem", "color": "#7eb952", "item": true }, 154 | {"blockId":104, "data": 7, "name": "PumpkinStem.Ripe", "color": "#b18c4b", "item": true }, 155 | {"blockId":105, "data": [0,1,2,3,4,5,6],"name": "MelonStem", "color": "#7eb952", "item": true }, 156 | {"blockId":105, "data": 7, "name": "MelonStem.Ripe", "color": "#b18c4b", "item": true }, 157 | {"blockId":106, "name": "Vines", "color": "#1f4f0a", "transparent": true}, 158 | {"blockId":107, "name": "Fence.Gate", "color": "#9d804f", "item": true }, 159 | {"blockId":108, "name": "StairsBrick", "color": "#9c6e62", "item": true }, 160 | {"blockId":109, "name": "StairsStoneBrick", "color": "#777777", "item": true }, 161 | {"blockId":110, "name": "Mycelium", "color": "#4e4240" }, 162 | {"blockId":111, "name": "LilyPad", "color": "#0f5f17", "item": true }, 163 | {"blockId":112, "name": "NetherBrick", "color": "#2a1519" }, 164 | {"blockId":113, "name": "NetherBrickFence", "color": "#2a1519", "item": true }, 165 | {"blockId":114, "name": "NetherBrickStairs", "color": "#2a1519", "item": true }, 166 | {"blockId":115, "name": "NetherWart", "color": "#67110e", "item": true }, 167 | {"blockId":116, "name": "Enchantment Table", "color": "#2a2c2e", "item": true }, 168 | {"blockId":117, "name": "BrewingStand", "color": "#7a6755", "item": true }, 169 | {"blockId":118, "name": "Cauldron", "color": "#3d3d3d", "item": true }, 170 | {"blockId":119, "name": "EndPortal", "color": "#0c0b0d", "item": true }, 171 | {"blockId":120, "name": "EndPortalFrame", "color": "#94a07b", "item": true }, 172 | {"blockId":121, "name": "EndStone", "color": "#dde0a5" }, 173 | {"blockId":122, "name": "DragonEgg", "color": "#0d0a10", "item": true }, 174 | {"blockId":123, "name": "RedstoneLampOff", "color": "#462c1b" }, 175 | {"blockId":124, "name": "RedstoneLampOn", "color": "#775937" } 176 | ] 177 | --------------------------------------------------------------------------------