using System; using System.Collections.Generic; using System.Text; namespace AcDream.Core.Net.Tests.Messages; /// /// Test-only faithful mirror of ACE's server-side wire writer, so golden-byte /// conformance tests generate their fixtures the same way the authoritative /// server generates them. Hand-rolled hex would only prove that our parser /// agrees with whoever typed the hex; generating from the oracle's own /// algorithm proves it agrees with the server. /// /// /// Every primitive below is a line-for-line port of /// ACE/Source/ACE.Server/Network/Extensions.cs (read 2026-07-29 from /// the ACE checkout). Cited line numbers are that file's: /// /// /// CalculatePadMultiple — Extensions.cs:10 /// WriteString16L — Extensions.cs:12-21 /// WritePackedDword — Extensions.cs:23-34 /// Pad — Extensions.cs:51 /// Align — Extensions.cs:55-58 /// WriteGuid — Extensions.cs:121 (writes guid.Full, a u32) /// /// /// /// ACE's underlying is little-endian for /// the fixed-width overloads, which is what Write(uint), /// Write(ushort) and Write(float) reproduce here. /// /// internal sealed class AceWireWriter { private readonly List _buffer = new(); public int Length => _buffer.Count; /// ACE Extensions.cs:10. private static uint CalculatePadMultiple(uint length, uint multiple) => multiple * ((length + multiple - 1u) / multiple) - length; /// BinaryWriter.Write(uint) — little-endian. public AceWireWriter Write(uint value) { _buffer.Add((byte)(value & 0xFF)); _buffer.Add((byte)((value >> 8) & 0xFF)); _buffer.Add((byte)((value >> 16) & 0xFF)); _buffer.Add((byte)((value >> 24) & 0xFF)); return this; } /// BinaryWriter.Write(ushort) — little-endian. public AceWireWriter Write(ushort value) { _buffer.Add((byte)(value & 0xFF)); _buffer.Add((byte)((value >> 8) & 0xFF)); return this; } /// BinaryWriter.Write(float) — little-endian IEEE-754. public AceWireWriter Write(float value) => Write((uint)BitConverter.SingleToInt32Bits(value)); /// /// ACE Extensions.cs:121WriteGuid writes the full 32-bit /// guid with no packing. /// public AceWireWriter WriteGuid(uint guid) => Write(guid); /// /// ACE Extensions.cs:12-21. Writes a u16 length, the CP1252 bytes, /// then pads so that (2 + length) is a multiple of 4 — the comment in ACE /// reads "client expects string length to be a multiple of 4 including the /// 2 bytes for length". /// public AceWireWriter WriteString16L(string? data) { data ??= ""; byte[] bytes = Encoding.GetEncoding(1252).GetBytes(data); Write((ushort)data.Length); _buffer.AddRange(bytes); return Pad(CalculatePadMultiple(sizeof(ushort) + (uint)data.Length, 4u)); } /// ACE Extensions.cs:23-34. public AceWireWriter WritePackedDword(uint value) { if (value <= 32767) return Write((ushort)value); uint packed = (value << 16) | ((value >> 16) | 0x8000); return Write(packed); } /// ACE Extensions.cs:51. public AceWireWriter Pad(uint pad) { for (uint i = 0; i < pad; i++) _buffer.Add(0); return this; } /// ACE Extensions.cs:55-58 — pad the stream to a 4-byte boundary. public AceWireWriter Align() => Pad(CalculatePadMultiple((uint)_buffer.Count, 4u)); public byte[] ToArray() => _buffer.ToArray(); /// /// Start a top-level GameMessage body: the 4-byte opcode, exactly as /// ACE.Server.Network.GameMessages.GameMessage's constructor writes /// it before the per-message payload. /// public static AceWireWriter GameMessage(uint opcode) => new AceWireWriter().Write(opcode); /// /// Start a GameEvent body (top-level opcode 0xF7B0), mirroring /// ACE/Source/ACE.Server/Network/GameEvent/GameEventMessage.cs:21-25: /// WriteGuid(guid), Write(session.GameEventSequence++), /// Write((uint)EventType). /// public static AceWireWriter GameEvent(uint guid, uint eventSequence, uint eventType) => new AceWireWriter() .Write(0xF7B0u) .WriteGuid(guid) .Write(eventSequence) .Write(eventType); }