acdream/docs/research/2026-07-20-retail-graceful-logout-pseudocode.md
Erik 68578fa5fa 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.
2026-07-20 23:31:23 +02:00

70 lines
2.6 KiB
Markdown

# Retail graceful character logout
## Oracle
- `Proto_UI::LogOffCharacter` `0x00546A20`
- `CPlayerSystem::RequestLogOff` `0x00562DD0`
- inbound login-message dispatch at `0x0055C963`
- `CPlayerSystem::ExecuteLogOff` `0x0055D780`
- `ClientNet::ExitWorldDisconnect` `0x00541E00`
- `ClientNet::LogOffServer` `0x00543EF0`
- `SharedNet::SendOptionalHeader` `0x00543160`
- `CPlayerSystem::LogOffCharacter` `0x00563520`
Cross-checks:
- ACE `CharacterHandler.CharacterLogOff` begins asynchronous player removal.
- ACE `Session.SendFinalLogOffMessages` sends the opcode-only `CharacterLogOff`
confirmation only after the player no longer owns a landblock, then returns
the session to `AuthConnected`.
- Holtburger currently sends opcode-only and disconnects immediately. Its own
comment records that AC normally waits for the server response. That shortcut
is not the retail oracle and was the source of acdream's reconnect race.
## Pseudocode
```text
LogOffCharacter(force):
save player module
if force:
ExecuteLogOff()
return
if no interaction is pending:
RequestLogOff()
else:
print "Logging off..."
remember that logout must begin after the interaction
RequestLogOff():
print "Logging off..."
send_to_logon([0xF653, active_player_id])
logOffRequested = true
logOffRequestTime = now + 3 seconds
if player is PK:
logOffRequestTime += 20 seconds
on inbound login message 0xF653:
ExecuteLogOff()
ExecuteLogOff():
clear player-login state
for every live ReceiverData connection:
send a cleartext, empty DISCONNECT optional header
use sequence 0 plus that receiver's network id and iteration
disconnect the world connection
clear the active player id
```
## acdream close-only adaptation
acdream does not yet return to character selection when its native window is
closed. It retains retail's important ordering: send the eight-byte request,
continue receiving until the server's opcode-only confirmation arrives, then
send the transport `DISCONNECT` and release the socket. World callbacks are not
dispatched while shutdown drains the raw receive queue because their App owners
may already be tearing down. A bounded 35-second wait covers retail's additional
20-second PK logout delay and is only a transport safety
limit; the normal path advances on the authoritative confirmation, not elapsed
time. The connected gate separately observes ACE accepting the transport header
before opening a replacement process; ACE removes the authenticated session on
its world-manager tick, after the UDP sender has already completed.