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.
70 lines
2.6 KiB
Markdown
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.
|