From 5ac9ed673c26ebaf591a519a6158fca90d516bff Mon Sep 17 00:00:00 2001 From: nico Date: Mon, 12 Oct 2026 14:44:45 +0000 Subject: [PATCH] core: add clientless player manager and controller API --- src/server/game/World/ClientlessPlayerMgr.cpp | 175 ++++++++++++++++++ src/server/game/World/ClientlessPlayerMgr.h | 77 ++++++++ src/server/game/World/World.cpp | 29 +-- .../scripts/Custom/viano_bot_commands.cpp | 12 +- 4 files changed, 276 insertions(+), 17 deletions(-) create mode 100644 src/server/game/World/ClientlessPlayerMgr.cpp create mode 100644 src/server/game/World/ClientlessPlayerMgr.h diff --git a/src/server/game/World/ClientlessPlayerMgr.cpp b/src/server/game/World/ClientlessPlayerMgr.cpp new file mode 100644 index 0000000000..770d7c575e --- /dev/null +++ b/src/server/game/World/ClientlessPlayerMgr.cpp @@ -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 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(); + + 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; + } +} diff --git a/src/server/game/World/ClientlessPlayerMgr.h b/src/server/game/World/ClientlessPlayerMgr.h new file mode 100644 index 0000000000..4153cf723a --- /dev/null +++ b/src/server/game/World/ClientlessPlayerMgr.h @@ -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 +#include +#include +#include + +class Player; + +struct TC_GAME_API ClientlessIntent +{ + std::string Name; + std::unordered_map Parameters; + bool Empty() const { return Name.empty(); } +}; + +struct TC_GAME_API ClientlessAction +{ + std::string Name; + std::unordered_map 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 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 Controller; + ClientlessIntent Intent; + std::deque Actions; + bool OnlineNotified = false; + bool LogoutNotified = false; + }; + + std::unordered_map _records; +}; + +#define sClientlessPlayerMgr ClientlessPlayerMgr::instance() +#endif diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 6ece5e27a6..eab40b9527 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -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 diff --git a/src/server/scripts/Custom/viano_bot_commands.cpp b/src/server/scripts/Custom/viano_bot_commands.cpp index af4c7b27c6..57919e0697 100644 --- a/src/server/scripts/Custom/viano_bot_commands.cpp +++ b/src/server/scripts/Custom/viano_bot_commands.cpp @@ -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.",