|
- using System;
- using System.Collections.Generic;
- using System.IO;
- using System.IO.Compression;
- using System.Linq;
- using Console = System.Console;
- /*
- * 2011 January 5
- *
- * The author disclaims copyright to this source code. In place of
- * a legal notice, here is a blessing:
- *
- * May you do good and not evil.
- * May you find forgiveness for yourself and forgive others.
- * May you share freely, never taking more than you give.
- */
-
- /*
- * 2011 February 16
- *
- * This source code is based on the work of Scaevolus (see notice above).
- * It has been slightly odified by MojangAB (constants instead of magic
- * numbers, a chunk timestamp header, and auto-formatted according to our
- * formatter template).
- *
- * 2019 December 19
- *
- * This source has been modified to work with .NET programs through the power
- * of IKVM by apotter96. The above notices are still in effect, and are to remain affect.
- * The copyrights over this source are still disclaimed.
- */
-
- /*
- * 2019 December 30
- *
- * Modified to not depend on IKVM by NorbiPeti
- */
-
- // Interfaces with region files on the disk
-
- /*
- *Region File Format
- Concept: The minimum unit of storage on hard drives is 4KB. 90% of Minecraft
- chunks are smaller than 4KB. 99% are smaller than 8KB. Write a simple
- container to store chunks in single files in runs of 4KB sectors.
- Each region file represents a 32x32 group of chunks. The conversion from
- chunk number to region number is floor(coord / 32): a chunk at (30, -3)
- would be in region (0, -1), and one at (70, -30) would be at (3, -1).
- Region files are named "r.x.z.data", where x and z are the region coordinates.
- A region file begins with a 4KB header that describes where chunks are stored
- in the file. A 4-byte big-endian integer represents sector offsets and sector
- counts. The chunk offset for a chunk (x, z) begins at byte 4*(x+z*32) in the
- file. The bottom byte of the chunk offset indicates the number of sectors the
- chunk takes up, and the top 3 bytes represent the sector number of the chunk.
- Given a chunk offset o, the chunk data begins at byte 4096*(o/256) and takes up
- at most 4096*(o%256) bytes. A chunk cannot exceed 1MB in size. If a chunk
- offset is 0, the corresponding chunk is not stored in the region file.
- Chunk data begins with a 4-byte big-endian integer representing the chunk data
- length in bytes, not counting the length field. The length must be smaller than
- 4096 times the number of sectors. The next byte is a version field, to allow
- backwards-compatible updates to how chunks are encoded.
- A version of 1 represents a gzipped NBT file. The gzipped data is the chunk
- length - 1.
- A version of 2 represents a deflated (zlib compressed) NBT file. The deflated
- data is the chunk length - 1.
-
- */
- namespace GCMC
- {
- public class RegionFile : IDisposable
- {
- private const int VersionGzip = 1;
- private const int VersionDeflate = 2;
- private const int SectorBytes = 4096;
- private const int SectorInts = SectorBytes / 4;
- private const int ChunkHeaderSize = 5;
- private readonly byte[] _emptySector = new byte[4096];
- private readonly string _fileName;
- private readonly FileStream _file;
- private readonly int[] _offsets;
- private readonly int[] _chunkTimeStamps;
- private readonly List<bool> _sectorFree;
- private int _sizeDelta;
- private BinaryWriter _sw;
- private BinaryReader _sr;
-
- public RegionFile(string path)
- {
- _offsets = new int[SectorInts];
- _chunkTimeStamps = new int[SectorInts];
-
- _fileName = path;
-
- _sizeDelta = 0;
- try
- {
- if (File.Exists(path))
- {
- LastModified = File.GetLastWriteTime(path).ToFileTime();
- }
-
- _file = File.Open(path, FileMode.OpenOrCreate);
- _sw = new BinaryWriter(_file);
- _sr = new BinaryReader(_file);
-
- if (_file.Length < SectorBytes)
- {
- // we need to write the chunk offset table
- for (int i = 0; i < SectorInts; ++i)
- {
- _sw.Write(0);
- }
-
- // write another sector for the timestamp info
- for (int i = 0; i < SectorInts; ++i)
- {
- _sw.Write(0);
- }
-
- _sizeDelta += SectorBytes * 2;
- }
-
- if ((_file.Length & 0xfff) != 0)
- {
- // the file size is not a multiple of 4KB, grow it
- for (int i = 0; i < (_file.Length & 0xfff); ++i)
- {
- _sw.Write(0);
- }
- }
-
- // set up the available sector map
- int nSectors = (int) _file.Length / SectorBytes;
- _sectorFree = new List<bool>(nSectors);
-
- for (int i = 0; i < nSectors; ++i)
- {
- _sectorFree.Add(true);
- }
-
- _sectorFree[0] = false; // chunk offset table
- _sectorFree[1] = false; // for the last modified info
-
- _file.Seek(0, SeekOrigin.Begin);
- for (int i = 0; i < SectorInts; ++i)
- {
- int offset = _sr.ReadInt32();
- _offsets[i] = offset;
- if (offset == 0 || (offset >> 8) + (offset & 0xFF) > _sectorFree.Count) continue;
- for (int sectorNum = 0; sectorNum < (offset & 0xFF); ++sectorNum)
- {
- _sectorFree[(offset >> 8) + sectorNum] = false;
- }
- }
-
- for (int i = 0; i < SectorInts; i++)
- {
- int lastModValue = _sr.ReadInt32();
- _chunkTimeStamps[i] = lastModValue;
- }
- }
- catch (IOException e)
- {
- Console.WriteLine(e.ToString());
- }
- }
-
- public long LastModified { get; } = 0;
-
- public virtual int SizeDelta
- {
- get
- {
- int ret = _sizeDelta;
- _sizeDelta = 0;
- return ret;
- }
- }
-
- public virtual BinaryReader GetChunkDataInputStream(int x, int z)
- {
- if (OutOfBounds(x, z)) return null;
-
- int offset = GetOffset(x, z);
- if (offset == 0) return null;
-
- return GetChunkDataInputStream(offset);
- }
-
- private BinaryReader GetChunkDataInputStream(int offset)
- {
- try
- {
- int sectorNumber = offset >> 8;
- int numSectors = offset & 0xFF;
-
- if (sectorNumber + numSectors > _sectorFree.Count) return null;
-
- _file.Seek(sectorNumber * SectorBytes, SeekOrigin.Begin);
- int length = _sr.ReadInt32();
-
- if (length > SectorBytes * numSectors) return null;
-
- byte version = _sr.ReadByte();
- if (version == VersionGzip)
- {
- byte[] data = new byte[length - 1];
- _file.Read(data, 0, data.Length);
- return new BinaryReader(new MemoryStream(data));
- }
-
- if (version != VersionDeflate) return null;
- {
- byte[] data = new byte[length - 1];
- _file.Read(data, 0, data.Length);
- return new BinaryReader(new DeflateStream(new MemoryStream(data), CompressionMode.Decompress));
- }
-
- }
- catch (Exception)
- {
- return null;
- }
- }
-
- public BinaryWriter GetChunkDataOutputStream(int x, int z)
- {
- return OutOfBounds(x, z)
- ? null
- : new BinaryWriter(
- new DeflateStream(
- new ChunkBuffer(x, z, this), CompressionMode.Compress));
- }
-
- private class ChunkBuffer : MemoryStream
- {
- private readonly int _x, _z;
- private readonly RegionFile _parent;
-
- public ChunkBuffer(int x, int z, RegionFile parent) : base(8096) // initialize to 9KB
- {
- _x = x;
- _z = z;
- _parent = parent;
- }
-
- public override void Close()
- {
- _parent.Write(_x, _z, base.GetBuffer(), (int) base.Length);
- base.Close();
- }
- }
-
- protected virtual void Write(int x, int z, byte[] data, int length)
- {
- try
- {
- int offset = GetOffset(x, z);
- int sectorNumber = offset >> 8;
- int sectorsAllocated = offset & 0xFF;
- int sectorsNeeded = (length + ChunkHeaderSize) / SectorBytes + 1;
-
- // maximum chunk size is 1MB
- if (sectorsNeeded >= 256) return;
-
- if (sectorNumber != 0 && sectorsAllocated == sectorsNeeded)
- {
- // we can simply overwrite the old sectors
- Write(sectorNumber, data, length);
- }
- else
- {
- // we need to allocate new sectors
- // mark the sectors previously used for this chunk as free
- for (int i = 0; i < sectorsAllocated; ++i)
- {
- _sectorFree[sectorNumber + i] = true;
- }
-
- // scan for a free space large enough to store this chunk
- int runStart = _sectorFree.IndexOf(true);
- int runLength = 0;
- if (runStart != -1)
- {
- for (int i = runStart; i < _sectorFree.Count; ++i)
- {
- if (runLength != 0)
- {
- if (_sectorFree[i]) runLength++;
- else runLength = 0;
- }
- else if (_sectorFree[i])
- {
- runStart = i;
- runLength = 1;
- }
-
- if (runLength >= sectorsNeeded) break;
- }
- }
-
- if (runLength >= sectorsNeeded)
- {
- // we found a free space large enough
- sectorNumber = runStart;
- SetOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
- for (int i = 0; i < sectorsNeeded; ++i)
- {
- _sectorFree[sectorNumber + i] = false;
- }
-
- Write(sectorNumber, data, length);
- }
- else
- {
- // no free space large enough found -- we need to grow
- // the file
- _file.Seek(0, SeekOrigin.End);
- sectorNumber = _sectorFree.Count;
- for (int i = 0; i < sectorsNeeded; ++i)
- {
- _sw.Write(_emptySector);
- _sectorFree.Add(false);
- }
-
- _sizeDelta += SectorBytes * sectorsNeeded;
-
- Write(sectorNumber, data, length);
- SetOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
- }
- }
-
- SetTimestamp(x, z, DateTime.Now.Second);
- }
- catch (IOException e)
- {
- Console.WriteLine(e);
- }
- }
-
- private void Write(int sectorNumber, byte[] data, int length)
- {
- _file.Seek(sectorNumber * SectorBytes, SeekOrigin.Begin);
- _sw.Write(length + 1); // chunk length
- _sw.Write(VersionDeflate); // chunk version number
- _sw.Write(data, 0, length); // chunk data
- }
-
- /// <summary>
- /// Is this an invalid chunk coordinate?
- /// </summary>
- private static bool OutOfBounds(int x, int z)
- {
- return x < 0 || x >= 32 || z < 0 || z >= 32;
- }
-
- private int GetOffset(int x, int z)
- {
- return _offsets[x + z * 32];
- }
-
- public bool HasChunk(int x, int z)
- {
- if (OutOfBounds(x, z)) return false;
- return GetOffset(x, z) != 0;
- }
-
- private void SetOffset(int x, int z, int offset)
- {
- int index = x + z * 32;
- _offsets[index] = offset;
- _file.Seek(index * 4, SeekOrigin.Begin);
- _sw.Write(offset);
- }
-
- private void SetTimestamp(int x, int z, int value)
- {
- int index = x + z * 32;
- _chunkTimeStamps[index] = value;
- _file.Seek(SectorBytes + index * 4, SeekOrigin.Begin);
- _sw.Write(value);
- }
-
- public IEnumerable<BinaryReader> GetChunks()
- {
- return _offsets.Where(i => i != 0)
- .Select(i => GetChunkDataInputStream(i)).Where(br => br != null);
- }
-
- public void Dispose()
- {
- _file?.Dispose();
- _sw?.Dispose();
- _sr?.Dispose();
- }
- }
- }
|