acdream/docs/research/2026-07-21-retail-live-session-lifecycle-pseudocode.md

5.6 KiB

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

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

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:

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

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

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:

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.