Outbound GameAction message builders for player movement: - MoveToState (0xF61C): sent on motion state changes (start/stop walking, turn, speed change). Carries RawMotionState (flag-driven variable fields) + WorldPosition + sequence numbers. - AutonomousPosition (0xF753): periodic position heartbeat sent every ~200ms while moving. No RawMotionState — just WorldPosition + sequences + contact byte. Both follow the GameAction envelope pattern (0xF7B1 + sequence + action type) established by GameActionLoginComplete. Wire format ported from references/holtburger movement protocol — field order and alignment match exactly (contact byte + pad_to_4). Also: - Adds WriteFloat to PacketWriter (needed by both builders) - Adds SendGameAction + NextGameActionSequence to WorldSession (public wrappers for PlayerMovementController in Task 2) 11 new tests, 265 total, all green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
169 lines
5.7 KiB
C#
169 lines
5.7 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace AcDream.Core.Net.Packets;
|
|
|
|
/// <summary>
|
|
/// Growable byte buffer with AC-specific write helpers. The wire format
|
|
/// uses little-endian primitives plus two string encodings inherited from
|
|
/// the retail client:
|
|
///
|
|
/// <list type="bullet">
|
|
/// <item><b>String16L</b> — <c>u16 length</c> + ASCII bytes + zero
|
|
/// padding to the next 4-byte boundary (counting from the length
|
|
/// prefix).</item>
|
|
/// <item><b>String32L</b> — used only in the login header. <c>u32 outer
|
|
/// length</c> + a single marker byte + ASCII bytes + optional
|
|
/// padding. Outer length = strlen + 1. The marker byte's value is
|
|
/// ignored by the reader so we emit <c>0x00</c>. Strings longer
|
|
/// than 255 chars need a two-byte marker; acdream asserts usernames
|
|
/// and passwords stay short and avoids that case.</item>
|
|
/// </list>
|
|
///
|
|
/// Uses Windows-1252 at the .NET level by falling back to ASCII bytes for
|
|
/// the characters we actually care about (usernames and passwords in
|
|
/// ACE-auto-created accounts are ASCII-safe). A future phase can fold in
|
|
/// <c>System.Text.Encoding.CodePages</c> if we ever see non-ASCII identifiers.
|
|
/// </summary>
|
|
public sealed class PacketWriter
|
|
{
|
|
private byte[] _buffer;
|
|
private int _position;
|
|
|
|
public PacketWriter(int initialCapacity = 256)
|
|
{
|
|
_buffer = new byte[initialCapacity];
|
|
_position = 0;
|
|
}
|
|
|
|
public int Position => _position;
|
|
|
|
/// <summary>Bytes written so far, as a freshly-allocated array.</summary>
|
|
public byte[] ToArray()
|
|
{
|
|
var copy = new byte[_position];
|
|
Array.Copy(_buffer, copy, _position);
|
|
return copy;
|
|
}
|
|
|
|
/// <summary>Bytes written so far, as a span view over the internal buffer.
|
|
/// Do not hold on to this after more writes — the buffer may be reallocated.</summary>
|
|
public ReadOnlySpan<byte> AsSpan() => _buffer.AsSpan(0, _position);
|
|
|
|
private void EnsureCapacity(int additional)
|
|
{
|
|
int required = _position + additional;
|
|
if (required <= _buffer.Length) return;
|
|
int newSize = _buffer.Length;
|
|
while (newSize < required) newSize *= 2;
|
|
var bigger = new byte[newSize];
|
|
Array.Copy(_buffer, bigger, _position);
|
|
_buffer = bigger;
|
|
}
|
|
|
|
public void WriteByte(byte value)
|
|
{
|
|
EnsureCapacity(1);
|
|
_buffer[_position++] = value;
|
|
}
|
|
|
|
public void WriteUInt16(ushort value)
|
|
{
|
|
EnsureCapacity(2);
|
|
BinaryPrimitives.WriteUInt16LittleEndian(_buffer.AsSpan(_position), value);
|
|
_position += 2;
|
|
}
|
|
|
|
public void WriteUInt32(uint value)
|
|
{
|
|
EnsureCapacity(4);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(_buffer.AsSpan(_position), value);
|
|
_position += 4;
|
|
}
|
|
|
|
public void WriteBytes(ReadOnlySpan<byte> bytes)
|
|
{
|
|
EnsureCapacity(bytes.Length);
|
|
bytes.CopyTo(_buffer.AsSpan(_position));
|
|
_position += bytes.Length;
|
|
}
|
|
|
|
public void WriteFloat(float value)
|
|
{
|
|
EnsureCapacity(4);
|
|
BinaryPrimitives.WriteSingleLittleEndian(_buffer.AsSpan(_position), value);
|
|
_position += 4;
|
|
}
|
|
|
|
/// <summary>Pad with zeros so the buffer length is a multiple of 4.</summary>
|
|
public void AlignTo4()
|
|
{
|
|
int padding = (4 - (_position & 3)) & 3;
|
|
EnsureCapacity(padding);
|
|
for (int i = 0; i < padding; i++) _buffer[_position++] = 0;
|
|
}
|
|
|
|
/// <summary>Pad with N zero bytes from the current position.</summary>
|
|
public void Pad(int count)
|
|
{
|
|
if (count <= 0) return;
|
|
EnsureCapacity(count);
|
|
for (int i = 0; i < count; i++) _buffer[_position++] = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a String16L: u16 length + ASCII bytes + pad-to-4 from the
|
|
/// start of the length prefix.
|
|
/// </summary>
|
|
public void WriteString16L(string value)
|
|
{
|
|
value ??= string.Empty;
|
|
int asciiLen = value.Length;
|
|
WriteUInt16((ushort)asciiLen);
|
|
if (asciiLen > 0)
|
|
{
|
|
EnsureCapacity(asciiLen);
|
|
// Encoding.ASCII is sufficient for dev account names/passwords.
|
|
int written = Encoding.ASCII.GetBytes(value, 0, asciiLen, _buffer, _position);
|
|
_position += written;
|
|
}
|
|
// Pad from the start of the string record (u16 + asciiLen).
|
|
int recordSize = 2 + asciiLen;
|
|
int padding = (4 - (recordSize & 3)) & 3;
|
|
Pad(padding);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a String32L: u32 outer length (= asciiLen + 1), one marker
|
|
/// byte (value 0, ignored by reader), asciiLen bytes, then pad to 4
|
|
/// from the start of the length prefix.
|
|
/// Asserts <paramref name="value"/> is ≤ 255 characters.
|
|
/// </summary>
|
|
public void WriteString32L(string value)
|
|
{
|
|
value ??= string.Empty;
|
|
if (value.Length > 255)
|
|
throw new ArgumentException($"String32L only supports short strings (≤255), got {value.Length}", nameof(value));
|
|
|
|
int asciiLen = value.Length;
|
|
if (asciiLen == 0)
|
|
{
|
|
// Reader treats length==0 as empty string with no marker, no content.
|
|
WriteUInt32(0);
|
|
return;
|
|
}
|
|
|
|
WriteUInt32((uint)(asciiLen + 1)); // outer length includes the marker
|
|
WriteByte(0); // marker byte — value is ignored
|
|
if (asciiLen > 0)
|
|
{
|
|
EnsureCapacity(asciiLen);
|
|
int written = Encoding.ASCII.GetBytes(value, 0, asciiLen, _buffer, _position);
|
|
_position += written;
|
|
}
|
|
// Pad from start of the u32 length prefix: 4 + 1 + asciiLen.
|
|
int recordSize = 4 + 1 + asciiLen;
|
|
int padding = (4 - (recordSize & 3)) & 3;
|
|
Pad(padding);
|
|
}
|
|
}
|