Brings the codec to end-to-end: a raw UDP datagram goes in, a parsed
Packet comes out with verified CRC (both plain and ISAAC-encrypted
variants). Synthetic packets built inside tests round-trip through
TryDecode cleanly.
Added:
- Packets/PacketHeaderOptional.cs: parses every flag-gated section
that lives between the 20-byte header and the body fragments —
AckSequence, RequestRetransmit (with count + array), RejectRetransmit,
ServerSwitch, LoginRequest (tail slurp), WorldLoginRequest,
ConnectResponse, CICMDCommand, TimeSync (double), EchoRequest (float),
Flow (FlowBytes + FlowInterval). Records the raw consumed bytes into
RawBytes so CalculateHash32 can hash them verbatim — AC's CRC requires
hashing the optional section separately from the main header and the
fragments.
- Packets/Packet.cs: a record type bundling Header, Optional, Fragments,
and the raw body bytes. Produced by the decoder, consumed by downstream
handlers in Phase 4.5.
- Packets/PacketCodec.cs: TryDecode(datagram, isaac?) that
1. Unpacks the header,
2. Bounds-checks DataSize against the buffer,
3. Parses the optional section,
4. If BlobFragments is set, walks the body tail as back-to-back
MessageFragment.TryParse calls,
5. Computes headerHash + optionalHash + fragmentHash,
6. Verifies CRC:
- Unencrypted: sum equals header.Checksum
- Encrypted: (header.Checksum - headerHash) XOR payloadHash must
equal the next ISAAC keystream word (which is consumed on match)
Returns a PacketDecodeResult(Packet?, DecodeError) so callers can log
and drop malformed packets instead of throwing.
- Public helper PacketCodec.CalculateFragmentHash32 so tests (and later
the encode path) can reuse the fragment-hash math.
Tests (7 new, 44 total in net project, 121 across both test projects):
- Minimal valid packet with AckSequence optional, no fragments, plain
checksum — verifies optional parse + CRC accept
- Wrong checksum rejected
- Buffer shorter than header → TooShort
- Header DataSize > buffer → HeaderSizeExceedsBuffer
- Packet with BlobFragments flag + one fragment: parses fragment and
validates the full headerHash + fragmentHash equals wire checksum
- Encrypted checksum ROUND TRIP: two ISAAC instances with same seed,
one encodes the checksum key, one decodes — validates the
(Header.Checksum - headerHash) XOR payloadHash == isaacNext contract
byte-for-byte
- Encrypted checksum with wrong key on the wire → rejected
Known limitation: the parser advances past WorldLoginRequest and
ConnectResponse their full 8 bytes whereas ACE "peeks" them (seek/reset).
The on-wire byte count is the same, only the read-position behavior
differs; any consumer that wanted to re-read those sections can do so
from Packet.BodyBytes.
Phase 4.5 (NetClient UDP pump + handshake state machine) next.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
28 lines
1.1 KiB
C#
28 lines
1.1 KiB
C#
namespace AcDream.Core.Net.Packets;
|
|
|
|
/// <summary>
|
|
/// A fully-parsed AC UDP packet: 20-byte <see cref="Header"/>,
|
|
/// variable-length <see cref="Optional"/> section, zero or more
|
|
/// <see cref="Fragments"/>, and the raw body bytes following the header
|
|
/// (for callers that want to re-read parts of the login/connect payload
|
|
/// without re-invoking the parser).
|
|
///
|
|
/// <para>
|
|
/// Produced by <see cref="PacketCodec.TryDecode"/> on the receive path;
|
|
/// passed to <see cref="PacketCodec.Encode"/> on the transmit path after
|
|
/// callers fill in the fields they want.
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class Packet
|
|
{
|
|
public PacketHeader Header;
|
|
public PacketHeaderOptional Optional { get; } = new();
|
|
public List<MessageFragment> Fragments { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Copy of the body bytes (everything after the 20-byte header). Useful
|
|
/// for LoginRequest packets where the login payload is the body tail and
|
|
/// the caller wants to extract username/password bytes directly.
|
|
/// </summary>
|
|
public byte[] BodyBytes { get; set; } = Array.Empty<byte>();
|
|
}
|