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:
parent
b03371c03d
commit
68578fa5fa
10 changed files with 372 additions and 41 deletions
|
|
@ -0,0 +1,70 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue