fix(net): conform character entry and session shutdown

This commit is contained in:
Erik 2026-07-21 10:33:03 +02:00
parent bacc7e45a9
commit aea957f845
12 changed files with 1016 additions and 95 deletions

View file

@ -102,7 +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` |
| AD-44 | acdream has no retained character-management screen: startup deterministically selects the first active, non-greyed CharacterList identity, and native-window close performs retail's complete character-logoff handshake plus transport disconnect before exiting instead of returning to character selection. One active `ReceiverData` equivalent means `ClientNet::LogOffServer`'s per-receiver loop sends one header. | `src/AcDream.Core.Net/Messages/CharacterList.cs` (`TrySelectFirstAvailable`); `src/AcDream.App/Rendering/GameWindow.cs` (live-session bootstrap, moving to `LiveSessionController` in Slice 3); `src/AcDream.Core.Net/WorldSession.cs` (`SelectCharacterForEnterWorld`, `Dispose`); `src/AcDream.Core.Net/Packets/TransportDisconnect.cs` | This preserves unattended startup and immediate ACE endpoint release while validating that the chosen identity is active/non-greyed and using the server's canonical account. A future retained character-management owner is separate UI/session work. | An account with multiple playable characters enters the first wire-order identity without retail's explicit choice. An eventual in-client "log off character" action cannot reuse the process-exit path; it must retain the authenticated socket after server `0xF653` and return to character management. | `gmCharacterManagementUI::SelectCharacter @ 0x004EC160`; `gmCharacterManagementUI::EnterGame @ 0x004ED440`; `gmCharGenMainUI::Update @ 0x004E8460`; `Proto_UI::LogOffCharacter @ 0x00546A20`; `CPlayerSystem::RequestLogOff @ 0x00562DD0`; `CPlayerSystem::ExecuteLogOff @ 0x0055D780`; `ClientNet::LogOffServer @ 0x00543EF0`; `SharedNet::SendOptionalHeader @ 0x00543160` |
---

View file

@ -0,0 +1,157 @@
# Retail live-session lifecycle pseudocode
**Scope:** GameWindow Slice 3 character-list, character-entry, and transport-
disconnect conformance corrections.
## Named-retail oracle
- `CharacterIdentity::UnPack @ 0x004FE880`
- `CharacterSet::UnPack @ 0x004FE340`
- `CharacterSet::GetGreyedOutFor @ 0x004FDFA0`
- `gmCharacterManagementUI::EnterGame @ 0x004ED440`
- `gmCharGenMainUI::Update @ 0x004E8460`
- `CPlayerSystem::Handle_Login__CharacterSet @ 0x0055F6D0`
- `Proto_UI::SendEnterWorldRequest @ 0x00546A00`
- `CPlayerSystem::Handle_Character__EnterGame_ServerReady @ 0x0055E4F0`
- `Proto_UI::SendEnterWorld @ 0x00546BC0`
- `CPlayerSystem::LogOnCharacter @ 0x0055F890`
- `CPlayerSystem::UseTime @ 0x00563110`
- `Proto_UI::LogOffCharacter @ 0x00546A20`
- `CPlayerSystem::RequestLogOff @ 0x00562DD0`
- inbound F653 dispatch `@ 0x0055C963`
- `CPlayerSystem::ExecuteLogOff @ 0x0055D780`
- `ClientNet::ExitWorldDisconnect @ 0x00541E00`
- `ClientNet::LogOffServer @ 0x00543EF0`
- `SharedNet::SendOptionalHeader @ 0x00543160`
The verbatim layouts are `CharacterIdentity` and `CharacterSet` in
`docs/research/named-retail/acclient.h` (types 3206 and 3209).
## CharacterSet wire decode
```text
CharacterIdentity.UnPack(cursor):
identity.guid = read_u32(cursor)
identity.name = read_PString(cursor)
identity.secondsGreyedOut = read_u32(cursor)
align cursor to 4 bytes
CharacterSet.UnPack(cursor):
result.status = read_u32(cursor)
activeCount = read_u32(cursor)
result.active = empty
repeat activeCount times:
result.active.append(CharacterIdentity.UnPack(cursor))
deletedCount = read_u32(cursor)
result.deleted = empty
repeat deletedCount times:
result.deleted.append(CharacterIdentity.UnPack(cursor))
result.numAllowedCharacters = read_u32(cursor)
result.account = read_PString(cursor)
result.useTurbineChat = read_u32(cursor)
result.hasThroneOfDestiny = read_u32(cursor)
```
The two values previously described as "leading/trailing padding" are the
retail status and deleted-character count. ACE currently writes zero for both,
which hid the schema error in acdream and in Holtburger's current reader.
Cross-checks:
- ACE `GameMessageCharacterList` writes the same active identity records,
allowed-slot count, canonical `Session.Account`, and booleans, but currently
emits status/deleted-count as zero and no deleted records.
- Holtburger `CharacterListData` agrees on active records and the tail, but
likewise labels both zero values as padding. The named retail `CharacterSet`
layout and `UnPack` control flow win where the references differ.
## Retail selection and acdream unattended entry
```text
gmCharacterManagementUI.EnterGame():
playerSystem = GetPlayerSystem()
if playerSystem exists
and selectedGuid != 0
and CharacterSet.GetGreyedOutFor(
CharacterSet.GetSlot(selectedGuid)) <= 0:
show entering-world dialog
playerSystem.LogOnCharacter(selectedGuid)
```
Retail ordinarily requires an explicit selected active character. The only
automatic retail path found is `gmCharGenMainUI::Update`: after character
creation it walks the active list, skips entries greyed for a positive number
of seconds, matches the newly-created name case-insensitively, and logs that
GUID on.
acdream does not yet have the retained character-management screen. Its
unattended auto-entry is therefore an explicit adaptation with a narrow rule:
```text
for each identity in active wire order:
if identity.guid != 0 and identity.secondsGreyedOut == 0:
select this active-list index
stop
if none matched:
do not enter a character
```
Deleted entries are never candidates. This preserves the established "first
available character" behavior without treating a pending-deletion character
as available.
## Canonical account used by EnterWorld
```text
CPlayerSystem.LogOnCharacter(selectedGuid), when ready:
selectedGuid = persistentData.selectedCharacter
account = playerSystem.account // populated from CharacterSet.account
Proto_UI.SendEnterWorld(account, selectedGuid)
ClientNet.EnterWorld()
```
The account string in the final F657 message comes from the server-returned
`CharacterSet.account`, not from the spelling typed into the startup login
form. acdream must therefore make `WorldSession.EnterWorld` consume
`Characters.AccountName` internally.
## Logout and negotiated transport disconnect
```text
ClientNet.LogOffServer():
packet = make optional-header packet(flags = Disconnect)
for each negotiated ReceiverData:
SharedNet.SendOptionalHeader(packet, receiver.address, receiver)
logOffSent = true
logonReceiverId = 0
ExitWorldDisconnect()
```
The in-world character path remains:
```text
send F653 + active character GUID
wait for server's opcode-only F653
send negotiated transport Disconnect
release transport
```
ACE's `GameMessageCharacterLogOff` confirms the server form is exactly four
bytes. Its `NetworkSession` terminates a session when it receives a packet with
the transport Disconnect flag.
The transport Disconnect is connection-scoped, not character-state-scoped.
Once acdream has accepted ConnectRequest receiver id/iteration, disposal sends
it whether the session is still selecting a character, entering the world, in
world, or failed later. F653 remains restricted to an active in-world
character. A pre-negotiation failure has no receiver identity and only closes
the local socket.
## Deliberately unchanged registered gaps
- UN-6: the fixed 200 ms ConnectResponse delay remains registered.
- TS-28: LoginComplete readiness remains a later protocol slice.
- AD-44: closing exits rather than returning to a retained character screen.