Many more optimizations and bugfixes

Again, sorry for the huge commit. Just taking on performance issues as I
see them. Changes in this:

- Deadlocks in region code finally fixed
- Chunk packet preparation optimized (saves ~10-20ms per packet, since
  we're sending these like 30 at a time that's pretty important) by
  storing chunks pre-encoded in memory (basically just using a single
  big array for IDs, metadata, and light)
- Move chunk generation and compression to the thread pool
- Move client chunk updates to the scheduler
- Improve profiler coverage
- Add knob to disable scheduling chunk events on chunk load
- Make it possible to disable specific scheduled events in config.yml
This commit is contained in:
Drew DeVault 2017-05-23 18:17:44 -04:00
parent 362c852f51
commit 6fb8ee7ba5
25 changed files with 460 additions and 325 deletions

View File

@ -29,7 +29,7 @@ namespace TrueCraft.API
config = new T(); config = new T();
} }
var serializer = new Serializer(); var serializer = new Serializer(SerializationOptions.EmitDefaults);
using (var writer = new StreamWriter(configFileName)) using (var writer = new StreamWriter(configFileName))
serializer.Serialize(writer, config); serializer.Serialize(writer, config);

View File

@ -1,85 +0,0 @@
using fNbt;
using fNbt.Serialization;
using System;
using System.Collections.ObjectModel;
namespace TrueCraft.API
{
/// <summary>
/// Represents an array of 4-bit values.
/// </summary>
public class NibbleArray : INbtSerializable
{
/// <summary>
/// The data in the nibble array. Each byte contains
/// two nibbles, stored in big-endian.
/// </summary>
public byte[] Data { get; set; }
public NibbleArray()
{
}
/// <summary>
/// Creates a new nibble array with the given number of nibbles.
/// </summary>
public NibbleArray(int length)
{
Data = new byte[length/2];
}
/// <summary>
/// Gets the current number of nibbles in this array.
/// </summary>
[NbtIgnore]
public int Length
{
get { return Data.Length * 2; }
}
/// <summary>
/// Gets or sets a nibble at the given index.
/// </summary>
[NbtIgnore]
public byte this[int index]
{
get { return (byte)(Data[index / 2] >> ((index) % 2 * 4) & 0xF); }
set
{
value &= 0xF;
Data[index/2] &= (byte)(0xF << ((index + 1) % 2 * 4));
Data[index/2] |= (byte)(value << (index % 2 * 4));
}
}
public NbtTag Serialize(string tagName)
{
return new NbtByteArray(tagName, Data);
}
public void Deserialize(NbtTag value)
{
Data = value.ByteArrayValue;
}
}
public class ReadOnlyNibbleArray
{
private NibbleArray NibbleArray { get; set; }
public ReadOnlyNibbleArray(NibbleArray array)
{
NibbleArray = array;
}
public byte this[int index]
{
get { return NibbleArray[index]; }
}
public ReadOnlyCollection<byte> Data
{
get { return Array.AsReadOnly(NibbleArray.Data); }
}
}
}

View File

@ -0,0 +1,82 @@
using fNbt;
using fNbt.Serialization;
using System;
using System.Collections.ObjectModel;
namespace TrueCraft.API
{
/// <summary>
/// Represents a slice of an array of 4-bit values.
/// </summary>
public class NibbleSlice : INbtSerializable
{
/// <summary>
/// The data in the nibble array. Each byte contains
/// two nibbles, stored in big-endian.
/// </summary>
public byte[] Data { get; private set; }
public int Offset { get; private set; }
public int Length { get; private set; }
public NibbleSlice(byte[] data, int offset, int length)
{
Data = data;
Offset = offset;
Length = length;
}
/// <summary>
/// Gets or sets a nibble at the given index.
/// </summary>
[NbtIgnore]
public byte this[int index]
{
get { return (byte)(Data[Offset + index / 2] >> (index % 2 * 4) & 0xF); }
set
{
value &= 0xF;
Data[Offset + index / 2] &= (byte)(~(0xF << (index % 2 * 4)));
Data[Offset + index / 2] |= (byte)(value << (index % 2 * 4));
}
}
public byte[] ToArray()
{
byte[] array = new byte[Length];
Buffer.BlockCopy(Data, Offset, array, 0, Length);
return array;
}
public NbtTag Serialize(string tagName)
{
return new NbtByteArray(tagName, ToArray());
}
public void Deserialize(NbtTag value)
{
Length = value.ByteArrayValue.Length;
Buffer.BlockCopy(value.ByteArrayValue, 0,
Data, Offset, Length);
}
}
public class ReadOnlyNibbleArray
{
private NibbleSlice NibbleArray { get; set; }
public ReadOnlyNibbleArray(NibbleSlice array)
{
NibbleArray = array;
}
public byte this[int index]
{
get { return NibbleArray[index]; }
}
public ReadOnlyCollection<byte> Data
{
get { return Array.AsReadOnly(NibbleArray.Data); }
}
}
}

View File

@ -1,9 +1,11 @@
using System; using System;
using System.Collections.Generic;
namespace TrueCraft.API.Server namespace TrueCraft.API.Server
{ {
public interface IEventScheduler public interface IEventScheduler
{ {
HashSet<string> DisabledEvents { get; }
/// <summary> /// <summary>
/// Schedules an event to occur some time in the future. /// Schedules an event to occur some time in the future.
/// </summary> /// </summary>

View File

@ -88,7 +88,7 @@
<Compile Include="World\IWorld.cs" /> <Compile Include="World\IWorld.cs" />
<Compile Include="World\IChunk.cs" /> <Compile Include="World\IChunk.cs" />
<Compile Include="World\IChunkProvider.cs" /> <Compile Include="World\IChunkProvider.cs" />
<Compile Include="NibbleArray.cs" /> <Compile Include="NibbleSlice.cs" />
<Compile Include="World\IRegion.cs" /> <Compile Include="World\IRegion.cs" />
<Compile Include="Biome.cs" /> <Compile Include="Biome.cs" />
<Compile Include="Logging\LogCategory.cs" /> <Compile Include="Logging\LogCategory.cs" />

View File

@ -15,12 +15,13 @@ namespace TrueCraft.API.World
int[] HeightMap { get; } int[] HeightMap { get; }
byte[] Biomes { get; } byte[] Biomes { get; }
DateTime LastAccessed { get; set; } DateTime LastAccessed { get; set; }
byte[] Blocks { get; } byte[] Data { get; }
bool TerrainPopulated { get; set; } bool TerrainPopulated { get; set; }
Dictionary<Coordinates3D, NbtCompound> TileEntities { get; set; } Dictionary<Coordinates3D, NbtCompound> TileEntities { get; set; }
NibbleArray Metadata { get; } NibbleSlice Metadata { get; }
NibbleArray BlockLight { get; } NibbleSlice BlockLight { get; }
NibbleArray SkyLight { get; } NibbleSlice SkyLight { get; }
IRegion ParentRegion { get; set; }
int GetHeight(byte x, byte z); int GetHeight(byte x, byte z);
void UpdateHeightMap(); void UpdateHeightMap();
byte GetBlockID(Coordinates3D coordinates); byte GetBlockID(Coordinates3D coordinates);

View File

@ -9,6 +9,10 @@ namespace TrueCraft.API.World
Coordinates2D Position { get; } Coordinates2D Position { get; }
IChunk GetChunk(Coordinates2D position, bool generate = true); IChunk GetChunk(Coordinates2D position, bool generate = true);
/// <summary>
/// Marks the chunk for saving in the next Save().
/// </summary>
void DamageChunk(Coordinates2D position);
void UnloadChunk(Coordinates2D position); void UnloadChunk(Coordinates2D position);
void Save(string path); void Save(string path);
} }

View File

@ -53,14 +53,14 @@ namespace TrueCraft.Client.Handlers
&& packet.Depth == Chunk.Depth) // Fast path && packet.Depth == Chunk.Depth) // Fast path
{ {
// Block IDs // Block IDs
Buffer.BlockCopy(data, 0, chunk.Blocks, 0, chunk.Blocks.Length); Buffer.BlockCopy(data, 0, chunk.Data, 0, chunk.Data.Length);
// Block metadata // Block metadata
Buffer.BlockCopy(data, chunk.Blocks.Length, chunk.Metadata.Data, 0, chunk.Metadata.Data.Length); Buffer.BlockCopy(data, chunk.Data.Length, chunk.Metadata.Data, 0, chunk.Metadata.Data.Length);
// Block light // Block light
Buffer.BlockCopy(data, chunk.Blocks.Length + chunk.Metadata.Data.Length, Buffer.BlockCopy(data, chunk.Data.Length + chunk.Metadata.Data.Length,
chunk.BlockLight.Data, 0, chunk.BlockLight.Data.Length); chunk.BlockLight.Data, 0, chunk.BlockLight.Data.Length);
// Sky light // Sky light
Buffer.BlockCopy(data, chunk.Blocks.Length + chunk.Metadata.Data.Length + chunk.BlockLight.Data.Length, Buffer.BlockCopy(data, chunk.Data.Length + chunk.Metadata.Data.Length + chunk.BlockLight.Data.Length,
chunk.SkyLight.Data, 0, chunk.SkyLight.Data.Length); chunk.SkyLight.Data, 0, chunk.SkyLight.Data.Length);
} }
else // Slow path else // Slow path

View File

@ -116,7 +116,7 @@ namespace TrueCraft.Client
public int X { get { return Chunk.X; } } public int X { get { return Chunk.X; } }
public int Z { get { return Chunk.Z; } } public int Z { get { return Chunk.Z; } }
public ReadOnlyCollection<byte> Blocks { get { return Array.AsReadOnly(Chunk.Blocks); } } public ReadOnlyCollection<byte> Blocks { get { return Array.AsReadOnly(Chunk.Data); } }
public ReadOnlyNibbleArray Metadata { get { return new ReadOnlyNibbleArray(Chunk.Metadata); } } public ReadOnlyNibbleArray Metadata { get { return new ReadOnlyNibbleArray(Chunk.Metadata); } }
public ReadOnlyNibbleArray BlockLight { get { return new ReadOnlyNibbleArray(Chunk.BlockLight); } } public ReadOnlyNibbleArray BlockLight { get { return new ReadOnlyNibbleArray(Chunk.BlockLight); } }
public ReadOnlyNibbleArray SkyLight { get { return new ReadOnlyNibbleArray(Chunk.SkyLight); } } public ReadOnlyNibbleArray SkyLight { get { return new ReadOnlyNibbleArray(Chunk.SkyLight); } }

View File

@ -12,51 +12,80 @@ namespace TrueCraft.Core.Test.World
[TestFixture] [TestFixture]
public class ChunkTest public class ChunkTest
{ {
public Chunk Chunk { get; set; }
[OneTimeSetUp]
public void SetUp()
{
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var file = new NbtFile(Path.Combine(assemblyDir, "Files", "TestChunk.nbt"));
Chunk = Chunk.FromNbt(file);
}
[Test] [Test]
public void TestGetBlockID() public void TestGetBlockID()
{ {
Assert.AreEqual(BedrockBlock.BlockID, Chunk.GetBlockID(Coordinates3D.Zero)); var chunk = new Chunk();
Chunk.SetBlockID(Coordinates3D.Zero, 12); chunk.SetBlockID(Coordinates3D.Zero, 12);
Assert.AreEqual(12, Chunk.GetBlockID(Coordinates3D.Zero)); Assert.AreEqual(12, chunk.GetBlockID(Coordinates3D.Zero));
Chunk.SetBlockID(Coordinates3D.Zero, BedrockBlock.BlockID);
} }
[Test] [Test]
public void TestGetBlockLight() public void TestGetBlockLight()
{ {
Assert.AreEqual(0, Chunk.GetBlockLight(Coordinates3D.Zero)); var chunk = new Chunk();
chunk.SetBlockLight(Coordinates3D.Zero, 5);
Assert.AreEqual(5, chunk.GetBlockLight(Coordinates3D.Zero));
} }
[Test] [Test]
public void TestGetSkyLight() public void TestGetSkyLight()
{ {
Assert.AreEqual(0, Chunk.GetBlockLight(Coordinates3D.Zero)); var chunk = new Chunk();
chunk.SetSkyLight(Coordinates3D.Zero, 5);
Assert.AreEqual(5, chunk.GetSkyLight(Coordinates3D.Zero));
} }
[Test] [Test]
public void TestGetMetadata() public void TestGetMetadata()
{ {
Assert.AreEqual(0, Chunk.GetBlockLight(Coordinates3D.Zero)); var chunk = new Chunk();
chunk.SetMetadata(Coordinates3D.Zero, 5);
Assert.AreEqual(5, chunk.GetMetadata(Coordinates3D.Zero));
} }
[Test] [Test]
public void TestHeightMap() public void TestHeightMap()
{ {
Chunk.UpdateHeightMap(); var chunk = new Chunk();
Assert.AreEqual(59, Chunk.GetHeight(0, 0)); for (int x = 0; x < Chunk.Width; ++x)
Assert.AreEqual(58, Chunk.GetHeight(1, 0)); for (int z = 0; z < Chunk.Width; ++z)
Chunk.SetBlockID(new Coordinates3D(1, 80, 0), 1); chunk.SetBlockID(new Coordinates3D(x, 20, z), StoneBlock.BlockID);
Assert.AreEqual(80, Chunk.GetHeight(1, 0)); chunk.UpdateHeightMap();
Assert.AreEqual(20, chunk.GetHeight(0, 0));
Assert.AreEqual(20, chunk.GetHeight(1, 0));
chunk.SetBlockID(new Coordinates3D(1, 80, 0), 1);
Assert.AreEqual(80, chunk.GetHeight(1, 0));
}
[Test]
public void TestConsistency()
{
var chunk = new Chunk();
byte val = 0;
for (int y = 0; y < Chunk.Height; y++)
for (int x = 0; x < Chunk.Width; x++)
for (int z = 0; z < Chunk.Depth; z++)
{
var coords = new Coordinates3D(x, y, z);
chunk.SetBlockID(coords, val);
chunk.SetMetadata(coords, (byte)(val % 16));
chunk.SetBlockLight(coords, (byte)(val % 16));
chunk.SetSkyLight(coords, (byte)(val % 16));
val++;
}
val = 0;
for (int y = 0; y < Chunk.Height; y++)
for (int x = 0; x < Chunk.Width; x++)
for (int z = 0; z < Chunk.Depth; z++)
{
var coords = new Coordinates3D(x, y, z);
Assert.AreEqual(val, chunk.GetBlockID(coords));
Assert.AreEqual((byte)(val % 16), chunk.GetMetadata(coords));
Assert.AreEqual((byte)(val % 16), chunk.GetBlockLight(coords));
Assert.AreEqual((byte)(val % 16), chunk.GetSkyLight(coords));
val++;
}
} }
} }
} }

View File

@ -6,6 +6,7 @@ using TrueCraft.API;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using TrueCraft.Profiling; using TrueCraft.Profiling;
using System.Collections.Concurrent;
namespace TrueCraft.Core.Lighting namespace TrueCraft.Core.Lighting
{ {
@ -34,15 +35,14 @@ namespace TrueCraft.Core.Lighting
public IBlockRepository BlockRepository { get; set; } public IBlockRepository BlockRepository { get; set; }
public IWorld World { get; set; } public IWorld World { get; set; }
private object _Lock = new object(); private ConcurrentQueue<LightingOperation> PendingOperations { get; set; }
private List<LightingOperation> PendingOperations { get; set; }
private Dictionary<Coordinates2D, byte[,]> HeightMaps { get; set; } private Dictionary<Coordinates2D, byte[,]> HeightMaps { get; set; }
public WorldLighting(IWorld world, IBlockRepository blockRepository) public WorldLighting(IWorld world, IBlockRepository blockRepository)
{ {
BlockRepository = blockRepository; BlockRepository = blockRepository;
World = world; World = world;
PendingOperations = new List<LightingOperation>(); PendingOperations = new ConcurrentQueue<LightingOperation>();
HeightMaps = new Dictionary<Coordinates2D, byte[,]>(); HeightMaps = new Dictionary<Coordinates2D, byte[,]>();
world.ChunkGenerated += (sender, e) => GenerateHeightMap(e.Chunk); world.ChunkGenerated += (sender, e) => GenerateHeightMap(e.Chunk);
world.ChunkLoaded += (sender, e) => GenerateHeightMap(e.Chunk); world.ChunkLoaded += (sender, e) => GenerateHeightMap(e.Chunk);
@ -72,7 +72,7 @@ namespace TrueCraft.Core.Lighting
if (id == 0) if (id == 0)
continue; continue;
var provider = BlockRepository.GetBlockProvider(id); var provider = BlockRepository.GetBlockProvider(id);
if (provider.LightOpacity != 0) if (provider == null || provider.LightOpacity != 0)
{ {
map[x, z] = y; map[x, z] = y;
break; break;
@ -252,33 +252,31 @@ namespace TrueCraft.Core.Lighting
public bool TryLightNext() public bool TryLightNext()
{ {
LightingOperation op; LightingOperation op;
lock (_Lock) if (PendingOperations.Count == 0)
{ return false;
if (PendingOperations.Count == 0) // TODO: Maybe a timeout or something?
return false; bool dequeued = false;
op = PendingOperations[0]; while (!(dequeued = PendingOperations.TryDequeue(out op)) && PendingOperations.Count > 0) ;
PendingOperations.RemoveAt(0); if (dequeued)
} LightBox(op);
LightBox(op); return dequeued;
return true;
} }
public void EnqueueOperation(BoundingBox box, bool skyLight, bool initial = false) public void EnqueueOperation(BoundingBox box, bool skyLight, bool initial = false)
{ {
lock (_Lock) // Try to merge with existing operation
/*
for (int i = PendingOperations.Count - 1; i > PendingOperations.Count - 5 && i > 0; i--)
{ {
// Try to merge with existing operation var op = PendingOperations[i];
for (int i = PendingOperations.Count - 1; i > PendingOperations.Count - 5 && i > 0; i--) if (op.Box.Intersects(box))
{ {
var op = PendingOperations[i]; op.Box = new BoundingBox(Vector3.Min(op.Box.Min, box.Min), Vector3.Max(op.Box.Max, box.Max));
if (op.Box.Intersects(box)) return;
{
op.Box = new BoundingBox(Vector3.Min(op.Box.Min, box.Min), Vector3.Max(op.Box.Max, box.Max));
return;
}
} }
PendingOperations.Add(new LightingOperation { SkyLight = skyLight, Box = box, Initial = initial });
} }
*/
PendingOperations.Enqueue(new LightingOperation { SkyLight = skyLight, Box = box, Initial = initial });
} }
private void SetUpperVoxels(IChunk chunk) private void SetUpperVoxels(IChunk chunk)

View File

@ -220,7 +220,7 @@ namespace TrueCraft.Core.TerrainGen
var coords = new Coordinates2D(x, z); var coords = new Coordinates2D(x, z);
double distance = IsSpawnCoordinate(x, z) ? coords.Distance : 1000; double distance = IsSpawnCoordinate(x, z) ? coords.Distance : 1000;
if (distance < 1000) // Avoids deep water within 1km sq of spawn if (distance < 1000) // Avoids deep water within 1km sq of spawn
value += (1 - distance / 1000f) * 12; value += (1 - distance / 1000f) * 18;
if (value < 0) if (value < 0)
value = GroundLevel; value = GroundLevel;
if (value > Chunk.Height) if (value > Chunk.Height)

View File

@ -25,13 +25,13 @@ namespace TrueCraft.Core.World
[NbtIgnore] [NbtIgnore]
public bool IsModified { get; set; } public bool IsModified { get; set; }
[NbtIgnore] [NbtIgnore]
public byte[] Blocks { get; set; } public byte[] Data { get; set; }
[NbtIgnore] [NbtIgnore]
public NibbleArray Metadata { get; set; } public NibbleSlice Metadata { get; set; }
[NbtIgnore] [NbtIgnore]
public NibbleArray BlockLight { get; set; } public NibbleSlice BlockLight { get; set; }
[NbtIgnore] [NbtIgnore]
public NibbleArray SkyLight { get; set; } public NibbleSlice SkyLight { get; set; }
public byte[] Biomes { get; set; } public byte[] Biomes { get; set; }
public int[] HeightMap { get; set; } public int[] HeightMap { get; set; }
public int MaxHeight { get; private set; } public int MaxHeight { get; private set; }
@ -73,7 +73,7 @@ namespace TrueCraft.Core.World
public bool TerrainPopulated { get; set; } public bool TerrainPopulated { get; set; }
[NbtIgnore] [NbtIgnore]
public Region ParentRegion { get; set; } public IRegion ParentRegion { get; set; }
public Chunk() public Chunk()
{ {
@ -83,23 +83,24 @@ namespace TrueCraft.Core.World
TerrainPopulated = false; TerrainPopulated = false;
LightPopulated = false; LightPopulated = false;
MaxHeight = 0; MaxHeight = 0;
const int size = Width * Height * Depth;
const int halfSize = size / 2;
Data = new byte[size + halfSize * 3];
Metadata = new NibbleSlice(Data, size, halfSize);
BlockLight = new NibbleSlice(Data, size + halfSize, halfSize);
SkyLight = new NibbleSlice(Data, size + halfSize * 2, halfSize);
} }
public Chunk(Coordinates2D coordinates) : this() public Chunk(Coordinates2D coordinates) : this()
{ {
X = coordinates.X; X = coordinates.X;
Z = coordinates.Z; Z = coordinates.Z;
const int size = Width * Height * Depth;
Blocks = new byte[size];
Metadata = new NibbleArray(size);
BlockLight = new NibbleArray(size);
SkyLight = new NibbleArray(size);
} }
public byte GetBlockID(Coordinates3D coordinates) public byte GetBlockID(Coordinates3D coordinates)
{ {
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width); int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
return Blocks[index]; return Data[index];
} }
public byte GetMetadata(Coordinates3D coordinates) public byte GetMetadata(Coordinates3D coordinates)
@ -127,8 +128,9 @@ namespace TrueCraft.Core.World
public void SetBlockID(Coordinates3D coordinates, byte value) public void SetBlockID(Coordinates3D coordinates, byte value)
{ {
IsModified = true; IsModified = true;
ParentRegion.DamageChunk(Coordinates);
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width); int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
Blocks[index] = value; Data[index] = value;
if (value == AirBlock.BlockID) if (value == AirBlock.BlockID)
Metadata[index] = 0x0; Metadata[index] = 0x0;
var oldHeight = GetHeight((byte)coordinates.X, (byte)coordinates.Z); var oldHeight = GetHeight((byte)coordinates.X, (byte)coordinates.Z);
@ -164,6 +166,7 @@ namespace TrueCraft.Core.World
public void SetMetadata(Coordinates3D coordinates, byte value) public void SetMetadata(Coordinates3D coordinates, byte value)
{ {
IsModified = true; IsModified = true;
ParentRegion.DamageChunk(Coordinates);
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width); int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
Metadata[index] = value; Metadata[index] = value;
} }
@ -175,6 +178,7 @@ namespace TrueCraft.Core.World
public void SetSkyLight(Coordinates3D coordinates, byte value) public void SetSkyLight(Coordinates3D coordinates, byte value)
{ {
IsModified = true; IsModified = true;
ParentRegion.DamageChunk(Coordinates);
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width); int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
SkyLight[index] = value; SkyLight[index] = value;
} }
@ -186,6 +190,7 @@ namespace TrueCraft.Core.World
public void SetBlockLight(Coordinates3D coordinates, byte value) public void SetBlockLight(Coordinates3D coordinates, byte value)
{ {
IsModified = true; IsModified = true;
ParentRegion.DamageChunk(Coordinates);
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width); int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
BlockLight[index] = value; BlockLight[index] = value;
} }
@ -210,6 +215,7 @@ namespace TrueCraft.Core.World
else else
TileEntities[coordinates] = value; TileEntities[coordinates] = value;
IsModified = true; IsModified = true;
ParentRegion.DamageChunk(Coordinates);
} }
/// <summary> /// <summary>
@ -236,7 +242,7 @@ namespace TrueCraft.Core.World
for (y = Chunk.Height - 1; y >= 0; y--) for (y = Chunk.Height - 1; y >= 0; y--)
{ {
int index = y + (z * Height) + (x * Height * Width); int index = y + (z * Height) + (x * Height * Width);
if (Blocks[index] != 0) if (Data[index] != 0)
{ {
SetHeight(x, z, y); SetHeight(x, z, y);
if (y > MaxHeight) if (y > MaxHeight)
@ -275,10 +281,10 @@ namespace TrueCraft.Core.World
chunk.Add(new NbtInt("Z", Z)); chunk.Add(new NbtInt("Z", Z));
chunk.Add(new NbtByte("LightPopulated", (byte)(LightPopulated ? 1 : 0))); chunk.Add(new NbtByte("LightPopulated", (byte)(LightPopulated ? 1 : 0)));
chunk.Add(new NbtByte("TerrainPopulated", (byte)(TerrainPopulated ? 1 : 0))); chunk.Add(new NbtByte("TerrainPopulated", (byte)(TerrainPopulated ? 1 : 0)));
chunk.Add(new NbtByteArray("Blocks", Blocks)); chunk.Add(new NbtByteArray("Blocks", Data));
chunk.Add(new NbtByteArray("Data", Metadata.Data)); chunk.Add(new NbtByteArray("Data", Metadata.ToArray()));
chunk.Add(new NbtByteArray("SkyLight", SkyLight.Data)); chunk.Add(new NbtByteArray("SkyLight", SkyLight.ToArray()));
chunk.Add(new NbtByteArray("BlockLight", BlockLight.Data)); chunk.Add(new NbtByteArray("BlockLight", BlockLight.ToArray()));
var tiles = new NbtList("TileEntities", NbtTagType.Compound); var tiles = new NbtList("TileEntities", NbtTagType.Compound);
foreach (var kvp in TileEntities) foreach (var kvp in TileEntities)
@ -308,13 +314,17 @@ namespace TrueCraft.Core.World
TerrainPopulated = tag["TerrainPopulated"].ByteValue > 0; TerrainPopulated = tag["TerrainPopulated"].ByteValue > 0;
if (tag.Contains("LightPopulated")) if (tag.Contains("LightPopulated"))
LightPopulated = tag["LightPopulated"].ByteValue > 0; LightPopulated = tag["LightPopulated"].ByteValue > 0;
Blocks = tag["Blocks"].ByteArrayValue; const int size = Width * Height * Depth;
Metadata = new NibbleArray(); const int halfSize = size / 2;
Metadata.Data = tag["Data"].ByteArrayValue; Data = new byte[(int)(size * 2.5)];
BlockLight = new NibbleArray(); Buffer.BlockCopy(tag["Blocks"].ByteArrayValue, 0, Data, 0, size);
BlockLight.Data = tag["BlockLight"].ByteArrayValue; Metadata = new NibbleSlice(Data, size, halfSize);
SkyLight = new NibbleArray(); BlockLight = new NibbleSlice(Data, size + halfSize, halfSize);
SkyLight.Data = tag["SkyLight"].ByteArrayValue; SkyLight = new NibbleSlice(Data, size + halfSize * 2, halfSize);
Metadata.Deserialize(tag["Data"]);
BlockLight.Deserialize(tag["BlockLight"]);
SkyLight.Deserialize(tag["SkyLight"]);
if (tag.Contains("TileEntities")) if (tag.Contains("TileEntities"))
{ {

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -20,10 +21,11 @@ namespace TrueCraft.Core.World
// In chunks // In chunks
public const int Width = 32, Depth = 32; public const int Width = 32, Depth = 32;
private ConcurrentDictionary<Coordinates2D, IChunk> _Chunks { get; set; }
/// <summary> /// <summary>
/// The currently loaded chunk list. /// The currently loaded chunk list.
/// </summary> /// </summary>
public IDictionary<Coordinates2D, IChunk> Chunks { get; set; } public IDictionary<Coordinates2D, IChunk> Chunks { get { return _Chunks; } }
/// <summary> /// <summary>
/// The location of this region in the overworld. /// The location of this region in the overworld.
/// </summary> /// </summary>
@ -31,6 +33,7 @@ namespace TrueCraft.Core.World
public World World { get; set; } public World World { get; set; }
private HashSet<Coordinates2D> DirtyChunks { get; set; } = new HashSet<Coordinates2D>();
private Stream regionFile { get; set; } private Stream regionFile { get; set; }
private object streamLock = new object(); private object streamLock = new object();
@ -40,7 +43,7 @@ namespace TrueCraft.Core.World
/// </summary> /// </summary>
public Region(Coordinates2D position, World world) public Region(Coordinates2D position, World world)
{ {
Chunks = new Dictionary<Coordinates2D, IChunk>(); _Chunks = new ConcurrentDictionary<Coordinates2D, IChunk>();
Position = position; Position = position;
World = world; World = world;
} }
@ -51,7 +54,10 @@ namespace TrueCraft.Core.World
public Region(Coordinates2D position, World world, string file) : this(position, world) public Region(Coordinates2D position, World world, string file) : this(position, world)
{ {
if (File.Exists(file)) if (File.Exists(file))
{
regionFile = File.Open(file, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); regionFile = File.Open(file, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
regionFile.Read(HeaderCache, 0, 8192);
}
else else
{ {
regionFile = File.Open(file, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); regionFile = File.Open(file, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite);
@ -59,6 +65,13 @@ namespace TrueCraft.Core.World
} }
} }
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));
}
/// <summary> /// <summary>
/// Retrieves the requested chunk from the region, or /// Retrieves the requested chunk from the region, or
/// generates it if a world generator is provided. /// generates it if a world generator is provided.
@ -66,25 +79,24 @@ namespace TrueCraft.Core.World
/// <param name="position">The position of the requested local chunk coordinates.</param> /// <param name="position">The position of the requested local chunk coordinates.</param>
public IChunk GetChunk(Coordinates2D position, bool generate = true) public IChunk GetChunk(Coordinates2D position, bool generate = true)
{ {
// TODO: This could use some refactoring
if (!Chunks.ContainsKey(position)) if (!Chunks.ContainsKey(position))
{ {
if (regionFile != null) if (regionFile != null)
{ {
// Search the stream for that region // 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) lock (streamLock)
{ {
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];
}
regionFile.Seek(chunkData.Item1, SeekOrigin.Begin); regionFile.Seek(chunkData.Item1, SeekOrigin.Begin);
/*int length = */ /*int length = */
new MinecraftStream(regionFile).ReadInt32(); // TODO: Avoid making new objects here, and in the WriteInt32 new MinecraftStream(regionFile).ReadInt32(); // TODO: Avoid making new objects here, and in the WriteInt32
@ -97,6 +109,7 @@ namespace TrueCraft.Core.World
var nbt = new NbtFile(); var nbt = new NbtFile();
nbt.LoadFromStream(regionFile, NbtCompression.ZLib, null); nbt.LoadFromStream(regionFile, NbtCompression.ZLib, null);
var chunk = Chunk.FromNbt(nbt); var chunk = Chunk.FromNbt(nbt);
chunk.ParentRegion = this;
Chunks.Add(position, chunk); Chunks.Add(position, chunk);
World.OnChunkLoaded(new ChunkLoadedEventArgs(chunk)); World.OnChunkLoaded(new ChunkLoadedEventArgs(chunk));
break; break;
@ -106,7 +119,7 @@ namespace TrueCraft.Core.World
} }
} }
else if (World.ChunkProvider == null) else if (World.ChunkProvider == null)
throw new ArgumentException("The requested chunk is not loaded.", "position"); throw new ArgumentException("The requested chunk is not loaded.", nameof(position));
else else
{ {
if (generate) if (generate)
@ -124,7 +137,9 @@ namespace TrueCraft.Core.World
var chunk = World.ChunkProvider.GenerateChunk(World, globalPosition); var chunk = World.ChunkProvider.GenerateChunk(World, globalPosition);
chunk.IsModified = true; chunk.IsModified = true;
chunk.Coordinates = globalPosition; chunk.Coordinates = globalPosition;
Chunks.Add(position, chunk); chunk.ParentRegion = this;
DirtyChunks.Add(position);
Chunks[position] = chunk;
World.OnChunkGenerated(new ChunkLoadedEventArgs(chunk)); World.OnChunkGenerated(new ChunkLoadedEventArgs(chunk));
} }
@ -136,6 +151,8 @@ namespace TrueCraft.Core.World
if (!Chunks.ContainsKey(position)) if (!Chunks.ContainsKey(position))
Chunks.Add(position, chunk); Chunks.Add(position, chunk);
chunk.IsModified = true; chunk.IsModified = true;
DirtyChunks.Add(position);
chunk.ParentRegion = this;
Chunks[position] = chunk; Chunks[position] = chunk;
} }
@ -162,17 +179,19 @@ namespace TrueCraft.Core.World
lock (streamLock) lock (streamLock)
{ {
var toRemove = new List<Coordinates2D>(); var toRemove = new List<Coordinates2D>();
foreach (var kvp in Chunks) var chunks = DirtyChunks.ToList();
DirtyChunks.Clear();
foreach (var coords in chunks)
{ {
var chunk = kvp.Value; var chunk = GetChunk(coords, generate: false);
if (chunk.IsModified) if (chunk.IsModified)
{ {
var data = ((Chunk)chunk).ToNbt(); var data = ((Chunk)chunk).ToNbt();
byte[] raw = data.SaveToBuffer(NbtCompression.ZLib); byte[] raw = data.SaveToBuffer(NbtCompression.ZLib);
var header = GetChunkFromTable(kvp.Key); var header = GetChunkFromTable(coords);
if (header == null || header.Item2 > raw.Length) if (header == null || header.Item2 > raw.Length)
header = AllocateNewChunks(kvp.Key, raw.Length); header = AllocateNewChunks(coords, raw.Length);
regionFile.Seek(header.Item1, SeekOrigin.Begin); regionFile.Seek(header.Item1, SeekOrigin.Begin);
new MinecraftStream(regionFile).WriteInt32(raw.Length); new MinecraftStream(regionFile).WriteInt32(raw.Length);
@ -182,7 +201,7 @@ namespace TrueCraft.Core.World
chunk.IsModified = false; chunk.IsModified = false;
} }
if ((DateTime.UtcNow - chunk.LastAccessed).TotalMinutes > 5) if ((DateTime.UtcNow - chunk.LastAccessed).TotalMinutes > 5)
toRemove.Add(kvp.Key); toRemove.Add(coords);
} }
regionFile.Flush(); regionFile.Flush();
// Unload idle chunks // Unload idle chunks
@ -198,14 +217,15 @@ namespace TrueCraft.Core.World
#region Stream Helpers #region Stream Helpers
private const int ChunkSizeMultiplier = 4096; private const int ChunkSizeMultiplier = 4096;
private byte[] HeaderCache = new byte[8192];
private Tuple<int, int> GetChunkFromTable(Coordinates2D position) // <offset, length> private Tuple<int, int> GetChunkFromTable(Coordinates2D position) // <offset, length>
{ {
int tableOffset = ((position.X % Width) + (position.Z % Depth) * Width) * 4; int tableOffset = ((position.X % Width) + (position.Z % Depth) * Width) * 4;
regionFile.Seek(tableOffset, SeekOrigin.Begin);
byte[] offsetBuffer = new byte[4]; byte[] offsetBuffer = new byte[4];
regionFile.Read(offsetBuffer, 0, 3); Buffer.BlockCopy(HeaderCache, tableOffset, offsetBuffer, 0, 3);
Array.Reverse(offsetBuffer); Array.Reverse(offsetBuffer);
int length = regionFile.ReadByte(); int length = HeaderCache[tableOffset + 3];
int offset = BitConverter.ToInt32(offsetBuffer, 0) << 4; int offset = BitConverter.ToInt32(offsetBuffer, 0) << 4;
if (offset == 0 || length == 0) if (offset == 0 || length == 0)
return null; return null;
@ -215,7 +235,8 @@ namespace TrueCraft.Core.World
private void CreateRegionHeader() private void CreateRegionHeader()
{ {
regionFile.Write(new byte[8192], 0, 8192); HeaderCache = new byte[8192];
regionFile.Write(HeaderCache, 0, 8192);
regionFile.Flush(); regionFile.Flush();
} }
@ -237,6 +258,7 @@ namespace TrueCraft.Core.World
entry[0] = (byte)length; entry[0] = (byte)length;
Array.Reverse(entry); Array.Reverse(entry);
regionFile.Write(entry, 0, entry.Length); regionFile.Write(entry, 0, entry.Length);
Buffer.BlockCopy(entry, 0, HeaderCache, tableOffset, 4);
return new Tuple<int, int>(dataOffset, length * ChunkSizeMultiplier); return new Tuple<int, int>(dataOffset, length * ChunkSizeMultiplier);
} }

View File

@ -69,7 +69,8 @@ namespace TrueCraft.Profiling
{ {
if (Match(EnabledBuckets[i], timer.Bucket)) if (Match(EnabledBuckets[i], timer.Bucket))
{ {
Console.WriteLine("{0} took {1}ms", timer.Bucket, elapsed); Console.WriteLine("[@{0:0.00}s] {1} took {2}ms",
Stopwatch.ElapsedMilliseconds / 1000.0, timer.Bucket, elapsed);
break; break;
} }
} }

View File

@ -519,7 +519,7 @@ namespace TrueCraft.Commands
{ {
lighter.InitialLighting(chunk, true); lighter.InitialLighting(chunk, true);
(client as RemoteClient).UnloadChunk(chunk.Coordinates); (client as RemoteClient).UnloadChunk(chunk.Coordinates);
(client as RemoteClient).LoadChunk(chunk.Coordinates); (client as RemoteClient).LoadChunk(chunk);
} }
} }

View File

@ -79,7 +79,8 @@ namespace TrueCraft
(int)(entity.Position.Z) >> 4 != (int)(entity.OldPosition.Z) >> 4) (int)(entity.Position.Z) >> 4 != (int)(entity.OldPosition.Z) >> 4)
{ {
client.Log("Passed chunk boundary at {0}, {1}", (int)(entity.Position.X) >> 4, (int)(entity.Position.Z) >> 4); client.Log("Passed chunk boundary at {0}, {1}", (int)(entity.Position.X) >> 4, (int)(entity.Position.Z) >> 4);
Task.Factory.StartNew(client.UpdateChunks); Server.Scheduler.ScheduleEvent("client.update-chunks", client,
TimeSpan.Zero, s => client.UpdateChunks());
UpdateClientEntities(client); UpdateClientEntities(client);
} }
break; break;

View File

@ -1,10 +1,12 @@
using System; using System;
using System.Linq;
using TrueCraft.API.Server; using TrueCraft.API.Server;
using System.Collections.Generic; using System.Collections.Generic;
using TrueCraft.API; using TrueCraft.API;
using System.Diagnostics; using System.Diagnostics;
using TrueCraft.Profiling; using TrueCraft.Profiling;
using System.Threading; using System.Threading;
using System.Collections.Concurrent;
namespace TrueCraft namespace TrueCraft
{ {
@ -15,80 +17,122 @@ namespace TrueCraft
private IMultiplayerServer Server { get; set; } private IMultiplayerServer Server { get; set; }
private HashSet<IEventSubject> Subjects { get; set; } private HashSet<IEventSubject> Subjects { get; set; }
private Stopwatch Stopwatch { get; set; } private Stopwatch Stopwatch { get; set; }
private ConcurrentQueue<ScheduledEvent> ImmediateEventQueue { get; set; }
private ConcurrentQueue<ScheduledEvent> LaterEventQueue { get; set; }
private ConcurrentQueue<IEventSubject> DisposedSubjects { get; set; }
public HashSet<string> DisabledEvents { get; private set; }
public EventScheduler(IMultiplayerServer server) public EventScheduler(IMultiplayerServer server)
{ {
Events = new List<ScheduledEvent>(); Events = new List<ScheduledEvent>();
ImmediateEventQueue = new ConcurrentQueue<ScheduledEvent>();
LaterEventQueue = new ConcurrentQueue<ScheduledEvent>();
DisposedSubjects = new ConcurrentQueue<IEventSubject>();
Server = server; Server = server;
Subjects = new HashSet<IEventSubject>(); Subjects = new HashSet<IEventSubject>();
Stopwatch = new Stopwatch(); Stopwatch = new Stopwatch();
DisabledEvents = new HashSet<string>();
Stopwatch.Start(); Stopwatch.Start();
} }
private void ScheduleEvent(ScheduledEvent e)
{
int i;
for (i = 0; i < Events.Count; i++)
{
if (Events[i].When > e.When)
break;
}
Events.Insert(i, e);
}
public void ScheduleEvent(string name, IEventSubject subject, TimeSpan when, Action<IMultiplayerServer> action) public void ScheduleEvent(string name, IEventSubject subject, TimeSpan when, Action<IMultiplayerServer> action)
{ {
lock (EventLock) if (DisabledEvents.Contains(name))
return;
long _when = Stopwatch.ElapsedTicks + when.Ticks;
if (subject != null && !Subjects.Contains(subject))
{ {
long _when = Stopwatch.ElapsedTicks + when.Ticks; Subjects.Add(subject);
if (!Subjects.Contains(subject)) subject.Disposed += Subject_Disposed;
{
Subjects.Add(subject);
subject.Disposed += Subject_Disposed;
}
int i;
for (i = 0; i < Events.Count; i++)
{
if (Events[i].When > _when)
break;
}
Events.Insert(i, new ScheduledEvent
{
Name = name,
Subject = subject,
When = _when,
Action = action
});
} }
var queue = when.TotalSeconds > 3 ? LaterEventQueue : ImmediateEventQueue;
queue.Enqueue(new ScheduledEvent
{
Name = name,
Subject = subject,
When = _when,
Action = action
});
} }
void Subject_Disposed(object sender, EventArgs e) void Subject_Disposed(object sender, EventArgs e)
{ {
// Cancel all events with this subject DisposedSubjects.Enqueue((IEventSubject)sender);
lock (EventLock)
{
for (int i = 0; i < Events.Count; i++)
{
if (Events[i].Subject == sender)
{
Events.RemoveAt(i);
i--;
}
}
Subjects.Remove((IEventSubject)sender);
}
} }
public void Update() public void Update()
{ {
Profiler.Start("scheduler"); Profiler.Start("scheduler");
lock (EventLock) Profiler.Start("scheduler.receive-events");
long start = Stopwatch.ElapsedTicks;
long limit = Stopwatch.ElapsedMilliseconds + 10;
while (ImmediateEventQueue.Count > 0 && Stopwatch.ElapsedMilliseconds < limit)
{ {
var start = Stopwatch.ElapsedTicks; ScheduledEvent e;
for (int i = 0; i < Events.Count; i++) bool dequeued = false;
while (!(dequeued = ImmediateEventQueue.TryDequeue(out e))
&& Stopwatch.ElapsedMilliseconds < limit) ;
if (dequeued)
ScheduleEvent(e);
}
while (LaterEventQueue.Count > 0 && Stopwatch.ElapsedMilliseconds < limit)
{
ScheduledEvent e;
bool dequeued = false;
while (!(dequeued = LaterEventQueue.TryDequeue(out e))
&& Stopwatch.ElapsedMilliseconds < limit) ;
if (dequeued)
ScheduleEvent(e);
}
Profiler.Done();
Profiler.Start("scheduler.dispose-subjects");
while (DisposedSubjects.Count > 0 && Stopwatch.ElapsedMilliseconds < limit)
{
IEventSubject subject;
bool dequeued = false;
while (!(dequeued = DisposedSubjects.TryDequeue(out subject))
&& Stopwatch.ElapsedMilliseconds < limit) ;
if (dequeued)
{ {
var e = Events[i]; // Cancel all events with this subject
if (e.When <= start) for (int i = 0; i < Events.Count; i++)
{ {
Profiler.Start("scheduler." + e.Name); if (Events[i].Subject == subject)
e.Action(Server); {
Events.RemoveAt(i); Events.RemoveAt(i);
i--; i--;
Profiler.Done(); }
} }
if (e.When > start) Subjects.Remove(subject);
break; // List is sorted, we can exit early
} }
} }
limit = Stopwatch.ElapsedMilliseconds + 10;
Profiler.Done();
for (int i = 0; i < Events.Count && Stopwatch.ElapsedMilliseconds < limit; i++)
{
var e = Events[i];
if (e.When <= start)
{
Profiler.Start("scheduler." + e.Name);
e.Action(Server);
Events.RemoveAt(i);
i--;
Profiler.Done();
}
if (e.When > start)
break; // List is sorted, we can exit early
}
Profiler.Done(20); Profiler.Done(20);
} }

View File

@ -59,7 +59,8 @@ namespace TrueCraft.Handlers
// Send setup packets // Send setup packets
remoteClient.QueuePacket(new LoginResponsePacket(client.Entity.EntityID, 0, Dimension.Overworld)); remoteClient.QueuePacket(new LoginResponsePacket(client.Entity.EntityID, 0, Dimension.Overworld));
remoteClient.UpdateChunks(); server.Scheduler.ScheduleEvent("client.update-chunks", remoteClient,
TimeSpan.Zero, s => remoteClient.UpdateChunks());
remoteClient.QueuePacket(new WindowItemsPacket(0, remoteClient.Inventory.GetSlots())); remoteClient.QueuePacket(new WindowItemsPacket(0, remoteClient.Inventory.GetSlots()));
remoteClient.QueuePacket(new UpdateHealthPacket((remoteClient.Entity as PlayerEntity).Health)); remoteClient.QueuePacket(new UpdateHealthPacket((remoteClient.Entity as PlayerEntity).Health));
remoteClient.QueuePacket(new SpawnPositionPacket((int)remoteClient.Entity.Position.X, remoteClient.QueuePacket(new SpawnPositionPacket((int)remoteClient.Entity.Position.X,

View File

@ -125,6 +125,10 @@ namespace TrueCraft
public void Start(IPEndPoint endPoint) public void Start(IPEndPoint endPoint)
{ {
Scheduler.DisabledEvents.Clear();
if (Program.ServerConfiguration.DisabledEvents != null)
Program.ServerConfiguration.DisabledEvents.ToList().ForEach(
ev => Scheduler.DisabledEvents.Add(ev));
ShuttingDown = false; ShuttingDown = false;
Time.Reset(); Time.Reset();
Time.Start(); Time.Start();
@ -156,16 +160,6 @@ namespace TrueCraft
DisconnectClient(c); DisconnectClient(c);
} }
public void Pause()
{
EnvironmentWorker.Change(Timeout.Infinite, Timeout.Infinite);
}
public void Resume()
{
EnvironmentWorker.Change(0, Timeout.Infinite);
}
public void AddWorld(IWorld world) public void AddWorld(IWorld world)
{ {
Worlds.Add(world); Worlds.Add(world);
@ -183,7 +177,8 @@ namespace TrueCraft
void HandleChunkLoaded(object sender, ChunkLoadedEventArgs e) void HandleChunkLoaded(object sender, ChunkLoadedEventArgs e)
{ {
ChunksToSchedule.Add(new Tuple<IWorld, IChunk>(sender as IWorld, e.Chunk)); if (Program.ServerConfiguration.EnableEventLoading)
ChunksToSchedule.Add(new Tuple<IWorld, IChunk>(sender as IWorld, e.Chunk));
if (Program.ServerConfiguration.EnableLighting) if (Program.ServerConfiguration.EnableLighting)
{ {
var lighter = WorldLighters.SingleOrDefault(l => l.World == sender); var lighter = WorldLighters.SingleOrDefault(l => l.World == sender);
@ -234,9 +229,9 @@ namespace TrueCraft
} }
else else
{ {
for (int i = 0; i < e.Chunk.SkyLight.Data.Length; i++) for (int i = 0; i < e.Chunk.SkyLight.Length * 2; i++)
{ {
e.Chunk.SkyLight.Data[i] = 0xFF; e.Chunk.SkyLight[i] = 0xF;
} }
} }
HandleChunkLoaded(sender, e); HandleChunkLoaded(sender, e);
@ -427,11 +422,14 @@ namespace TrueCraft
Profiler.Done(); Profiler.Done();
} }
Profiler.Start("environment.chunks"); if (Program.ServerConfiguration.EnableEventLoading)
Tuple<IWorld, IChunk> t; {
if (ChunksToSchedule.TryTake(out t)) Profiler.Start("environment.chunks");
ScheduleUpdatesForChunk(t.Item1, t.Item2); Tuple<IWorld, IChunk> t;
Profiler.Done(); if (ChunksToSchedule.TryTake(out t))
ScheduleUpdatesForChunk(t.Item1, t.Item2);
Profiler.Done();
}
Profiler.Done(MillisecondsPerTick); Profiler.Done(MillisecondsPerTick);
long end = Time.ElapsedMilliseconds; long end = Time.ElapsedMilliseconds;

View File

@ -109,20 +109,24 @@ namespace TrueCraft
Server.ChatMessageReceived += HandleChatMessageReceived; Server.ChatMessageReceived += HandleChatMessageReceived;
Server.Start(new IPEndPoint(IPAddress.Parse(ServerConfiguration.ServerAddress), ServerConfiguration.ServerPort)); Server.Start(new IPEndPoint(IPAddress.Parse(ServerConfiguration.ServerAddress), ServerConfiguration.ServerPort));
Console.CancelKeyPress += HandleCancelKeyPress; Console.CancelKeyPress += HandleCancelKeyPress;
Server.Scheduler.ScheduleEvent("world.save", null,
TimeSpan.FromSeconds(ServerConfiguration.WorldSaveInterval), SaveWorlds);
while (true) while (true)
{ {
Thread.Sleep(1000 * ServerConfiguration.WorldSaveInterval); Thread.Yield();
Server.Pause();
Server.Log(LogCategory.Notice, "Saving world...");
foreach (var w in Server.Worlds)
{
w.Save();
}
Server.Log(LogCategory.Notice, "Done.");
Server.Resume();
} }
} }
static void SaveWorlds(IMultiplayerServer server)
{
Server.Log(LogCategory.Notice, "Saving world...");
foreach (var w in Server.Worlds)
w.Save();
Server.Log(LogCategory.Notice, "Done.");
server.Scheduler.ScheduleEvent("world.save", null,
TimeSpan.FromSeconds(ServerConfiguration.WorldSaveInterval), SaveWorlds);
}
static void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e) static void HandleCancelKeyPress(object sender, ConsoleCancelEventArgs e)
{ {
Server.Stop(); Server.Stop();

View File

@ -22,6 +22,7 @@ using fNbt;
using TrueCraft.API.Logging; using TrueCraft.API.Logging;
using TrueCraft.API.Logic; using TrueCraft.API.Logic;
using TrueCraft.Exceptions; using TrueCraft.Exceptions;
using TrueCraft.Profiling;
namespace TrueCraft namespace TrueCraft
{ {
@ -29,7 +30,7 @@ namespace TrueCraft
{ {
public RemoteClient(IMultiplayerServer server, IPacketReader packetReader, PacketHandler[] packetHandlers, Socket connection) public RemoteClient(IMultiplayerServer server, IPacketReader packetReader, PacketHandler[] packetHandlers, Socket connection)
{ {
LoadedChunks = new List<Coordinates2D>(); LoadedChunks = new HashSet<Coordinates2D>();
Server = server; Server = server;
Inventory = new InventoryWindow(server.CraftingRepository); Inventory = new InventoryWindow(server.CraftingRepository);
InventoryWindow.WindowChange += HandleWindowChange; InventoryWindow.WindowChange += HandleWindowChange;
@ -145,7 +146,7 @@ namespace TrueCraft
} }
internal int ChunkRadius { get; set; } internal int ChunkRadius { get; set; }
internal IList<Coordinates2D> LoadedChunks { get; set; } internal HashSet<Coordinates2D> LoadedChunks { get; set; }
public bool DataAvailable public bool DataAvailable
{ {
@ -398,8 +399,10 @@ namespace TrueCraft
if (ChunkRadius < 8) // TODO: Allow customization of this number if (ChunkRadius < 8) // TODO: Allow customization of this number
{ {
ChunkRadius++; ChunkRadius++;
UpdateChunks(); server.Scheduler.ScheduleEvent("client.update-chunks", this,
server.Scheduler.ScheduleEvent("remote.chunks", this, TimeSpan.FromSeconds(1), ExpandChunkRadius); TimeSpan.Zero, s => UpdateChunks());
server.Scheduler.ScheduleEvent("remote.chunks", this,
TimeSpan.FromSeconds(1), ExpandChunkRadius);
} }
}); });
} }
@ -412,67 +415,77 @@ namespace TrueCraft
internal void UpdateChunks() internal void UpdateChunks()
{ {
var newChunks = new List<Coordinates2D>(); var newChunks = new HashSet<Coordinates2D>();
var toLoad = new List<Tuple<Coordinates2D, IChunk>>();
Profiler.Start("client.new-chunks");
for (int x = -ChunkRadius; x < ChunkRadius; x++) for (int x = -ChunkRadius; x < ChunkRadius; x++)
{ {
for (int z = -ChunkRadius; z < ChunkRadius; z++) for (int z = -ChunkRadius; z < ChunkRadius; z++)
{ {
newChunks.Add(new Coordinates2D( var coords = new Coordinates2D(
((int)Entity.Position.X >> 4) + x, ((int)Entity.Position.X >> 4) + x,
((int)Entity.Position.Z >> 4) + z)); ((int)Entity.Position.Z >> 4) + z);
newChunks.Add(coords);
if (!LoadedChunks.Contains(coords))
toLoad.Add(new Tuple<Coordinates2D, IChunk>(
coords, World.GetChunk(coords, generate: false)));
} }
} }
// Unload extraneous columns Profiler.Done();
lock (LoadedChunks) Task.Factory.StartNew(() =>
{ {
var currentChunks = new List<Coordinates2D>(LoadedChunks); Profiler.Start("client.encode-chunks");
foreach (Coordinates2D chunk in currentChunks) foreach (var tup in toLoad)
{ {
if (!newChunks.Contains(chunk)) var coords = tup.Item1;
UnloadChunk(chunk); var chunk = tup.Item2;
if (chunk == null)
chunk = World.GetChunk(coords);
chunk.LastAccessed = DateTime.UtcNow;
LoadChunk(chunk);
} }
// Load new columns Profiler.Done();
foreach (Coordinates2D chunk in newChunks) });
{ Profiler.Start("client.old-chunks");
if (!LoadedChunks.Contains(chunk)) LoadedChunks.IntersectWith(newChunks);
LoadChunk(chunk); Profiler.Done();
} Profiler.Start("client.update-entities");
}
((EntityManager)Server.GetEntityManagerForWorld(World)).UpdateClientEntities(this); ((EntityManager)Server.GetEntityManagerForWorld(World)).UpdateClientEntities(this);
Profiler.Done();
} }
internal void UnloadAllChunks() internal void UnloadAllChunks()
{ {
lock (LoadedChunks) while (LoadedChunks.Any())
{ {
while (LoadedChunks.Any()) UnloadChunk(LoadedChunks.First());
{
UnloadChunk(LoadedChunks[0]);
}
} }
} }
internal void LoadChunk(Coordinates2D position) internal void LoadChunk(IChunk chunk)
{ {
var chunk = World.GetChunk(position);
chunk.LastAccessed = DateTime.UtcNow;
QueuePacket(new ChunkPreamblePacket(chunk.Coordinates.X, chunk.Coordinates.Z)); QueuePacket(new ChunkPreamblePacket(chunk.Coordinates.X, chunk.Coordinates.Z));
QueuePacket(CreatePacket(chunk)); QueuePacket(CreatePacket(chunk));
LoadedChunks.Add(position); Server.Scheduler.ScheduleEvent("client.finalize-chunks", this,
foreach (var kvp in chunk.TileEntities) TimeSpan.Zero, server =>
{
var coords = kvp.Key;
var descriptor = new BlockDescriptor
{ {
Coordinates = coords + new Coordinates3D(chunk.X, 0, chunk.Z), return;
Metadata = chunk.GetMetadata(coords), LoadedChunks.Add(chunk.Coordinates);
ID = chunk.GetBlockID(coords), foreach (var kvp in chunk.TileEntities)
BlockLight = chunk.GetBlockLight(coords), {
SkyLight = chunk.GetSkyLight(coords) var coords = kvp.Key;
}; var descriptor = new BlockDescriptor
var provider = Server.BlockRepository.GetBlockProvider(descriptor.ID); {
provider.TileEntityLoadedForClient(descriptor, World, kvp.Value, this); Coordinates = coords + new Coordinates3D(chunk.X, 0, chunk.Z),
} Metadata = chunk.GetMetadata(coords),
ID = chunk.GetBlockID(coords),
BlockLight = chunk.GetBlockLight(coords),
SkyLight = chunk.GetSkyLight(coords)
};
var provider = Server.BlockRepository.GetBlockProvider(descriptor.ID);
provider.TileEntityLoadedForClient(descriptor, World, kvp.Value, this);
}
});
} }
internal void UnloadChunk(Coordinates2D position) internal void UnloadChunk(Coordinates2D position)
@ -511,19 +524,20 @@ namespace TrueCraft
var X = chunk.Coordinates.X; var X = chunk.Coordinates.X;
var Z = chunk.Coordinates.Z; var Z = chunk.Coordinates.Z;
const int blocksPerChunk = Chunk.Width * Chunk.Height * Chunk.Depth; Profiler.Start("client.encode-chunks.compress");
const int bytesPerChunk = (int)(blocksPerChunk * 2.5); byte[] result;
using (var ms = new MemoryStream())
byte[] data = new byte[bytesPerChunk]; {
using (var deflate = new ZlibStream(new MemoryStream(chunk.Data),
Buffer.BlockCopy(chunk.Blocks, 0, data, 0, chunk.Blocks.Length); CompressionMode.Compress,
Buffer.BlockCopy(chunk.Metadata.Data, 0, data, chunk.Blocks.Length, chunk.Metadata.Data.Length); CompressionLevel.BestSpeed))
Buffer.BlockCopy(chunk.BlockLight.Data, 0, data, chunk.Blocks.Length + chunk.Metadata.Data.Length, chunk.BlockLight.Data.Length); deflate.CopyTo(ms);
Buffer.BlockCopy(chunk.SkyLight.Data, 0, data, chunk.Blocks.Length + chunk.Metadata.Data.Length result = ms.ToArray();
+ chunk.BlockLight.Data.Length, chunk.SkyLight.Data.Length); }
Profiler.Done();
var result = ZlibStream.CompressBuffer(data);
return new ChunkDataPacket(X * Chunk.Width, 0, Z * Chunk.Depth, Chunk.Width, Chunk.Height, Chunk.Depth, result); return new ChunkDataPacket(X * Chunk.Width, 0, Z * Chunk.Depth,
Chunk.Width, Chunk.Height, Chunk.Depth, result);
} }
public void Dispose() public void Dispose()

View File

@ -48,6 +48,8 @@ namespace TrueCraft
Query = true; Query = true;
QueryPort = 25566; QueryPort = 25566;
EnableLighting = true; EnableLighting = true;
EnableEventLoading = true;
DisabledEvents = new string[0];
} }
[YamlMember(Alias = "motd")] [YamlMember(Alias = "motd")]
@ -76,5 +78,11 @@ namespace TrueCraft
[YamlMember(Alias = "enable-lighting")] [YamlMember(Alias = "enable-lighting")]
public bool EnableLighting { get; set; } public bool EnableLighting { get; set; }
[YamlMember(Alias = "enable-event-loading")]
public bool EnableEventLoading { get; set; }
[YamlMember(Alias = "disable-events")]
public string[] DisabledEvents { get; set; }
} }
} }

View File

@ -34,12 +34,12 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="Ionic.Zip.Reduced">
<HintPath>..\lib\Ionic.Zip.Reduced.dll</HintPath>
</Reference>
<Reference Include="YamlDotNet"> <Reference Include="YamlDotNet">
<HintPath>..\packages\YamlDotNet.3.9.0\lib\net35\YamlDotNet.dll</HintPath> <HintPath>..\packages\YamlDotNet.3.9.0\lib\net35\YamlDotNet.dll</HintPath>
</Reference> </Reference>
<Reference Include="DotNetZip">
<HintPath>..\packages\DotNetZip.1.10.1\lib\net20\DotNetZip.dll</HintPath>
</Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="AccessConfiguration.cs" /> <Compile Include="AccessConfiguration.cs" />

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="DotNetZip" version="1.10.1" targetFramework="net45" />
<package id="YamlDotNet" version="3.9.0" targetFramework="net45" /> <package id="YamlDotNet" version="3.9.0" targetFramework="net45" />
</packages> </packages>