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
|
|
@ -102,6 +102,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
| AD-41 | The `candidateMoved` gate (retail UpdateObjectInternal pc:283657 `candidate != m_position`) suppresses ONLY `handle_all_collisions` + `cached_velocity` on a no-move frame; acdream still runs `ResolveWithTransition` (zero-distance) for cell/contact tracking, where retail skips the whole transition (#182 rebuild, 2026-07-07) | `src/AcDream.App/Input/PlayerMovementController.cs` (`candidateMoved` guard) | The load-bearing effect is not re-zeroing the gravity velocity that rebuilds after a stuck-fall bleed; the zero-distance resolve is a near-no-op (numSteps 0 → the zero-step early return, no ValidateTransition, contact plane persists via the writeback), so running it is harmless while keeping acdream's per-frame cell/membership refresh | If the zero-distance resolve ever gains a side effect on a no-move frame (a contact-plane clear, an fsf change), it would diverge from retail's skip — a no-move frame must stay a near-no-op | `CPhysicsObj::UpdateObjectInternal` 0x005156b0 pc:283657 (candidate-moved gate) |
|
||||
| AD-42 | Enter-world placement is split across two Core calls: legacy `Resolve` performs retail `AdjustPosition` + the host's established floor snap, then `ResolvePlacement` runs the verbatim object-aware `find_placement_pos` ring search. Retail runs initial environment placement, ring search, and final step-down inside one `find_placement_position` transition | `src/AcDream.App/Rendering/GameWindow.cs` (`EnterPlayerModeNow`); `src/AcDream.Core/Physics/PhysicsEngine.cs` (`ResolvePlacement`) | The first call has already committed the same validated cell/floor point that feeds the ring search; the second call uses the same sphere dimensions, collision registry, and cell id. Keeping the split preserves the proven indoor-login snap while adding the missing occupied-position behavior | A spawn that requires retail's final placement step-down after a ring candidate (rather than the existing floor snap before it) could settle at a slightly different Z on a ledge/water boundary; the overlap is still cleared | `CPhysicsObj::enter_world` 0x00516170; `CTransition::find_placement_position` 0x0050C170; `CTransition::find_placement_pos` 0x0050BA50 |
|
||||
| AD-43 | A malformed/custom PhysicsScript `CallPES` cycle whose script timeline never advances is rejected with a diagnostic; retail's linked scheduler would continue draining that zero-time tail indefinitely | `src/AcDream.Core/Vfx/PhysicsScriptRunner.cs` (timeline-progress ancestry guard) | Prevents corrupt DAT content from hanging the single update/render thread. Installed-DAT audit plus conformance tests prove the real rolling-weather cycles advance 2.8 seconds per edge and continue unchanged; only a no-progress strongly connected cycle is rejected | A custom DAT that deliberately relies on an infinite zero-time loop observes a rejected play instead of freezing the client | `ScriptManager::AddScriptInternal` 0x0051B310; `ScriptManager::UpdateScripts` 0x0051B480; `CPhysicsObj::CallPES` 0x00511AF0 |
|
||||
| AD-44 | Native-window close performs retail's complete character-logoff handshake and transport disconnect, then exits the process instead of retaining the authenticated logon connection and returning to character selection. acdream has one active `ReceiverData` equivalent, so `ClientNet::LogOffServer`'s per-receiver disconnect loop sends one header. | `src/AcDream.Core.Net/WorldSession.cs` (`Dispose`); `src/AcDream.Core.Net/Packets/TransportDisconnect.cs` | The application currently auto-selects one character and has no character-selection presentation/state owner to receive the returned authenticated session. Close must still release ACE immediately and in retail order. | Closing is correct, but an eventual in-client "log off character" action cannot reuse this process-exit path; it needs an App/session transition that retains the socket after server `0xF653` rather than calling `Dispose`. | `Proto_UI::LogOffCharacter @ 0x00546A20`; `CPlayerSystem::RequestLogOff @ 0x00562DD0`; `CPlayerSystem::ExecuteLogOff @ 0x0055D780`; `ClientNet::LogOffServer @ 0x00543EF0`; `SharedNet::SendOptionalHeader @ 0x00543160` |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ In-scope: 51. Implemented in acdream: 21. Phase M target delta: 30.
|
|||
| 0xEA60 | inbound | AdminEnvirons | `CPlayerSystem::Handle_Admin__Environs` | – | B | P+W | P+W | Fog presets / sound cues |
|
||||
| 0xF625 | inbound | ObjDescEvent | `SmartBox::HandleObjDescEvent` | PB | B | P+W | P+W | Per-entity appearance update |
|
||||
| 0xF643 | inbound | CharacterCreateResponse | – | PB | B | – | –defer:char-creation | Char-creation flow not yet built |
|
||||
| 0xF653 | outbound | CharacterLogOff | – | PB | P | B | PB+W | Sent on Dispose; ACE accepts |
|
||||
| 0xF653 | both | CharacterLogOff | – | PB | P | B | PB+W | Request carries active character GUID; opcode-only server confirmation gates transport disconnect |
|
||||
| 0xF655 | both | CharacterDelete | – | PB | P | – | –defer:char-mgmt | Char-management UI deferred |
|
||||
| 0xF656 | outbound | CharacterCreate | – | PB | P | – | –defer:char-creation | Char-creation flow not yet built |
|
||||
| 0xF657 | outbound | CharacterEnterWorld | `CM_Login::SendNotice_BeginEnterWorld` [^m-2] | PB | P | B | PB+W | Built; sent during handshake |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -181,7 +181,7 @@ is the opcode itself, followed immediately by the payload.
|
|||
| 0xF619 | PositionAndMovement | GM | S→C | unhandled | P4 | Ghost opcode — declared but never fired by ACE. |
|
||||
| 0xF625 | ObjDescEvent | GM | S→C | unhandled | P1 | `u32 guid, ObjectDescription` — full re-send of visual description (body parts, textures, palettes). Critical for seeing other players' gear changes. |
|
||||
| 0xF643 | CharacterCreateResponse / CharacterRestoreResponse | GM | S→C | unhandled | P0+ | `u32 responseCode` — login-phase, only relevant after char-create. Same opcode, two semantics (disambiguated by session state). |
|
||||
| 0xF653 | CharacterLogOff | GM | bi | partial | P0 | No payload. Client sends before Disconnect to release the character lock immediately. acdream sends it from `Dispose`. |
|
||||
| 0xF653 | CharacterLogOff | GM | bi | parsed | P0 | Client request is opcode + active character GUID; server confirmation is opcode-only. acdream waits for confirmation before transport Disconnect. |
|
||||
| 0xF655 | CharacterDelete | GM | bi | unhandled | P4 | `u32 slot` — char-select-screen deletion. |
|
||||
| 0xF656 | CharacterCreate | GM | C→S | unhandled | P4 | Full character-creation blob — heritage, gender, starting town, appearance. |
|
||||
| 0xF657 | CharacterEnterWorld | GM | C→S | done | P0 | `u32 characterGuid, string16L account`. Built by `Messages/CharacterEnterWorld.cs`. |
|
||||
|
|
@ -859,8 +859,9 @@ All the P3/P4 tails. Ship when we need them, not before.
|
|||
- **0x00A3/0x00A4 FellowshipQuit/Dismiss** — same pairing; C→S in
|
||||
GameAction, S→C in GameEvent.
|
||||
- **0xF653 CharacterLogOff** — bi-directional, same opcode, no
|
||||
envelope. Client sends to request logout; server sends the echo
|
||||
before Disconnect.
|
||||
envelope. Retail's client request carries the active character GUID;
|
||||
the server confirmation is opcode-only. The client waits for that
|
||||
confirmation before disconnecting the world transport.
|
||||
- **0x01A8 MagicRemoveSpell** — C→S is GameAction, S→C is GameEvent.
|
||||
- **0xF7DE TurbineChat** — bi-directional GameMessage. Blob layout
|
||||
includes a `ChatNetworkBlobType` discriminant (EVENT_BINARY=1,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue