core: add clientless player manager and controller API
clang PCH / build (push) Canceled after 0s
GCC PCH / build (push) Canceled after 0s

This commit is contained in:
Nico committed 2026-10-12 14:44:45 +00:00
1 parent 0c2e333bb4
commit 5ac9ed673c
4 files changed
+276 -17

No files matched your search

@@ -0,0 +1,175 @@
#include "ClientlessPlayerMgr.h"
#include "Log.h"
#include "Player.h"
#include "World.h"
#include "WorldSession.h"
ClientlessPlayerMgr* ClientlessPlayerMgr::instance()
{
static ClientlessPlayerMgr instance;
return &instance;
}
bool ClientlessPlayerMgr::StartPlayer(uint32 accountId, uint32 characterGuid,
std::unique_ptr<ClientlessPlayerController> controller)
{
if (!accountId || !characterGuid)
return false;
if (IsManaged(characterGuid) ||
sWorld->GetClientlessPlayerState(characterGuid) != ClientlessPlayerState::Offline)
{
TC_LOG_ERROR("server.worldserver",
"ClientlessPlayerMgr::StartPlayer rejected character GUID %u because it is already managed or not Offline.",
characterGuid);
return false;
}
if (!controller)
controller = std::make_unique<ClientlessPlayerController>();
ManagedPlayer record;
record.AccountId = accountId;
record.Controller = std::move(controller);
auto inserted = _records.emplace(characterGuid, std::move(record));
if (!inserted.second)
return false;
if (!sWorld->CreateClientlessPlayerSession(accountId, characterGuid))
{
_records.erase(characterGuid);
return false;
}
TC_LOG_INFO("server.worldserver",
"ClientlessPlayerMgr started character GUID %u (account %u).",
characterGuid, accountId);
return true;
}
bool ClientlessPlayerMgr::StopPlayer(uint32 characterGuid)
{
if (_records.find(characterGuid) == _records.end())
return false;
return sWorld->RemoveClientlessSession(characterGuid);
}
bool ClientlessPlayerMgr::IsManaged(uint32 characterGuid) const
{
return _records.find(characterGuid) != _records.end();
}
uint32 ClientlessPlayerMgr::GetAccountId(uint32 characterGuid) const
{
auto itr = _records.find(characterGuid);
return itr != _records.end() ? itr->second.AccountId : 0u;
}
bool ClientlessPlayerMgr::SetIntent(uint32 characterGuid, ClientlessIntent intent)
{
auto itr = _records.find(characterGuid);
if (itr == _records.end() || intent.Empty())
return false;
itr->second.Intent = std::move(intent);
if (WorldSession* session = sWorld->FindClientlessSession(characterGuid))
if (Player* player = session->GetPlayer())
if (itr->second.Controller)
itr->second.Controller->OnIntentChanged(player, &itr->second.Intent);
return true;
}
bool ClientlessPlayerMgr::ClearIntent(uint32 characterGuid)
{
auto itr = _records.find(characterGuid);
if (itr == _records.end())
return false;
itr->second.Intent = ClientlessIntent();
if (WorldSession* session = sWorld->FindClientlessSession(characterGuid))
if (Player* player = session->GetPlayer())
if (itr->second.Controller)
itr->second.Controller->OnIntentChanged(player, nullptr);
return true;
}
ClientlessIntent const* ClientlessPlayerMgr::GetIntent(uint32 characterGuid) const
{
auto itr = _records.find(characterGuid);
if (itr == _records.end() || itr->second.Intent.Empty())
return nullptr;
return &itr->second.Intent;
}
bool ClientlessPlayerMgr::QueueAction(uint32 characterGuid, ClientlessAction action)
{
auto itr = _records.find(characterGuid);
if (itr == _records.end() || action.Name.empty())
return false;
itr->second.Actions.push_back(std::move(action));
return true;
}
bool ClientlessPlayerMgr::PopNextAction(uint32 characterGuid, ClientlessAction& action)
{
auto itr = _records.find(characterGuid);
if (itr == _records.end() || itr->second.Actions.empty())
return false;
action = std::move(itr->second.Actions.front());
itr->second.Actions.pop_front();
return true;
}
uint32 ClientlessPlayerMgr::GetQueuedActionCount(uint32 characterGuid) const
{
auto itr = _records.find(characterGuid);
return itr != _records.end() ? uint32(itr->second.Actions.size()) : 0u;
}
void ClientlessPlayerMgr::Update(uint32 diff)
{
for (auto itr = _records.begin(); itr != _records.end(); )
{
uint32 characterGuid = itr->first;
ManagedPlayer& record = itr->second;
ClientlessPlayerState state = sWorld->GetClientlessPlayerState(characterGuid);
if (state == ClientlessPlayerState::Offline)
{
itr = _records.erase(itr);
continue;
}
WorldSession* session = sWorld->FindClientlessSession(characterGuid);
Player* player = session ? session->GetPlayer() : nullptr;
if (state == ClientlessPlayerState::Online && player)
{
if (!record.OnlineNotified)
{
record.OnlineNotified = true;
if (record.Controller)
{
record.Controller->OnPlayerOnline(player);
if (!record.Intent.Empty())
record.Controller->OnIntentChanged(player, &record.Intent);
}
}
if (record.Controller)
record.Controller->Update(player, diff);
}
else if (state == ClientlessPlayerState::Removing && !record.LogoutNotified)
{
record.LogoutNotified = true;
if (record.Controller && player)
record.Controller->OnPlayerLogout(player);
}
++itr;
}
}
@@ -0,0 +1,77 @@
/*
* Vcore generic clientless Player manager.
* C6 contains no AdventureBot policy and no Lua runtime.
*/
#ifndef VCORE_CLIENTLESS_PLAYER_MGR_H
#define VCORE_CLIENTLESS_PLAYER_MGR_H
#include "Common.h"
#include <deque>
#include <memory>
#include <string>
#include <unordered_map>
class Player;
struct TC_GAME_API ClientlessIntent
{
std::string Name;
std::unordered_map<std::string, std::string> Parameters;
bool Empty() const { return Name.empty(); }
};
struct TC_GAME_API ClientlessAction
{
std::string Name;
std::unordered_map<std::string, std::string> Parameters;
};
class TC_GAME_API ClientlessPlayerController
{
public:
virtual ~ClientlessPlayerController() = default;
virtual void OnPlayerOnline(Player*) { }
virtual void OnIntentChanged(Player*, ClientlessIntent const*) { }
virtual void Update(Player*, uint32) { }
virtual void OnPlayerLogout(Player*) { }
};
class TC_GAME_API ClientlessPlayerMgr
{
public:
static ClientlessPlayerMgr* instance();
bool StartPlayer(uint32 accountId, uint32 characterGuid,
std::unique_ptr<ClientlessPlayerController> controller = nullptr);
bool StopPlayer(uint32 characterGuid);
bool IsManaged(uint32 characterGuid) const;
uint32 GetAccountId(uint32 characterGuid) const;
uint32 GetManagedCount() const { return uint32(_records.size()); }
bool SetIntent(uint32 characterGuid, ClientlessIntent intent);
bool ClearIntent(uint32 characterGuid);
ClientlessIntent const* GetIntent(uint32 characterGuid) const;
bool QueueAction(uint32 characterGuid, ClientlessAction action);
bool PopNextAction(uint32 characterGuid, ClientlessAction& action);
uint32 GetQueuedActionCount(uint32 characterGuid) const;
void Update(uint32 diff);
private:
struct ManagedPlayer
{
uint32 AccountId = 0;
std::unique_ptr<ClientlessPlayerController> Controller;
ClientlessIntent Intent;
std::deque<ClientlessAction> Actions;
bool OnlineNotified = false;
bool LogoutNotified = false;
};
std::unordered_map<uint32, ManagedPlayer> _records;
};
#define sClientlessPlayerMgr ClientlessPlayerMgr::instance()
#endif
+18 -11
View File
@@ -20,6 +20,7 @@
*/
#include "World.h"
#include "ClientlessPlayerMgr.h"
#include "AccountMgr.h"
#include "AchievementMgr.h"
#include "AddonMgr.h"
@@ -420,17 +421,6 @@ 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)
@@ -540,6 +530,19 @@ void World::AddSession_(WorldSession* s)
{
ASSERT(s);
for (SessionMap::const_iterator itr = m_clientlessSessions.begin(); itr != m_clientlessSessions.end(); ++itr)
{
if (itr->second && itr->second->GetAccountId() == s->GetAccountId())
{
TC_LOG_ERROR("server.worldserver",
"World::AddSession_ rejected network session for account %u because clientless character GUID %u is active.",
s->GetAccountId(), itr->first);
s->KickPlayer();
delete s;
return;
}
}
//NOTE - Still there is race condition in WorldSession* being used in the Sockets
///- kick already loaded player with same account (if any) and remove session
@@ -3387,6 +3390,10 @@ void World::UpdateSessions(uint32 diff)
}
// High-level clientless controllers observe state before the low-level
// lifecycle loop may destroy a Removing session.
sClientlessPlayerMgr->Update(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
@@ -11,6 +11,7 @@
#include "ScriptMgr.h"
#include "Chat.h"
#include "ClientlessPlayerMgr.h"
#include "DatabaseEnv.h"
#include "Creature.h"
#include "Group.h"
@@ -632,7 +633,7 @@ namespace VianoBotCommands
uint32 accountId = fields[0].GetUInt32();
std::string characterName = fields[1].GetString();
if (!sWorld->CreateClientlessPlayerSession(accountId, characterGuid))
if (!sClientlessPlayerMgr->StartPlayer(accountId, characterGuid))
{
handler->PSendSysMessage(
"Clientless login failed for '%s' (GUID %u, account %u). Check worldserver log for the rejection reason.",
@@ -652,18 +653,17 @@ namespace VianoBotCommands
if (!ParseCharacterGuid(handler, args, characterGuid))
return true;
WorldSession* session = sWorld->FindClientlessSession(characterGuid);
if (!session)
if (!sClientlessPlayerMgr->IsManaged(characterGuid))
{
handler->PSendSysMessage(
"No clientless session is registered for character GUID %u.",
"No managed clientless player is registered for character GUID %u.",
characterGuid);
return true;
}
uint32 accountId = session->GetAccountId();
uint32 accountId = sClientlessPlayerMgr->GetAccountId(characterGuid);
if (!sWorld->RemoveClientlessSession(characterGuid))
if (!sClientlessPlayerMgr->StopPlayer(characterGuid))
{
handler->PSendSysMessage(
"Clientless logout failed for character GUID %u.",