namespace Unity.Netcode { /// /// Two-way serializer wrapping FastBufferReader or FastBufferWriter. /// /// Implemented as a ref struct for two reasons: /// 1. The BufferSerializer cannot outlive the FBR/FBW it wraps or using it will cause a crash /// 2. The BufferSerializer must always be passed by reference and can't be copied /// /// Ref structs help enforce both of those rules: they can't out live the stack context in which they were /// created, and they're always passed by reference no matter what. /// /// BufferSerializer doesn't wrapp FastBufferReader or FastBufferWriter directly because it can't. /// ref structs can't implement interfaces, and in order to be able to have two different implementations with /// the same interface (which allows us to avoid an "if(IsReader)" on every call), the thing directly wrapping /// the struct has to implement an interface. So IReaderWriter exists as the interface, /// which is implemented by a normal struct, while the ref struct wraps the normal one to enforce the two above /// requirements. (Allowing direct access to the IReaderWriter struct would allow dangerous /// things to happen because the struct's lifetime could outlive the Reader/Writer's.) /// /// The implementation struct public ref struct BufferSerializer where TReaderWriter : IReaderWriter { private TReaderWriter m_Implementation; /// /// Check if the contained implementation is a reader /// public bool IsReader => m_Implementation.IsReader; /// /// Check if the contained implementation is a writer /// public bool IsWriter => m_Implementation.IsWriter; internal BufferSerializer(TReaderWriter implementation) { m_Implementation = implementation; } /// /// Retrieves the FastBufferReader instance. Only valid if IsReader = true, throws /// InvalidOperationException otherwise. /// /// Reader instance public FastBufferReader GetFastBufferReader() { return m_Implementation.GetFastBufferReader(); } /// /// Retrieves the FastBufferWriter instance. Only valid if IsWriter = true, throws /// InvalidOperationException otherwise. /// /// Writer instance public FastBufferWriter GetFastBufferWriter() { return m_Implementation.GetFastBufferWriter(); } /// /// Serialize an INetworkSerializable /// /// Throws OverflowException if the end of the buffer has been reached. /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. /// /// Value to serialize public void SerializeNetworkSerializable(ref T value) where T : INetworkSerializable, new() { m_Implementation.SerializeNetworkSerializable(ref value); } /// /// Serialize a string. /// /// Note: Will ALWAYS allocate a new string when reading. /// /// Throws OverflowException if the end of the buffer has been reached. /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. /// /// Value to serialize /// /// If true, will truncate each char to one byte. /// This is slower than two-byte chars, but uses less bandwidth. /// public void SerializeValue(ref string s, bool oneByteChars = false) { m_Implementation.SerializeValue(ref s, oneByteChars); } /// /// Serialize an array value. /// /// Note: Will ALWAYS allocate a new array when reading. /// If you have a statically-sized array that you know is large enough, it's recommended to /// serialize the size yourself and iterate serializing array members. /// /// (This is because C# doesn't allow setting an array's length value, so deserializing /// into an existing array of larger size would result in an array that doesn't have as many values /// as its Length indicates it should.) /// /// Throws OverflowException if the end of the buffer has been reached. /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. /// /// Value to serialize public void SerializeValue(ref T[] array) where T : unmanaged { m_Implementation.SerializeValue(ref array); } /// /// Serialize a single byte /// /// Throws OverflowException if the end of the buffer has been reached. /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. /// /// Value to serialize public void SerializeValue(ref byte value) { m_Implementation.SerializeValue(ref value); } /// /// Serialize an unmanaged type. Supports basic value types as well as structs. /// The provided type will be copied to/from the buffer as it exists in memory. /// /// Throws OverflowException if the end of the buffer has been reached. /// Write buffers will grow up to the maximum allowable message size before throwing OverflowException. /// /// Value to serialize public void SerializeValue(ref T value) where T : unmanaged { m_Implementation.SerializeValue(ref value); } /// /// Allows faster serialization by batching bounds checking. /// When you know you will be writing multiple fields back-to-back and you know the total size, /// you can call PreCheck() once on the total size, and then follow it with calls to /// SerializeValuePreChecked() for faster serialization. Write buffers will grow during PreCheck() /// if needed. /// /// PreChecked serialization operations will throw OverflowException in editor and development builds if you /// go past the point you've marked using PreCheck(). In release builds, OverflowException will not be thrown /// for performance reasons, since the point of using PreCheck is to avoid bounds checking in the following /// operations in release builds. /// /// To get the correct size to check for, use FastBufferWriter.GetWriteSize(value) or /// FastBufferWriter.GetWriteSize<type>() /// /// Number of bytes you plan to read or write /// True if the read/write can proceed, false otherwise. public bool PreCheck(int amount) { return m_Implementation.PreCheck(amount); } /// /// Serialize a string. /// /// Note: Will ALWAYS allocate a new string when reading. /// /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple /// serialization operations in one function call instead of having to do bounds checking on every call. /// /// Value to serialize /// /// If true, will truncate each char to one byte. /// This is slower than two-byte chars, but uses less bandwidth. /// public void SerializeValuePreChecked(ref string s, bool oneByteChars = false) { m_Implementation.SerializeValuePreChecked(ref s, oneByteChars); } /// /// Serialize an array value. /// /// Note: Will ALWAYS allocate a new array when reading. /// If you have a statically-sized array that you know is large enough, it's recommended to /// serialize the size yourself and iterate serializing array members. /// /// (This is because C# doesn't allow setting an array's length value, so deserializing /// into an existing array of larger size would result in an array that doesn't have as many values /// as its Length indicates it should.) /// /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple /// serialization operations in one function call instead of having to do bounds checking on every call. /// /// Value to serialize public void SerializeValuePreChecked(ref T[] array) where T : unmanaged { m_Implementation.SerializeValuePreChecked(ref array); } /// /// Serialize a single byte /// /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple /// serialization operations in one function call instead of having to do bounds checking on every call. /// /// Value to serialize public void SerializeValuePreChecked(ref byte value) { m_Implementation.SerializeValuePreChecked(ref value); } /// /// Serialize an unmanaged type. Supports basic value types as well as structs. /// The provided type will be copied to/from the buffer as it exists in memory. /// /// Using the PreChecked versions of these functions requires calling PreCheck() ahead of time, and they should only /// be called if PreCheck() returns true. This is an efficiency option, as it allows you to PreCheck() multiple /// serialization operations in one function call instead of having to do bounds checking on every call. /// /// Value to serialize public void SerializeValuePreChecked(ref T value) where T : unmanaged { m_Implementation.SerializeValuePreChecked(ref value); } } }