core: add clientless player lifecycle state machine
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-12 13:20:37 +00:00
1 parent 7013087bcd
commit 0c2e333bb4
4 files changed
+97 -9

No files matched your search

+4
View File
@@ -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.
+82 -8
View File
@@ -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<HighGuid::Player>(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);
}
+10
View File
@@ -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<WorldSocket> 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<uint32, ClientlessPlayerState> m_clientlessPlayerStates;
typedef std::unordered_map<uint32, time_t> DisconnectMap;
DisconnectMap m_disconnects;
uint32 m_maxActiveSessionCount;
@@ -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;
}