using System; using System.IO; using static MLAPI.Serialization.Arithmetic; namespace MLAPI.Serialization { /// /// A buffer that can be used at the bit level /// public class NetworkBuffer : Stream { private const int k_InitialCapacity = 16; private const float k_InitialGrowthFactor = 2.0f; private byte[] m_Target; /// /// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data. /// /// Initial capacity of buffer in bytes. /// Factor by which buffer should grow when necessary. public NetworkBuffer(int capacity, float growthFactor) { m_Target = new byte[capacity]; GrowthFactor = growthFactor; Resizable = true; } /// /// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data. /// /// Factor by which buffer should grow when necessary. public NetworkBuffer(float growthFactor) : this(k_InitialCapacity, growthFactor) { } /// /// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data. /// /// public NetworkBuffer(int capacity) : this(capacity, k_InitialGrowthFactor) { } /// /// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data. /// public NetworkBuffer() : this(k_InitialCapacity, k_InitialGrowthFactor) { } /// /// A buffer that supports writing data smaller than a single byte. This buffer also has a built-in compression algorithm that can (optionally) be used to write compressed data. /// NOTE: when using a pre-allocated buffer, the buffer will not grow! /// /// Pre-allocated buffer to write to public NetworkBuffer(byte[] target) { m_Target = target; Resizable = false; BitLength = (ulong)(target.Length << 3); } internal void SetTarget(byte[] target) { m_Target = target; BitLength = (ulong)(target.Length << 3); Position = 0; } /// /// Whether or not the buffer will grow the buffer to accomodate more data. /// public bool Resizable { get; } private float m_GrowthFactor; /// /// Factor by which buffer should grow when necessary. /// public float GrowthFactor { set { m_GrowthFactor = value <= 1 ? 1.5f : value; } get { return m_GrowthFactor; } } /// /// Whether or not buffeer supports reading. (Always true) /// public override bool CanRead => true; /// /// Whether or not or there is any data to be read from the buffer. /// public bool HasDataToRead => Position < Length; /// /// Whether or not seeking is supported by this buffer. (Always true) /// public override bool CanSeek => true; /// /// Whether or not this buffer can accept new data. NOTE: this will return true even if only fewer than 8 bits can be written! /// public override bool CanWrite => !BitAligned || Position < m_Target.LongLength || Resizable; /// /// Current buffer size. The buffer will not be resized (if possible) until Position is equal to Capacity and an attempt to write data is made. /// public long Capacity { get => m_Target.LongLength; // Optimized CeilingExact set { if (value < Length) throw new ArgumentOutOfRangeException("New capcity too small!"); SetCapacity(value); } } /// /// The current length of data considered to be "written" to the buffer. /// public override long Length { get => Div8Ceil(BitLength); } /// /// The index that will be written to when any call to write data is made to this buffer. /// public override long Position { get => (long)(BitPosition >> 3); set => BitPosition = (ulong)value << 3; } /// /// Bit offset into the buffer that new data will be written to. /// public ulong BitPosition { get; set; } /// /// Length of data (in bits) that is considered to be written to the buffer. /// public ulong BitLength { get; private set; } /// /// Whether or not the current BitPosition is evenly divisible by 8. I.e. whether or not the BitPosition is at a byte boundary. /// public bool BitAligned { get => (BitPosition & 7) == 0; } /// /// Flush buffer. This does nothing since data is written directly to a byte buffer. /// public override void Flush() { } // NOP /// /// Grow buffer if possible. According to Max(bufferLength, 1) * growthFactor^Ceil(newContent/Max(bufferLength, 1)) /// /// How many new values need to be accomodated (at least). //private void Grow(long newContent) => SetCapacity(Math.Max(target.LongLength, 1) * (long)Math.Pow(GrowthFactor, CeilingExact(newContent, Math.Max(target.LongLength, 1)))); /* private void Grow(long newContent) { float grow = newContent / 64; if (((long)grow) != grow) grow += 1; SetCapacity((Capacity + 64) * (long)grow); } */ private void Grow(long newContent) { long value = newContent + Capacity; long newCapacity = value; if (newCapacity < 256) newCapacity = 256; // We are ok with this overflowing since the next statement will deal // with the cases where _capacity*2 overflows. if (newCapacity < Capacity * 2) newCapacity = Capacity * 2; // We want to expand the array up to Array.MaxArrayLengthOneDimensional // And we want to give the user the value that they asked for if ((uint)(Capacity * 2) > int.MaxValue) newCapacity = value > int.MaxValue ? value : int.MaxValue; SetCapacity(newCapacity); } /// /// Read a misaligned byte. WARNING: If the current BitPosition isn't byte misaligned, /// avoid using this method as it may cause an IndexOutOfBoundsException in such a case. /// /// A byte extracted from up to two separate buffer indices. private byte ReadByteMisaligned() { int mod = (int)(BitPosition & 7); return (byte)((m_Target[(int)Position] >> mod) | (m_Target[(int)(BitPosition += 8) >> 3] << (8 - mod))); } /// /// Read an aligned byte from the buffer. It's recommended to not use this when the BitPosition is byte-misaligned. /// /// The byte stored at the current Position index private byte ReadByteAligned() => m_Target[Position++]; /// /// Read a byte as a byte. This is just for internal use so as to minimize casts (cuz they ugly af). /// /// internal byte ReadByteInternal() => BitAligned ? ReadByteAligned() : ReadByteMisaligned(); /// /// Read a byte from the buffer. This takes into account possible byte misalignment. /// /// A byte from the buffer or, if a byte can't be read, -1. public override int ReadByte() => HasDataToRead ? BitAligned ? ReadByteAligned() : ReadByteMisaligned() : -1; /// /// Peeks a byte without advancing the position /// /// The peeked byte public int PeekByte() => HasDataToRead ? BitAligned ? m_Target[Position] : (byte)((m_Target[(int)Position] >> (int)(BitPosition & 7)) | (m_Target[(int)(BitPosition + 8) >> 3] << (8 - (int)(BitPosition & 7)))) : -1; /// /// Read a single bit from the buffer. /// /// A bit in bool format. (True represents 1, False represents 0) public bool ReadBit() => (m_Target[Position] & (1 << (int)(BitPosition++ & 7))) != 0; /// /// Read a subset of the buffer buffer and write the contents to the supplied buffer. /// /// Buffer to copy data to. /// Offset into the buffer to write data to. /// How many bytes to attempt to read. /// Amount of bytes read. public override int Read(byte[] buffer, int offset, int count) { int tLen = Math.Min(count, (int)(m_Target.LongLength - Position) - ((BitPosition & 7) == 0 ? 0 : 1)); for (int i = 0; i < tLen; ++i) buffer[offset + i] = ReadByteInternal(); return tLen; } /// /// Set position in buffer to read from/write to. /// /// Offset from position origin. /// How to calculate offset. /// The new position in the buffer that data will be written to. public override long Seek(long offset, SeekOrigin origin) { return (long)(( BitPosition = ( origin == SeekOrigin.Current ? offset > 0 ? Math.Min(BitPosition + ((ulong)offset << 3), (ulong)m_Target.Length << 3) : (offset ^ SIGN_BIT_64) > Position ? 0UL : BitPosition - (ulong)((offset ^ SIGN_BIT_64) << 3) : origin == SeekOrigin.Begin ? (ulong)Math.Max(0, offset) << 3 : (ulong)Math.Max(m_Target.Length - offset, 0) << 3 )) >> 3) + (long)((BitPosition & 1UL) | ((BitPosition >> 1) & 1UL) | ((BitPosition >> 2) & 1UL)); } /// /// Set the capacity of the internal buffer. /// /// New capacity of the buffer private void SetCapacity(long value) { if (!Resizable) throw new NotSupportedException("Can't resize non resizable buffer"); // Don't do shit because fuck you (comment by @GabrielTofvesson -TwoTen) byte[] newTarg = new byte[value]; long len = Math.Min(value, m_Target.LongLength); Buffer.BlockCopy(m_Target, 0, newTarg, 0, (int)len); if (value < m_Target.LongLength) BitPosition = (ulong)value << 3; m_Target = newTarg; } /// /// Set length of data considered to be "written" to the buffer. /// /// New length of the written data. public override void SetLength(long value) { if (value < 0) throw new IndexOutOfRangeException("Cannot set a negative length!"); if (value > Capacity) Grow(value - Capacity); BitLength = (ulong)value << 3; BitPosition = Math.Min((ulong)value << 3, BitPosition); } /// /// Write data from the given buffer to the internal buffer. /// /// Buffer to write from. /// Offset in given buffer to start reading from. /// Amount of bytes to read copy from given buffer to buffer. public override void Write(byte[] buffer, int offset, int count) { // Check bit alignment. If misaligned, each byte written has to be misaligned if (BitAligned) { if (Position + count >= m_Target.Length) Grow(count); Buffer.BlockCopy(buffer, offset, m_Target, (int)Position, count); Position += count; } else { if (Position + count + 1 >= m_Target.Length) Grow(count); for (int i = 0; i < count; ++i) WriteMisaligned(buffer[offset + i]); } if (BitPosition > BitLength) BitLength = BitPosition; } /// /// Write byte value to the internal buffer. /// /// The byte value to write. public override void WriteByte(byte value) { // Check bit alignment. If misaligned, each byte written has to be misaligned if (BitAligned) { if (Position + 1 >= m_Target.Length) Grow(1); m_Target[Position] = value; Position += 1; } else { if (Position + 1 + 1 >= m_Target.Length) Grow(1); WriteMisaligned(value); } if (BitPosition > BitLength) BitLength = BitPosition; } /// /// Write a misaligned byte. NOTE: Using this when the bit position isn't byte-misaligned may cause an IndexOutOfBoundsException! This does not update the current Length of the buffer. /// /// Value to write private void WriteMisaligned(byte value) { int off = (int)(BitPosition & 7); int shift1 = 8 - off; m_Target[Position + 1] = (byte)((m_Target[Position + 1] & (0xFF << off)) | (value >> shift1)); m_Target[Position] = (byte)((m_Target[Position] & (0xFF >> shift1)) | (value << off)); BitPosition += 8; } /// /// Write a byte (in an int format) to the buffer. This does not update the current Length of the buffer. /// /// Value to write private void WriteIntByte(int value) => WriteBytePrivate((byte)value); /// /// Write a byte (in a ulong format) to the buffer. This does not update the current Length of the buffer. /// /// Value to write private void WriteULongByte(ulong byteValue) => WriteBytePrivate((byte)byteValue); /// /// Write a byte to the buffer. This does not update the current Length of the buffer. /// /// Value to write private void WriteBytePrivate(byte value) { if (Div8Ceil(BitPosition) == m_Target.LongLength) Grow(1); if (BitAligned) { m_Target[Position] = value; BitPosition += 8; } else WriteMisaligned(value); UpdateLength(); } /// /// Write data from the given buffer to the internal buffer. /// /// Buffer to write from. public void Write(byte[] buffer) => Write(buffer, 0, buffer.Length); /// /// Write a single bit to the buffer /// /// Value of the bit. True represents 1, False represents 0 public void WriteBit(bool bit) { if (BitAligned && Position == m_Target.Length) Grow(1); int offset = (int)(BitPosition & 7); long pos = Position; ++BitPosition; m_Target[pos] = (byte)(bit ? (m_Target[pos] & ~(1 << offset)) | (1 << offset) : (m_Target[pos] & ~(1 << offset))); UpdateLength(); } /// /// Copy data from another stream /// /// Stream to copy from /// How many bytes to read. Set to value less than one to read until ReadByte returns -1 public void CopyFrom(Stream s, int count = -1) { if (s is NetworkBuffer b) Write(b.m_Target, 0, count < 0 ? (int)b.Length : count); else { long currentPosition = s.Position; s.Position = 0; int read; bool readToEnd = count < 0; while ((readToEnd || count-- > 0) && (read = s.ReadByte()) != -1) WriteIntByte(read); UpdateLength(); s.Position = currentPosition; } } /// /// Copies internal buffer to stream /// /// The stream to copy to /// The maximum amount of bytes to copy. Set to value less than one to copy the full length #if !NET35 public new void CopyTo(Stream stream, int count = -1) #else public void CopyTo(Stream stream, int count = -1) #endif { stream.Write(m_Target, 0, count < 0 ? (int)Length : count); } /// /// Copies urnead bytes from the source stream /// /// The source stream to copy from /// The max amount of bytes to copy public void CopyUnreadFrom(Stream s, int count = -1) { long currentPosition = s.Position; int read; bool readToEnd = count < 0; while ((readToEnd || count-- > 0) && (read = s.ReadByte()) != -1) WriteIntByte(read); UpdateLength(); s.Position = currentPosition; } // TODO: Implement CopyFrom() for NetworkBuffer with bitCount parameter /// /// Copys the bits from the provided NetworkBuffer /// /// The buffer to copy from /// The amount of data evel /// Whether or not to copy at the bit level rather than the byte level public void CopyFrom(NetworkBuffer buffer, int dataCount, bool copyBits) { if (!copyBits) { CopyFrom(buffer, dataCount); } else { ulong count = dataCount < 0 ? buffer.BitLength : (ulong)dataCount; if (buffer.BitLength < count) throw new IndexOutOfRangeException("Attempted to read more data than is available"); Write(buffer.GetBuffer(), 0, (int)(count >> 3)); for (int i = (int)(count & 7); i >= 0; --i) WriteBit(buffer.ReadBit()); } } /// /// Update length of data considered to be "written" to the buffer. /// private void UpdateLength() { if (BitPosition > BitLength) BitLength = BitPosition; } /// /// Get the internal buffer being written to by this buffer. /// /// public byte[] GetBuffer() => m_Target; /// /// Creates a copy of the internal buffer. This only contains the used bytes /// /// A copy of used bytes in the internal buffer public byte[] ToArray() { byte[] copy = new byte[Length]; Buffer.BlockCopy(m_Target, 0, copy, 0, (int)Length); return copy; } /// /// Writes zeros to fill the last byte /// public void PadBuffer() { while (!BitAligned) { WriteBit(false); } } /// /// Reads zeros until the the buffer is byte aligned /// public void SkipPadBits() { while (!BitAligned) { ReadBit(); } } /// /// Returns hex encoded version of the buffer /// /// Hex encoded version of the buffer public override string ToString() => BitConverter.ToString(m_Target, 0, (int)Length); } }