diff --git a/README.md b/README.md index 7dc5664..e97a3b6 100644 --- a/README.md +++ b/README.md @@ -46,10 +46,12 @@ this project if you ask nicely. ## Resources -[1.7.3 protocol docs](http://wiki.vg/index.php?title=Protocol&oldid=615) +[1.7.3 protocol docs](http://wiki.vg/index.php?title=Protocol&oldid=517) [1.7.3 inventory docs](http://wiki.vg/index.php?title=Inventory&oldid=2356) +[1.7.3 protocol faq](http://wiki.vg/index.php?title=Protocol_FAQ&oldid=74) + ## Blah blah blah TrueCraft is not associated with Mojang or Minecraft in any sort of official diff --git a/TrueCraft.API/Networking/IPacketReader.cs b/TrueCraft.API/Networking/IPacketReader.cs index 2e57ca6..8fa4eed 100644 --- a/TrueCraft.API/Networking/IPacketReader.cs +++ b/TrueCraft.API/Networking/IPacketReader.cs @@ -4,6 +4,8 @@ namespace TrueCraft.API.Networking { public interface IPacketReader { + int ProtocolVersion { get; } + void RegisterPacketType(bool clientbound = true, bool serverbound = true) where T : IPacket; IPacket ReadPacket(IMinecraftStream stream, bool serverbound = true); void WritePacket(IMinecraftStream stream, IPacket packet); diff --git a/TrueCraft.API/NibbleArray.cs b/TrueCraft.API/NibbleArray.cs new file mode 100644 index 0000000..fd6ce28 --- /dev/null +++ b/TrueCraft.API/NibbleArray.cs @@ -0,0 +1,85 @@ +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/TrueCraft.API.csproj b/TrueCraft.API/TrueCraft.API.csproj index 9200822..7ca0aa2 100644 --- a/TrueCraft.API/TrueCraft.API.csproj +++ b/TrueCraft.API/TrueCraft.API.csproj @@ -53,11 +53,18 @@ + + + + + + + diff --git a/TrueCraft.API/World/IChunk.cs b/TrueCraft.API/World/IChunk.cs new file mode 100644 index 0000000..d47dbb5 --- /dev/null +++ b/TrueCraft.API/World/IChunk.cs @@ -0,0 +1,22 @@ +using System; + +namespace TrueCraft.API.World +{ + public interface IChunk + { + Coordinates2D Coordinates { get; set; } + bool IsModified { get; set; } + int[] HeightMap { get; } + ISection[] Sections { get; } + byte[] Biomes { get; } + DateTime LastAccessed { get; set; } + short GetBlockID(Coordinates3D coordinates); + byte GetMetadata(Coordinates3D coordinates); + byte GetSkyLight(Coordinates3D coordinates); + byte GetBlockLight(Coordinates3D coordinates); + void SetBlockID(Coordinates3D coordinates, short value); + void SetMetadata(Coordinates3D coordinates, byte value); + void SetSkyLight(Coordinates3D coordinates, byte value); + void SetBlockLight(Coordinates3D coordinates, byte value); + } +} \ No newline at end of file diff --git a/TrueCraft.API/World/IChunkProvider.cs b/TrueCraft.API/World/IChunkProvider.cs new file mode 100644 index 0000000..bf765eb --- /dev/null +++ b/TrueCraft.API/World/IChunkProvider.cs @@ -0,0 +1,12 @@ +using System; + +namespace TrueCraft.API.World +{ + /// + /// Provides new chunks to worlds. Generally speaking this is a terrain generator. + /// + public interface IChunkProvider + { + IChunk GenerateChunk(IWorld world, Coordinates2D coordinates); + } +} \ No newline at end of file diff --git a/TrueCraft.API/World/IRegion.cs b/TrueCraft.API/World/IRegion.cs new file mode 100644 index 0000000..7fcabbc --- /dev/null +++ b/TrueCraft.API/World/IRegion.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace TrueCraft.API.World +{ + public interface IRegion : IDisposable + { + IDictionary Chunks { get; } + Coordinates2D Position { get; } + + IChunk GetChunk(Coordinates2D position); + void UnloadChunk(Coordinates2D position); + void Save(string path); + } +} \ No newline at end of file diff --git a/TrueCraft.API/World/ISection.cs b/TrueCraft.API/World/ISection.cs new file mode 100644 index 0000000..54b4cc6 --- /dev/null +++ b/TrueCraft.API/World/ISection.cs @@ -0,0 +1,22 @@ +using System; + +namespace TrueCraft.API.World +{ + public interface ISection + { + byte[] Blocks { get; } + NibbleArray Metadata { get; } + NibbleArray BlockLight { get; } + NibbleArray SkyLight { get; } + byte Y { get; } + short GetBlockID(Coordinates3D coordinates); + byte GetMetadata(Coordinates3D coordinates); + byte GetSkyLight(Coordinates3D coordinates); + byte GetBlockLight(Coordinates3D coordinates); + void SetBlockID(Coordinates3D coordinates, short value); + void SetMetadata(Coordinates3D coordinates, byte value); + void SetSkyLight(Coordinates3D coordinates, byte value); + void SetBlockLight(Coordinates3D coordinates, byte value); + void ProcessSection(); + } +} \ No newline at end of file diff --git a/TrueCraft.API/World/IWorld.cs b/TrueCraft.API/World/IWorld.cs new file mode 100644 index 0000000..3f52d92 --- /dev/null +++ b/TrueCraft.API/World/IWorld.cs @@ -0,0 +1,24 @@ +using System; + +namespace TrueCraft.API.World +{ + // TODO: Entities + /// + /// An in-game world composed of chunks and blocks. + /// + public interface IWorld + { + string Name { get; set; } + + IChunk GetChunk(Coordinates2D coordinates); + short GetBlockID(Coordinates3D coordinates); + byte GetMetadata(Coordinates3D coordinates); + byte GetSkyLight(Coordinates3D coordinates); + void SetBlockID(Coordinates3D coordinates, short value); + void SetMetadata(Coordinates3D coordinates, byte value); + void SetSkyLight(Coordinates3D coordinates, byte value); + void SetBlockLight(Coordinates3D coordinates, byte value); + bool IsValidPosition(Coordinates3D position); + void Save(); + } +} \ No newline at end of file diff --git a/TrueCraft.Core/Networking/PacketReader.cs b/TrueCraft.Core/Networking/PacketReader.cs index 003d1ea..15d683e 100644 --- a/TrueCraft.Core/Networking/PacketReader.cs +++ b/TrueCraft.Core/Networking/PacketReader.cs @@ -6,7 +6,8 @@ namespace TrueCraft.Core.Networking { public class PacketReader : IPacketReader { - public static readonly int ProtocolVersion = 18; + public static readonly int Version = 14; + public int ProtocolVersion { get { return Version; } } private Type[] ClientboundPackets = new Type[0x100]; private Type[] ServerboundPackets = new Type[0x100]; diff --git a/TrueCraft.Core/TrueCraft.Core.csproj b/TrueCraft.Core/TrueCraft.Core.csproj index 7217284..dfb9dd3 100644 --- a/TrueCraft.Core/TrueCraft.Core.csproj +++ b/TrueCraft.Core/TrueCraft.Core.csproj @@ -30,6 +30,9 @@ + + ..\lib\Ionic.Zip.Reduced.dll + @@ -93,6 +96,10 @@ + + + + @@ -100,8 +107,13 @@ {FEE55B54-91B0-4325-A2C3-D576C0B7A81F} TrueCraft.API + + {4488498D-976D-4DA3-BF72-109531AF0488} + fNbt + + \ No newline at end of file diff --git a/TrueCraft.Core/World/Chunk.cs b/TrueCraft.Core/World/Chunk.cs new file mode 100644 index 0000000..1621f24 --- /dev/null +++ b/TrueCraft.Core/World/Chunk.cs @@ -0,0 +1,248 @@ +using System; +using System.Linq; +using System.Runtime.Serialization; +using System.Collections.Generic; +using System.Reflection; +using fNbt; +using fNbt.Serialization; +using TrueCraft.API.World; +using TrueCraft.API; + +namespace TrueCraft.Core.World +{ + public class Chunk : INbtSerializable, IChunk + { + public const int Width = 16, Height = 256, Depth = 16; + + private static readonly NbtSerializer Serializer = new NbtSerializer(typeof(Chunk)); + + [NbtIgnore] + public DateTime LastAccessed { get; set; } + + public bool IsModified { get; set; } + + public byte[] Biomes { get; set; } + + public int[] HeightMap { get; set; } + + [NbtIgnore] + public ISection[] Sections { get; set; } + + [TagName("xPos")] + public int X { get; set; } + + [TagName("zPos")] + public int Z { get; set; } + + public Coordinates2D Coordinates + { + get + { + return new Coordinates2D(X, Z); + } + set + { + X = value.X; + Z = value.Z; + } + } + + public long LastUpdate { get; set; } + + public bool TerrainPopulated { get; set; } + + [NbtIgnore] + public Region ParentRegion { get; set; } + + public Chunk() + { + TerrainPopulated = true; + Sections = new Section[16]; + for (int i = 0; i < Sections.Length; i++) + Sections[i] = new Section((byte)i); + Biomes = new byte[Width * Depth]; + HeightMap = new int[Width * Depth]; + LastAccessed = DateTime.Now; + } + + public Chunk(Coordinates2D coordinates) : this() + { + X = coordinates.X; + Z = coordinates.Z; + } + + public short GetBlockID(Coordinates3D coordinates) + { + LastAccessed = DateTime.Now; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + return Sections[section].GetBlockID(coordinates); + } + + public byte GetMetadata(Coordinates3D coordinates) + { + LastAccessed = DateTime.Now; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + return Sections[section].GetMetadata(coordinates); + } + + public byte GetSkyLight(Coordinates3D coordinates) + { + LastAccessed = DateTime.Now; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + return Sections[section].GetSkyLight(coordinates); + } + + public byte GetBlockLight(Coordinates3D coordinates) + { + LastAccessed = DateTime.Now; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + return Sections[section].GetBlockLight(coordinates); + } + + public void SetBlockID(Coordinates3D coordinates, short value) + { + LastAccessed = DateTime.Now; + IsModified = true; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + Sections[section].SetBlockID(coordinates, value); + var oldHeight = GetHeight((byte)coordinates.X, (byte)coordinates.Z); + if (value == 0) // Air + { + if (oldHeight <= coordinates.Y) + { + // Shift height downwards + while (coordinates.Y > 0) + { + coordinates.Y--; + if (GetBlockID(coordinates) != 0) + SetHeight((byte)coordinates.X, (byte)coordinates.Z, coordinates.Y); + } + } + } + else + { + if (oldHeight < coordinates.Y) + SetHeight((byte)coordinates.X, (byte)coordinates.Z, coordinates.Y); + } + } + + public void SetMetadata(Coordinates3D coordinates, byte value) + { + LastAccessed = DateTime.Now; + IsModified = true; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + Sections[section].SetMetadata(coordinates, value); + } + + public void SetSkyLight(Coordinates3D coordinates, byte value) + { + LastAccessed = DateTime.Now; + IsModified = true; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + Sections[section].SetSkyLight(coordinates, value); + } + + public void SetBlockLight(Coordinates3D coordinates, byte value) + { + LastAccessed = DateTime.Now; + IsModified = true; + int section = GetSectionNumber(coordinates.Y); + coordinates.Y = GetPositionInSection(coordinates.Y); + Sections[section].SetBlockLight(coordinates, value); + } + + private static int GetSectionNumber(int yPos) + { + return yPos / 16; + } + + private static int GetPositionInSection(int yPos) + { + return yPos % 16; + } + + /// + /// Gets the height of the specified column. + /// + public int GetHeight(byte x, byte z) + { + LastAccessed = DateTime.Now; + return HeightMap[(byte)(z * Depth) + x]; + } + + private void SetHeight(byte x, byte z, int value) + { + LastAccessed = DateTime.Now; + IsModified = true; + HeightMap[(byte)(z * Depth) + x] = value; + } + + public NbtFile ToNbt() + { + LastAccessed = DateTime.Now; + var serializer = new NbtSerializer(typeof(Chunk)); + var compound = serializer.Serialize(this, "Level") as NbtCompound; + var file = new NbtFile(); + file.RootTag.Add(compound); + return file; + } + + public static Chunk FromNbt(NbtFile nbt) + { + var serializer = new NbtSerializer(typeof(Chunk)); + var chunk = (Chunk)serializer.Deserialize(nbt.RootTag["Level"]); + return chunk; + } + + public NbtTag Serialize(string tagName) + { + var chunk = (NbtCompound)Serializer.Serialize(this, tagName, true); + var entities = new NbtList("Entities", NbtTagType.Compound); + chunk.Add(entities); + var sections = new NbtList("Sections", NbtTagType.Compound); + var serializer = new NbtSerializer(typeof(Section)); + for (int i = 0; i < Sections.Length; i++) + { + if (Sections[i] is Section) + { + if (!(Sections[i] as Section).IsAir) + sections.Add(serializer.Serialize(Sections[i])); + } + else + sections.Add(serializer.Serialize(Sections[i])); + } + chunk.Add(sections); + return chunk; + } + + public void Deserialize(NbtTag value) + { + IsModified = true; + var compound = value as NbtCompound; + var chunk = (Chunk)Serializer.Deserialize(value, true); + + this.Biomes = chunk.Biomes; + this.HeightMap = chunk.HeightMap; + this.LastUpdate = chunk.LastUpdate; + this.Sections = chunk.Sections; + this.TerrainPopulated = chunk.TerrainPopulated; + this.X = chunk.X; + this.Z = chunk.Z; + + var serializer = new NbtSerializer(typeof(Section)); + foreach (var section in compound["Sections"] as NbtList) + { + int index = section["Y"].IntValue; + Sections[index] = (Section)serializer.Deserialize(section); + Sections[index].ProcessSection(); + } + } + } +} diff --git a/TrueCraft.Core/World/Region.cs b/TrueCraft.Core/World/Region.cs new file mode 100644 index 0000000..0b43996 --- /dev/null +++ b/TrueCraft.Core/World/Region.cs @@ -0,0 +1,306 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using fNbt; +using Ionic.Zlib; +using TrueCraft.API; +using TrueCraft.API.World; +using TrueCraft.Core.Networking; + +namespace TrueCraft.Core.World +{ + /// + /// Represents a 32x32 area of objects. + /// Not all of these chunks are represented at any given time, and + /// will be loaded from disk or generated when the need arises. + /// + public class Region : IDisposable, IRegion + { + // In chunks + public const int Width = 32, Depth = 32; + + /// + /// The currently loaded chunk list. + /// + public IDictionary Chunks { get; set; } + /// + /// The location of this region in the overworld. + /// + public Coordinates2D Position { get; set; } + + public World World { get; set; } + + private Stream regionFile { get; set; } + + /// + /// Creates a new Region for server-side use at the given position using + /// the provided terrain generator. + /// + public Region(Coordinates2D position, World world) + { + Chunks = new Dictionary(); + Position = position; + World = world; + } + + /// + /// Creates a region from the given region file. + /// + public Region(Coordinates2D position, World world, string file) : this(position, world) + { + if (File.Exists(file)) + regionFile = File.Open(file, FileMode.OpenOrCreate); + else + { + regionFile = File.Open(file, FileMode.OpenOrCreate); + CreateRegionHeader(); + } + } + + /// + /// Retrieves the requested chunk from the region, or + /// generates it if a world generator is provided. + /// + /// The position of the requested local chunk coordinates. + public IChunk GetChunk(Coordinates2D position) + { + // TODO: This could use some refactoring + lock (Chunks) + { + if (!Chunks.ContainsKey(position)) + { + if (regionFile != null) + { + // Search the stream for that region + lock (regionFile) + { + var chunkData = GetChunkFromTable(position); + if (chunkData == null) + { + if (World.ChunkProvider == null) + throw new ArgumentException("The requested chunk is not loaded.", "position"); + GenerateChunk(position); + 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 + int compressionMode = regionFile.ReadByte(); + switch (compressionMode) + { + case 1: // gzip + throw new NotImplementedException("gzipped chunks are not implemented"); + case 2: // zlib + var nbt = new NbtFile(); + nbt.LoadFromStream(regionFile, NbtCompression.ZLib, null); + var chunk = Chunk.FromNbt(nbt); + Chunks.Add(position, (IChunk)chunk); + break; + default: + throw new InvalidDataException("Invalid compression scheme provided by region file."); + } + } + } + else if (World.ChunkProvider == null) + throw new ArgumentException("The requested chunk is not loaded.", "position"); + else + GenerateChunk(position); + } + return Chunks[position]; + } + } + + /// + /// Retrieves the requested chunk from the region, without using the + /// world generator if it does not exist. + /// + /// The position of the requested local chunk coordinates. + public IChunk GetChunkWithoutGeneration(Coordinates2D position) + { + // TODO: This could use some refactoring + lock (Chunks) + { + if (!Chunks.ContainsKey(position)) + { + if (regionFile != null) + { + // Search the stream for that region + lock (regionFile) + { + var chunkData = GetChunkFromTable(position); + if (chunkData == null) + return null; + regionFile.Seek(chunkData.Item1, SeekOrigin.Begin); + /*int length = */new MinecraftStream(regionFile).ReadInt32(); // TODO: Avoid making new objects here, and in the WriteInt32 + int compressionMode = regionFile.ReadByte(); + switch (compressionMode) + { + case 1: // gzip + break; + case 2: // zlib + var nbt = new NbtFile(); + nbt.LoadFromStream(regionFile, NbtCompression.ZLib, null); + var chunk = Chunk.FromNbt(nbt); + Chunks.Add(position, (IChunk)chunk); + break; + default: + throw new InvalidDataException("Invalid compression scheme provided by region file."); + } + } + } + else if (World.ChunkProvider == null) + throw new ArgumentException("The requested chunk is not loaded.", "position"); + else + GenerateChunk(position); + } + return Chunks[position]; + } + } + + public void GenerateChunk(Coordinates2D position) + { + var globalPosition = (Position * new Coordinates2D(Width, Depth)) + position; + var chunk = World.ChunkProvider.GenerateChunk(World, globalPosition); + chunk.IsModified = true; + chunk.Coordinates = globalPosition; + Chunks.Add(position, (IChunk)chunk); + } + + /// + /// Sets the chunk at the specified local position to the given value. + /// + public void SetChunk(Coordinates2D position, IChunk chunk) + { + if (!Chunks.ContainsKey(position)) + Chunks.Add(position, chunk); + chunk.IsModified = true; + chunk.Coordinates = position; + chunk.LastAccessed = DateTime.Now; + Chunks[position] = chunk; + } + + /// + /// Saves this region to the specified file. + /// + public void Save(string file) + { + if(File.Exists(file)) + regionFile = regionFile ?? File.Open(file, FileMode.OpenOrCreate); + else + { + regionFile = regionFile ?? File.Open(file, FileMode.OpenOrCreate); + CreateRegionHeader(); + } + Save(); + } + + /// + /// Saves this region to the open region file. + /// + public void Save() + { + lock (Chunks) + { + lock (regionFile) + { + var toRemove = new List(); + foreach (var kvp in Chunks) + { + var chunk = kvp.Value; + if (chunk.IsModified) + { + var data = ((Chunk)chunk).ToNbt(); + byte[] raw = data.SaveToBuffer(NbtCompression.ZLib); + + var header = GetChunkFromTable(kvp.Key); + if (header == null || header.Item2 > raw.Length) + header = AllocateNewChunks(kvp.Key, raw.Length); + + regionFile.Seek(header.Item1, SeekOrigin.Begin); + new MinecraftStream(regionFile).WriteInt32(raw.Length); + regionFile.WriteByte(2); // Compressed with zlib + regionFile.Write(raw, 0, raw.Length); + + chunk.IsModified = false; + } + if ((DateTime.Now - chunk.LastAccessed).TotalMinutes > 5) + toRemove.Add(kvp.Key); + } + regionFile.Flush(); + // Unload idle chunks + foreach (var chunk in toRemove) + Chunks.Remove(chunk); + } + } + } + + #region Stream Helpers + + private const int ChunkSizeMultiplier = 4096; + 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); + Array.Reverse(offsetBuffer); + int length = regionFile.ReadByte(); + int offset = BitConverter.ToInt32(offsetBuffer, 0) << 4; + if (offset == 0 || length == 0) + return null; + return new Tuple(offset, + length * ChunkSizeMultiplier); + } + + private void CreateRegionHeader() + { + regionFile.Write(new byte[8192], 0, 8192); + regionFile.Flush(); + } + + private Tuple AllocateNewChunks(Coordinates2D position, int length) + { + // Expand region file + regionFile.Seek(0, SeekOrigin.End); + int dataOffset = (int)regionFile.Position; + + length /= ChunkSizeMultiplier; + length++; + regionFile.Write(new byte[length * ChunkSizeMultiplier], 0, length * ChunkSizeMultiplier); + + // Write table entry + int tableOffset = ((position.X % Width) + (position.Z % Depth) * Width) * 4; + regionFile.Seek(tableOffset, SeekOrigin.Begin); + + byte[] entry = BitConverter.GetBytes(dataOffset >> 4); + entry[0] = (byte)length; + Array.Reverse(entry); + regionFile.Write(entry, 0, entry.Length); + + return new Tuple(dataOffset, length * ChunkSizeMultiplier); + } + + #endregion + + public static string GetRegionFileName(Coordinates2D position) + { + return string.Format("r.{0}.{1}.mca", position.X, position.Z); + } + + public void UnloadChunk(Coordinates2D position) + { + Chunks.Remove(position); + } + + public void Dispose() + { + if (regionFile == null) + return; + lock (regionFile) + { + regionFile.Flush(); + regionFile.Close(); + } + } + } +} diff --git a/TrueCraft.Core/World/Section.cs b/TrueCraft.Core/World/Section.cs new file mode 100644 index 0000000..d3e436d --- /dev/null +++ b/TrueCraft.Core/World/Section.cs @@ -0,0 +1,123 @@ +using fNbt.Serialization; +using TrueCraft.API; +using TrueCraft.API.World; + +namespace TrueCraft.Core.World +{ + public class Section : ISection + { + public const byte Width = 16, Height = 16, Depth = 16; + + public byte[] Blocks { get; set; } + [TagName("Data")] + public NibbleArray Metadata { get; set; } + public NibbleArray BlockLight { get; set; } + public NibbleArray SkyLight { get; set; } + [IgnoreOnNull] + public NibbleArray Add { get; set; } + public byte Y { get; set; } + + private int nonAirCount; + + public Section() + { + } + + public Section(byte y) + { + const int size = Width * Height * Depth; + this.Y = y; + Blocks = new byte[size]; + Metadata = new NibbleArray(size); + BlockLight = new NibbleArray(size); + SkyLight = new NibbleArray(size); + for (int i = 0; i < size; i++) + SkyLight[i] = 0xFF; + Add = null; // Only used when needed + nonAirCount = 0; + } + + [NbtIgnore] + public bool IsAir + { + get { return nonAirCount == 0; } + } + + public short GetBlockID(Coordinates3D coordinates) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + short value = Blocks[index]; + if (Add != null) + value |= (short)(Add[index] << 8); + return value; + } + + public byte GetMetadata(Coordinates3D coordinates) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + return Metadata[index]; + } + + public byte GetSkyLight(Coordinates3D coordinates) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + return SkyLight[index]; + } + + public byte GetBlockLight(Coordinates3D coordinates) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + return BlockLight[index]; + } + + public void SetBlockID(Coordinates3D coordinates, short value) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + if (value == 0) + { + if (Blocks[index] != 0) + nonAirCount--; + } + else + { + if (Blocks[index] == 0) + nonAirCount++; + } + Blocks[index] = (byte)value; + if ((value & ~0xFF) != 0) + { + if (Add == null) Add = new NibbleArray(Width * Height * Depth); + Add[index] = (byte)((ushort)value >> 8); + } + } + + public void SetMetadata(Coordinates3D coordinates, byte value) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + Metadata[index] = value; + } + + public void SetSkyLight(Coordinates3D coordinates, byte value) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + SkyLight[index] = value; + } + + public void SetBlockLight(Coordinates3D coordinates, byte value) + { + int index = coordinates.X + (coordinates.Z * Width) + (coordinates.Y * Height * Width); + BlockLight[index] = value; + } + + public void ProcessSection() + { + // TODO: Schedule updates + nonAirCount = 0; + for (int i = 0; i < Blocks.Length; i++) + { + if (Blocks[i] != 0) + nonAirCount++; + } + } + } +} \ No newline at end of file diff --git a/TrueCraft.Core/World/World.cs b/TrueCraft.Core/World/World.cs new file mode 100644 index 0000000..a699ce6 --- /dev/null +++ b/TrueCraft.Core/World/World.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Threading; +using TrueCraft.API; +using TrueCraft.API.World; + +namespace TrueCraft.Core.World +{ + public class World : IDisposable, IWorld + { + public const int Height = 256; + + public string Name { get; set; } + public string BaseDirectory { get; internal set; } + public IDictionary Regions { get; set; } + public IChunkProvider ChunkProvider { get; set; } + + public World(string name) + { + Name = name; + Regions = new Dictionary(); + } + + public World(string name, IChunkProvider chunkProvider) : this(name) + { + ChunkProvider = chunkProvider; + } + + public static World LoadWorld(string baseDirectory) + { + if (!Directory.Exists(baseDirectory)) + throw new DirectoryNotFoundException(); + var world = new World(Path.GetFileName(baseDirectory)); + world.BaseDirectory = baseDirectory; + return world; + } + + /// + /// Finds a chunk that contains the specified block coordinates. + /// + public IChunk FindChunk(Coordinates3D coordinates) + { + IChunk chunk; + FindBlockPosition(coordinates, out chunk); + return chunk; + } + + public IChunk GetChunk(Coordinates2D coordinates) + { + int regionX = coordinates.X / Region.Width - ((coordinates.X < 0) ? 1 : 0); + int regionZ = coordinates.Z / Region.Depth - ((coordinates.Z < 0) ? 1 : 0); + + var region = LoadOrGenerateRegion(new Coordinates2D(regionX, regionZ)); + return region.GetChunk(new Coordinates2D(coordinates.X - regionX * 32, coordinates.Z - regionZ * 32)); + } + + public void GenerateChunk(Coordinates2D coordinates) + { + int regionX = coordinates.X / Region.Width - ((coordinates.X < 0) ? 1 : 0); + int regionZ = coordinates.Z / Region.Depth - ((coordinates.Z < 0) ? 1 : 0); + + var region = LoadOrGenerateRegion(new Coordinates2D(regionX, regionZ)); + region.GenerateChunk(new Coordinates2D(coordinates.X - regionX * 32, coordinates.Z - regionZ * 32)); + } + + public Chunk GetChunkWithoutGeneration(Coordinates2D coordinates) + { + int regionX = coordinates.X / Region.Width - ((coordinates.X < 0) ? 1 : 0); + int regionZ = coordinates.Z / Region.Depth - ((coordinates.Z < 0) ? 1 : 0); + + var regionPosition = new Coordinates2D(regionX, regionZ); + if (!Regions.ContainsKey(regionPosition)) return null; + return (Chunk)((Region)Regions[regionPosition]).GetChunkWithoutGeneration( + new Coordinates2D(coordinates.X - regionX * 32, coordinates.Z - regionZ * 32)); + } + + public void SetChunk(Coordinates2D coordinates, Chunk chunk) + { + int regionX = coordinates.X / Region.Width - ((coordinates.X < 0) ? 1 : 0); + int regionZ = coordinates.Z / Region.Depth - ((coordinates.Z < 0) ? 1 : 0); + + var region = LoadOrGenerateRegion(new Coordinates2D(regionX, regionZ)); + lock (region) + { + chunk.IsModified = true; + region.SetChunk(new Coordinates2D(coordinates.X - regionX * 32, coordinates.Z - regionZ * 32), chunk); + } + } + + public void UnloadRegion(Coordinates2D coordinates) + { + lock (Regions) + { + Regions[coordinates].Save(Path.Combine(BaseDirectory, Region.GetRegionFileName(coordinates))); + Regions.Remove(coordinates); + } + } + + public void UnloadChunk(Coordinates2D coordinates) + { + int regionX = coordinates.X / Region.Width - ((coordinates.X < 0) ? 1 : 0); + int regionZ = coordinates.Z / Region.Depth - ((coordinates.Z < 0) ? 1 : 0); + + var regionPosition = new Coordinates2D(regionX, regionZ); + if (!Regions.ContainsKey(regionPosition)) + throw new ArgumentOutOfRangeException("coordinates"); + Regions[regionPosition].UnloadChunk(new Coordinates2D(coordinates.X - regionX * 32, coordinates.Z - regionZ * 32)); + } + + public short GetBlockID(Coordinates3D coordinates) + { + IChunk chunk; + coordinates = FindBlockPosition(coordinates, out chunk); + return chunk.GetBlockID(coordinates); + } + + public byte GetMetadata(Coordinates3D coordinates) + { + IChunk chunk; + coordinates = FindBlockPosition(coordinates, out chunk); + return chunk.GetMetadata(coordinates); + } + + public byte GetSkyLight(Coordinates3D coordinates) + { + IChunk chunk; + coordinates = FindBlockPosition(coordinates, out chunk); + return chunk.GetSkyLight(coordinates); + } + + public byte GetBlockLight(Coordinates3D coordinates) + { + IChunk chunk; + coordinates = FindBlockPosition(coordinates, out chunk); + return chunk.GetBlockLight(coordinates); + } + + public void SetBlockID(Coordinates3D coordinates, short value) + { + IChunk chunk; + var adjustedCoordinates = FindBlockPosition(coordinates, out chunk); + chunk.SetBlockID(adjustedCoordinates, value); + } + + public void SetMetadata(Coordinates3D coordinates, byte value) + { + IChunk chunk; + var adjustedCoordinates = FindBlockPosition(coordinates, out chunk); + chunk.SetMetadata(adjustedCoordinates, value); + } + + public void SetSkyLight(Coordinates3D coordinates, byte value) + { + IChunk chunk; + coordinates = FindBlockPosition(coordinates, out chunk); + chunk.SetSkyLight(coordinates, value); + } + + public void SetBlockLight(Coordinates3D coordinates, byte value) + { + IChunk chunk; + coordinates = FindBlockPosition(coordinates, out chunk); + chunk.SetBlockLight(coordinates, value); + } + + public void Save() + { + lock (Regions) + { + foreach (var region in Regions) + region.Value.Save(Path.Combine(BaseDirectory, Region.GetRegionFileName(region.Key))); + } + } + + public void Save(string path) + { + if (!Directory.Exists(path)) + Directory.CreateDirectory(path); + BaseDirectory = path; + lock (Regions) + { + foreach (var region in Regions) + region.Value.Save(Path.Combine(BaseDirectory, Region.GetRegionFileName(region.Key))); + } + } + + public Coordinates3D FindBlockPosition(Coordinates3D coordinates, out IChunk chunk) + { + if (coordinates.Y < 0 || coordinates.Y >= Chunk.Height) + throw new ArgumentOutOfRangeException("coordinates", "Coordinates are out of range"); + + var chunkX = (int)Math.Floor((double)coordinates.X / Chunk.Width); + var chunkZ = (int)Math.Floor((double)coordinates.Z / Chunk.Depth); + + chunk = GetChunk(new Coordinates2D(chunkX, chunkZ)); + return new Coordinates3D( + (coordinates.X - chunkX * Chunk.Width) % Chunk.Width, + coordinates.Y, + (coordinates.Z - chunkZ * Chunk.Depth) % Chunk.Depth); + } + + public bool IsValidPosition(Coordinates3D position) + { + return position.Y >= 0 && position.Y <= 255; + } + + private Region LoadOrGenerateRegion(Coordinates2D coordinates) + { + if (Regions.ContainsKey(coordinates)) + return (Region)Regions[coordinates]; + Region region; + if (BaseDirectory != null) + { + var file = Path.Combine(BaseDirectory, Region.GetRegionFileName(coordinates)); + if (File.Exists(file)) + region = new Region(coordinates, this, file); + else + region = new Region(coordinates, this); + } + else + region = new Region(coordinates, this); + lock (Regions) + Regions[coordinates] = region; + return region; + } + + public void Dispose() + { + foreach (var region in Regions) + region.Value.Dispose(); + } + } +} \ No newline at end of file diff --git a/TrueCraft/Handlers/LoginHandlers.cs b/TrueCraft/Handlers/LoginHandlers.cs index 94c71e6..bdd0646 100644 --- a/TrueCraft/Handlers/LoginHandlers.cs +++ b/TrueCraft/Handlers/LoginHandlers.cs @@ -19,7 +19,15 @@ namespace TrueCraft.Handlers { var packet = (LoginRequestPacket)_packet; var client = (RemoteClient)_client; - client.QueuePacket(new DisconnectPacket("It works!")); + Console.WriteLine(packet.ProtocolVersion); + if (packet.ProtocolVersion < server.PacketReader.ProtocolVersion) + client.QueuePacket(new DisconnectPacket("Client outdated! Use beta 1.7.3!")); + else if (packet.ProtocolVersion > server.PacketReader.ProtocolVersion) + client.QueuePacket(new DisconnectPacket("Server outdated! Use beta 1.7.3!")); + else + { + client.LoggedIn = true; + } } } } \ No newline at end of file diff --git a/TrueCraft/MultiplayerServer.cs b/TrueCraft/MultiplayerServer.cs index 6eff105..f80787a 100644 --- a/TrueCraft/MultiplayerServer.cs +++ b/TrueCraft/MultiplayerServer.cs @@ -72,7 +72,7 @@ namespace TrueCraft { PacketHandlers[packet.ID](packet, client, this); } - catch (Exception e) + catch (Exception) { // TODO: Something else Clients.Remove(client); diff --git a/TrueCraft/RemoteClient.cs b/TrueCraft/RemoteClient.cs index 129fb4f..1d20fcc 100644 --- a/TrueCraft/RemoteClient.cs +++ b/TrueCraft/RemoteClient.cs @@ -19,6 +19,7 @@ namespace TrueCraft public IMinecraftStream MinecraftStream { get; internal set; } public ConcurrentQueue PacketQueue { get; private set; } public string Username { get; internal set; } + public bool LoggedIn { get; internal set; } public bool DataAvailable { diff --git a/doc/differences-from-vanilla b/doc/differences-from-vanilla new file mode 100644 index 0000000..8d3027b --- /dev/null +++ b/doc/differences-from-vanilla @@ -0,0 +1,3 @@ +Differences between TrueCraft and vanilla beta 1.7.3: + +- Uses the Anvil level format instead of MCRegion diff --git a/doc/login-sequence b/doc/login-sequence new file mode 100644 index 0000000..1c6cdb3 --- /dev/null +++ b/doc/login-sequence @@ -0,0 +1,10 @@ +C->S: Handshake +C<-S: Handshake response +[authenticate if needed] +C->S: Login request +C<-S: Login response or kick +C<-S: Chunks and entities +C<-S: Spawn position +C<-S: Inventory +C<-S: Position+Look +S<-C: Position+Look, player is now logged in diff --git a/lib/Ionic.Zip.Reduced.dll b/lib/Ionic.Zip.Reduced.dll new file mode 100644 index 0000000..9622cc5 Binary files /dev/null and b/lib/Ionic.Zip.Reduced.dll differ