using System.Buffers.Binary; using System.Text; namespace AcDream.Core.Net.Packets; /// /// Growable byte buffer with AC-specific write helpers. The wire format /// uses little-endian primitives plus two string encodings inherited from /// the retail client: /// /// /// String16Lu16 length + ASCII bytes + zero /// padding to the next 4-byte boundary (counting from the length /// prefix). /// String32L — used only in the login header. u32 outer /// length + a single marker byte + ASCII bytes + optional /// padding. Outer length = strlen + 1. The marker byte's value is /// ignored by the reader so we emit 0x00. Strings longer /// than 255 chars need a two-byte marker; acdream asserts usernames /// and passwords stay short and avoids that case. /// /// /// Uses Windows-1252 at the .NET level by falling back to ASCII bytes for /// the characters we actually care about (usernames and passwords in /// ACE-auto-created accounts are ASCII-safe). A future phase can fold in /// System.Text.Encoding.CodePages if we ever see non-ASCII identifiers. /// public sealed class PacketWriter { private byte[] _buffer; private int _position; public PacketWriter(int initialCapacity = 256) { _buffer = new byte[initialCapacity]; _position = 0; } public int Position => _position; /// Bytes written so far, as a freshly-allocated array. public byte[] ToArray() { var copy = new byte[_position]; Array.Copy(_buffer, copy, _position); return copy; } /// Bytes written so far, as a span view over the internal buffer. /// Do not hold on to this after more writes — the buffer may be reallocated. public ReadOnlySpan AsSpan() => _buffer.AsSpan(0, _position); private void EnsureCapacity(int additional) { int required = _position + additional; if (required <= _buffer.Length) return; int newSize = _buffer.Length; while (newSize < required) newSize *= 2; var bigger = new byte[newSize]; Array.Copy(_buffer, bigger, _position); _buffer = bigger; } public void WriteByte(byte value) { EnsureCapacity(1); _buffer[_position++] = value; } public void WriteUInt16(ushort value) { EnsureCapacity(2); BinaryPrimitives.WriteUInt16LittleEndian(_buffer.AsSpan(_position), value); _position += 2; } public void WriteUInt32(uint value) { EnsureCapacity(4); BinaryPrimitives.WriteUInt32LittleEndian(_buffer.AsSpan(_position), value); _position += 4; } public void WriteBytes(ReadOnlySpan bytes) { EnsureCapacity(bytes.Length); bytes.CopyTo(_buffer.AsSpan(_position)); _position += bytes.Length; } public void WriteFloat(float value) { EnsureCapacity(4); BinaryPrimitives.WriteSingleLittleEndian(_buffer.AsSpan(_position), value); _position += 4; } /// Pad with zeros so the buffer length is a multiple of 4. public void AlignTo4() { int padding = (4 - (_position & 3)) & 3; EnsureCapacity(padding); for (int i = 0; i < padding; i++) _buffer[_position++] = 0; } /// Pad with N zero bytes from the current position. public void Pad(int count) { if (count <= 0) return; EnsureCapacity(count); for (int i = 0; i < count; i++) _buffer[_position++] = 0; } /// /// Write a String16L: u16 length + ASCII bytes + pad-to-4 from the /// start of the length prefix. /// public void WriteString16L(string value) { value ??= string.Empty; int asciiLen = value.Length; WriteUInt16((ushort)asciiLen); if (asciiLen > 0) { EnsureCapacity(asciiLen); // Encoding.ASCII is sufficient for dev account names/passwords. int written = Encoding.ASCII.GetBytes(value, 0, asciiLen, _buffer, _position); _position += written; } // Pad from the start of the string record (u16 + asciiLen). int recordSize = 2 + asciiLen; int padding = (4 - (recordSize & 3)) & 3; Pad(padding); } /// /// Write a String32L: u32 outer length (= asciiLen + 1), one marker /// byte (value 0, ignored by reader), asciiLen bytes, then pad to 4 /// from the start of the length prefix. /// Asserts is ≤ 255 characters. /// public void WriteString32L(string value) { value ??= string.Empty; if (value.Length > 255) throw new ArgumentException($"String32L only supports short strings (≤255), got {value.Length}", nameof(value)); int asciiLen = value.Length; if (asciiLen == 0) { // Reader treats length==0 as empty string with no marker, no content. WriteUInt32(0); return; } WriteUInt32((uint)(asciiLen + 1)); // outer length includes the marker WriteByte(0); // marker byte — value is ignored if (asciiLen > 0) { EnsureCapacity(asciiLen); int written = Encoding.ASCII.GetBytes(value, 0, asciiLen, _buffer, _position); _position += written; } // Pad from start of the u32 length prefix: 4 + 1 + asciiLen. int recordSize = 4 + 1 + asciiLen; int padding = (4 - (recordSize & 3)) & 3; Pad(padding); } }