Minecraft world importer for Gamecraft.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

398 line
13KB

  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.IO.Compression;
  5. using System.Linq;
  6. using Console = System.Console;
  7. /*
  8. * 2011 January 5
  9. *
  10. * The author disclaims copyright to this source code. In place of
  11. * a legal notice, here is a blessing:
  12. *
  13. * May you do good and not evil.
  14. * May you find forgiveness for yourself and forgive others.
  15. * May you share freely, never taking more than you give.
  16. */
  17. /*
  18. * 2011 February 16
  19. *
  20. * This source code is based on the work of Scaevolus (see notice above).
  21. * It has been slightly odified by MojangAB (constants instead of magic
  22. * numbers, a chunk timestamp header, and auto-formatted according to our
  23. * formatter template).
  24. *
  25. * 2019 December 19
  26. *
  27. * This source has been modified to work with .NET programs through the power
  28. * of IKVM by apotter96. The above notices are still in effect, and are to remain affect.
  29. * The copyrights over this source are still disclaimed.
  30. */
  31. /*
  32. * 2019 December 30
  33. *
  34. * Modified to not depend on IKVM by NorbiPeti
  35. */
  36. // Interfaces with region files on the disk
  37. /*
  38. *Region File Format
  39. Concept: The minimum unit of storage on hard drives is 4KB. 90% of Minecraft
  40. chunks are smaller than 4KB. 99% are smaller than 8KB. Write a simple
  41. container to store chunks in single files in runs of 4KB sectors.
  42. Each region file represents a 32x32 group of chunks. The conversion from
  43. chunk number to region number is floor(coord / 32): a chunk at (30, -3)
  44. would be in region (0, -1), and one at (70, -30) would be at (3, -1).
  45. Region files are named "r.x.z.data", where x and z are the region coordinates.
  46. A region file begins with a 4KB header that describes where chunks are stored
  47. in the file. A 4-byte big-endian integer represents sector offsets and sector
  48. counts. The chunk offset for a chunk (x, z) begins at byte 4*(x+z*32) in the
  49. file. The bottom byte of the chunk offset indicates the number of sectors the
  50. chunk takes up, and the top 3 bytes represent the sector number of the chunk.
  51. Given a chunk offset o, the chunk data begins at byte 4096*(o/256) and takes up
  52. at most 4096*(o%256) bytes. A chunk cannot exceed 1MB in size. If a chunk
  53. offset is 0, the corresponding chunk is not stored in the region file.
  54. Chunk data begins with a 4-byte big-endian integer representing the chunk data
  55. length in bytes, not counting the length field. The length must be smaller than
  56. 4096 times the number of sectors. The next byte is a version field, to allow
  57. backwards-compatible updates to how chunks are encoded.
  58. A version of 1 represents a gzipped NBT file. The gzipped data is the chunk
  59. length - 1.
  60. A version of 2 represents a deflated (zlib compressed) NBT file. The deflated
  61. data is the chunk length - 1.
  62. */
  63. namespace GCMC
  64. {
  65. public class RegionFile : IDisposable
  66. {
  67. private const int VersionGzip = 1;
  68. private const int VersionDeflate = 2;
  69. private const int SectorBytes = 4096;
  70. private const int SectorInts = SectorBytes / 4;
  71. private const int ChunkHeaderSize = 5;
  72. private readonly byte[] _emptySector = new byte[4096];
  73. private readonly string _fileName;
  74. private readonly FileStream _file;
  75. private readonly int[] _offsets;
  76. private readonly int[] _chunkTimeStamps;
  77. private readonly List<bool> _sectorFree;
  78. private int _sizeDelta;
  79. private BinaryWriter _sw;
  80. private BinaryReader _sr;
  81. public RegionFile(string path)
  82. {
  83. _offsets = new int[SectorInts];
  84. _chunkTimeStamps = new int[SectorInts];
  85. _fileName = path;
  86. _sizeDelta = 0;
  87. try
  88. {
  89. if (File.Exists(path))
  90. {
  91. LastModified = File.GetLastWriteTime(path).ToFileTime();
  92. }
  93. _file = File.Open(path, FileMode.OpenOrCreate);
  94. _sw = new BinaryWriter(_file);
  95. _sr = new BinaryReader(_file);
  96. if (_file.Length < SectorBytes)
  97. {
  98. // we need to write the chunk offset table
  99. for (int i = 0; i < SectorInts; ++i)
  100. {
  101. _sw.Write(0);
  102. }
  103. // write another sector for the timestamp info
  104. for (int i = 0; i < SectorInts; ++i)
  105. {
  106. _sw.Write(0);
  107. }
  108. _sizeDelta += SectorBytes * 2;
  109. }
  110. if ((_file.Length & 0xfff) != 0)
  111. {
  112. // the file size is not a multiple of 4KB, grow it
  113. for (int i = 0; i < (_file.Length & 0xfff); ++i)
  114. {
  115. _sw.Write(0);
  116. }
  117. }
  118. // set up the available sector map
  119. int nSectors = (int) _file.Length / SectorBytes;
  120. _sectorFree = new List<bool>(nSectors);
  121. for (int i = 0; i < nSectors; ++i)
  122. {
  123. _sectorFree.Add(true);
  124. }
  125. _sectorFree[0] = false; // chunk offset table
  126. _sectorFree[1] = false; // for the last modified info
  127. _file.Seek(0, SeekOrigin.Begin);
  128. for (int i = 0; i < SectorInts; ++i)
  129. {
  130. int offset = _sr.ReadInt32();
  131. _offsets[i] = offset;
  132. if (offset == 0 || (offset >> 8) + (offset & 0xFF) > _sectorFree.Count) continue;
  133. for (int sectorNum = 0; sectorNum < (offset & 0xFF); ++sectorNum)
  134. {
  135. _sectorFree[(offset >> 8) + sectorNum] = false;
  136. }
  137. }
  138. for (int i = 0; i < SectorInts; i++)
  139. {
  140. int lastModValue = _sr.ReadInt32();
  141. _chunkTimeStamps[i] = lastModValue;
  142. }
  143. }
  144. catch (IOException e)
  145. {
  146. Console.WriteLine(e.ToString());
  147. }
  148. }
  149. public long LastModified { get; } = 0;
  150. public virtual int SizeDelta
  151. {
  152. get
  153. {
  154. int ret = _sizeDelta;
  155. _sizeDelta = 0;
  156. return ret;
  157. }
  158. }
  159. public virtual BinaryReader GetChunkDataInputStream(int x, int z)
  160. {
  161. if (OutOfBounds(x, z)) return null;
  162. int offset = GetOffset(x, z);
  163. if (offset == 0) return null;
  164. return GetChunkDataInputStream(offset);
  165. }
  166. private BinaryReader GetChunkDataInputStream(int offset)
  167. {
  168. try
  169. {
  170. int sectorNumber = offset >> 8;
  171. int numSectors = offset & 0xFF;
  172. if (sectorNumber + numSectors > _sectorFree.Count) return null;
  173. _file.Seek(sectorNumber * SectorBytes, SeekOrigin.Begin);
  174. int length = _sr.ReadInt32();
  175. if (length > SectorBytes * numSectors) return null;
  176. byte version = _sr.ReadByte();
  177. if (version == VersionGzip)
  178. {
  179. byte[] data = new byte[length - 1];
  180. _file.Read(data, 0, data.Length);
  181. return new BinaryReader(new MemoryStream(data));
  182. }
  183. if (version != VersionDeflate) return null;
  184. {
  185. byte[] data = new byte[length - 1];
  186. _file.Read(data, 0, data.Length);
  187. return new BinaryReader(new DeflateStream(new MemoryStream(data), CompressionMode.Decompress));
  188. }
  189. }
  190. catch (Exception)
  191. {
  192. return null;
  193. }
  194. }
  195. public BinaryWriter GetChunkDataOutputStream(int x, int z)
  196. {
  197. return OutOfBounds(x, z)
  198. ? null
  199. : new BinaryWriter(
  200. new DeflateStream(
  201. new ChunkBuffer(x, z, this), CompressionMode.Compress));
  202. }
  203. private class ChunkBuffer : MemoryStream
  204. {
  205. private readonly int _x, _z;
  206. private readonly RegionFile _parent;
  207. public ChunkBuffer(int x, int z, RegionFile parent) : base(8096) // initialize to 9KB
  208. {
  209. _x = x;
  210. _z = z;
  211. _parent = parent;
  212. }
  213. public override void Close()
  214. {
  215. _parent.Write(_x, _z, base.GetBuffer(), (int) base.Length);
  216. base.Close();
  217. }
  218. }
  219. protected virtual void Write(int x, int z, byte[] data, int length)
  220. {
  221. try
  222. {
  223. int offset = GetOffset(x, z);
  224. int sectorNumber = offset >> 8;
  225. int sectorsAllocated = offset & 0xFF;
  226. int sectorsNeeded = (length + ChunkHeaderSize) / SectorBytes + 1;
  227. // maximum chunk size is 1MB
  228. if (sectorsNeeded >= 256) return;
  229. if (sectorNumber != 0 && sectorsAllocated == sectorsNeeded)
  230. {
  231. // we can simply overwrite the old sectors
  232. Write(sectorNumber, data, length);
  233. }
  234. else
  235. {
  236. // we need to allocate new sectors
  237. // mark the sectors previously used for this chunk as free
  238. for (int i = 0; i < sectorsAllocated; ++i)
  239. {
  240. _sectorFree[sectorNumber + i] = true;
  241. }
  242. // scan for a free space large enough to store this chunk
  243. int runStart = _sectorFree.IndexOf(true);
  244. int runLength = 0;
  245. if (runStart != -1)
  246. {
  247. for (int i = runStart; i < _sectorFree.Count; ++i)
  248. {
  249. if (runLength != 0)
  250. {
  251. if (_sectorFree[i]) runLength++;
  252. else runLength = 0;
  253. }
  254. else if (_sectorFree[i])
  255. {
  256. runStart = i;
  257. runLength = 1;
  258. }
  259. if (runLength >= sectorsNeeded) break;
  260. }
  261. }
  262. if (runLength >= sectorsNeeded)
  263. {
  264. // we found a free space large enough
  265. sectorNumber = runStart;
  266. SetOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
  267. for (int i = 0; i < sectorsNeeded; ++i)
  268. {
  269. _sectorFree[sectorNumber + i] = false;
  270. }
  271. Write(sectorNumber, data, length);
  272. }
  273. else
  274. {
  275. // no free space large enough found -- we need to grow
  276. // the file
  277. _file.Seek(0, SeekOrigin.End);
  278. sectorNumber = _sectorFree.Count;
  279. for (int i = 0; i < sectorsNeeded; ++i)
  280. {
  281. _sw.Write(_emptySector);
  282. _sectorFree.Add(false);
  283. }
  284. _sizeDelta += SectorBytes * sectorsNeeded;
  285. Write(sectorNumber, data, length);
  286. SetOffset(x, z, (sectorNumber << 8) | sectorsNeeded);
  287. }
  288. }
  289. SetTimestamp(x, z, DateTime.Now.Second);
  290. }
  291. catch (IOException e)
  292. {
  293. Console.WriteLine(e);
  294. }
  295. }
  296. private void Write(int sectorNumber, byte[] data, int length)
  297. {
  298. _file.Seek(sectorNumber * SectorBytes, SeekOrigin.Begin);
  299. _sw.Write(length + 1); // chunk length
  300. _sw.Write(VersionDeflate); // chunk version number
  301. _sw.Write(data, 0, length); // chunk data
  302. }
  303. /// <summary>
  304. /// Is this an invalid chunk coordinate?
  305. /// </summary>
  306. private static bool OutOfBounds(int x, int z)
  307. {
  308. return x < 0 || x >= 32 || z < 0 || z >= 32;
  309. }
  310. private int GetOffset(int x, int z)
  311. {
  312. return _offsets[x + z * 32];
  313. }
  314. public bool HasChunk(int x, int z)
  315. {
  316. if (OutOfBounds(x, z)) return false;
  317. return GetOffset(x, z) != 0;
  318. }
  319. private void SetOffset(int x, int z, int offset)
  320. {
  321. int index = x + z * 32;
  322. _offsets[index] = offset;
  323. _file.Seek(index * 4, SeekOrigin.Begin);
  324. _sw.Write(offset);
  325. }
  326. private void SetTimestamp(int x, int z, int value)
  327. {
  328. int index = x + z * 32;
  329. _chunkTimeStamps[index] = value;
  330. _file.Seek(SectorBytes + index * 4, SeekOrigin.Begin);
  331. _sw.Write(value);
  332. }
  333. public IEnumerable<BinaryReader> GetChunks()
  334. {
  335. return _offsets.Where(i => i != 0)
  336. .Select(i => GetChunkDataInputStream(i)).Where(br => br != null);
  337. }
  338. public void Dispose()
  339. {
  340. _file?.Dispose();
  341. _sw?.Dispose();
  342. _sr?.Dispose();
  343. }
  344. }
  345. }