Browse Source

Attempt to read a region file

tags/v1.0.0
NorbiPeti 4 years ago
commit
609e4e96e3
Signed by: NorbiPeti <szatmari.norbert.peter@gmail.com> GPG Key ID: DBA4C4549A927E56
5 changed files with 433 additions and 0 deletions
  1. +3
    -0
      .gitignore
  2. +0
    -0
      .idea/.gitignore
  3. +22
    -0
      GCMC.sln
  4. +11
    -0
      GCMC/GCMC.csproj
  5. +397
    -0
      GCMC/RegionFile.cs

+ 3
- 0
.gitignore View File

@@ -0,0 +1,3 @@
bin/
obj/
/packages/

+ 0
- 0
.idea/.gitignore View File


+ 22
- 0
GCMC.sln View File

@@ -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

+ 11
- 0
GCMC/GCMC.csproj View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="fNbt" Version="0.6.4" />
</ItemGroup>

</Project>

+ 397
- 0
GCMC/RegionFile.cs View File

@@ -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<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();
}
}
}

Loading…
Cancel
Save