/************************************************************************* * ModernUO * * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: BufferWriter.cs * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * *************************************************************************/ using System; using System.Buffers; using System.Buffers.Binary; using System.Collections; using System.Diagnostics; using System.IO; using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Server.Text; namespace Server; public class BufferWriter : IGenericWriter { private readonly Encoding _encoding; private readonly bool _prefixStrings; private long _bytesWritten; private long _index; protected long Index { get => _index; set { if (value < 0 || value > _buffer.Length) { // If you are receiving this exception and your value is too large, you may need to use `Resize` // If you are receiving this exception and your value is negative, you probably used Seek incorrectly. throw new ArgumentOutOfRangeException(nameof(value)); } _index = value; if (value > _bytesWritten) { _bytesWritten = value; } } } private byte[] _buffer; public BufferWriter(byte[] buffer, bool prefixStr) { _prefixStrings = prefixStr; _encoding = TextEncoding.UTF8; _buffer = buffer; } public BufferWriter(bool prefixStr) : this(0, prefixStr) { } public BufferWriter(int count, bool prefixStr) { _prefixStrings = prefixStr; _encoding = TextEncoding.UTF8; _buffer = GC.AllocateUninitializedArray(count < 1 ? BufferSize : count); } public virtual long Position => _index; protected virtual int BufferSize => 256; public byte[] Buffer => _buffer; public virtual void Close() { } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Resize(int size) { _bytesWritten = Math.Max(_bytesWritten, _index); // We shouldn't ever resize to a 0 length buffer. That is dangerous if (size <= 0) { size = BufferSize; } if (size < _buffer.Length) { _bytesWritten = size; } var newBuffer = GC.AllocateUninitializedArray(size); _buffer.AsSpan(0, Math.Min(size, _buffer.Length)).CopyTo(newBuffer); _buffer = newBuffer; } public virtual void Flush() => Resize(Math.Clamp(_buffer.Length * 2, BufferSize, _buffer.Length + 1024 * 1024 * 64)); /// /// Ensures capacity, returns a ref at the current position, and advances the index. /// The capacity check proves the caller's unaligned store is in-bounds, and the index /// only moves forward between Seek calls, so no per-write validation is needed. Growth /// (Flush -> Resize) always adds at least BufferSize, covering any primitive width. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private ref byte Reserve(int bytes) { if ((uint)(_index + bytes) > (uint)_buffer.Length) { Flush(); } ref var result = ref Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(_buffer), (nint)_index); _index += bytes; return ref result; } public virtual void Write(byte[] bytes) => Write(bytes.AsSpan()); public virtual void Write(byte[] bytes, int offset, int count) => Write(bytes.AsSpan(offset, count)); public virtual void Write(ReadOnlySpan bytes) { var length = bytes.Length; while (_buffer.Length - _index < length) { Flush(); } bytes.CopyTo(_buffer.AsSpan((int)_index)); _index += length; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public virtual long Seek(long offset, SeekOrigin origin) { Debug.Assert( origin != SeekOrigin.End || offset <= 0 && offset > -_buffer.Length, "Attempting to seek to an invalid position using SeekOrigin.End" ); Debug.Assert( origin != SeekOrigin.Begin || offset >= 0 && offset < _buffer.Length, "Attempting to seek to an invalid position using SeekOrigin.Begin" ); Debug.Assert( origin != SeekOrigin.Current || _index + offset >= 0 && _index + offset < _buffer.Length, "Attempting to seek to an invalid position using SeekOrigin.Current" ); _bytesWritten = Math.Max(_bytesWritten, _index); return Index = Math.Max(0, origin switch { SeekOrigin.Current => _index + offset, SeekOrigin.End => _bytesWritten + offset, _ => offset // Begin }); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(string value) { if (_prefixStrings) { if (value == null) { Write(false); } else { Write(true); WriteRaw(value); } } else { WriteRaw(value); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(long value) { if (!BitConverter.IsLittleEndian) { value = BinaryPrimitives.ReverseEndianness(value); } Unsafe.WriteUnaligned(ref Reserve(8), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ulong value) { if (!BitConverter.IsLittleEndian) { value = BinaryPrimitives.ReverseEndianness(value); } Unsafe.WriteUnaligned(ref Reserve(8), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(int value) { if (!BitConverter.IsLittleEndian) { value = BinaryPrimitives.ReverseEndianness(value); } Unsafe.WriteUnaligned(ref Reserve(4), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(uint value) { if (!BitConverter.IsLittleEndian) { value = BinaryPrimitives.ReverseEndianness(value); } Unsafe.WriteUnaligned(ref Reserve(4), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(short value) { if (!BitConverter.IsLittleEndian) { value = BinaryPrimitives.ReverseEndianness(value); } Unsafe.WriteUnaligned(ref Reserve(2), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(ushort value) { if (!BitConverter.IsLittleEndian) { value = BinaryPrimitives.ReverseEndianness(value); } Unsafe.WriteUnaligned(ref Reserve(2), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(double value) { if (!BitConverter.IsLittleEndian) { value = BitConverter.Int64BitsToDouble(BinaryPrimitives.ReverseEndianness(BitConverter.DoubleToInt64Bits(value))); } Unsafe.WriteUnaligned(ref Reserve(8), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(float value) { if (!BitConverter.IsLittleEndian) { value = BitConverter.Int32BitsToSingle(BinaryPrimitives.ReverseEndianness(BitConverter.SingleToInt32Bits(value))); } Unsafe.WriteUnaligned(ref Reserve(4), value); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(byte value) => Reserve(1) = value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(sbyte value) => Reserve(1) = (byte)value; [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(bool value) => Reserve(1) = Unsafe.As(ref value); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Serial serial) => Write(serial.Value); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Type type) { if (type == null) { Write((byte)0); } else { Write((byte)0x2); // xxHash3 64bit Write(AssemblyHandler.GetTypeHash(type)); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(decimal value) { Span buffer = stackalloc int[sizeof(decimal) / 4]; decimal.GetBits(value, buffer); Write(MemoryMarshal.Cast(buffer)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteEncodedInt(int value) { var v = (uint)value; // FAST PATH: 1 byte (0 to 127). // This keeps the inlined code incredibly tiny at the call site. if (v < 0x80) { Reserve(1) = (byte)v; } else { // SLOW PATH: Push to a non-inlined method to prevent code bloat. WriteEncodedIntMultiByte(v); } } [MethodImpl(MethodImplOptions.NoInlining)] private void WriteEncodedIntMultiByte(uint v) { // We already know v >= 0x80. Unroll the loop entirely based on magnitude. // This allows us to call Reserve() exactly ONE time. if (v < 0x4000) // 2 bytes { ref byte ptr = ref Reserve(2); ptr = (byte)(v | 0x80); Unsafe.Add(ref ptr, 1) = (byte)(v >> 7); } else if (v < 0x200000) // 3 bytes { ref byte ptr = ref Reserve(3); ptr = (byte)(v | 0x80); Unsafe.Add(ref ptr, 1) = (byte)((v >> 7) | 0x80); Unsafe.Add(ref ptr, 2) = (byte)(v >> 14); } else if (v < 0x10000000) // 4 bytes { ref byte ptr = ref Reserve(4); ptr = (byte)(v | 0x80); Unsafe.Add(ref ptr, 1) = (byte)((v >> 7) | 0x80); Unsafe.Add(ref ptr, 2) = (byte)((v >> 14) | 0x80); Unsafe.Add(ref ptr, 3) = (byte)(v >> 21); } else // 5 bytes (including all negative numbers due to logical shift) { ref byte ptr = ref Reserve(5); ptr = (byte)(v | 0x80); Unsafe.Add(ref ptr, 1) = (byte)((v >> 7) | 0x80); Unsafe.Add(ref ptr, 2) = (byte)((v >> 14) | 0x80); Unsafe.Add(ref ptr, 3) = (byte)((v >> 21) | 0x80); Unsafe.Add(ref ptr, 4) = (byte)(v >> 28); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(DateTime value) { // If DateTimeKind is Unspecified, we can't assume it needs to be converted. if (value.Kind == DateTimeKind.Local) { value = value.ToUniversalTime(); } Write(value.Ticks); } [MethodImpl(MethodImplOptions.AggressiveInlining)] [Obsolete("Delta time rewrites its bytes on every save. Write anchored time instead (WriteAnchoredTime, or [AnchoredDateTime] on generated fields); bump the containing type's version, as the wire format changes. Existing delta payloads remain readable through ReadDeltaTime in old-version fallbacks.")] public void WriteDeltaTime(DateTime value) { if (value == DateTime.MinValue) { Write(long.MinValue); return; } if (value == DateTime.MaxValue) { Write(long.MaxValue); return; } if (value.Kind == DateTimeKind.Local) { value = value.ToUniversalTime(); } // Technically supports negative deltas for times in the past Write(value.Ticks - DateTime.UtcNow.Ticks); } /// /// Writes the absolute value; re-bases it /// by the elapsed time since the save started, so downtime does not age it and an /// unchanged value serializes to identical bytes. /// public void WriteAnchoredTime(DateTime value) { if (value.Kind == DateTimeKind.Local) { value = value.ToUniversalTime(); } Write(value.Ticks); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(IPAddress value) { Span stack = stackalloc byte[16]; value.TryWriteBytes(stack, out var bytesWritten); Write((byte)bytesWritten); Write(stack[..bytesWritten]); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(TimeSpan value) => Write(value.Ticks); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Point3D value) { Write(value.m_X); Write(value.m_Y); Write(value.m_Z); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Point2D value) { Write(value.m_X); Write(value.m_Y); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Rectangle2D value) { Write(value.Start); Write(value.End); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Rectangle3D value) { Write(value.Start); Write(value.End); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Map value) => Write((byte)(value?.MapIndex ?? 0xFF)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Race value) => Write((byte)(value?.RaceIndex ?? 0xFF)); [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteEnum(T value) where T : unmanaged, Enum { switch (sizeof(T)) { default: { throw new ArgumentException($"Argument of type {typeof(T)} is not a normal enum"); } case 1: { Write(*(byte*)&value); break; } case 2: { Write(*(ushort*)&value); break; } case 4: { WriteEncodedInt(*(int*)&value); break; } case 8: { Write(*(ulong*)&value); break; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(Guid guid) { Span stack = stackalloc byte[16]; guid.TryWriteBytes(stack); Write(stack); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(BitArray bitArray) { var bitLength = bitArray.Length; var byteLength = (bitLength + 7) / 8; WriteEncodedInt(bitLength); var arrayBuffer = ArrayPool.Shared.Rent(byteLength); try { bitArray.CopyTo(arrayBuffer, 0); Write(arrayBuffer.AsSpan(0, byteLength)); } finally { ArrayPool.Shared.Return(arrayBuffer); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Write(TextDefinition def) { if (def == null) { WriteEncodedInt(3); } else if (def.Number > 0) { WriteEncodedInt(1); WriteEncodedInt(def.Number); } else if (def.String != null) { WriteEncodedInt(2); Write(def.String); } else { WriteEncodedInt(0); // Empty } } public void WriteRaw(string value) { // Single pass, in place: reserve the UTF-8 worst case (3 bytes per char) plus a // length prefix sized for that worst case, encode directly into the buffer, then // write the actual byte count into the reserved prefix zero-padded to the same // width. Readers accumulate 7-bit groups, so non-minimal prefixes decode // identically — no second pass over the string, no scratch copy, no pooling. var maxLength = value.Length * 3; var prefixWidth = EncodedIntWidth(maxLength); while (_buffer.Length - _index < prefixWidth + maxLength) { Flush(); } var written = _encoding.GetBytes(value, _buffer.AsSpan((int)(_index + prefixWidth))); WriteEncodedIntPadded(written, prefixWidth); _index += written; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int EncodedIntWidth(int value) => value < 0x80 ? 1 : value < 0x4000 ? 2 : value < 0x20_0000 ? 3 : value < 0x1000_0000 ? 4 : 5; private void WriteEncodedIntPadded(int value, int width) { var v = (uint)value; for (var i = 1; i < width; i++) { _buffer[_index++] = (byte)(v | 0x80); v >>= 7; } _buffer[_index++] = (byte)v; // fits in 7 bits because width >= EncodedIntWidth(value) } }