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:
parent
362c852f51
commit
6fb8ee7ba5
@ -29,7 +29,7 @@ namespace TrueCraft.API
|
||||
config = new T();
|
||||
}
|
||||
|
||||
var serializer = new Serializer();
|
||||
var serializer = new Serializer(SerializationOptions.EmitDefaults);
|
||||
using (var writer = new StreamWriter(configFileName))
|
||||
serializer.Serialize(writer, config);
|
||||
|
||||
|
@ -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); }
|
||||
}
|
||||
}
|
||||
}
|
82
TrueCraft.API/NibbleSlice.cs
Normal file
82
TrueCraft.API/NibbleSlice.cs
Normal 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); }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TrueCraft.API.Server
|
||||
{
|
||||
public interface IEventScheduler
|
||||
{
|
||||
HashSet<string> DisabledEvents { get; }
|
||||
/// <summary>
|
||||
/// Schedules an event to occur some time in the future.
|
||||
/// </summary>
|
||||
|
@ -88,7 +88,7 @@
|
||||
<Compile Include="World\IWorld.cs" />
|
||||
<Compile Include="World\IChunk.cs" />
|
||||
<Compile Include="World\IChunkProvider.cs" />
|
||||
<Compile Include="NibbleArray.cs" />
|
||||
<Compile Include="NibbleSlice.cs" />
|
||||
<Compile Include="World\IRegion.cs" />
|
||||
<Compile Include="Biome.cs" />
|
||||
<Compile Include="Logging\LogCategory.cs" />
|
||||
|
@ -15,12 +15,13 @@ namespace TrueCraft.API.World
|
||||
int[] HeightMap { get; }
|
||||
byte[] Biomes { get; }
|
||||
DateTime LastAccessed { get; set; }
|
||||
byte[] Blocks { get; }
|
||||
byte[] Data { get; }
|
||||
bool TerrainPopulated { get; set; }
|
||||
Dictionary<Coordinates3D, NbtCompound> TileEntities { get; set; }
|
||||
NibbleArray Metadata { get; }
|
||||
NibbleArray BlockLight { get; }
|
||||
NibbleArray SkyLight { get; }
|
||||
NibbleSlice Metadata { get; }
|
||||
NibbleSlice BlockLight { get; }
|
||||
NibbleSlice SkyLight { get; }
|
||||
IRegion ParentRegion { get; set; }
|
||||
int GetHeight(byte x, byte z);
|
||||
void UpdateHeightMap();
|
||||
byte GetBlockID(Coordinates3D coordinates);
|
||||
|
@ -9,6 +9,10 @@ namespace TrueCraft.API.World
|
||||
Coordinates2D Position { get; }
|
||||
|
||||
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 Save(string path);
|
||||
}
|
||||
|
@ -53,14 +53,14 @@ namespace TrueCraft.Client.Handlers
|
||||
&& packet.Depth == Chunk.Depth) // Fast path
|
||||
{
|
||||
// Block IDs
|
||||
Buffer.BlockCopy(data, 0, chunk.Blocks, 0, chunk.Blocks.Length);
|
||||
Buffer.BlockCopy(data, 0, chunk.Data, 0, chunk.Data.Length);
|
||||
// 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
|
||||
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);
|
||||
// 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);
|
||||
}
|
||||
else // Slow path
|
||||
|
@ -116,7 +116,7 @@ namespace TrueCraft.Client
|
||||
public int X { get { return Chunk.X; } }
|
||||
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 BlockLight { get { return new ReadOnlyNibbleArray(Chunk.BlockLight); } }
|
||||
public ReadOnlyNibbleArray SkyLight { get { return new ReadOnlyNibbleArray(Chunk.SkyLight); } }
|
||||
|
@ -12,51 +12,80 @@ namespace TrueCraft.Core.Test.World
|
||||
[TestFixture]
|
||||
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]
|
||||
public void TestGetBlockID()
|
||||
{
|
||||
Assert.AreEqual(BedrockBlock.BlockID, Chunk.GetBlockID(Coordinates3D.Zero));
|
||||
Chunk.SetBlockID(Coordinates3D.Zero, 12);
|
||||
Assert.AreEqual(12, Chunk.GetBlockID(Coordinates3D.Zero));
|
||||
Chunk.SetBlockID(Coordinates3D.Zero, BedrockBlock.BlockID);
|
||||
var chunk = new Chunk();
|
||||
chunk.SetBlockID(Coordinates3D.Zero, 12);
|
||||
Assert.AreEqual(12, chunk.GetBlockID(Coordinates3D.Zero));
|
||||
}
|
||||
|
||||
[Test]
|
||||
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]
|
||||
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]
|
||||
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]
|
||||
public void TestHeightMap()
|
||||
{
|
||||
Chunk.UpdateHeightMap();
|
||||
Assert.AreEqual(59, Chunk.GetHeight(0, 0));
|
||||
Assert.AreEqual(58, Chunk.GetHeight(1, 0));
|
||||
Chunk.SetBlockID(new Coordinates3D(1, 80, 0), 1);
|
||||
Assert.AreEqual(80, Chunk.GetHeight(1, 0));
|
||||
var chunk = new Chunk();
|
||||
for (int x = 0; x < Chunk.Width; ++x)
|
||||
for (int z = 0; z < Chunk.Width; ++z)
|
||||
chunk.SetBlockID(new Coordinates3D(x, 20, z), StoneBlock.BlockID);
|
||||
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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ using TrueCraft.API;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using TrueCraft.Profiling;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace TrueCraft.Core.Lighting
|
||||
{
|
||||
@ -34,15 +35,14 @@ namespace TrueCraft.Core.Lighting
|
||||
public IBlockRepository BlockRepository { get; set; }
|
||||
public IWorld World { get; set; }
|
||||
|
||||
private object _Lock = new object();
|
||||
private List<LightingOperation> PendingOperations { get; set; }
|
||||
private ConcurrentQueue<LightingOperation> PendingOperations { get; set; }
|
||||
private Dictionary<Coordinates2D, byte[,]> HeightMaps { get; set; }
|
||||
|
||||
public WorldLighting(IWorld world, IBlockRepository blockRepository)
|
||||
{
|
||||
BlockRepository = blockRepository;
|
||||
World = world;
|
||||
PendingOperations = new List<LightingOperation>();
|
||||
PendingOperations = new ConcurrentQueue<LightingOperation>();
|
||||
HeightMaps = new Dictionary<Coordinates2D, byte[,]>();
|
||||
world.ChunkGenerated += (sender, e) => GenerateHeightMap(e.Chunk);
|
||||
world.ChunkLoaded += (sender, e) => GenerateHeightMap(e.Chunk);
|
||||
@ -72,7 +72,7 @@ namespace TrueCraft.Core.Lighting
|
||||
if (id == 0)
|
||||
continue;
|
||||
var provider = BlockRepository.GetBlockProvider(id);
|
||||
if (provider.LightOpacity != 0)
|
||||
if (provider == null || provider.LightOpacity != 0)
|
||||
{
|
||||
map[x, z] = y;
|
||||
break;
|
||||
@ -252,33 +252,31 @@ namespace TrueCraft.Core.Lighting
|
||||
public bool TryLightNext()
|
||||
{
|
||||
LightingOperation op;
|
||||
lock (_Lock)
|
||||
{
|
||||
if (PendingOperations.Count == 0)
|
||||
return false;
|
||||
op = PendingOperations[0];
|
||||
PendingOperations.RemoveAt(0);
|
||||
}
|
||||
LightBox(op);
|
||||
return true;
|
||||
if (PendingOperations.Count == 0)
|
||||
return false;
|
||||
// TODO: Maybe a timeout or something?
|
||||
bool dequeued = false;
|
||||
while (!(dequeued = PendingOperations.TryDequeue(out op)) && PendingOperations.Count > 0) ;
|
||||
if (dequeued)
|
||||
LightBox(op);
|
||||
return dequeued;
|
||||
}
|
||||
|
||||
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
|
||||
for (int i = PendingOperations.Count - 1; i > PendingOperations.Count - 5 && i > 0; i--)
|
||||
var op = PendingOperations[i];
|
||||
if (op.Box.Intersects(box))
|
||||
{
|
||||
var op = PendingOperations[i];
|
||||
if (op.Box.Intersects(box))
|
||||
{
|
||||
op.Box = new BoundingBox(Vector3.Min(op.Box.Min, box.Min), Vector3.Max(op.Box.Max, box.Max));
|
||||
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)
|
||||
|
@ -220,7 +220,7 @@ namespace TrueCraft.Core.TerrainGen
|
||||
var coords = new Coordinates2D(x, z);
|
||||
double distance = IsSpawnCoordinate(x, z) ? coords.Distance : 1000;
|
||||
if (distance < 1000) // Avoids deep water within 1km sq of spawn
|
||||
value += (1 - distance / 1000f) * 12;
|
||||
value += (1 - distance / 1000f) * 18;
|
||||
if (value < 0)
|
||||
value = GroundLevel;
|
||||
if (value > Chunk.Height)
|
||||
|
@ -25,13 +25,13 @@ namespace TrueCraft.Core.World
|
||||
[NbtIgnore]
|
||||
public bool IsModified { get; set; }
|
||||
[NbtIgnore]
|
||||
public byte[] Blocks { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
[NbtIgnore]
|
||||
public NibbleArray Metadata { get; set; }
|
||||
public NibbleSlice Metadata { get; set; }
|
||||
[NbtIgnore]
|
||||
public NibbleArray BlockLight { get; set; }
|
||||
public NibbleSlice BlockLight { get; set; }
|
||||
[NbtIgnore]
|
||||
public NibbleArray SkyLight { get; set; }
|
||||
public NibbleSlice SkyLight { get; set; }
|
||||
public byte[] Biomes { get; set; }
|
||||
public int[] HeightMap { get; set; }
|
||||
public int MaxHeight { get; private set; }
|
||||
@ -73,7 +73,7 @@ namespace TrueCraft.Core.World
|
||||
public bool TerrainPopulated { get; set; }
|
||||
|
||||
[NbtIgnore]
|
||||
public Region ParentRegion { get; set; }
|
||||
public IRegion ParentRegion { get; set; }
|
||||
|
||||
public Chunk()
|
||||
{
|
||||
@ -83,23 +83,24 @@ namespace TrueCraft.Core.World
|
||||
TerrainPopulated = false;
|
||||
LightPopulated = false;
|
||||
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()
|
||||
{
|
||||
X = coordinates.X;
|
||||
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)
|
||||
{
|
||||
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
|
||||
return Blocks[index];
|
||||
return Data[index];
|
||||
}
|
||||
|
||||
public byte GetMetadata(Coordinates3D coordinates)
|
||||
@ -127,8 +128,9 @@ namespace TrueCraft.Core.World
|
||||
public void SetBlockID(Coordinates3D coordinates, byte value)
|
||||
{
|
||||
IsModified = true;
|
||||
ParentRegion.DamageChunk(Coordinates);
|
||||
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
|
||||
Blocks[index] = value;
|
||||
Data[index] = value;
|
||||
if (value == AirBlock.BlockID)
|
||||
Metadata[index] = 0x0;
|
||||
var oldHeight = GetHeight((byte)coordinates.X, (byte)coordinates.Z);
|
||||
@ -164,6 +166,7 @@ namespace TrueCraft.Core.World
|
||||
public void SetMetadata(Coordinates3D coordinates, byte value)
|
||||
{
|
||||
IsModified = true;
|
||||
ParentRegion.DamageChunk(Coordinates);
|
||||
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
|
||||
Metadata[index] = value;
|
||||
}
|
||||
@ -175,6 +178,7 @@ namespace TrueCraft.Core.World
|
||||
public void SetSkyLight(Coordinates3D coordinates, byte value)
|
||||
{
|
||||
IsModified = true;
|
||||
ParentRegion.DamageChunk(Coordinates);
|
||||
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
|
||||
SkyLight[index] = value;
|
||||
}
|
||||
@ -186,6 +190,7 @@ namespace TrueCraft.Core.World
|
||||
public void SetBlockLight(Coordinates3D coordinates, byte value)
|
||||
{
|
||||
IsModified = true;
|
||||
ParentRegion.DamageChunk(Coordinates);
|
||||
int index = coordinates.Y + (coordinates.Z * Height) + (coordinates.X * Height * Width);
|
||||
BlockLight[index] = value;
|
||||
}
|
||||
@ -210,6 +215,7 @@ namespace TrueCraft.Core.World
|
||||
else
|
||||
TileEntities[coordinates] = value;
|
||||
IsModified = true;
|
||||
ParentRegion.DamageChunk(Coordinates);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -236,7 +242,7 @@ namespace TrueCraft.Core.World
|
||||
for (y = Chunk.Height - 1; y >= 0; y--)
|
||||
{
|
||||
int index = y + (z * Height) + (x * Height * Width);
|
||||
if (Blocks[index] != 0)
|
||||
if (Data[index] != 0)
|
||||
{
|
||||
SetHeight(x, z, y);
|
||||
if (y > MaxHeight)
|
||||
@ -275,10 +281,10 @@ namespace TrueCraft.Core.World
|
||||
chunk.Add(new NbtInt("Z", Z));
|
||||
chunk.Add(new NbtByte("LightPopulated", (byte)(LightPopulated ? 1 : 0)));
|
||||
chunk.Add(new NbtByte("TerrainPopulated", (byte)(TerrainPopulated ? 1 : 0)));
|
||||
chunk.Add(new NbtByteArray("Blocks", Blocks));
|
||||
chunk.Add(new NbtByteArray("Data", Metadata.Data));
|
||||
chunk.Add(new NbtByteArray("SkyLight", SkyLight.Data));
|
||||
chunk.Add(new NbtByteArray("BlockLight", BlockLight.Data));
|
||||
chunk.Add(new NbtByteArray("Blocks", Data));
|
||||
chunk.Add(new NbtByteArray("Data", Metadata.ToArray()));
|
||||
chunk.Add(new NbtByteArray("SkyLight", SkyLight.ToArray()));
|
||||
chunk.Add(new NbtByteArray("BlockLight", BlockLight.ToArray()));
|
||||
|
||||
var tiles = new NbtList("TileEntities", NbtTagType.Compound);
|
||||
foreach (var kvp in TileEntities)
|
||||
@ -308,13 +314,17 @@ namespace TrueCraft.Core.World
|
||||
TerrainPopulated = tag["TerrainPopulated"].ByteValue > 0;
|
||||
if (tag.Contains("LightPopulated"))
|
||||
LightPopulated = tag["LightPopulated"].ByteValue > 0;
|
||||
Blocks = tag["Blocks"].ByteArrayValue;
|
||||
Metadata = new NibbleArray();
|
||||
Metadata.Data = tag["Data"].ByteArrayValue;
|
||||
BlockLight = new NibbleArray();
|
||||
BlockLight.Data = tag["BlockLight"].ByteArrayValue;
|
||||
SkyLight = new NibbleArray();
|
||||
SkyLight.Data = tag["SkyLight"].ByteArrayValue;
|
||||
const int size = Width * Height * Depth;
|
||||
const int halfSize = size / 2;
|
||||
Data = new byte[(int)(size * 2.5)];
|
||||
Buffer.BlockCopy(tag["Blocks"].ByteArrayValue, 0, Data, 0, size);
|
||||
Metadata = new NibbleSlice(Data, size, halfSize);
|
||||
BlockLight = new NibbleSlice(Data, size + halfSize, halfSize);
|
||||
SkyLight = new NibbleSlice(Data, size + halfSize * 2, halfSize);
|
||||
|
||||
Metadata.Deserialize(tag["Data"]);
|
||||
BlockLight.Deserialize(tag["BlockLight"]);
|
||||
SkyLight.Deserialize(tag["SkyLight"]);
|
||||
|
||||
if (tag.Contains("TileEntities"))
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -20,10 +21,11 @@ namespace TrueCraft.Core.World
|
||||
// In chunks
|
||||
public const int Width = 32, Depth = 32;
|
||||
|
||||
private ConcurrentDictionary<Coordinates2D, IChunk> _Chunks { get; set; }
|
||||
/// <summary>
|
||||
/// The currently loaded chunk list.
|
||||
/// </summary>
|
||||
public IDictionary<Coordinates2D, IChunk> Chunks { get; set; }
|
||||
public IDictionary<Coordinates2D, IChunk> Chunks { get { return _Chunks; } }
|
||||
/// <summary>
|
||||
/// The location of this region in the overworld.
|
||||
/// </summary>
|
||||
@ -31,6 +33,7 @@ namespace TrueCraft.Core.World
|
||||
|
||||
public World World { get; set; }
|
||||
|
||||
private HashSet<Coordinates2D> DirtyChunks { get; set; } = new HashSet<Coordinates2D>();
|
||||
private Stream regionFile { get; set; }
|
||||
private object streamLock = new object();
|
||||
|
||||
@ -40,7 +43,7 @@ namespace TrueCraft.Core.World
|
||||
/// </summary>
|
||||
public Region(Coordinates2D position, World world)
|
||||
{
|
||||
Chunks = new Dictionary<Coordinates2D, IChunk>();
|
||||
_Chunks = new ConcurrentDictionary<Coordinates2D, IChunk>();
|
||||
Position = position;
|
||||
World = world;
|
||||
}
|
||||
@ -51,7 +54,10 @@ namespace TrueCraft.Core.World
|
||||
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);
|
||||
@ -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>
|
||||
/// Retrieves the requested chunk from the region, or
|
||||
/// 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>
|
||||
public IChunk GetChunk(Coordinates2D position, bool generate = true)
|
||||
{
|
||||
// TODO: This could use some refactoring
|
||||
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)
|
||||
{
|
||||
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);
|
||||
/*int length = */
|
||||
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();
|
||||
nbt.LoadFromStream(regionFile, NbtCompression.ZLib, null);
|
||||
var chunk = Chunk.FromNbt(nbt);
|
||||
chunk.ParentRegion = this;
|
||||
Chunks.Add(position, chunk);
|
||||
World.OnChunkLoaded(new ChunkLoadedEventArgs(chunk));
|
||||
break;
|
||||
@ -106,7 +119,7 @@ namespace TrueCraft.Core.World
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
if (generate)
|
||||
@ -124,7 +137,9 @@ namespace TrueCraft.Core.World
|
||||
var chunk = World.ChunkProvider.GenerateChunk(World, globalPosition);
|
||||
chunk.IsModified = true;
|
||||
chunk.Coordinates = globalPosition;
|
||||
Chunks.Add(position, chunk);
|
||||
chunk.ParentRegion = this;
|
||||
DirtyChunks.Add(position);
|
||||
Chunks[position] = chunk;
|
||||
World.OnChunkGenerated(new ChunkLoadedEventArgs(chunk));
|
||||
}
|
||||
|
||||
@ -136,6 +151,8 @@ namespace TrueCraft.Core.World
|
||||
if (!Chunks.ContainsKey(position))
|
||||
Chunks.Add(position, chunk);
|
||||
chunk.IsModified = true;
|
||||
DirtyChunks.Add(position);
|
||||
chunk.ParentRegion = this;
|
||||
Chunks[position] = chunk;
|
||||
}
|
||||
|
||||
@ -162,17 +179,19 @@ namespace TrueCraft.Core.World
|
||||
lock (streamLock)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var data = ((Chunk)chunk).ToNbt();
|
||||
byte[] raw = data.SaveToBuffer(NbtCompression.ZLib);
|
||||
|
||||
var header = GetChunkFromTable(kvp.Key);
|
||||
var header = GetChunkFromTable(coords);
|
||||
if (header == null || header.Item2 > raw.Length)
|
||||
header = AllocateNewChunks(kvp.Key, raw.Length);
|
||||
header = AllocateNewChunks(coords, raw.Length);
|
||||
|
||||
regionFile.Seek(header.Item1, SeekOrigin.Begin);
|
||||
new MinecraftStream(regionFile).WriteInt32(raw.Length);
|
||||
@ -182,7 +201,7 @@ namespace TrueCraft.Core.World
|
||||
chunk.IsModified = false;
|
||||
}
|
||||
if ((DateTime.UtcNow - chunk.LastAccessed).TotalMinutes > 5)
|
||||
toRemove.Add(kvp.Key);
|
||||
toRemove.Add(coords);
|
||||
}
|
||||
regionFile.Flush();
|
||||
// Unload idle chunks
|
||||
@ -198,14 +217,15 @@ namespace TrueCraft.Core.World
|
||||
#region Stream Helpers
|
||||
|
||||
private const int ChunkSizeMultiplier = 4096;
|
||||
private byte[] HeaderCache = new byte[8192];
|
||||
|
||||
private Tuple<int, int> GetChunkFromTable(Coordinates2D position) // <offset, length>
|
||||
{
|
||||
int tableOffset = ((position.X % Width) + (position.Z % Depth) * Width) * 4;
|
||||
regionFile.Seek(tableOffset, SeekOrigin.Begin);
|
||||
byte[] offsetBuffer = new byte[4];
|
||||
regionFile.Read(offsetBuffer, 0, 3);
|
||||
Buffer.BlockCopy(HeaderCache, tableOffset, offsetBuffer, 0, 3);
|
||||
Array.Reverse(offsetBuffer);
|
||||
int length = regionFile.ReadByte();
|
||||
int length = HeaderCache[tableOffset + 3];
|
||||
int offset = BitConverter.ToInt32(offsetBuffer, 0) << 4;
|
||||
if (offset == 0 || length == 0)
|
||||
return null;
|
||||
@ -215,7 +235,8 @@ namespace TrueCraft.Core.World
|
||||
|
||||
private void CreateRegionHeader()
|
||||
{
|
||||
regionFile.Write(new byte[8192], 0, 8192);
|
||||
HeaderCache = new byte[8192];
|
||||
regionFile.Write(HeaderCache, 0, 8192);
|
||||
regionFile.Flush();
|
||||
}
|
||||
|
||||
@ -237,6 +258,7 @@ namespace TrueCraft.Core.World
|
||||
entry[0] = (byte)length;
|
||||
Array.Reverse(entry);
|
||||
regionFile.Write(entry, 0, entry.Length);
|
||||
Buffer.BlockCopy(entry, 0, HeaderCache, tableOffset, 4);
|
||||
|
||||
return new Tuple<int, int>(dataOffset, length * ChunkSizeMultiplier);
|
||||
}
|
||||
|
@ -69,7 +69,8 @@ namespace TrueCraft.Profiling
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -519,7 +519,7 @@ namespace TrueCraft.Commands
|
||||
{
|
||||
lighter.InitialLighting(chunk, true);
|
||||
(client as RemoteClient).UnloadChunk(chunk.Coordinates);
|
||||
(client as RemoteClient).LoadChunk(chunk.Coordinates);
|
||||
(client as RemoteClient).LoadChunk(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,7 +79,8 @@ namespace TrueCraft
|
||||
(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);
|
||||
Task.Factory.StartNew(client.UpdateChunks);
|
||||
Server.Scheduler.ScheduleEvent("client.update-chunks", client,
|
||||
TimeSpan.Zero, s => client.UpdateChunks());
|
||||
UpdateClientEntities(client);
|
||||
}
|
||||
break;
|
||||
|
@ -1,10 +1,12 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using TrueCraft.API.Server;
|
||||
using System.Collections.Generic;
|
||||
using TrueCraft.API;
|
||||
using System.Diagnostics;
|
||||
using TrueCraft.Profiling;
|
||||
using System.Threading;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace TrueCraft
|
||||
{
|
||||
@ -15,80 +17,122 @@ namespace TrueCraft
|
||||
private IMultiplayerServer Server { get; set; }
|
||||
private HashSet<IEventSubject> Subjects { 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)
|
||||
{
|
||||
Events = new List<ScheduledEvent>();
|
||||
ImmediateEventQueue = new ConcurrentQueue<ScheduledEvent>();
|
||||
LaterEventQueue = new ConcurrentQueue<ScheduledEvent>();
|
||||
DisposedSubjects = new ConcurrentQueue<IEventSubject>();
|
||||
Server = server;
|
||||
Subjects = new HashSet<IEventSubject>();
|
||||
Stopwatch = new Stopwatch();
|
||||
DisabledEvents = new HashSet<string>();
|
||||
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)
|
||||
{
|
||||
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;
|
||||
if (!Subjects.Contains(subject))
|
||||
{
|
||||
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
|
||||
});
|
||||
Subjects.Add(subject);
|
||||
subject.Disposed += Subject_Disposed;
|
||||
}
|
||||
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)
|
||||
{
|
||||
// Cancel all events with this subject
|
||||
lock (EventLock)
|
||||
{
|
||||
for (int i = 0; i < Events.Count; i++)
|
||||
{
|
||||
if (Events[i].Subject == sender)
|
||||
{
|
||||
Events.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
Subjects.Remove((IEventSubject)sender);
|
||||
}
|
||||
DisposedSubjects.Enqueue((IEventSubject)sender);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
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;
|
||||
for (int i = 0; i < Events.Count; i++)
|
||||
ScheduledEvent e;
|
||||
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];
|
||||
if (e.When <= start)
|
||||
// Cancel all events with this subject
|
||||
for (int i = 0; i < Events.Count; i++)
|
||||
{
|
||||
Profiler.Start("scheduler." + e.Name);
|
||||
e.Action(Server);
|
||||
Events.RemoveAt(i);
|
||||
i--;
|
||||
Profiler.Done();
|
||||
if (Events[i].Subject == subject)
|
||||
{
|
||||
Events.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
if (e.When > start)
|
||||
break; // List is sorted, we can exit early
|
||||
Subjects.Remove(subject);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,8 @@ namespace TrueCraft.Handlers
|
||||
|
||||
// Send setup packets
|
||||
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 UpdateHealthPacket((remoteClient.Entity as PlayerEntity).Health));
|
||||
remoteClient.QueuePacket(new SpawnPositionPacket((int)remoteClient.Entity.Position.X,
|
||||
|
@ -125,6 +125,10 @@ namespace TrueCraft
|
||||
|
||||
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;
|
||||
Time.Reset();
|
||||
Time.Start();
|
||||
@ -156,16 +160,6 @@ namespace TrueCraft
|
||||
DisconnectClient(c);
|
||||
}
|
||||
|
||||
public void Pause()
|
||||
{
|
||||
EnvironmentWorker.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
}
|
||||
|
||||
public void Resume()
|
||||
{
|
||||
EnvironmentWorker.Change(0, Timeout.Infinite);
|
||||
}
|
||||
|
||||
public void AddWorld(IWorld world)
|
||||
{
|
||||
Worlds.Add(world);
|
||||
@ -183,7 +177,8 @@ namespace TrueCraft
|
||||
|
||||
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)
|
||||
{
|
||||
var lighter = WorldLighters.SingleOrDefault(l => l.World == sender);
|
||||
@ -234,9 +229,9 @@ namespace TrueCraft
|
||||
}
|
||||
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);
|
||||
@ -427,11 +422,14 @@ namespace TrueCraft
|
||||
Profiler.Done();
|
||||
}
|
||||
|
||||
Profiler.Start("environment.chunks");
|
||||
Tuple<IWorld, IChunk> t;
|
||||
if (ChunksToSchedule.TryTake(out t))
|
||||
ScheduleUpdatesForChunk(t.Item1, t.Item2);
|
||||
Profiler.Done();
|
||||
if (Program.ServerConfiguration.EnableEventLoading)
|
||||
{
|
||||
Profiler.Start("environment.chunks");
|
||||
Tuple<IWorld, IChunk> t;
|
||||
if (ChunksToSchedule.TryTake(out t))
|
||||
ScheduleUpdatesForChunk(t.Item1, t.Item2);
|
||||
Profiler.Done();
|
||||
}
|
||||
|
||||
Profiler.Done(MillisecondsPerTick);
|
||||
long end = Time.ElapsedMilliseconds;
|
||||
|
@ -109,20 +109,24 @@ namespace TrueCraft
|
||||
Server.ChatMessageReceived += HandleChatMessageReceived;
|
||||
Server.Start(new IPEndPoint(IPAddress.Parse(ServerConfiguration.ServerAddress), ServerConfiguration.ServerPort));
|
||||
Console.CancelKeyPress += HandleCancelKeyPress;
|
||||
Server.Scheduler.ScheduleEvent("world.save", null,
|
||||
TimeSpan.FromSeconds(ServerConfiguration.WorldSaveInterval), SaveWorlds);
|
||||
while (true)
|
||||
{
|
||||
Thread.Sleep(1000 * ServerConfiguration.WorldSaveInterval);
|
||||
Server.Pause();
|
||||
Server.Log(LogCategory.Notice, "Saving world...");
|
||||
foreach (var w in Server.Worlds)
|
||||
{
|
||||
w.Save();
|
||||
}
|
||||
Server.Log(LogCategory.Notice, "Done.");
|
||||
Server.Resume();
|
||||
Thread.Yield();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
Server.Stop();
|
||||
|
@ -22,6 +22,7 @@ using fNbt;
|
||||
using TrueCraft.API.Logging;
|
||||
using TrueCraft.API.Logic;
|
||||
using TrueCraft.Exceptions;
|
||||
using TrueCraft.Profiling;
|
||||
|
||||
namespace TrueCraft
|
||||
{
|
||||
@ -29,7 +30,7 @@ namespace TrueCraft
|
||||
{
|
||||
public RemoteClient(IMultiplayerServer server, IPacketReader packetReader, PacketHandler[] packetHandlers, Socket connection)
|
||||
{
|
||||
LoadedChunks = new List<Coordinates2D>();
|
||||
LoadedChunks = new HashSet<Coordinates2D>();
|
||||
Server = server;
|
||||
Inventory = new InventoryWindow(server.CraftingRepository);
|
||||
InventoryWindow.WindowChange += HandleWindowChange;
|
||||
@ -145,7 +146,7 @@ namespace TrueCraft
|
||||
}
|
||||
|
||||
internal int ChunkRadius { get; set; }
|
||||
internal IList<Coordinates2D> LoadedChunks { get; set; }
|
||||
internal HashSet<Coordinates2D> LoadedChunks { get; set; }
|
||||
|
||||
public bool DataAvailable
|
||||
{
|
||||
@ -398,8 +399,10 @@ namespace TrueCraft
|
||||
if (ChunkRadius < 8) // TODO: Allow customization of this number
|
||||
{
|
||||
ChunkRadius++;
|
||||
UpdateChunks();
|
||||
server.Scheduler.ScheduleEvent("remote.chunks", this, TimeSpan.FromSeconds(1), ExpandChunkRadius);
|
||||
server.Scheduler.ScheduleEvent("client.update-chunks", this,
|
||||
TimeSpan.Zero, s => UpdateChunks());
|
||||
server.Scheduler.ScheduleEvent("remote.chunks", this,
|
||||
TimeSpan.FromSeconds(1), ExpandChunkRadius);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -412,67 +415,77 @@ namespace TrueCraft
|
||||
|
||||
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 z = -ChunkRadius; z < ChunkRadius; z++)
|
||||
{
|
||||
newChunks.Add(new Coordinates2D(
|
||||
var coords = new Coordinates2D(
|
||||
((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
|
||||
lock (LoadedChunks)
|
||||
Profiler.Done();
|
||||
Task.Factory.StartNew(() =>
|
||||
{
|
||||
var currentChunks = new List<Coordinates2D>(LoadedChunks);
|
||||
foreach (Coordinates2D chunk in currentChunks)
|
||||
Profiler.Start("client.encode-chunks");
|
||||
foreach (var tup in toLoad)
|
||||
{
|
||||
if (!newChunks.Contains(chunk))
|
||||
UnloadChunk(chunk);
|
||||
var coords = tup.Item1;
|
||||
var chunk = tup.Item2;
|
||||
if (chunk == null)
|
||||
chunk = World.GetChunk(coords);
|
||||
chunk.LastAccessed = DateTime.UtcNow;
|
||||
LoadChunk(chunk);
|
||||
}
|
||||
// Load new columns
|
||||
foreach (Coordinates2D chunk in newChunks)
|
||||
{
|
||||
if (!LoadedChunks.Contains(chunk))
|
||||
LoadChunk(chunk);
|
||||
}
|
||||
}
|
||||
Profiler.Done();
|
||||
});
|
||||
Profiler.Start("client.old-chunks");
|
||||
LoadedChunks.IntersectWith(newChunks);
|
||||
Profiler.Done();
|
||||
Profiler.Start("client.update-entities");
|
||||
((EntityManager)Server.GetEntityManagerForWorld(World)).UpdateClientEntities(this);
|
||||
Profiler.Done();
|
||||
}
|
||||
|
||||
internal void UnloadAllChunks()
|
||||
{
|
||||
lock (LoadedChunks)
|
||||
while (LoadedChunks.Any())
|
||||
{
|
||||
while (LoadedChunks.Any())
|
||||
{
|
||||
UnloadChunk(LoadedChunks[0]);
|
||||
}
|
||||
UnloadChunk(LoadedChunks.First());
|
||||
}
|
||||
}
|
||||
|
||||
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(CreatePacket(chunk));
|
||||
LoadedChunks.Add(position);
|
||||
foreach (var kvp in chunk.TileEntities)
|
||||
{
|
||||
var coords = kvp.Key;
|
||||
var descriptor = new BlockDescriptor
|
||||
Server.Scheduler.ScheduleEvent("client.finalize-chunks", this,
|
||||
TimeSpan.Zero, server =>
|
||||
{
|
||||
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);
|
||||
}
|
||||
return;
|
||||
LoadedChunks.Add(chunk.Coordinates);
|
||||
foreach (var kvp in chunk.TileEntities)
|
||||
{
|
||||
var coords = kvp.Key;
|
||||
var descriptor = new BlockDescriptor
|
||||
{
|
||||
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)
|
||||
@ -511,19 +524,20 @@ namespace TrueCraft
|
||||
var X = chunk.Coordinates.X;
|
||||
var Z = chunk.Coordinates.Z;
|
||||
|
||||
const int blocksPerChunk = Chunk.Width * Chunk.Height * Chunk.Depth;
|
||||
const int bytesPerChunk = (int)(blocksPerChunk * 2.5);
|
||||
|
||||
byte[] data = new byte[bytesPerChunk];
|
||||
|
||||
Buffer.BlockCopy(chunk.Blocks, 0, data, 0, chunk.Blocks.Length);
|
||||
Buffer.BlockCopy(chunk.Metadata.Data, 0, data, chunk.Blocks.Length, chunk.Metadata.Data.Length);
|
||||
Buffer.BlockCopy(chunk.BlockLight.Data, 0, data, chunk.Blocks.Length + chunk.Metadata.Data.Length, chunk.BlockLight.Data.Length);
|
||||
Buffer.BlockCopy(chunk.SkyLight.Data, 0, data, chunk.Blocks.Length + chunk.Metadata.Data.Length
|
||||
+ chunk.BlockLight.Data.Length, chunk.SkyLight.Data.Length);
|
||||
|
||||
var result = ZlibStream.CompressBuffer(data);
|
||||
return new ChunkDataPacket(X * Chunk.Width, 0, Z * Chunk.Depth, Chunk.Width, Chunk.Height, Chunk.Depth, result);
|
||||
Profiler.Start("client.encode-chunks.compress");
|
||||
byte[] result;
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
using (var deflate = new ZlibStream(new MemoryStream(chunk.Data),
|
||||
CompressionMode.Compress,
|
||||
CompressionLevel.BestSpeed))
|
||||
deflate.CopyTo(ms);
|
||||
result = ms.ToArray();
|
||||
}
|
||||
Profiler.Done();
|
||||
|
||||
return new ChunkDataPacket(X * Chunk.Width, 0, Z * Chunk.Depth,
|
||||
Chunk.Width, Chunk.Height, Chunk.Depth, result);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -48,6 +48,8 @@ namespace TrueCraft
|
||||
Query = true;
|
||||
QueryPort = 25566;
|
||||
EnableLighting = true;
|
||||
EnableEventLoading = true;
|
||||
DisabledEvents = new string[0];
|
||||
}
|
||||
|
||||
[YamlMember(Alias = "motd")]
|
||||
@ -76,5 +78,11 @@ namespace TrueCraft
|
||||
|
||||
[YamlMember(Alias = "enable-lighting")]
|
||||
public bool EnableLighting { get; set; }
|
||||
|
||||
[YamlMember(Alias = "enable-event-loading")]
|
||||
public bool EnableEventLoading { get; set; }
|
||||
|
||||
[YamlMember(Alias = "disable-events")]
|
||||
public string[] DisabledEvents { get; set; }
|
||||
}
|
||||
}
|
@ -34,12 +34,12 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="Ionic.Zip.Reduced">
|
||||
<HintPath>..\lib\Ionic.Zip.Reduced.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="YamlDotNet">
|
||||
<HintPath>..\packages\YamlDotNet.3.9.0\lib\net35\YamlDotNet.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="DotNetZip">
|
||||
<HintPath>..\packages\DotNetZip.1.10.1\lib\net20\DotNetZip.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="AccessConfiguration.cs" />
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="DotNetZip" version="1.10.1" targetFramework="net45" />
|
||||
<package id="YamlDotNet" version="3.9.0" targetFramework="net45" />
|
||||
</packages>
|
Loading…
x
Reference in New Issue
Block a user