feat(net): graceful CharacterLogOff + DISCONNECT on session Dispose
When closing the acdream window, WorldSession.Dispose now sends CharacterLogOff (game message 0xF653, no payload) followed by a bare DISCONNECT control packet (header flag 0x8000) before closing the UDP socket. This tells ACE to release the character lock immediately instead of waiting for the ~60s network timeout — which was blocking rapid iteration during testing since acdream now does a proper login (Phase 4.8-4.10) and ACE holds the character in-world. Pattern from references/holtburger/.../client/commands.rs lines 879-892 (Quit handler). Best-effort: if the socket is already dead, the exception is eaten and Dispose finishes cleanup normally. 220 tests still green. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5666e05a85
commit
fb3c8ebdaa
1 changed files with 41 additions and 1 deletions
|
|
@ -431,5 +431,45 @@ public sealed class WorldSession : IDisposable
|
|||
StateChanged?.Invoke(next);
|
||||
}
|
||||
|
||||
public void Dispose() => _net.Dispose();
|
||||
/// <summary>
|
||||
/// Graceful shutdown: tell the server we're leaving so it releases the
|
||||
/// character lock immediately instead of waiting 60s for the session to
|
||||
/// time out. Pattern from
|
||||
/// <c>references/holtburger/crates/holtburger-core/src/client/commands.rs</c>
|
||||
/// lines 879-892: send <c>CharacterLogOff</c> game message (opcode
|
||||
/// 0xF653, no payload) then send a bare <c>DISCONNECT</c> control
|
||||
/// packet (header flag 0x8000, no payload).
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (CurrentState == State.InWorld)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Tell ACE "player is leaving the world" so it cleans up
|
||||
// the character immediately.
|
||||
var logoff = new Packets.PacketWriter(8);
|
||||
logoff.WriteUInt32(0xF653u); // CharacterLogOff opcode
|
||||
SendGameMessage(logoff.ToArray());
|
||||
|
||||
// Tell the transport layer "close this session."
|
||||
var disconnectHeader = new PacketHeader
|
||||
{
|
||||
Sequence = _clientPacketSequence++,
|
||||
Flags = PacketHeaderFlags.Disconnect,
|
||||
Id = _sessionClientId,
|
||||
};
|
||||
byte[] disconnectPacket = PacketCodec.Encode(
|
||||
disconnectHeader, ReadOnlySpan<byte>.Empty, outboundIsaac: null);
|
||||
_net.Send(disconnectPacket);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort — if the socket is already dead, eat the
|
||||
// exception and let Dispose finish cleaning up.
|
||||
}
|
||||
}
|
||||
|
||||
_net.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue