csharp/0blu/PhotonPackageParser/Protocol16/Protocol16Serializer.cs

Protocol16Serializer.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Protocol16.Photon;

namespace Protocol16
{
    public static clast Protocol16Serializer
    {
        private static readonly ThreadLocal _byteBuffer = new ThreadLocal(() => new byte[sizeof(long)]);
        private static readonly ThreadLocal _longBuffer = new ThreadLocal(() => new long[1]);
        private static readonly ThreadLocal _floatBuffer = new ThreadLocal(() => new float[1]);
        private static readonly ThreadLocal _doubleBuffer = new ThreadLocal(() => new double[1]);

        public static void Serialize(Protocol16Stream output, object obj, bool writeTypeCode)
        {
            if (obj == null)
            {
                output.WriteTypeCodeIfTrue(Protocol16Type.Null, writeTypeCode);
                return;
            }

            Protocol16Type type = TypeCodeToProtocol16Type(obj.GetType());
            switch (type)
            {
                case Protocol16Type.Boolean:
                    SerializeBoolean(output, (bool)obj, writeTypeCode);
                    return;
                case Protocol16Type.Byte:
                    SerializeByte(output, (byte)obj, writeTypeCode);
                    return;
                case Protocol16Type.Short:
                    SerializeShort(output, (short)obj, writeTypeCode);
                    return;
                case Protocol16Type.Integer:
                    SerializeInteger(output, (int)obj, writeTypeCode);
                    return;
                case Protocol16Type.Long:
                    SerializeLong(output, (long)obj, writeTypeCode);
                    return;
                case Protocol16Type.Float:
                    SerializeFloat(output, (float)obj, writeTypeCode);
                    return;
                case Protocol16Type.Double:
                    SerializeDouble(output, (double)obj, writeTypeCode);
                    return;
                case Protocol16Type.String:
                    SerializeString(output, (string)obj, writeTypeCode);
                    return;
                case Protocol16Type.EventData:
                    SerializeEventData(output, (EventData)obj, writeTypeCode);
                    return;
                case Protocol16Type.Hashtable:
                    SerializeHashtable(output, (Hashtable)obj, writeTypeCode);
                    return;
                case Protocol16Type.Dictionary:
                    SerializeDictionary(output, (IDictionary)obj, writeTypeCode);
                    return;
                case Protocol16Type.OperationResponse:
                    SerializeOperationResponse(output, (OperationResponse)obj, writeTypeCode);
                    return;
                case Protocol16Type.OperationRequest:
                    SerializeOperationRequest(output, (OperationRequest)obj, writeTypeCode);
                    return;
                case Protocol16Type.IntegerArray:
                case Protocol16Type.StringArray:
                case Protocol16Type.ByteArray:
                case Protocol16Type.ObjectArray:
                case Protocol16Type.Array:
                    SerializeAnyArray(output, (Array)obj, writeTypeCode, type);
                    return;
            }

            // Special case
            if (obj is ArraySegment arraySegment)
            {
                SerializeArraySegment(output, arraySegment, writeTypeCode);
                return;
            }

            throw new ArgumentException($"Cannot serialize objects of type {obj.GetType()} / System.TypeCode: {Type.GetTypeCode(obj.GetType())}");
        }

        private static void SerializeArraySegment(Protocol16Stream output, ArraySegment arraySegment, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.ByteArray, writeTypeCode);
            SerializeInteger(output, arraySegment.Count, false);
            output.Write(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
        }

        private static void SerializeBoolean(Protocol16Stream output, bool value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Boolean, writeTypeCode);
            output.WriteByte(value ? (byte)1 : (byte)0);
        }

        private static void SerializeByte(Protocol16Stream output, byte value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Byte, writeTypeCode);
            output.WriteByte(value);
        }

        private static void SerializeShort(Protocol16Stream output, short value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Short, writeTypeCode);
            var buffer = _byteBuffer.Value;
            buffer[0] = (byte)(value >> 8);
            buffer[1] = (byte)(value);
            output.Write(buffer, 0, sizeof(short));
        }

        private static void SerializeInteger(Protocol16Stream output, int value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Integer, writeTypeCode);
            var buffer = _byteBuffer.Value;
            buffer[0] = (byte)(value >> 24);
            buffer[1] = (byte)(value >> 16);
            buffer[2] = (byte)(value >> 8);
            buffer[3] = (byte)(value);
            output.Write(buffer, 0, sizeof(int));
        }

        private static void SerializeLong(Protocol16Stream output, long value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Long, writeTypeCode);
            var longBuffer = _longBuffer.Value;
            longBuffer[0] = value;
            var buffer = _byteBuffer.Value;
            Buffer.BlockCopy(longBuffer, 0, buffer, 0, sizeof(long));
            if (BitConverter.IsLittleEndian)
            {
                byte b0 = buffer[0];
                byte b1 = buffer[1];
                byte b2 = buffer[2];
                byte b3 = buffer[3];
                buffer[0] = buffer[7];
                buffer[1] = buffer[6];
                buffer[2] = buffer[5];
                buffer[3] = buffer[4];
                buffer[4] = b3;
                buffer[5] = b2;
                buffer[6] = b1;
                buffer[7] = b0;
            }
            output.Write(buffer, 0, sizeof(long));
        }

        private static void SerializeFloat(Protocol16Stream output, float value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Float, writeTypeCode);
            var floatBuffer = _floatBuffer.Value;
            floatBuffer[0] = value;
            var buffer = _byteBuffer.Value;
            Buffer.BlockCopy(floatBuffer, 0, buffer, 0, sizeof(float));
            if (BitConverter.IsLittleEndian)
            {
                byte b0 = buffer[0];
                byte b1 = buffer[1];
                buffer[0] = buffer[3];
                buffer[1] = buffer[2];
                buffer[2] = b1;
                buffer[3] = b0;
            }
            output.Write(buffer, 0, sizeof(float));
        }

        private static void SerializeDouble(Protocol16Stream output, double value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Double, writeTypeCode);
            var doubleBuffer = _doubleBuffer.Value;
            doubleBuffer[0] = value;
            var buffer = _byteBuffer.Value;
            Buffer.BlockCopy(doubleBuffer, 0, buffer, 0, sizeof(double));
            if (BitConverter.IsLittleEndian)
            {
                byte b0 = buffer[0];
                byte b1 = buffer[1];
                byte b2 = buffer[2];
                byte b3 = buffer[3];
                buffer[0] = buffer[7];
                buffer[1] = buffer[6];
                buffer[2] = buffer[5];
                buffer[3] = buffer[4];
                buffer[4] = b3;
                buffer[5] = b2;
                buffer[6] = b1;
                buffer[7] = b0;
            }
            output.Write(buffer, 0, sizeof(double));
        }

        private static void SerializeIntArray(Protocol16Stream output, int[] ints, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.IntegerArray, writeTypeCode);
            SerializeInteger(output, ints.Length, false);
            var array = new byte[ints.Length * sizeof(int)];
            var idx = 0;
            foreach (var number in ints)
            {
                array[idx++] = (byte)(number >> 24);
                array[idx++] = (byte)(number >> 16);
                array[idx++] = (byte)(number >> 8);
                array[idx++] = (byte)(number);
            }
            output.Write(array, 0, array.Length);
        }

        private static void SerializeString(Protocol16Stream output, string value, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.String, writeTypeCode);
            byte[] bytes = Encoding.UTF8.GetBytes(value);
            if (bytes.Length > short.MaxValue)
            {
                throw new NotSupportedException($"Strings that exceed a UTF8-encoded byte-length of {short.MaxValue} (short.MaxValue) are not supported. Yours is: {bytes.Length}");
            }
            SerializeShort(output, (short)bytes.Length, false);
            output.Write(bytes, 0, bytes.Length);
        }
        
        private static void SerializeStringArray(Protocol16Stream output, string[] strings, bool writeTypeCode)
        {
            if (strings.Length > short.MaxValue)
            {
                throw new NotSupportedException($"string[] can only have a maximum size of {short.MaxValue} (short.MaxValue). Yours is: {strings.Length}");
            }
            output.WriteTypeCodeIfTrue(Protocol16Type.StringArray, writeTypeCode);
            SerializeShort(output, (short)strings.Length, false);
            foreach (var s in strings)
            {
                SerializeString(output, s, false);
            }
        }

        private static void SerializeEventData(Protocol16Stream output, EventData data, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.EventData, writeTypeCode);
            output.WriteByte(data.Code);
            SerializeParameterTable(output, data.Parameters);
        }

        private static void SerializeOperationResponse(Protocol16Stream output, OperationResponse data, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.OperationResponse, writeTypeCode);
            output.WriteByte(data.OperationCode);
            SerializeShort(output, data.ReturnCode, false);
            if (string.IsNullOrEmpty(data.DebugMessage))
            {
                output.WriteTypeCodeIfTrue(Protocol16Type.Null, true);
            }
            else
            {
                // WTF ExitGames, why did you set the writeCode to false?!
                SerializeString(output, data.DebugMessage, true);
            }
            SerializeParameterTable(output, data.Parameters);
        }

        private static void SerializeOperationRequest(Protocol16Stream output, OperationRequest data, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.OperationRequest, writeTypeCode);
            output.WriteByte(data.OperationCode);
            SerializeParameterTable(output, data.Parameters);
        }

        private static void SerializeParameterTable(Protocol16Stream output, Dictionary parameters)
        {
            if (parameters == null || parameters.Count == 0)
            {
                SerializeShort(output, 0, false);
                return;
            }

            SerializeShort(output, (short)parameters.Count, false);
            foreach (KeyValuePair keyValuePair in parameters)
            {
                output.WriteByte(keyValuePair.Key);
                Serialize(output, keyValuePair.Value, true);
            }
        }

        private static void SerializeByteArray(Protocol16Stream output, byte[] obj, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.ByteArray, writeTypeCode);
            SerializeInteger(output, obj.Length, false);
            output.Write(obj, 0, obj.Length);
        }

        private static void SerializeAnyArray(Protocol16Stream output, Array array, bool writeTypeCode, Protocol16Type arrayType)
        {
            if (arrayType == Protocol16Type.ObjectArray)
            {
                SerializeObjectArray(output, (object[])array, writeTypeCode);
                return;
            }

            // Fallback to object array if null is included
            var containsNull = false;
            foreach (var element in array)
            {
                if (element == null)
                {
                    containsNull = true;
                    break;
                }
            }

            if (containsNull) {
                SerializeObjectArray(output, (object[])array, writeTypeCode);
                return;
            }

            switch (arrayType)
            {
                case Protocol16Type.StringArray:
                    SerializeStringArray(output, (string[])array, writeTypeCode);
                    return;
                case Protocol16Type.IntegerArray:
                    SerializeIntArray(output, (int[])array, writeTypeCode);
                    break;
                case Protocol16Type.ByteArray:
                    SerializeByteArray(output, (byte[])array, writeTypeCode);
                    break;
                case Protocol16Type.Array:
                    SerializeArrayWithSameElements(output, array, writeTypeCode);
                    break;
                default:
                    throw new Exception("Unknown array type");
            }


        }

        private static void SerializeArrayWithSameElements(Protocol16Stream output, Array array, bool writeTypeCode)
        {
            if (array.Length > short.MaxValue)
            {
                throw new NotSupportedException($"Arrays can only have a maximum size of {short.MaxValue} (short.MaxValue). Yours is: {array.Length}");
            }
            output.WriteTypeCodeIfTrue(Protocol16Type.Array, writeTypeCode);
            SerializeShort(output, (short)array.Length, false);

            Type elementType = array.GetType().GetElementType();

            Protocol16Type protocol16Type = TypeCodeToProtocol16Type(elementType);
            if (protocol16Type == Protocol16Type.Unknown)
            {
                throw new Exception("Custom types are currently not supported");
            }
            output.WriteTypeCodeIfTrue(protocol16Type, true);
            if (protocol16Type == Protocol16Type.Dictionary)
            {
                // WTF ExitGames, why are you trying to get GetGenericArguments() of an array..?!
                // I think what you wanted to do is just give the element type to SerializeDictionaryHeader not the array type
                SerializeDictionaryHeader(output, elementType, out var writeKeyCode, out var writeValueCode);
                foreach (var o in array)
                {
                    SerializeDictionaryElements(output, (IDictionary)o, writeKeyCode, writeValueCode);
                }
            }
            else
            {
                foreach (object o in array)
                {
                    Serialize(output, o, false);
                }
            }
        }

        private static void SerializeObjectArray(Protocol16Stream output, object[] objects, bool writeTypeCode)
        {
            if (objects.Length > short.MaxValue)
            {
                throw new NotSupportedException($"objects[] can only have a maximum size of {short.MaxValue} (short.MaxValue). Yours is: {objects.Length}");
            }
            output.WriteTypeCodeIfTrue(Protocol16Type.ObjectArray, writeTypeCode);
            SerializeShort(output, (short)objects.Length, false);
            foreach (var s in objects)
            {
                Serialize(output, s, true);
            }
        }
        private static void SerializeHashtable(Protocol16Stream output, Hashtable hashtable, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Hashtable, writeTypeCode);
            SerializeDictionaryElements(output, hashtable, true, true);
        }

        private static void SerializeDictionary(Protocol16Stream output, IDictionary dictionary, bool writeTypeCode)
        {
            output.WriteTypeCodeIfTrue(Protocol16Type.Dictionary, writeTypeCode);

            SerializeDictionaryHeader(output, dictionary.GetType(), out var writeKeyCode, out var writeValueCode);
            SerializeDictionaryElements(output, dictionary, writeKeyCode, writeValueCode);
        }

        private static void SerializeDictionaryHeader(Protocol16Stream output, Type dictionaryType, out bool writeKeyCode, out bool writeValueCode)
        {
            Type[] genericArguments = dictionaryType.GetGenericArguments();
            writeKeyCode = (genericArguments[0] == typeof(object));
            writeValueCode = (genericArguments[1] == typeof(object));
            if (writeKeyCode)
            {
                output.WriteTypeCodeIfTrue(Protocol16Type.Unknown, true);
            }
            else
            {
                Protocol16Type typeOfKey = TypeCodeToProtocol16Type(genericArguments[0]);
                if (typeOfKey == Protocol16Type.Unknown || typeOfKey == Protocol16Type.Dictionary)
                {
                    throw new Exception("Unexpected - cannot serialize Dictionary with key type: " + genericArguments[0]);
                }
                output.WriteTypeCodeIfTrue(typeOfKey, true);
            }
            if (writeValueCode)
            {
                output.WriteTypeCodeIfTrue(Protocol16Type.Unknown, true);
            }
            else
            {
                Protocol16Type typeOfValue = TypeCodeToProtocol16Type(genericArguments[1]);
                if (typeOfValue == Protocol16Type.Unknown)
                {
                    throw new Exception("Unexpected - cannot serialize Dictionary with value type: " + genericArguments[0]);
                }
                output.WriteTypeCodeIfTrue(typeOfValue, true);
                if (typeOfValue == Protocol16Type.Dictionary)
                {
                    throw new Exception("TODO: Nested Dictionaries");
                }
            }
        }

        private static void SerializeDictionaryElements(Protocol16Stream output, IDictionary data, bool writeKeyCode, bool writeValueCode)
        {
            if (data.Count > short.MaxValue)
            {
                throw new NotSupportedException($"Dictionaries can only have a maximum size of {short.MaxValue} (short.MaxValue). Yours is: {data.Count}");
            }
            SerializeShort(output, (short)data.Count, false);

            foreach (DictionaryEntry entry in data)
            {
                if (!writeKeyCode && entry.Key == null)
                {
                    throw new Exception("This should never happen. Cannot serialize the null(key) object in Dictionary when writing the key code is disabled.");
                }

                if (!writeValueCode && entry.Value == null)
                {
                    throw new Exception("This should never happen. Cannot serialize the null(value) object in Dictionary when writing the value code is disabled.");
                }
                Serialize(output, entry.Key, writeKeyCode);
                Serialize(output, entry.Value, writeValueCode);
            }
        }


        private static Protocol16Type TypeCodeToProtocol16Type(Type type)
        {
            switch (Type.GetTypeCode(type))
            {
                case TypeCode.Boolean:
                    return Protocol16Type.Boolean;
                case TypeCode.Byte:
                    return Protocol16Type.Byte;
                case TypeCode.Int16:
                    return Protocol16Type.Short;
                case TypeCode.Int32:
                    return Protocol16Type.Integer;
                case TypeCode.Int64:
                    return Protocol16Type.Long;
                case TypeCode.Single:
                    return Protocol16Type.Float;
                case TypeCode.Double:
                    return Protocol16Type.Double;
                case TypeCode.String:
                    return Protocol16Type.String;
            }

            if (type.IsArray)
            {
                var elementType = type.GetElementType();
                
                if (elementType == typeof(byte))
                {
                    return Protocol16Type.ByteArray;
                }

                if (elementType == typeof(string))
                {
                    return Protocol16Type.StringArray;
                }

                if (elementType == typeof(int))
                {
                    return Protocol16Type.IntegerArray;
                }

                if (elementType == typeof(object))
                {
                    return Protocol16Type.ObjectArray;
                }

                return Protocol16Type.Array;
            }

            if (type == typeof(Hashtable))
            {
                return Protocol16Type.Hashtable;
            }

            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary))
            {
                return Protocol16Type.Dictionary;
            }

            if (type == typeof(EventData))
            {
                return Protocol16Type.EventData;
            }

            if (type == typeof(OperationRequest))
            {
                return Protocol16Type.OperationRequest;
            }

            if (type == typeof(OperationResponse))
            {
                return Protocol16Type.OperationResponse;
            }

            return Protocol16Type.Unknown;
        }
    }
}