acdream/tests/AcDream.Core.Net.Tests/Messages/AceWireWriter.cs
Erik 47e40900f3 fix(chat): Campaign CH user-gate round 1 — jump-in-air edge, portal cue cadence, wrap/prefix/color fixes
The user tested Campaign CH's CODE-COMPLETE build live and reported ten
defects (docs/plans/2026-08-09-chat-parity-campaign.md, "User gate —
round 1"). Items A-G are fixed here; the remaining three (extra chat
windows on 1/2/3/4, resize working in only one corner, transparency/
artifacts) are out of scope for a fix and filed as slice CH6.

A. Jump-in-air refusal never fired live: the jump block only ever
   evaluated input.Jump inside the grounded-charge or already-charging
   branches. PlayerMovementController now detects the press RISING EDGE
   while airborne and reports WeenieError.NotGrounded once per press,
   leaving the grounded charge/fire path untouched.
B. ChatVM's invented "[System] " prefix is dropped — retail prints
   system text bare. [Popup] is unchanged (AP-175).
C. SpewBoxController's color is now the user-pinned exact value
   (1, 1, 0.247, 1), the same bright yellow as an incoming Tell.
   Register row AP-178 updated: color CLOSES, size/position/font stay
   open per the user's live report that they still differ.
D. Closes #329: PortalTunnelPresentation now emits the portal wait cue
   unconditionally on every rotation-segment boundary, matching
   gmSmartBoxUI::UseTime's decompiled else-arm exactly instead of gating
   on a 5-second hold local transits never reached. PortalWaitNotice
   Controller now renders it in the same pinned yellow as item C.
   Register row AP-150 retired.
E. Closes #362: new ClientCommandResponses.cs parses and renders the
   four previously-unhandled inbound GameEvents (ChannelIndex,
   ChannelList, AvailableHouses, AllegianceInfoResponse), each ported
   line-for-line from the named-retail decomp's inbound handlers.
   Register row TS-70 retired.
F. ChatWindowController.WrapText now splits on embedded '\n'/'\r\n'
   first, then word-wraps each segment independently — server text like
   /help's reply no longer collapses onto one line.
G. The chat input field's right edge no longer holds a fixed absolute
   pixel position across a window resize; Bind now upgrades it to
   retail edge-mode 1 (UiLayoutPolicy) or the AnchorEdges.Right stretch
   fallback so it tracks the window's client width instead of
   overflowing past a narrower resize.

Full Release suite: 12,247 passed / 4 skipped / 0 failed (baseline
12,221/4/0 + 26 new tests across items A, E, F, G).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 23:42:32 +02:00

147 lines
5.6 KiB
C#

using System;
using System.Collections.Generic;
using System.Text;
namespace AcDream.Core.Net.Tests.Messages;
/// <summary>
/// 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.
///
/// <para>
/// Every primitive below is a line-for-line port of
/// <c>ACE/Source/ACE.Server/Network/Extensions.cs</c> (read 2026-07-29 from
/// the ACE checkout). Cited line numbers are that file's:
/// </para>
/// <list type="bullet">
/// <item><c>CalculatePadMultiple</c> — Extensions.cs:10</item>
/// <item><c>WriteString16L</c> — Extensions.cs:12-21</item>
/// <item><c>WritePackedDword</c> — Extensions.cs:23-34</item>
/// <item><c>Pad</c> — Extensions.cs:51</item>
/// <item><c>Align</c> — Extensions.cs:55-58</item>
/// <item><c>WriteGuid</c> — Extensions.cs:121 (writes <c>guid.Full</c>, a u32)</item>
/// </list>
///
/// <para>
/// ACE's underlying <see cref="System.IO.BinaryWriter"/> is little-endian for
/// the fixed-width overloads, which is what <c>Write(uint)</c>,
/// <c>Write(ushort)</c> and <c>Write(float)</c> reproduce here.
/// </para>
/// </summary>
internal sealed class AceWireWriter
{
private readonly List<byte> _buffer = new();
public int Length => _buffer.Count;
/// <summary>ACE <c>Extensions.cs:10</c>.</summary>
private static uint CalculatePadMultiple(uint length, uint multiple)
=> multiple * ((length + multiple - 1u) / multiple) - length;
/// <summary>BinaryWriter.Write(uint) — little-endian.</summary>
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;
}
/// <summary>BinaryWriter.Write(ushort) — little-endian.</summary>
public AceWireWriter Write(ushort value)
{
_buffer.Add((byte)(value & 0xFF));
_buffer.Add((byte)((value >> 8) & 0xFF));
return this;
}
/// <summary>BinaryWriter.Write(int) — little-endian, same bit pattern as Write(uint).</summary>
public AceWireWriter Write(int value) => Write(unchecked((uint)value));
/// <summary>BinaryWriter.Write(byte).</summary>
public AceWireWriter Write(byte value)
{
_buffer.Add(value);
return this;
}
/// <summary>BinaryWriter.Write(bool) — one byte, 0 or 1 (used via Convert.ToUInt32 sites as a plain u32 instead; kept for completeness).</summary>
public AceWireWriter Write(bool value) => Write(value ? (byte)1 : (byte)0);
/// <summary>BinaryWriter.Write(ulong) — little-endian.</summary>
public AceWireWriter Write(ulong value) =>
Write((uint)(value & 0xFFFFFFFFu)).Write((uint)(value >> 32));
/// <summary>BinaryWriter.Write(float) — little-endian IEEE-754.</summary>
public AceWireWriter Write(float value)
=> Write((uint)BitConverter.SingleToInt32Bits(value));
/// <summary>
/// ACE <c>Extensions.cs:121</c> — <c>WriteGuid</c> writes the full 32-bit
/// guid with no packing.
/// </summary>
public AceWireWriter WriteGuid(uint guid) => Write(guid);
/// <summary>
/// ACE <c>Extensions.cs:12-21</c>. 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".
/// </summary>
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));
}
/// <summary>ACE <c>Extensions.cs:23-34</c>.</summary>
public AceWireWriter WritePackedDword(uint value)
{
if (value <= 32767)
return Write((ushort)value);
uint packed = (value << 16) | ((value >> 16) | 0x8000);
return Write(packed);
}
/// <summary>ACE <c>Extensions.cs:51</c>.</summary>
public AceWireWriter Pad(uint pad)
{
for (uint i = 0; i < pad; i++)
_buffer.Add(0);
return this;
}
/// <summary>ACE <c>Extensions.cs:55-58</c> — pad the stream to a 4-byte boundary.</summary>
public AceWireWriter Align() => Pad(CalculatePadMultiple((uint)_buffer.Count, 4u));
public byte[] ToArray() => _buffer.ToArray();
/// <summary>
/// Start a top-level GameMessage body: the 4-byte opcode, exactly as
/// <c>ACE.Server.Network.GameMessages.GameMessage</c>'s constructor writes
/// it before the per-message payload.
/// </summary>
public static AceWireWriter GameMessage(uint opcode)
=> new AceWireWriter().Write(opcode);
/// <summary>
/// Start a GameEvent body (top-level opcode <c>0xF7B0</c>), mirroring
/// <c>ACE/Source/ACE.Server/Network/GameEvent/GameEventMessage.cs:21-25</c>:
/// <c>WriteGuid(guid)</c>, <c>Write(session.GameEventSequence++)</c>,
/// <c>Write((uint)EventType)</c>.
/// </summary>
public static AceWireWriter GameEvent(uint guid, uint eventSequence, uint eventType)
=> new AceWireWriter()
.Write(0xF7B0u)
.WriteGuid(guid)
.Write(eventSequence)
.Write(eventType);
}