1 package minecraft 2 3 import ( 4 "compress/gzip" 5 "compress/zlib" 6 "io" 7 "os" 8 "path" 9 "regexp" 10 "sort" 11 "strconv" 12 "time" 13 14 "vimagination.zapto.org/byteio" 15 "vimagination.zapto.org/memio" 16 "vimagination.zapto.org/minecraft/nbt" 17 ) 18 19 var filename *regexp.Regexp 20 21 // The Path interface allows the minecraft level to be created from/saved 22 // to different formats. 23 type Path interface { 24 // Returns an empty nbt.Tag (TagEnd) when chunk does not exists 25 GetChunk(int32, int32) (nbt.Tag, error) 26 SetChunk(...nbt.Tag) error 27 RemoveChunk(int32, int32) error 28 ReadLevelDat() (nbt.Tag, error) 29 WriteLevelDat(nbt.Tag) error 30 } 31 32 // Compression convenience constants 33 const ( 34 GZip byte = 1 35 Zlib byte = 2 36 ) 37 38 // FilePath implements the Path interface and provides a standard minecraft 39 // save format. 40 type FilePath struct { 41 dirname string 42 lock int64 43 dimension string 44 } 45 46 // NewFilePath constructs a new directory based path to read from. 47 func NewFilePath(dirname string) (*FilePath, error) { 48 dirname = path.Clean(dirname) 49 if err := os.MkdirAll(dirname, 0o755); err != nil { 50 return nil, err 51 } 52 p := &FilePath{dirname: dirname} 53 return p, p.Lock() 54 } 55 56 type stickyEndianSeeker struct { 57 byteio.StickyBigEndianWriter 58 io.Seeker 59 } 60 61 func (s *stickyEndianSeeker) Seek(offset int64, whence int) (int64, error) { 62 if s.StickyBigEndianWriter.Err != nil { 63 return 0, s.StickyBigEndianWriter.Err 64 } 65 var n int64 66 n, s.StickyBigEndianWriter.Err = s.Seeker.Seek(offset, whence) 67 return n, s.StickyBigEndianWriter.Err 68 } 69 70 // NewFilePathDimension create a new FilePath, but with the option to set the 71 // dimension that chunks are loaded from. 72 // 73 // Example. Dimension -1 == The Nether 74 // 75 // Dimension 1 == The End 76 func NewFilePathDimension(dirname string, dimension int) (*FilePath, error) { 77 fp, err := NewFilePath(dirname) 78 if dimension != 0 { 79 fp.dimension = "DIM" + strconv.Itoa(dimension) 80 } 81 return fp, err 82 } 83 84 func (p *FilePath) getRegionPath(x, z int32) string { 85 return path.Join(p.dirname, p.dimension, "region", "r."+strconv.FormatInt(int64(x>>5), 10)+"."+strconv.FormatInt(int64(z>>5), 10)+".mca") 86 } 87 88 // GetChunk returns the chunk at chunk coords x, z. 89 func (p *FilePath) GetChunk(x, z int32) (nbt.Tag, error) { 90 if !p.HasLock() { 91 return nbt.Tag{}, ErrNoLock 92 } 93 f, err := os.Open(p.getRegionPath(x>>5, z>>5)) 94 if err != nil { 95 if os.IsNotExist(err) { 96 err = nil 97 } 98 return nbt.Tag{}, err 99 } 100 defer f.Close() 101 pos := int64((z&31)<<5 | (x & 31)) 102 if _, err = f.Seek(4*pos, io.SeekStart); err != nil { 103 return nbt.Tag{}, err 104 } 105 106 be := byteio.BigEndianReader{Reader: f} 107 108 locationSize, _, err := be.ReadUint32() 109 if err != nil { 110 return nbt.Tag{}, err 111 } else if locationSize>>8 == 0 { 112 return nbt.Tag{}, nil 113 } else if _, err = f.Seek(int64(locationSize>>8<<12), io.SeekStart); err != nil { 114 return nbt.Tag{}, err 115 } 116 117 reader := io.LimitReader(f, int64(locationSize&255<<12)) 118 119 length, _, err := be.ReadUint32() 120 if err != nil { 121 return nbt.Tag{}, err 122 } 123 124 reader = io.LimitReader(reader, int64(length)) 125 compression, _, err := be.ReadUint8() 126 if err != nil { 127 return nbt.Tag{}, err 128 } 129 130 switch compression { 131 case GZip: 132 gReader, err := gzip.NewReader(reader) 133 if err != nil { 134 return nbt.Tag{}, err 135 } 136 defer gReader.Close() 137 reader = gReader 138 case Zlib: 139 if reader, err = zlib.NewReader(reader); err != nil { 140 return nbt.Tag{}, err 141 } 142 default: 143 return nbt.Tag{}, UnknownCompression{compression} 144 } 145 146 return nbt.Decode(reader) 147 } 148 149 type rc struct { 150 pos int32 151 buf memio.Buffer 152 } 153 154 // SetChunk saves multiple chunks at once, possibly returning a MultiError if 155 // multiple errors were encountered. 156 func (p *FilePath) SetChunk(data ...nbt.Tag) error { 157 if !p.HasLock() { 158 return ErrNoLock 159 } 160 regions := make(map[uint64][]rc) 161 var ( 162 poses []uint64 163 errors []error 164 ) 165 for _, d := range data { 166 x, z, err := chunkCoords(d) 167 if err != nil { 168 errors = append(errors, FilePathSetError{x, z, err}) 169 continue 170 } 171 pos := uint64(z)<<32 | uint64(uint32(x)) 172 for _, p := range poses { 173 if p == pos { 174 errors = append(errors, ConflictError{x, z}) 175 continue 176 } 177 } 178 poses = append(poses, pos) 179 r := uint64(z>>5)<<32 | uint64(uint32(x>>5)) 180 reg := rc{pos: (z&31)<<5 | (x & 31)} 181 zl := zlib.NewWriter(®.buf) 182 err = nbt.Encode(zl, d) 183 zl.Close() 184 if err != nil { 185 errors = append(errors, FilePathSetError{x, z, err}) 186 continue 187 } 188 if regions[r] == nil { 189 regions[r] = []rc{reg} 190 } else { 191 regions[r] = append(regions[r], reg) 192 } 193 } 194 for rID, chunks := range regions { 195 x, z := int32(rID&0xffffffff), int32(rID>>32) 196 if err := p.setChunks(x, z, chunks); err != nil { 197 errors = append(errors, &FilePathSetError{x, z, err}) 198 } 199 } 200 if len(errors) > 0 { 201 return MultiError{errors} 202 } 203 return nil 204 } 205 206 type sia []uint32 207 208 func (s sia) Len() int { 209 return 1024 210 } 211 212 func (s sia) Less(i, j int) bool { 213 return s[i] < s[j] 214 } 215 216 func (s sia) Swap(i, j int) { 217 s[i], s[j] = s[j], s[i] 218 } 219 220 func (p *FilePath) setChunks(x, z int32, chunks []rc) error { 221 if err := os.MkdirAll(path.Join(p.dirname, p.dimension, "region"), 0o755); err != nil { 222 return err 223 } 224 f, err := os.OpenFile(p.getRegionPath(x, z), os.O_RDWR|os.O_CREATE, 0o666) 225 if err != nil { 226 return err 227 } 228 defer f.Close() 229 var ( 230 bytes [4096]byte 231 positions [1024]uint32 232 ) 233 pBytes := memio.Buffer(bytes[:]) 234 if _, err = io.ReadFull(f, pBytes); err != nil && err != io.EOF { 235 return err 236 } 237 posr := byteio.BigEndianReader{Reader: &pBytes} 238 for i := 0; i < 1024; i++ { 239 positions[i], _, _ = posr.ReadUint32() 240 } 241 var todoChunks []rc 242 bew := stickyEndianSeeker{byteio.StickyBigEndianWriter{Writer: f}, f} 243 for _, chunk := range chunks { 244 newSize := uint32(len(chunk.buf)+5) >> 12 245 if uint32(len(chunk.buf))&4095 > 0 { 246 newSize++ 247 } 248 if positions[chunk.pos]&255 == newSize { 249 bew.Seek(4*int64(chunk.pos)+4096, io.SeekStart) // Write the time, then the data 250 bew.WriteUint32(uint32(time.Now().Unix())) 251 bew.Seek(int64(positions[chunk.pos])>>8<<12, io.SeekStart) 252 bew.WriteUint32(uint32(len(chunk.buf)) + 1) 253 bew.WriteUint8(Zlib) 254 bew.Write(chunk.buf) 255 } else { 256 todoChunks = append(todoChunks, chunk) 257 positions[chunk.pos] = 0 258 } 259 } 260 if bew.Err != nil { 261 return bew.Err 262 } 263 for _, chunk := range todoChunks { 264 sort.Sort(sia(positions[:])) 265 newPosition := (positions[1023] >> 8) + (positions[1023] & 255) 266 if newPosition == 0 { 267 newPosition = 2 268 } 269 lastPos := uint32(2) 270 smallest := uint32(0xffffffff) 271 writeLastByte := true 272 newSize := uint32(len(chunk.buf) + 5) 273 if newSize&4095 > 0 { 274 newSize >>= 12 275 newSize++ 276 } else { 277 newSize >>= 12 278 } 279 // Find earliest, closest match in size for least fragmentation. 280 for i := 0; i < 1024; i++ { 281 loc := positions[i] >> 8 282 if loc > 0 { 283 size := positions[i] & 255 284 if space := loc - lastPos; space >= newSize && space < smallest { 285 smallest = space 286 newPosition = lastPos 287 writeLastByte = false // by definition it has data that is after it now, so no need to make up to mod 4096 bytes 288 } 289 lastPos = loc + size 290 } 291 } 292 positions[0] = newPosition<<8 | newSize&255 293 // Write the new position 294 bew.Seek(4*int64(chunk.pos), io.SeekStart) 295 bew.WriteUint32(positions[0]) 296 bew.Seek(4*(int64(chunk.pos)+1024), io.SeekStart) // Write the time, then the data 297 bew.WriteUint32(uint32(time.Now().Unix())) 298 bew.Seek(int64(newPosition)<<12, io.SeekStart) 299 bew.WriteUint32(uint32(len(chunk.buf)) + 1) 300 bew.WriteUint8(Zlib) 301 bew.Write(chunk.buf) 302 if writeLastByte { // Make filesize mod 4096 (for minecraft compatibility) 303 bew.Seek(int64(newPosition+newSize)<<12-1, io.SeekStart) 304 bew.WriteUint8(0) 305 } 306 } 307 return bew.Err 308 } 309 310 // RemoveChunk deletes the chunk at chunk coords x, z. 311 func (p *FilePath) RemoveChunk(x, z int32) error { 312 if !p.HasLock() { 313 return ErrNoLock 314 } 315 chunkX := x & 31 316 regionX := x >> 5 317 chunkZ := z & 31 318 regionZ := z >> 5 319 f, err := os.OpenFile(p.getRegionPath(regionX, regionZ), os.O_WRONLY, 0o666) 320 if os.IsNotExist(err) { 321 return nil 322 } else if err != nil { 323 return err 324 } 325 defer f.Close() 326 if _, err = f.Seek(int64(chunkZ<<5|chunkX)*4, io.SeekStart); err != nil { 327 return err 328 } 329 _, err = f.Write([]byte{0, 0, 0, 0}) 330 return err 331 } 332 333 // ReadLevelDat returns the level data. 334 func (p *FilePath) ReadLevelDat() (nbt.Tag, error) { 335 if !p.HasLock() { 336 return nbt.Tag{}, ErrNoLock 337 } 338 f, err := os.Open(path.Join(p.dirname, "level.dat")) 339 if os.IsNotExist(err) { 340 return nbt.Tag{}, nil 341 } else if err != nil { 342 return nbt.Tag{}, err 343 } 344 defer f.Close() 345 g, err := gzip.NewReader(f) 346 if err != nil { 347 return nbt.Tag{}, err 348 } 349 data, err := nbt.Decode(g) 350 return data, err 351 } 352 353 // WriteLevelDat Writes the level data. 354 func (p *FilePath) WriteLevelDat(data nbt.Tag) error { 355 if !p.HasLock() { 356 return ErrNoLock 357 } 358 f, err := os.OpenFile(path.Join(p.dirname, "level.dat"), os.O_WRONLY|os.O_CREATE, 0o666) 359 if err != nil { 360 return nil 361 } 362 defer f.Close() 363 g := gzip.NewWriter(f) 364 defer g.Close() 365 err = nbt.Encode(g, data) 366 return err 367 } 368 369 // GetRegions returns a list of region x,z coords of all generated regions. 370 func (p *FilePath) GetRegions() [][2]int32 { 371 files, _ := os.ReadDir(path.Join(p.dirname, p.dimension, "region")) 372 var toRet [][2]int32 373 for _, file := range files { 374 if !file.IsDir() { 375 if nums := filename.FindStringSubmatch(file.Name()); nums != nil { 376 x, _ := strconv.ParseInt(nums[1], 10, 32) 377 z, _ := strconv.ParseInt(nums[2], 10, 32) 378 toRet = append(toRet, [2]int32{int32(x), int32(z)}) 379 } 380 } 381 } 382 return toRet 383 } 384 385 // GetChunks returns a list of all chunks within a region with coords x,z 386 func (p *FilePath) GetChunks(x, z int32) ([][2]int32, error) { 387 if !p.HasLock() { 388 return nil, ErrNoLock 389 } 390 f, err := os.Open(p.getRegionPath(x, z)) 391 if err != nil { 392 return nil, err 393 } 394 defer f.Close() 395 396 pBytes := make(memio.Buffer, 4096) 397 if _, err = io.ReadFull(f, pBytes); err != nil { 398 return nil, err 399 } 400 401 baseX := x << 5 402 baseZ := z << 5 403 404 var toRet [][2]int32 405 posr := byteio.BigEndianReader{Reader: &pBytes} 406 for i := 0; i < 1024; i++ { 407 if n, _, _ := posr.ReadUint32(); n > 0 { 408 toRet = append(toRet, [2]int32{baseX + int32(i&31), baseZ + int32(i>>5)}) 409 } 410 } 411 return toRet, nil 412 } 413 414 // HasLock returns whether or not another program has taken the lock. 415 func (p *FilePath) HasLock() bool { 416 r, err := os.Open(path.Join(p.dirname, "session.lock")) 417 if err != nil { 418 return false 419 } 420 defer r.Close() 421 buf := make(memio.Buffer, 9) 422 n, err := io.ReadFull(r, buf) 423 if n != 8 || err != io.ErrUnexpectedEOF { 424 return false 425 } 426 bew := byteio.BigEndianReader{Reader: &buf} 427 b, _, _ := bew.ReadInt64() 428 return b == p.lock 429 } 430 431 // Lock will retake the lock file if it has been lost. May cause corruption. 432 func (p *FilePath) Lock() error { 433 if p.HasLock() { 434 return nil 435 } 436 p.lock = time.Now().UnixNano() / 1000000 // ms 437 session := path.Join(p.dirname, "session.lock") 438 f, err := os.Create(session) 439 if err != nil { 440 return err 441 } 442 bew := byteio.BigEndianWriter{Writer: f} 443 _, err = bew.WriteUint64(uint64(p.lock)) 444 f.Close() 445 return err 446 } 447 448 // Defrag rewrites a region file to reduce wasted space. 449 func (p *FilePath) Defrag(x, z int32) error { 450 if !p.HasLock() { 451 return ErrNoLock 452 } 453 f, err := os.OpenFile(p.getRegionPath(x, z), os.O_RDWR|os.O_CREATE, 0o666) 454 if err != nil { 455 return err 456 } 457 defer f.Close() 458 459 locationSizes := make(memio.Buffer, 4096) 460 if _, err = io.ReadFull(f, locationSizes); err != nil { 461 return err 462 } 463 464 var ( 465 data [1024][]byte 466 locations [4096]byte 467 currSector uint32 = 2 468 ) 469 locationr := byteio.BigEndianReader{Reader: &locationSizes} 470 l := memio.Buffer(locations[:0]) 471 locationw := byteio.BigEndianWriter{Writer: &l} 472 for i := 0; i < 1024; i++ { 473 locationSize, _, _ := locationr.ReadUint32() 474 if locationSize>>8 == 0 { 475 continue 476 } else if _, err = f.Seek(int64(locationSize>>8<<12), io.SeekStart); err != nil { 477 return err 478 } 479 480 data[i] = make([]byte, locationSize&255<<12) 481 482 if _, err := io.ReadFull(f, data[i]); err != nil { 483 return err 484 } 485 486 locationw.WriteUint32(currSector<<8 | locationSize&255) 487 488 currSector += locationSize & 255 489 } 490 491 _, err = f.Seek(0, io.SeekStart) 492 if err != nil { 493 return err 494 } 495 496 _, err = f.Write(locations[:]) 497 if err != nil { 498 return err 499 } 500 501 _, err = f.Seek(8192, io.SeekStart) 502 if err != nil { 503 return err // Try and recover first? 504 } 505 506 for _, d := range data { 507 if len(d) > 0 { 508 _, err = f.Write(d) 509 if err != nil { 510 return err 511 } 512 } 513 } 514 return nil 515 } 516 517 // MemPath is an in memory minecraft level format that implements the Path interface. 518 type MemPath struct { 519 level memio.Buffer 520 chunks map[uint64]memio.Buffer 521 } 522 523 // NewMemPath creates a new MemPath implementation. 524 func NewMemPath() *MemPath { 525 return &MemPath{chunks: make(map[uint64]memio.Buffer)} 526 } 527 528 // GetChunk returns the chunk at chunk coords x, z. 529 func (m *MemPath) GetChunk(x, z int32) (nbt.Tag, error) { 530 pos := uint64(z)<<32 | uint64(uint32(x)) 531 c := m.chunks[pos] 532 if c == nil { 533 return nbt.Tag{}, nil 534 } 535 return m.read(c) 536 } 537 538 // SetChunk saves multiple chunks at once. 539 func (m *MemPath) SetChunk(data ...nbt.Tag) error { 540 for _, d := range data { 541 x, z, err := chunkCoords(d) 542 if err != nil { 543 return err 544 } 545 var buf memio.Buffer 546 if err = m.write(d, &buf); err != nil { 547 return err 548 } 549 pos := uint64(z)<<32 | uint64(uint32(x)) 550 m.chunks[pos] = buf 551 } 552 return nil 553 } 554 555 // RemoveChunk deletes the chunk at chunk coords x, z. 556 func (m *MemPath) RemoveChunk(x, z int32) error { 557 pos := uint64(z)<<32 | uint64(uint32(x)) 558 delete(m.chunks, pos) 559 return nil 560 } 561 562 // ReadLevelDat Returns the level data. 563 func (m *MemPath) ReadLevelDat() (nbt.Tag, error) { 564 if len(m.level) == 0 { 565 return nbt.Tag{}, nil 566 } 567 return m.read(m.level) 568 } 569 570 // WriteLevelDat Writes the level data. 571 func (m *MemPath) WriteLevelDat(data nbt.Tag) error { 572 return m.write(data, &m.level) 573 } 574 575 func (m *MemPath) read(buf memio.Buffer) (nbt.Tag, error) { 576 z, err := zlib.NewReader(&buf) 577 if err != nil { 578 return nbt.Tag{}, err 579 } 580 data, err := nbt.Decode(z) 581 return data, err 582 } 583 584 func (m *MemPath) write(data nbt.Tag, buf io.Writer) error { 585 z := zlib.NewWriter(buf) 586 defer z.Close() 587 err := nbt.Encode(z, data) 588 return err 589 } 590 591 func chunkCoords(data nbt.Tag) (int32, int32, error) { 592 if data.TagID() != nbt.TagCompound { 593 return 0, 0, WrongTypeError{"[Chunk Base]", nbt.TagCompound, data.TagID()} 594 } 595 lTag := data.Data().(nbt.Compound).Get("Level") 596 if lTag.TagID() == 0 { 597 return 0, 0, MissingTagError{"[Chunk Base]->Level"} 598 } else if lTag.TagID() != nbt.TagCompound { 599 return 0, 0, WrongTypeError{"[Chunk Base]->Level", nbt.TagCompound, lTag.TagID()} 600 } 601 602 lCmp := lTag.Data().(nbt.Compound) 603 604 xPos := lCmp.Get("xPos") 605 if xPos.TagID() == 0 { 606 return 0, 0, MissingTagError{"[Chunk Base]->Level->xPos"} 607 } else if xPos.TagID() != nbt.TagInt { 608 return 0, 0, WrongTypeError{"[Chunk Base]->Level->xPos", nbt.TagInt, xPos.TagID()} 609 } 610 611 x := int32(xPos.Data().(nbt.Int)) 612 613 zPos := lCmp.Get("zPos") 614 if zPos.TagID() == 0 { 615 return 0, 0, MissingTagError{"[Chunk Base]->Level->zPos"} 616 } else if zPos.TagID() != nbt.TagInt { 617 return 0, 0, WrongTypeError{"[Chunk Base]->Level->zPos", nbt.TagInt, zPos.TagID()} 618 } 619 620 z := int32(zPos.Data().(nbt.Int)) 621 622 return x, z, nil 623 } 624 625 func init() { 626 filename = regexp.MustCompile(`^r.(-?[0-9]+).(-?[0-9]+).mca$`) 627 } 628