fix(net): honor retail graceful logout handshake

Send the active character id, drain until the authoritative server confirmation, then emit retail's zero-sequence connection disconnect with the negotiated receiver iteration. The connected gate now waits for ACE to remove the exact UDP session before reconnecting, eliminating fixed-delay races.
This commit is contained in:
Erik 2026-07-20 23:31:23 +02:00
parent b03371c03d
commit 68578fa5fa
10 changed files with 372 additions and 41 deletions

View file

@ -0,0 +1,38 @@
using System.Buffers.Binary;
using AcDream.Core.Net.Packets;
namespace AcDream.Core.Net.Messages;
/// <summary>
/// Retail character-logoff request and server confirmation (opcode
/// <c>0xF653</c>).
/// </summary>
/// <remarks>
/// Retail <c>Proto_UI::LogOffCharacter</c> at <c>0x00546A20</c> sends the
/// opcode followed by the active character id. The server confirmation uses
/// the same opcode with no trailing payload. <c>CPlayerSystem::RequestLogOff</c>
/// at <c>0x00562DD0</c> waits for that confirmation; its inbound dispatch calls
/// <c>CPlayerSystem::ExecuteLogOff</c> at <c>0x0055D780</c>, which only then
/// disconnects the world connection.
/// </remarks>
public static class CharacterLogOff
{
public const uint Opcode = 0xF653u;
/// <summary>Builds retail's eight-byte client request.</summary>
public static byte[] BuildRequestBody(uint characterId)
{
var writer = new PacketWriter(8);
writer.WriteUInt32(Opcode);
writer.WriteUInt32(characterId);
return writer.ToArray();
}
/// <summary>
/// Returns whether a complete game-message body is the server's logoff
/// confirmation. ACE emits the canonical four-byte opcode-only form.
/// </summary>
public static bool IsConfirmation(ReadOnlySpan<byte> body) =>
body.Length >= sizeof(uint) &&
BinaryPrimitives.ReadUInt32LittleEndian(body) == Opcode;
}