Adds the outbound-side primitives acdream needs to send a LoginRequest
packet to an ACE server: a growable byte buffer writer with AC's two
length-prefixed string formats, plus the LoginRequest payload builder
and parser.
PacketWriter (Packets/PacketWriter.cs):
- Growable byte[] buffer with little-endian WriteByte/UInt16/UInt32/Bytes
- WriteString16L: u16 length + ASCII bytes + zero-pad to 4-byte boundary
(pad counted from start of length prefix, matching ACE's Extensions.cs)
- WriteString32L: u32 outer length (= asciiLen+1) + 1 marker byte (value
ignored by reader, we emit 0) + ASCII + pad. Reader decrements the
outer length by 1 when consuming the marker, so asciiLen is recovered
correctly. Asserts ≤255 chars (two-byte-marker variant not needed for
acdream's dev credentials).
- ASCII encoding used instead of Windows-1252 since dev account names
and passwords are ASCII-safe; can switch to CodePages later if a
non-ASCII identifier ever turns up.
LoginRequest (Packets/LoginRequest.cs):
- Build(account, password, timestamp, clientVersion="1802") produces
the login payload bytes that go into the body of a packet whose
header has the LoginRequest flag set
- Parse(bytes) for tests and diagnostics — server never calls this
in production, but round-trip tests make the writer self-verifying
- NetAuthType enum mirrors ACE: Account/AccountPassword/GlsTicket
- Wire layout per ACE's PacketInboundLoginRequest:
String16L ClientVersion
u32 bodyLength (bytes remaining after this field)
u32 NetAuthType (2 = AccountPassword)
u32 AuthFlags (0 for normal client)
u32 Timestamp
String16L Account
String16L LoginAs (empty for non-admin)
String32L Password (when AccountPassword)
- bodyLength field is back-patched after the full body has been
written (classic "write placeholder, come back and patch" flow)
Tests (17 new, 61 total in net project, 138 across both test projects):
PacketWriter (11):
- u32 little-endian
- String16L: empty, 1/2/3-char with correct padding
- String32L: 2-char short, empty, >255 throws
- AlignTo4 no-op when aligned, pads when not
- Buffer grows past initial capacity on big writes
LoginRequest (6):
- Build→Parse round-trip with realistic credentials (testaccount/
testpassword/timestamp)
- Empty account/password round-trip (padding edge case)
- BodyLength field reflects actual remaining bytes after itself
- Total wire size is multiple of 4 (sanity check on padding)
- Different credentials produce different bytes
- End-to-end: payload embedded in a full Packet with LoginRequest
header flag + correct unencrypted checksum, PacketCodec.TryDecode
parses it, BodyBytes round-trips back to the same credentials
through LoginRequest.Parse
This gives acdream everything needed to construct the first datagram
of the handshake. Phase 4.5c next: WorldSession state machine to drive
the handshake sequence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
120 lines
3.5 KiB
C#
120 lines
3.5 KiB
C#
using AcDream.Core.Net.Packets;
|
|
|
|
namespace AcDream.Core.Net.Tests.Packets;
|
|
|
|
public class PacketWriterTests
|
|
{
|
|
[Fact]
|
|
public void WriteUInt32_WritesLittleEndianBytes()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteUInt32(0x04030201u);
|
|
Assert.Equal(new byte[] { 0x01, 0x02, 0x03, 0x04 }, w.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteString16L_EmptyString_WritesOnlyLengthZeroAndPadding()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteString16L(string.Empty);
|
|
|
|
// u16(0) = 2 bytes, pad to 4 = 2 more bytes
|
|
Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x00 }, w.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteString16L_TwoCharString_PadsToMultipleOfFour()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteString16L("ab");
|
|
|
|
// u16(2), 'a', 'b', 0 pad byte → total 5 bytes... wait that's not 4-aligned.
|
|
// Record = 2 (length prefix) + 2 (ascii) = 4. No padding needed.
|
|
Assert.Equal(new byte[] { 0x02, 0x00, (byte)'a', (byte)'b' }, w.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteString16L_OneCharString_PadsToFour()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteString16L("x");
|
|
// Record = 2 + 1 = 3, pad 1 byte → 4 total
|
|
Assert.Equal(new byte[] { 0x01, 0x00, (byte)'x', 0x00 }, w.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteString16L_ThreeCharString_PadsToEight()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteString16L("abc");
|
|
// Record = 2 + 3 = 5, pad 3 → 8 total
|
|
Assert.Equal(new byte[] { 0x03, 0x00, (byte)'a', (byte)'b', (byte)'c', 0, 0, 0 }, w.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteString32L_ShortString_WritesU32OuterMarkerStringPadded()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteString32L("hi");
|
|
|
|
// outer = 2 + 1 = 3, then marker byte 0, then 'h' 'i', pad from start
|
|
// of u32: record = 4 + 1 + 2 = 7, pad 1 → 8 total
|
|
Assert.Equal(new byte[]
|
|
{
|
|
0x03, 0x00, 0x00, 0x00, // outer length = 3
|
|
0x00, // marker
|
|
(byte)'h', (byte)'i',
|
|
0x00, // pad to 8
|
|
}, w.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteString32L_EmptyString_WritesOnlyZeroLength()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteString32L("");
|
|
Assert.Equal(new byte[] { 0x00, 0x00, 0x00, 0x00 }, w.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void WriteString32L_TooLong_Throws()
|
|
{
|
|
var w = new PacketWriter();
|
|
Assert.Throws<ArgumentException>(() => w.WriteString32L(new string('x', 256)));
|
|
}
|
|
|
|
[Fact]
|
|
public void AlignTo4_NoOpIfAlreadyAligned()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteUInt32(0xCAFEu);
|
|
int before = w.Position;
|
|
w.AlignTo4();
|
|
Assert.Equal(before, w.Position);
|
|
}
|
|
|
|
[Fact]
|
|
public void AlignTo4_AddsPaddingWhenMisaligned()
|
|
{
|
|
var w = new PacketWriter();
|
|
w.WriteByte(0xAA);
|
|
w.AlignTo4();
|
|
Assert.Equal(4, w.Position);
|
|
var buf = w.ToArray();
|
|
Assert.Equal(0xAA, buf[0]);
|
|
Assert.Equal(0x00, buf[1]);
|
|
Assert.Equal(0x00, buf[2]);
|
|
Assert.Equal(0x00, buf[3]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Grow_HandlesLargeWrites()
|
|
{
|
|
var w = new PacketWriter(16); // small initial
|
|
var big = new byte[500];
|
|
for (int i = 0; i < big.Length; i++) big[i] = (byte)(i & 0xFF);
|
|
w.WriteBytes(big);
|
|
Assert.Equal(500, w.Position);
|
|
Assert.Equal(big, w.ToArray());
|
|
}
|
|
}
|