feat(net): Phase 4.9 — send ACK_SEQUENCE for every received server packet
Root cause of the still-purple-haze symptom AND the ACE-side
"Network Timeout" drop after ~60s. acdream was never sending
acknowledgement packets back to the server, so the server's
reliability layer saw a one-way stream and eventually dropped the
session. During the 60s window the player rendered to other clients
as the stationary purple loading haze (AC's "this client is in
portal-space transition" indicator).
Pattern ported from
references/holtburger/crates/holtburger-session/src/session/
{send.rs::send_ack, receive.rs::finalize_ordered_server_packet}.
The proper holtburger pattern is per-packet acks, NOT a periodic
heartbeat: every received server packet with sequence > 0 and no
ACK_SEQUENCE flag of its own gets a bare control packet sent back
with:
PacketHeader {
Flags = ACK_SEQUENCE (0x4000),
Sequence = current_client_sequence (= last issued, no increment),
Id = session client id,
}
Body = u32 little-endian server sequence being acked
Acks are cleartext control packets (no EncryptedChecksum) and
re-use the most recently issued client sequence rather than
consuming a new one — they aren't part of the reliable stream the
server tracks for retransmits.
Wired into ProcessDatagram so both Tick (post-InWorld) and PumpOnce
(during Connect/EnterWorld) trigger acks on every received non-ack
server packet.
Also (per user request) upgrades the CLAUDE.md description of the
holtburger reference repo from "Rust AC client crate" to "almost-
complete Rust TUI AC client — the most authoritative reference for
client-side behavior in the project, look here FIRST for anything
WorldSession or message-builder related." This was the third time
in two days I would have saved hours by checking holtburger first
instead of guessing at the protocol from ACE alone.
220 tests green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8744bd6179
commit
af381ac6fb
2 changed files with 78 additions and 3 deletions
20
CLAUDE.md
20
CLAUDE.md
|
|
@ -164,9 +164,23 @@ these, ideally all four:
|
||||||
on field order, packed-dword conventions, type-prefix handling. The
|
on field order, packed-dword conventions, type-prefix handling. The
|
||||||
generated Types/*.cs files have accurate field comments (e.g. "If
|
generated Types/*.cs files have accurate field comments (e.g. "If
|
||||||
it is 0, it defaults to 256*8") that ACE's server-side code doesn't.
|
it is 0, it defaults to 256*8") that ACE's server-side code doesn't.
|
||||||
- **`references/holtburger/`** — Rust AC client crate. Cross-references
|
- **`references/holtburger/`** — **Almost-complete Rust TUI AC client.**
|
||||||
handshake quirks, race delays, and per-message encoding decisions
|
Not just a crate or a handshake reference: it's a full client that
|
||||||
that ACE doesn't document because it's server-side.
|
logs in, plays the game, sends/receives chat, handles combat, and
|
||||||
|
renders state in a terminal. **This is acdream's most authoritative
|
||||||
|
reference for client-side behavior** — anything about how a client
|
||||||
|
is *supposed* to talk to the server lives here. Specifically:
|
||||||
|
- Handshake / login flow including all the post-EnterWorld
|
||||||
|
messages retail clients send (LoginComplete, ack pump,
|
||||||
|
DDDInterrogation responses, etc).
|
||||||
|
- The proper ACK_SEQUENCE pattern (every received packet with
|
||||||
|
sequence > 0 gets an ack queued back; not periodic).
|
||||||
|
- Outbound game-action message construction with sequence
|
||||||
|
numbering.
|
||||||
|
- Message routing and session lifecycle.
|
||||||
|
Look here FIRST when implementing anything in `WorldSession` or
|
||||||
|
the message-builder layer. ACE shows what the server expects;
|
||||||
|
holtburger shows what a real client actually sends.
|
||||||
|
|
||||||
Pattern: when you encounter an unknown behavior, grep all four for the
|
Pattern: when you encounter an unknown behavior, grep all four for the
|
||||||
relevant term, read each hit, and compose a multi-source understanding
|
relevant term, read each hit, and compose a multi-source understanding
|
||||||
|
|
|
||||||
|
|
@ -254,6 +254,20 @@ public sealed class WorldSession : IDisposable
|
||||||
var dec = PacketCodec.TryDecode(bytes, _inboundIsaac);
|
var dec = PacketCodec.TryDecode(bytes, _inboundIsaac);
|
||||||
if (!dec.IsOk) return;
|
if (!dec.IsOk) return;
|
||||||
|
|
||||||
|
// Phase 4.9: send an ACK_SEQUENCE control packet for every received
|
||||||
|
// server packet with sequence > 0 and no ACK flag of its own. This
|
||||||
|
// is the proper holtburger pattern (every received packet gets an
|
||||||
|
// ack queued back; not periodic). Without it, ACE drops the session
|
||||||
|
// with "Network Timeout" because it sees no acks coming back —
|
||||||
|
// which surfaces in other clients' views as the player rendering
|
||||||
|
// as a stationary purple haze (loading state).
|
||||||
|
var serverHeader = dec.Packet!.Header;
|
||||||
|
if (serverHeader.Sequence > 0
|
||||||
|
&& (serverHeader.Flags & PacketHeaderFlags.AckSequence) == 0)
|
||||||
|
{
|
||||||
|
SendAck(serverHeader.Sequence);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var frag in dec.Packet!.Fragments)
|
foreach (var frag in dec.Packet!.Fragments)
|
||||||
{
|
{
|
||||||
var body = _assembler.Ingest(frag, out _);
|
var body = _assembler.Ingest(frag, out _);
|
||||||
|
|
@ -335,6 +349,53 @@ public sealed class WorldSession : IDisposable
|
||||||
_net.Send(datagram);
|
_net.Send(datagram);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 4.9: send a bare ACK_SEQUENCE control packet acknowledging
|
||||||
|
/// <paramref name="serverPacketSequence"/>. This is a cleartext control
|
||||||
|
/// packet (no EncryptedChecksum) — the body is just the 4-byte server
|
||||||
|
/// sequence number being acknowledged. The header re-uses the most
|
||||||
|
/// recently sent client sequence (no increment) because acks aren't
|
||||||
|
/// themselves part of the reliable stream the server tracks.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Without sending these, ACE drops the session with
|
||||||
|
/// <c>Network Timeout</c> after ~60s — and during that 60s the
|
||||||
|
/// character appears to other clients as a stationary purple haze
|
||||||
|
/// (loading state) because the server hasn't seen the client confirm
|
||||||
|
/// any post-EnterWorld traffic.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// Pattern ported from
|
||||||
|
/// <c>references/holtburger/crates/holtburger-session/src/session/send.rs::send_ack</c>
|
||||||
|
/// and the receive-side trigger at
|
||||||
|
/// <c>.../session/receive.rs::finalize_ordered_server_packet</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private void SendAck(uint serverPacketSequence)
|
||||||
|
{
|
||||||
|
// 4-byte body: little-endian u32 of the server sequence we're acking.
|
||||||
|
Span<byte> body = stackalloc byte[4];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(body, serverPacketSequence);
|
||||||
|
|
||||||
|
// Holtburger uses current_client_sequence (= packet_sequence - 1) for
|
||||||
|
// ack headers. We mirror that — acks borrow the most recently issued
|
||||||
|
// client sequence rather than consuming a new one.
|
||||||
|
uint ackHeaderSequence = _clientPacketSequence > 0
|
||||||
|
? _clientPacketSequence - 1
|
||||||
|
: 0u;
|
||||||
|
|
||||||
|
var header = new PacketHeader
|
||||||
|
{
|
||||||
|
Sequence = ackHeaderSequence,
|
||||||
|
Flags = PacketHeaderFlags.AckSequence,
|
||||||
|
Id = _sessionClientId,
|
||||||
|
};
|
||||||
|
|
||||||
|
byte[] datagram = PacketCodec.Encode(header, body, outboundIsaac: null);
|
||||||
|
_net.Send(datagram);
|
||||||
|
}
|
||||||
|
|
||||||
private void Transition(State next)
|
private void Transition(State next)
|
||||||
{
|
{
|
||||||
if (CurrentState == next) return;
|
if (CurrentState == next) return;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue