acdream/src/AcDream.Core.Net/Messages/GameMessageFragment.cs
Erik 41c1a59392 perf(net): frame recurring sends in place
Write normal game-message and ACK packet framing directly into bounded stack spans, hash fragments without materialization, and send the populated slice to the socket. Preserve exact wire bytes and ISAAC failure ordering with differential and zero-allocation tests.
2026-07-25 06:02:52 +02:00

133 lines
4.8 KiB
C#

using AcDream.Core.Net.Packets;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Helper to wrap a single GameMessage (opcode + body) in one
/// <see cref="MessageFragment"/>. Only handles the common "small message
/// fits in one fragment" case — the &gt;448-byte multi-fragment split is
/// deferred until we actually need it for outbound traffic.
///
/// <para>
/// Callers build the message body via <see cref="PacketWriter"/>
/// (starting with a 4-byte opcode, then the fields), pick a
/// <see cref="GameMessageGroup"/> for the queue, pick a fragment sequence
/// number for this message, and receive a fully-formed
/// <see cref="MessageFragment"/> ready to embed in a packet body.
/// </para>
/// </summary>
public static class GameMessageFragment
{
/// <summary>
/// Constant Id used on every outbound fragment. ACE's server-side
/// code uses the same literal — the per-message uniqueness lives in
/// the <c>Sequence</c> field, not the <c>Id</c> field.
/// </summary>
public const uint OutboundFragmentId = 0x80000000u;
/// <summary>
/// Build a single fragment that carries an entire GameMessage in its
/// payload. Throws if the payload exceeds the max single-fragment
/// body size (448 bytes).
/// </summary>
public static MessageFragment BuildSingleFragment(
uint fragmentSequence,
GameMessageGroup queue,
ReadOnlySpan<byte> gameMessageBytes)
{
if (gameMessageBytes.Length > MessageFragmentHeader.MaxFragmentDataSize)
throw new ArgumentException(
$"game message body ({gameMessageBytes.Length} bytes) exceeds single-fragment capacity " +
$"({MessageFragmentHeader.MaxFragmentDataSize} bytes). Multi-fragment split TBD.",
nameof(gameMessageBytes));
var header = new MessageFragmentHeader
{
Sequence = fragmentSequence,
Id = OutboundFragmentId,
Count = 1,
TotalSize = (ushort)(MessageFragmentHeader.Size + gameMessageBytes.Length),
Index = 0,
Queue = (ushort)queue,
};
return new MessageFragment(header, gameMessageBytes.ToArray());
}
/// <summary>
/// Write one complete fragment directly into caller-owned storage.
/// Returns the number of bytes written. This is the production send-path
/// primitive; it creates no intermediate payload or serialized array.
/// </summary>
internal static int WriteSingleFragment(
Span<byte> destination,
uint fragmentSequence,
GameMessageGroup queue,
ReadOnlySpan<byte> gameMessageBytes)
{
if (gameMessageBytes.Length
> MessageFragmentHeader.MaxFragmentDataSize)
{
throw new ArgumentException(
$"game message body ({gameMessageBytes.Length} bytes) exceeds single-fragment capacity "
+ $"({MessageFragmentHeader.MaxFragmentDataSize} bytes). Multi-fragment split TBD.",
nameof(gameMessageBytes));
}
int wireSize =
MessageFragmentHeader.Size + gameMessageBytes.Length;
if (destination.Length < wireSize)
{
throw new ArgumentException(
$"destination must be at least {wireSize} bytes",
nameof(destination));
}
var header = new MessageFragmentHeader
{
Sequence = fragmentSequence,
Id = OutboundFragmentId,
Count = 1,
TotalSize = checked((ushort)wireSize),
Index = 0,
Queue = (ushort)queue,
};
header.Pack(destination);
gameMessageBytes.CopyTo(
destination.Slice(MessageFragmentHeader.Size));
return wireSize;
}
/// <summary>
/// Concatenate a fragment's header + payload into the bytes that go
/// into a packet's body. Use when building the full <c>body</c> span
/// passed to <see cref="PacketCodec.Encode"/>.
/// </summary>
public static byte[] Serialize(in MessageFragment fragment)
{
byte[] buffer = new byte[MessageFragmentHeader.Size + fragment.Payload.Length];
fragment.Header.Pack(buffer);
fragment.Payload.CopyTo(buffer.AsSpan(MessageFragmentHeader.Size));
return buffer;
}
}
/// <summary>
/// AC's per-queue routing for GameMessages. Matches ACE's GameMessageGroup
/// enum byte-for-byte so ported handlers are unambiguous.
/// </summary>
public enum GameMessageGroup : ushort
{
InvalidQueue = 0x00,
EventQueue = 0x01,
ControlQueue = 0x02,
WeenieQueue = 0x03,
LoginQueue = 0x04,
DatabaseQueue = 0x05,
SecureControlQueue = 0x06,
SecureWeenieQueue = 0x07,
SecureLoginQueue = 0x08,
UIQueue = 0x09,
SmartboxQueue = 0x0A,
ObserverQueue = 0x0B,
}