From 6fb8ee7ba58e36c0f58bc43b1e949e6078370564 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 23 May 2017 18:17:44 -0400 Subject: [PATCH] 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 --- TrueCraft.API/Configuration.cs | 2 +- TrueCraft.API/NibbleArray.cs | 85 ----------- TrueCraft.API/NibbleSlice.cs | 82 +++++++++++ TrueCraft.API/Server/IEventScheduler.cs | 2 + TrueCraft.API/TrueCraft.API.csproj | 2 +- TrueCraft.API/World/IChunk.cs | 9 +- TrueCraft.API/World/IRegion.cs | 4 + TrueCraft.Client/Handlers/ChunkHandlers.cs | 8 +- TrueCraft.Client/ReadOnlyWorld.cs | 2 +- TrueCraft.Core.Test/World/ChunkTest.cs | 73 +++++++--- TrueCraft.Core/Lighting/WorldLighting.cs | 44 +++--- .../TerrainGen/StandardGenerator.cs | 2 +- TrueCraft.Core/World/Chunk.cs | 58 ++++---- TrueCraft.Core/World/Region.cs | 72 ++++++---- TrueCraft.Profiling/Profiler.cs | 3 +- TrueCraft/Commands/DebugCommands.cs | 2 +- TrueCraft/EntityManager.cs | 3 +- TrueCraft/EventScheduler.cs | 134 ++++++++++++------ TrueCraft/Handlers/LoginHandlers.cs | 3 +- TrueCraft/MultiplayerServer.cs | 34 +++-- TrueCraft/Program.cs | 22 +-- TrueCraft/RemoteClient.cs | 124 +++++++++------- TrueCraft/ServerConfiguration.cs | 8 ++ TrueCraft/TrueCraft.csproj | 6 +- TrueCraft/packages.config | 1 + 25 files changed, 460 insertions(+), 325 deletions(-) delete mode 100644 TrueCraft.API/NibbleArray.cs create mode 100644 TrueCraft.API/NibbleSlice.cs diff --git a/TrueCraft.API/Configuration.cs b/TrueCraft.API/Configuration.cs index 1a0ad74..ae2f7be 100644 --- a/TrueCraft.API/Configuration.cs +++ b/TrueCraft.API/Configuration.cs @@ -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); diff --git a/TrueCraft.API/NibbleArray.cs b/TrueCraft.API/NibbleArray.cs deleted file mode 100644 index fd6ce28..0000000 --- a/TrueCraft.API/NibbleArray.cs +++ /dev/null @@ -1,85 +0,0 @@ -using fNbt; -using fNbt.Serialization; -using System; -using System.Collections.ObjectModel; - -namespace TrueCraft.API -{ - /// - /// Represents an array of 4-bit values. - /// - public class NibbleArray : INbtSerializable - { - /// - /// The data in the nibble array. Each byte contains - /// two nibbles, stored in big-endian. - /// - public byte[] Data { get; set; } - - public NibbleArray() - { - } - - /// - /// Creates a new nibble array with the given number of nibbles. - /// - public NibbleArray(int length) - { - Data = new byte[length/2]; - } - - /// - /// Gets the current number of nibbles in this array. - /// - [NbtIgnore] - public int Length - { - get { return Data.Length * 2; } - } - - /// - /// Gets or sets a nibble at the given index. - /// - [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 Data - { - get { return Array.AsReadOnly(NibbleArray.Data); } - } - } -} \ No newline at end of file diff --git a/TrueCraft.API/NibbleSlice.cs b/TrueCraft.API/NibbleSlice.cs new file mode 100644 index 0000000..c5b0259 --- /dev/null +++ b/TrueCraft.API/NibbleSlice.cs @@ -0,0 +1,82 @@ +using fNbt; +using fNbt.Serialization; +using System; +using System.Collections.ObjectModel; + +namespace TrueCraft.API +{ + /// + /// Represents a slice of an array of 4-bit values. + /// + public class NibbleSlice : INbtSerializable + { + /// + /// The data in the nibble array. Each byte contains + /// two nibbles, stored in big-endian. + /// + 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; + } + + /// + /// Gets or sets a nibble at the given index. + /// + [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 Data + { + get { return Array.AsReadOnly(NibbleArray.Data); } + } + } +} \ No newline at end of file diff --git a/TrueCraft.API/Server/IEventScheduler.cs b/TrueCraft.API/Server/IEventScheduler.cs index 897cb63..0183678 100644 --- a/TrueCraft.API/Server/IEventScheduler.cs +++ b/TrueCraft.API/Server/IEventScheduler.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; namespace TrueCraft.API.Server { public interface IEventScheduler { + HashSet DisabledEvents { get; } /// /// Schedules an event to occur some time in the future. /// diff --git a/TrueCraft.API/TrueCraft.API.csproj b/TrueCraft.API/TrueCraft.API.csproj index efda6b0..57be35a 100644 --- a/TrueCraft.API/TrueCraft.API.csproj +++ b/TrueCraft.API/TrueCraft.API.csproj @@ -88,7 +88,7 @@ - + diff --git a/TrueCraft.API/World/IChunk.cs b/TrueCraft.API/World/IChunk.cs index bfeb4f7..08f729a 100644 --- a/TrueCraft.API/World/IChunk.cs +++ b/TrueCraft.API/World/IChunk.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 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); diff --git a/TrueCraft.API/World/IRegion.cs b/TrueCraft.API/World/IRegion.cs index 5a2ba63..c40b457 100644 --- a/TrueCraft.API/World/IRegion.cs +++ b/TrueCraft.API/World/IRegion.cs @@ -9,6 +9,10 @@ namespace TrueCraft.API.World Coordinates2D Position { get; } IChunk GetChunk(Coordinates2D position, bool generate = true); + /// + /// Marks the chunk for saving in the next Save(). + /// + void DamageChunk(Coordinates2D position); void UnloadChunk(Coordinates2D position); void Save(string path); } diff --git a/TrueCraft.Client/Handlers/ChunkHandlers.cs b/TrueCraft.Client/Handlers/ChunkHandlers.cs index 09dbe17..042892f 100644 --- a/TrueCraft.Client/Handlers/ChunkHandlers.cs +++ b/TrueCraft.Client/Handlers/ChunkHandlers.cs @@ -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 diff --git a/TrueCraft.Client/ReadOnlyWorld.cs b/TrueCraft.Client/ReadOnlyWorld.cs index a618b2f..1453f24 100644 --- a/TrueCraft.Client/ReadOnlyWorld.cs +++ b/TrueCraft.Client/ReadOnlyWorld.cs @@ -116,7 +116,7 @@ namespace TrueCraft.Client public int X { get { return Chunk.X; } } public int Z { get { return Chunk.Z; } } - public ReadOnlyCollection Blocks { get { return Array.AsReadOnly(Chunk.Blocks); } } + public ReadOnlyCollection 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); } } diff --git a/TrueCraft.Core.Test/World/ChunkTest.cs b/TrueCraft.Core.Test/World/ChunkTest.cs index 64caa3b..49934a0 100644 --- a/TrueCraft.Core.Test/World/ChunkTest.cs +++ b/TrueCraft.Core.Test/World/ChunkTest.cs @@ -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++; + } } } } \ No newline at end of file diff --git a/TrueCraft.Core/Lighting/WorldLighting.cs b/TrueCraft.Core/Lighting/WorldLighting.cs index a106814..d5eb8ac 100644 --- a/TrueCraft.Core/Lighting/WorldLighting.cs +++ b/TrueCraft.Core/Lighting/WorldLighting.cs @@ -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 PendingOperations { get; set; } + private ConcurrentQueue PendingOperations { get; set; } private Dictionary HeightMaps { get; set; } public WorldLighting(IWorld world, IBlockRepository blockRepository) { BlockRepository = blockRepository; World = world; - PendingOperations = new List(); + PendingOperations = new ConcurrentQueue(); HeightMaps = new Dictionary(); 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) diff --git a/TrueCraft.Core/TerrainGen/StandardGenerator.cs b/TrueCraft.Core/TerrainGen/StandardGenerator.cs index 4123a1d..95c26e3 100644 --- a/TrueCraft.Core/TerrainGen/StandardGenerator.cs +++ b/TrueCraft.Core/TerrainGen/StandardGenerator.cs @@ -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) diff --git a/TrueCraft.Core/World/Chunk.cs b/TrueCraft.Core/World/Chunk.cs index fd1584d..3394e00 100644 --- a/TrueCraft.Core/World/Chunk.cs +++ b/TrueCraft.Core/World/Chunk.cs @@ -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); } /// @@ -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")) { diff --git a/TrueCraft.Core/World/Region.cs b/TrueCraft.Core/World/Region.cs index 10b964f..c9c41aa 100644 --- a/TrueCraft.Core/World/Region.cs +++ b/TrueCraft.Core/World/Region.cs @@ -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 _Chunks { get; set; } /// /// The currently loaded chunk list. /// - public IDictionary Chunks { get; set; } + public IDictionary Chunks { get { return _Chunks; } } /// /// The location of this region in the overworld. /// @@ -31,6 +33,7 @@ namespace TrueCraft.Core.World public World World { get; set; } + private HashSet DirtyChunks { get; set; } = new HashSet(); private Stream regionFile { get; set; } private object streamLock = new object(); @@ -40,7 +43,7 @@ namespace TrueCraft.Core.World /// public Region(Coordinates2D position, World world) { - Chunks = new Dictionary(); + _Chunks = new ConcurrentDictionary(); 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)); + } + /// /// Retrieves the requested chunk from the region, or /// generates it if a world generator is provided. @@ -66,25 +79,24 @@ namespace TrueCraft.Core.World /// The position of the requested local chunk coordinates. 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(); - 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 GetChunkFromTable(Coordinates2D position) // { 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(dataOffset, length * ChunkSizeMultiplier); } diff --git a/TrueCraft.Profiling/Profiler.cs b/TrueCraft.Profiling/Profiler.cs index 3e9fb76..e839a74 100644 --- a/TrueCraft.Profiling/Profiler.cs +++ b/TrueCraft.Profiling/Profiler.cs @@ -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; } } diff --git a/TrueCraft/Commands/DebugCommands.cs b/TrueCraft/Commands/DebugCommands.cs index 13ba9e5..4178bd0 100644 --- a/TrueCraft/Commands/DebugCommands.cs +++ b/TrueCraft/Commands/DebugCommands.cs @@ -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); } } diff --git a/TrueCraft/EntityManager.cs b/TrueCraft/EntityManager.cs index 93baf3e..39bca21 100644 --- a/TrueCraft/EntityManager.cs +++ b/TrueCraft/EntityManager.cs @@ -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; diff --git a/TrueCraft/EventScheduler.cs b/TrueCraft/EventScheduler.cs index 7ceb253..6fff15c 100644 --- a/TrueCraft/EventScheduler.cs +++ b/TrueCraft/EventScheduler.cs @@ -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 Subjects { get; set; } private Stopwatch Stopwatch { get; set; } + private ConcurrentQueue ImmediateEventQueue { get; set; } + private ConcurrentQueue LaterEventQueue { get; set; } + private ConcurrentQueue DisposedSubjects { get; set; } + public HashSet DisabledEvents { get; private set; } public EventScheduler(IMultiplayerServer server) { Events = new List(); + ImmediateEventQueue = new ConcurrentQueue(); + LaterEventQueue = new ConcurrentQueue(); + DisposedSubjects = new ConcurrentQueue(); Server = server; Subjects = new HashSet(); Stopwatch = new Stopwatch(); + DisabledEvents = new HashSet(); 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 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); } diff --git a/TrueCraft/Handlers/LoginHandlers.cs b/TrueCraft/Handlers/LoginHandlers.cs index 4ba2842..239e23c 100644 --- a/TrueCraft/Handlers/LoginHandlers.cs +++ b/TrueCraft/Handlers/LoginHandlers.cs @@ -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, diff --git a/TrueCraft/MultiplayerServer.cs b/TrueCraft/MultiplayerServer.cs index bdd278d..cb18e02 100644 --- a/TrueCraft/MultiplayerServer.cs +++ b/TrueCraft/MultiplayerServer.cs @@ -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(sender as IWorld, e.Chunk)); + if (Program.ServerConfiguration.EnableEventLoading) + ChunksToSchedule.Add(new Tuple(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 t; - if (ChunksToSchedule.TryTake(out t)) - ScheduleUpdatesForChunk(t.Item1, t.Item2); - Profiler.Done(); + if (Program.ServerConfiguration.EnableEventLoading) + { + Profiler.Start("environment.chunks"); + Tuple t; + if (ChunksToSchedule.TryTake(out t)) + ScheduleUpdatesForChunk(t.Item1, t.Item2); + Profiler.Done(); + } Profiler.Done(MillisecondsPerTick); long end = Time.ElapsedMilliseconds; diff --git a/TrueCraft/Program.cs b/TrueCraft/Program.cs index 40139b2..6f8355b 100644 --- a/TrueCraft/Program.cs +++ b/TrueCraft/Program.cs @@ -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(); diff --git a/TrueCraft/RemoteClient.cs b/TrueCraft/RemoteClient.cs index 5940cde..d31b5f6 100644 --- a/TrueCraft/RemoteClient.cs +++ b/TrueCraft/RemoteClient.cs @@ -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(); + LoadedChunks = new HashSet(); Server = server; Inventory = new InventoryWindow(server.CraftingRepository); InventoryWindow.WindowChange += HandleWindowChange; @@ -145,7 +146,7 @@ namespace TrueCraft } internal int ChunkRadius { get; set; } - internal IList LoadedChunks { get; set; } + internal HashSet 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(); + var newChunks = new HashSet(); + var toLoad = new List>(); + 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( + coords, World.GetChunk(coords, generate: false))); } } - // Unload extraneous columns - lock (LoadedChunks) + Profiler.Done(); + Task.Factory.StartNew(() => { - var currentChunks = new List(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() diff --git a/TrueCraft/ServerConfiguration.cs b/TrueCraft/ServerConfiguration.cs index 3df8b54..b607ce3 100644 --- a/TrueCraft/ServerConfiguration.cs +++ b/TrueCraft/ServerConfiguration.cs @@ -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; } } } \ No newline at end of file diff --git a/TrueCraft/TrueCraft.csproj b/TrueCraft/TrueCraft.csproj index ec209eb..1883dec 100644 --- a/TrueCraft/TrueCraft.csproj +++ b/TrueCraft/TrueCraft.csproj @@ -34,12 +34,12 @@ - - ..\lib\Ionic.Zip.Reduced.dll - ..\packages\YamlDotNet.3.9.0\lib\net35\YamlDotNet.dll + + ..\packages\DotNetZip.1.10.1\lib\net20\DotNetZip.dll + diff --git a/TrueCraft/packages.config b/TrueCraft/packages.config index f61df4d..1616a62 100644 --- a/TrueCraft/packages.config +++ b/TrueCraft/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file