diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h index 93e110c9e3..ffb393ffd9 100644 --- a/src/server/game/Server/WorldSession.h +++ b/src/server/game/Server/WorldSession.h @@ -690,6 +690,10 @@ class TC_GAME_API WorldSession // Initializes shared account/session state and then starts the normal // Player DB-load/login pipeline for a registered Clientless session. bool InitializeClientlessPlayer(ObjectGuid playerGuid); + bool HasPendingClientlessPlayerLogin() const + { + return IsClientless() && (!_clientlessPendingPlayerGuid.IsEmpty() || PlayerLoading()); + } // Starts the normal Player DB-load/login pipeline without relying on // client character-enumeration state or a network login opcode. diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 478c893b6f..6ece5e27a6 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -185,6 +185,7 @@ World::~World() delete m_clientlessSessions.begin()->second; m_clientlessSessions.erase(m_clientlessSessions.begin()); } + m_clientlessPlayerStates.clear(); CliCommandHolder* command = nullptr; while (cliCmdQueue.next(command)) @@ -323,6 +324,15 @@ WorldSession* World::FindClientlessSession(uint32 characterGuid) const return nullptr; } +ClientlessPlayerState World::GetClientlessPlayerState(uint32 characterGuid) const +{ + auto itr = m_clientlessPlayerStates.find(characterGuid); + if (itr == m_clientlessPlayerStates.end()) + return ClientlessPlayerState::Offline; + + return itr->second; +} + bool World::AddClientlessSession(uint32 characterGuid, WorldSession* session) { if (!characterGuid || !session) @@ -360,7 +370,9 @@ bool World::AddClientlessSession(uint32 characterGuid, WorldSession* session) } m_clientlessSessions[characterGuid] = session; - TC_LOG_INFO("server.worldserver", "Registered clientless session for character GUID %u (account %u).", + m_clientlessPlayerStates[characterGuid] = ClientlessPlayerState::Loading; + TC_LOG_INFO("server.worldserver", + "Registered clientless session for character GUID %u (account %u), state=Loading.", characterGuid, session->GetAccountId()); return true; } @@ -371,11 +383,14 @@ bool World::RemoveClientlessSession(uint32 characterGuid) if (itr == m_clientlessSessions.end()) return false; - WorldSession* session = itr->second; - m_clientlessSessions.erase(itr); + ClientlessPlayerState& state = m_clientlessPlayerStates[characterGuid]; + if (state == ClientlessPlayerState::Removing) + return true; - TC_LOG_INFO("server.worldserver", "Removing clientless session for character GUID %u.", characterGuid); - delete session; + state = ClientlessPlayerState::Removing; + TC_LOG_INFO("server.worldserver", + "Clientless removal requested for character GUID %u (account %u), state=Removing.", + characterGuid, itr->second ? itr->second->GetAccountId() : 0u); return true; } @@ -389,10 +404,10 @@ bool World::CreateClientlessPlayerSession(uint32 accountId, uint32 characterGuid return false; } - if (FindClientlessSession(characterGuid)) + if (GetClientlessPlayerState(characterGuid) != ClientlessPlayerState::Offline || FindClientlessSession(characterGuid)) { TC_LOG_ERROR("server.worldserver", - "World::CreateClientlessPlayerSession rejected duplicate character GUID %u.", + "World::CreateClientlessPlayerSession rejected character GUID %u because its lifecycle is not Offline.", characterGuid); return false; } @@ -405,6 +420,17 @@ bool World::CreateClientlessPlayerSession(uint32 accountId, uint32 characterGuid return false; } + for (SessionMap::const_iterator itr = m_clientlessSessions.begin(); itr != m_clientlessSessions.end(); ++itr) + { + if (itr->second && itr->second->GetAccountId() == accountId) + { + TC_LOG_ERROR("server.worldserver", + "World::CreateClientlessPlayerSession rejected account %u because character GUID %u already owns a clientless session.", + accountId, itr->first); + return false; + } + } + QueryResult characterResult = CharacterDatabase.PQuery( "SELECT account FROM characters WHERE guid = %u", characterGuid); if (!characterResult) @@ -472,7 +498,9 @@ bool World::CreateClientlessPlayerSession(uint32 accountId, uint32 characterGuid ObjectGuid playerGuid = ObjectGuid::Create(characterGuid); if (!session->InitializeClientlessPlayer(playerGuid)) { - RemoveClientlessSession(characterGuid); + m_clientlessSessions.erase(characterGuid); + m_clientlessPlayerStates.erase(characterGuid); + delete session; return false; } @@ -3361,14 +3389,18 @@ void World::UpdateSessions(uint32 diff) // Clientless sessions share normal WorldSession callbacks/update machinery, // but are intentionally outside the network session registry and queue. + // Explicit states keep async initialization/player loading from being + // destroyed underneath an in-flight callback. for (SessionMap::iterator itr = m_clientlessSessions.begin(), next; itr != m_clientlessSessions.end(); itr = next) { next = itr; ++next; + uint32 characterGuid = itr->first; WorldSession* session = itr->second; if (!session) { + m_clientlessPlayerStates.erase(characterGuid); m_clientlessSessions.erase(itr); continue; } @@ -3376,6 +3408,48 @@ void World::UpdateSessions(uint32 diff) WorldSessionFilter updater(session); if (!session->Update(diff, updater)) { + m_clientlessPlayerStates.erase(characterGuid); + delete session; + m_clientlessSessions.erase(itr); + continue; + } + + ClientlessPlayerState& state = m_clientlessPlayerStates[characterGuid]; + + if (state == ClientlessPlayerState::Loading) + { + if (Player* player = session->GetPlayer()) + { + if (player->IsInWorld()) + { + state = ClientlessPlayerState::Online; + TC_LOG_INFO("server.worldserver", + "Clientless player character GUID %u (account %u) reached state=Online.", + characterGuid, session->GetAccountId()); + } + } + else if (!session->HasPendingClientlessPlayerLogin()) + { + TC_LOG_ERROR("server.worldserver", + "Clientless player character GUID %u (account %u) left Loading without a Player; cleaning up.", + characterGuid, session->GetAccountId()); + m_clientlessPlayerStates.erase(characterGuid); + delete session; + m_clientlessSessions.erase(itr); + } + continue; + } + + if (state == ClientlessPlayerState::Removing) + { + if (session->HasPendingClientlessPlayerLogin()) + continue; + + TC_LOG_INFO("server.worldserver", + "Clientless player character GUID %u (account %u) reached state=Offline.", + characterGuid, session->GetAccountId()); + + m_clientlessPlayerStates.erase(characterGuid); delete session; m_clientlessSessions.erase(itr); } diff --git a/src/server/game/World/World.h b/src/server/game/World/World.h index 79952075e1..9c0591a77d 100644 --- a/src/server/game/World/World.h +++ b/src/server/game/World/World.h @@ -596,6 +596,14 @@ struct CharacterInfo uint32 ArenaTeamId[3]; }; +enum class ClientlessPlayerState : uint8 +{ + Offline, + Loading, + Online, + Removing +}; + /// The World class TC_GAME_API World { @@ -613,6 +621,7 @@ class TC_GAME_API World bool AddClientlessSession(uint32 characterGuid, WorldSession* session); bool RemoveClientlessSession(uint32 characterGuid); bool CreateClientlessPlayerSession(uint32 accountId, uint32 characterGuid); + ClientlessPlayerState GetClientlessPlayerState(uint32 characterGuid) const; SessionMap const& GetAllClientlessSessions() const { return m_clientlessSessions; } uint32 GetClientlessSessionCount() const { return uint32(m_clientlessSessions.size()); } void AddInstanceSocket(std::weak_ptr sock, uint64 connectToKey); @@ -866,6 +875,7 @@ class TC_GAME_API World SessionMap m_sessions; // Network sessions, keyed by account id SessionMap m_clientlessSessions; // Clientless sessions, keyed by character low GUID + std::unordered_map m_clientlessPlayerStates; typedef std::unordered_map DisconnectMap; DisconnectMap m_disconnects; uint32 m_maxActiveSessionCount; diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index 5537d42a62..af4c7b27c6 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -672,7 +672,7 @@ namespace VianoBotCommands } handler->PSendSysMessage( - "Clientless session removed for character GUID %u (account %u).", + "Clientless logout requested for character GUID %u (account %u).", characterGuid, accountId); return true; }