using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using fNbt;
using Ionic.Zlib;
using TrueCraft.API;
using TrueCraft.API.World;
using TrueCraft.Core.Networking;
namespace TrueCraft.Core.World
{
///
/// Represents a 32x32 area of objects.
/// Not all of these chunks are represented at any given time, and
/// will be loaded from disk or generated when the need arises.
///
public class Region : IDisposable, IRegion
{
// In chunks
public const int Width = 32, Depth = 32;
private ConcurrentDictionary _Chunks { get; set; }
///
/// The currently loaded chunk list.
///
public IDictionary Chunks { get { return _Chunks; } }
///
/// The location of this region in the overworld.
///
public Coordinates2D Position { get; set; }
public World World { get; set; }
private HashSet DirtyChunks { get; set; } = new HashSet();
private Stream regionFile { get; set; }
private object streamLock = new object();
///
/// Creates a new Region for server-side use at the given position using
/// the provided terrain generator.
///
public Region(Coordinates2D position, World world)
{
_Chunks = new ConcurrentDictionary();
Position = position;
World = world;
}
///
/// Creates a region from the given region file.
///
public Region(Coordinates2D position, World world, string file) : this(position, world)
{
if (File.Exists(file))
{
regionFile = File.Open(file, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
regionFile.Read(HeaderCache, 0, 8192);
}
else
{
regionFile = File.Open(file, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
CreateRegionHeader();
}
}
public void DamageChunk(Coordinates2D coords)
{
int x = coords.X / Region.Width - ((coords.X < 0) ? 1 : 0);
int z = coords.Z / Region.Depth - ((coords.Z < 0) ? 1 : 0);
DirtyChunks.Add(new Coordinates2D(coords.X - x * 32, coords.Z - z * 32));
}
///
/// Retrieves the requested chunk from the region, or
/// generates it if a world generator is provided.
///
/// The position of the requested local chunk coordinates.
public IChunk GetChunk(Coordinates2D position, bool generate = true)
{
if (!Chunks.ContainsKey(position))
{
if (regionFile != null)
{
// Search the stream for that region
var chunkData = GetChunkFromTable(position);
if (chunkData == null)
{
if (World.ChunkProvider == null)
throw new ArgumentException("The requested chunk is not loaded.", "position");
if (generate)
GenerateChunk(position);
else
return null;
return Chunks[position];
}
lock (streamLock)
{
regionFile.Seek(chunkData.Item1, SeekOrigin.Begin);
/*int length = */
new MinecraftStream(regionFile).ReadInt32(); // TODO: Avoid making new objects here, and in the WriteInt32
int compressionMode = regionFile.ReadByte();
switch (compressionMode)
{
case 1: // gzip
throw new NotImplementedException("gzipped chunks are not implemented");
case 2: // zlib
var nbt = new NbtFile();
nbt.LoadFromStream(regionFile, NbtCompression.ZLib, null);
var chunk = Chunk.FromNbt(nbt);
chunk.ParentRegion = this;
Chunks.Add(position, chunk);
World.OnChunkLoaded(new ChunkLoadedEventArgs(chunk));
break;
default:
throw new InvalidDataException("Invalid compression scheme provided by region file.");
}
}
}
else if (World.ChunkProvider == null)
throw new ArgumentException("The requested chunk is not loaded.", nameof(position));
else
{
if (generate)
GenerateChunk(position);
else
return null;
}
}
return Chunks[position];
}
public void GenerateChunk(Coordinates2D position)
{
var globalPosition = (Position * new Coordinates2D(Width, Depth)) + position;
var chunk = World.ChunkProvider.GenerateChunk(World, globalPosition);
chunk.IsModified = true;
chunk.Coordinates = globalPosition;
chunk.ParentRegion = this;
DirtyChunks.Add(position);
Chunks[position] = chunk;
World.OnChunkGenerated(new ChunkLoadedEventArgs(chunk));
}
///
/// Sets the chunk at the specified local position to the given value.
///
public void SetChunk(Coordinates2D position, IChunk chunk)
{
if (!Chunks.ContainsKey(position))
Chunks.Add(position, chunk);
chunk.IsModified = true;
DirtyChunks.Add(position);
chunk.ParentRegion = this;
Chunks[position] = chunk;
}
///
/// Saves this region to the specified file.
///
public void Save(string file)
{
if(File.Exists(file))
regionFile = regionFile ?? File.Open(file, FileMode.OpenOrCreate);
else
{
regionFile = regionFile ?? File.Open(file, FileMode.OpenOrCreate);
CreateRegionHeader();
}
Save();
}
///
/// Saves this region to the open region file.
///
public void Save()
{
lock (streamLock)
{
var toRemove = new List();
var chunks = DirtyChunks.ToList();
DirtyChunks.Clear();
foreach (var coords in chunks)
{
var chunk = GetChunk(coords, generate: false);
if (chunk.IsModified)
{
var data = ((Chunk)chunk).ToNbt();
byte[] raw = data.SaveToBuffer(NbtCompression.ZLib);
var header = GetChunkFromTable(coords);
if (header == null || header.Item2 > raw.Length)
header = AllocateNewChunks(coords, raw.Length);
regionFile.Seek(header.Item1, SeekOrigin.Begin);
new MinecraftStream(regionFile).WriteInt32(raw.Length);
regionFile.WriteByte(2); // Compressed with zlib
regionFile.Write(raw, 0, raw.Length);
chunk.IsModified = false;
}
if ((DateTime.UtcNow - chunk.LastAccessed).TotalMinutes > 5)
toRemove.Add(coords);
}
regionFile.Flush();
// Unload idle chunks
foreach (var chunk in toRemove)
{
var c = Chunks[chunk];
Chunks.Remove(chunk);
c.Dispose();
}
}
}
#region Stream Helpers
private const int ChunkSizeMultiplier = 4096;
private byte[] HeaderCache = new byte[8192];
private Tuple GetChunkFromTable(Coordinates2D position) //
{
int tableOffset = ((position.X % Width) + (position.Z % Depth) * Width) * 4;
byte[] offsetBuffer = new byte[4];
Buffer.BlockCopy(HeaderCache, tableOffset, offsetBuffer, 0, 3);
Array.Reverse(offsetBuffer);
int length = HeaderCache[tableOffset + 3];
int offset = BitConverter.ToInt32(offsetBuffer, 0) << 4;
if (offset == 0 || length == 0)
return null;
return new Tuple(offset,
length * ChunkSizeMultiplier);
}
private void CreateRegionHeader()
{
HeaderCache = new byte[8192];
regionFile.Write(HeaderCache, 0, 8192);
regionFile.Flush();
}
private Tuple AllocateNewChunks(Coordinates2D position, int length)
{
// Expand region file
regionFile.Seek(0, SeekOrigin.End);
int dataOffset = (int)regionFile.Position;
length /= ChunkSizeMultiplier;
length++;
regionFile.Write(new byte[length * ChunkSizeMultiplier], 0, length * ChunkSizeMultiplier);
// Write table entry
int tableOffset = ((position.X % Width) + (position.Z % Depth) * Width) * 4;
regionFile.Seek(tableOffset, SeekOrigin.Begin);
byte[] entry = BitConverter.GetBytes(dataOffset >> 4);
entry[0] = (byte)length;
Array.Reverse(entry);
regionFile.Write(entry, 0, entry.Length);
Buffer.BlockCopy(entry, 0, HeaderCache, tableOffset, 4);
return new Tuple(dataOffset, length * ChunkSizeMultiplier);
}
#endregion
public static string GetRegionFileName(Coordinates2D position)
{
return string.Format("r.{0}.{1}.mca", position.X, position.Z);
}
public void UnloadChunk(Coordinates2D position)
{
Chunks.Remove(position);
}
public void Dispose()
{
if (regionFile == null)
return;
lock (streamLock)
{
regionFile.Flush();
regionFile.Close();
}
}
}
}