code/Library/NetworkedVoxelVolume/NetworkedVoxelVolume.cs
namespace Boxfish.Library;

/// <summary>
/// A networked voxel volume component.
/// <para>Remember to make your GameObject networked, and use the <see cref="BroadcastSet(Vector3Int, Boxfish.Library.Voxel)"/> etc.. methods to apply voxel changes!</para>
/// <para>NOTE: You should inherit this component if you want to serialize a volume with something such as world generation..</para>
/// </summary>
[Icon( "view_in_ar" ), Category( "Boxfish" )]
public partial class NetworkedVoxelVolume 
	: VoxelVolume, Component.INetworkSnapshot
{
	#region Snapshots
	void INetworkSnapshot.ReadSnapshot( ref ByteStream reader )
	{
		// Read from the ByteStream and deserialize.
		var length = reader.ReadRemaining;
		if ( length == 0 ) 
			return;

		var buffer = new byte[length];
		reader.Read( buffer, 0, length );

		// Uncompress and deserialize data.
		var uncompressed = buffer.Decompress();
		Deserialize( uncompressed );
	}

	void INetworkSnapshot.WriteSnapshot( ref ByteStream writer )
	{
		// Serialize and write to the ByteStream.
		var serialized = Serialize();
		if ( serialized.Length == 0 )
			return;

		var compressed = serialized.Compress();
		writer.Write( compressed );
	}
	#endregion

	/// <summary>
	/// This is called by the host to serialize the current world and send it to the connecting client.
	/// <para>NOTE: You should override this if you're doing something like: world generation... This can get expensive fast!</para>
	/// </summary>
	/// <returns></returns>
	public virtual byte[] Serialize()
	{
		if ( Chunks is null )
			return null;

		using var stream = new MemoryStream();
		using var writer = new BinaryWriter( stream );

		// Header
		writer.Write( Chunks.Count );

		// Chunks
		foreach ( var (position, chunk) in Chunks )
		{
			writer.Write( position.x );
			writer.Write( position.y );
			writer.Write( position.z );

			// Voxels
			var i = stream.Position;
			var count = (ushort)0;

			stream.Seek( i + sizeof( ushort ), SeekOrigin.Begin );
			for ( byte x = 0; x < VoxelUtils.CHUNK_SIZE; x++ )
				for ( byte y = 0; y < VoxelUtils.CHUNK_SIZE; y++ )
					for ( byte z = 0; z < VoxelUtils.CHUNK_SIZE; z++ )
					{
						var voxel = chunk.GetVoxel( x, y, z );
						if ( voxel.Valid )
						{
							writer.Write( x );
							writer.Write( y );
							writer.Write( z );

							writer.Write( voxel.R );
							writer.Write( voxel.G );
							writer.Write( voxel.B );
							writer.Write( voxel.Texture );

							count++;
						}
					}

			var j = stream.Position;
			stream.Seek( i, SeekOrigin.Begin );
			writer.Write( count );
			stream.Seek( j, SeekOrigin.Begin );
		}

		return stream
			.ToArray()
			.Compress();
	}

	/// <summary>
	/// This is called upon snapshot loading, we apply the snapshot data to the world here.
	/// <para>NOTE: You should override this if you're doing something like: world generation... This can get expensive fast!</para>
	/// </summary>
	/// <param name="data"></param>
	public virtual void Deserialize( byte[] data )
	{
		var chunks = new Dictionary<Vector3Int, Chunk>();
		using var stream = new MemoryStream( data.Decompress() );
		using var reader = new BinaryReader( stream );

		// Header
		var amount = reader.ReadInt32();

		// Chunks
		for ( int i = 0; i < amount; i++ )
		{
			var chunkX = reader.ReadInt32();
			var chunkY = reader.ReadInt32();
			var chunkZ = reader.ReadInt32();

			var chunk = new Chunk( chunkX, chunkY, chunkZ, chunks );
			chunks.TryAdd( new( chunkX, chunkY, chunkZ ), chunk );

			// Voxels
			var count = reader.ReadUInt16();
			for ( int j = 0; j < count; j++ )
			{
				var x = reader.ReadByte();
				var y = reader.ReadByte();
				var z = reader.ReadByte();

				var color = new Color32( reader.ReadByte(), reader.ReadByte(), reader.ReadByte() );
				var texture = reader.ReadUInt16();
				var voxel = new Voxel( color, texture );

				chunk.SetVoxel( x, y, z, voxel );
			}
		}

		// Set chunks, generate meshes.
		SetChunks( chunks );
		_ = GenerateMeshes( chunks.Values );
	}
}