From 609e4e96e30d255e05c03ac2fbfbf04eb9939b01 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Tue, 31 Dec 2019 03:29:58 +0100 Subject: [PATCH] Attempt to read a region file --- .gitignore | 3 + .idea/.gitignore | 0 GCMC.sln | 22 +++ GCMC/GCMC.csproj | 11 ++ GCMC/RegionFile.cs | 397 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 433 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 GCMC.sln create mode 100644 GCMC/GCMC.csproj create mode 100644 GCMC/RegionFile.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b230ab5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/ +obj/ +/packages/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/GCMC.sln b/GCMC.sln new file mode 100644 index 0000000..324ee82 --- /dev/null +++ b/GCMC.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCMC", "GCMC\GCMC.csproj", "{734116A4-263B-4C65-B944-16F0864B9752}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GCMCTest", "GCMCTest\GCMCTest.csproj", "{EA70F99D-AC56-4698-A2FE-B5677C1DAB0F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {734116A4-263B-4C65-B944-16F0864B9752}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {734116A4-263B-4C65-B944-16F0864B9752}.Debug|Any CPU.Build.0 = Debug|Any CPU + {734116A4-263B-4C65-B944-16F0864B9752}.Release|Any CPU.ActiveCfg = Release|Any CPU + {734116A4-263B-4C65-B944-16F0864B9752}.Release|Any CPU.Build.0 = Release|Any CPU + {EA70F99D-AC56-4698-A2FE-B5677C1DAB0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA70F99D-AC56-4698-A2FE-B5677C1DAB0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA70F99D-AC56-4698-A2FE-B5677C1DAB0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA70F99D-AC56-4698-A2FE-B5677C1DAB0F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/GCMC/GCMC.csproj b/GCMC/GCMC.csproj new file mode 100644 index 0000000..0d6279c --- /dev/null +++ b/GCMC/GCMC.csproj @@ -0,0 +1,11 @@ + + + + netstandard2.0 + + + + + + + diff --git a/GCMC/RegionFile.cs b/GCMC/RegionFile.cs new file mode 100644 index 0000000..d164ff8 --- /dev/null +++ b/GCMC/RegionFile.cs @@ -0,0 +1,397 @@ +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 _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(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 + } + + /// + /// Is this an invalid chunk coordinate? + /// + 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 GetChunks() + { + return _offsets.Where(i => i != 0) + .Select(i => GetChunkDataInputStream(i)).Where(br => br != null); + } + + public void Dispose() + { + _file?.Dispose(); + _sw?.Dispose(); + _sr?.Dispose(); + } + } +}