From 2ae086d9332dfceecfba63b5f325b3dacdca4bcd Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Wed, 30 Sep 2026 04:11:59 -0500 Subject: [PATCH] feat(chat): keep activity lobbies of every world and start their instances through master Co-Authored-By: Claude Opus 5.5 --- dChatServer/CMakeLists.txt | 2 + dChatServer/ChatMatchmaking.cpp | 126 ++++++++++++++++++++++++++++++++ dChatServer/ChatMatchmaking.h | 34 +++++++++ dChatServer/ChatServer.cpp | 15 +++- dChatServer/PlayerContainer.cpp | 4 + 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 dChatServer/ChatMatchmaking.cpp create mode 100644 dChatServer/ChatMatchmaking.h diff --git a/dChatServer/CMakeLists.txt b/dChatServer/CMakeLists.txt index 02add3843..65f53ba28 100644 --- a/dChatServer/CMakeLists.txt +++ b/dChatServer/CMakeLists.txt @@ -1,8 +1,10 @@ set(DCHATSERVER_SOURCES "ChatIgnoreList.cpp" + "ChatMatchmaking.cpp" "ChatPacketHandler.cpp" "ChatGuilds.cpp" "GuildManager.cpp" + "Matchmaking.cpp" "PlayerContainer.cpp" "TeamContainer.cpp" ) diff --git a/dChatServer/ChatMatchmaking.cpp b/dChatServer/ChatMatchmaking.cpp new file mode 100644 index 000000000..6372464e8 --- /dev/null +++ b/dChatServer/ChatMatchmaking.cpp @@ -0,0 +1,126 @@ +#include "ChatMatchmaking.h" + +#include + +#include "ChatPacketHandler.h" +#include "ChatPackets.h" +#include "ClientPackets.h" +#include "Game.h" +#include "GeneralUtils.h" +#include "Logger.h" +#include "PlayerContainer.h" +#include "TeamContainer.h" +#include "ZoneInstanceManager.h" +#include "dServer.h" + +namespace { + Matchmaking::Lobbies g_Lobbies; + + void SendUpdates(const std::vector& updates) { + for (const auto& update : updates) { + const auto& player = Game::playerContainer.GetPlayerData(update.to); + if (!player) continue; + ClientPackets::MatchUpdate msg; + msg.target = update.to; + msg.data = update.data; + msg.type = update.type; + ChatPacketHandler::SendRouted(update.to, player.worldServerSysAddr, msg); + } + } + + // The players' worlds send them to the instance master started + void SendToInstance(const Matchmaking::Match& match, const bool mythranShift, const uint32_t zoneID, const uint32_t zoneInstance, const uint32_t zoneClone, const std::string& serverIP, const uint16_t serverPort) { + std::map perWorld; + std::vector online; + for (const auto playerID : match.players) { + const auto& player = Game::playerContainer.GetPlayerData(playerID); + if (!player || Game::playerContainer.PlayerBeingRemoved(playerID)) continue; + online.push_back(playerID); + + auto& transfer = perWorld[player.worldServerSysAddr]; + transfer.activityID = match.settings.activityID; + transfer.zoneID = LWOZONEID(zoneID, zoneInstance, zoneClone); + transfer.serverIP = serverIP; + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.players.push_back(playerID); + } + + for (const auto& [world, transfer] : perWorld) { + RakNet::BitStream bitStream; + transfer.WritePacket(bitStream); + Game::server->Send(bitStream, world, false); + } + + // Players who start an activity together are a team, as when a world made the lobby (teams of up to + // ChatPackets::CreateTeam::MAX_MEMBERS players; bigger groups never were one) + if (online.size() > 1 && online.size() <= ChatPackets::CreateTeam::MAX_MEMBERS) { + ChatPackets::CreateTeam team; + team.leaderID = online.front(); + team.members = online; + team.zoneID = LWOZONEID(zoneID, zoneInstance, zoneClone); + TeamContainer::CreateTeamServer(team, UNASSIGNED_SYSTEM_ADDRESS); + } + + LOG("Matchmaking: activity %i starts in zone %u instance %u clone %u (%zu players, %zu worlds)", match.settings.activityID, zoneID, zoneInstance, zoneClone, online.size(), perWorld.size()); + } +} + +namespace ChatMatchmaking { + Matchmaking::Lobbies& GetLobbies() { + return g_Lobbies; + } + + void HandleMatchRequest(const ChatPackets::MatchRequest& request, const SystemAddress& sysAddr) { + const auto& player = Game::playerContainer.GetPlayerData(request.playerID); + // Only players this chat server knows are in the world that sent it + if (!player || player.worldServerSysAddr != sysAddr) return; + + std::vector updates; + switch (request.type) { + case ChatPackets::eMatchRequestType::JOIN: { + Matchmaking::ActivitySettings settings; + settings.activityID = request.activityID; + settings.instanceMapID = request.instanceMapID; + settings.minTeams = request.minTeams; + settings.maxTeams = request.maxTeams; + settings.minTeamSize = request.minTeamSize; + settings.maxTeamSize = request.maxTeamSize; + settings.waitTime = static_cast(request.waitTime) / 1000.0f; + settings.startDelay = static_cast(request.startDelay) / 1000.0f; + const auto& name = request.playerName.empty() ? player.playerName : request.playerName; + g_Lobbies.Join(request.playerID, name, request.playerChoices, settings, updates); + break; + } + case ChatPackets::eMatchRequestType::READY: + g_Lobbies.SetReady(request.playerID, request.value != 0, updates); + break; + case ChatPackets::eMatchRequestType::LEAVE: + g_Lobbies.Leave(request.playerID, updates); + break; + } + SendUpdates(updates); + } + + void PlayerLeftWorld(const LWOOBJID playerID) { + std::vector updates; + if (g_Lobbies.Leave(playerID, updates)) SendUpdates(updates); + } + + void Update(const float deltaTime) { + if (g_Lobbies.GetLobbies().empty()) return; + std::vector updates; + const auto matches = g_Lobbies.Tick(deltaTime, updates); + SendUpdates(updates); + for (const auto& match : matches) StartMatch(match); + } + + void StartMatch(const Matchmaking::Match& match) { + // A clone no other instance has: master starts a new instance of the zone for this match alone + const auto cloneID = GeneralUtils::GenerateRandomNumber(1, UINT32_MAX); + ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, match.settings.instanceMapID, cloneID, false, + [match](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { + SendToInstance(match, mythranShift, zoneID, zoneInstance, zoneClone, serverIP, serverPort); + }); + } +} diff --git a/dChatServer/ChatMatchmaking.h b/dChatServer/ChatMatchmaking.h new file mode 100644 index 000000000..6c3642b99 --- /dev/null +++ b/dChatServer/ChatMatchmaking.h @@ -0,0 +1,34 @@ +#ifndef CHATMATCHMAKING_H +#define CHATMATCHMAKING_H + +#include "dCommonVars.h" +#include "Matchmaking.h" + +struct SystemAddress; + +namespace ChatPackets { + struct MatchRequest; +} + +/** + * The chat server's activity matchmaking (docs/Matchmaking.md): lobbies across every world. Worlds send the players' + * lobby joins, readies and leaves; lobby updates go to the clients through their worlds; when a countdown runs out + * master starts one instance of the activity's zone and each player's world is told to send them there. + * Runs on the chat server's main thread only. + */ +namespace ChatMatchmaking { + void HandleMatchRequest(const ChatPackets::MatchRequest& request, const SystemAddress& sysAddr); + + // The player left their world (logged out, or loaded into another one): they leave their lobby + void PlayerLeftWorld(LWOOBJID playerID); + + // Once a frame: the lobby countdowns + void Update(float deltaTime); + + // Starts the instance of a formed match and tells the players' worlds (also used by tests) + void StartMatch(const Matchmaking::Match& match); + + Matchmaking::Lobbies& GetLobbies(); +} + +#endif // CHATMATCHMAKING_H diff --git a/dChatServer/ChatServer.cpp b/dChatServer/ChatServer.cpp index e7eb4fe0d..98b7c150b 100644 --- a/dChatServer/ChatServer.cpp +++ b/dChatServer/ChatServer.cpp @@ -33,6 +33,7 @@ #include "ChatPackets.h" #include "master/LiveUpdate.h" #include "MasterPackets.h" +#include "ChatMatchmaking.h" #include "Game.h" #include "Server.h" @@ -169,6 +170,10 @@ int main(int argc, char** argv) { Profiler::Scope scope("Player container update", Profiler::Phase::ENTITIES); Game::playerContainer.Update(deltaTime); } + { + Profiler::Scope scope("Matchmaking", Profiler::Phase::ENTITIES); + ChatMatchmaking::Update(deltaTime); + } //Check for packets here: //ReceiveFromMaster also handles the master packets if needed; it hands back the ones for us. @@ -283,7 +288,15 @@ namespace { handlers.On(Chat::GUILD_KICK, [](const GuildKick& kick, const SystemAddress&) { ChatGuilds::Get().Kick(kick.playerID, kick.kickedPlayer.GetAsString()); }); handlers.On(Chat::GUILD_SET_RANK, [](const GuildSetRank& rank, const SystemAddress&) { ChatGuilds::Get().SetRank(rank.playerID, rank.targetPlayer.GetAsString(), static_cast(rank.rank)); }); handlers.On(Chat::GUILD_DISBAND, [](const GuildDisband& disband, const SystemAddress&) { ChatGuilds::Get().Disband(disband.playerID); }); - handlers.On(Chat::UNEXPECTED_DISCONNECT, [](const UnexpectedDisconnect& notify, const SystemAddress& sysAddr) { Game::playerContainer.ScheduleRemovePlayer(notify, sysAddr); }); + handlers.On(Chat::UNEXPECTED_DISCONNECT, [](const UnexpectedDisconnect& notify, const SystemAddress& sysAddr) { + // The world they are in lost them: they can't be sent from it to an activity. (A world they already left + // can say so after they joined a lobby in their new one.) + const auto& player = Game::playerContainer.GetPlayerData(notify.playerID); + if (player && player.worldServerSysAddr == sysAddr) ChatMatchmaking::PlayerLeftWorld(notify.playerID); + Game::playerContainer.ScheduleRemovePlayer(notify, sysAddr); + }); + // Activity lobbies across every world (docs/Matchmaking.md) + handlers.On(Chat::MATCH_REQUEST, ChatMatchmaking::HandleMatchRequest); handlers.On(Chat::WHO, ChatPacketHandler::HandleWho); handlers.On(Chat::SHOW_ALL, ChatPacketHandler::HandleShowAll); handlers.On(Chat::ACHIEVEMENT_NOTIFY, ChatPacketHandler::OnAchievementNotify); diff --git a/dChatServer/PlayerContainer.cpp b/dChatServer/PlayerContainer.cpp index 367485d4d..29e05d37f 100644 --- a/dChatServer/PlayerContainer.cpp +++ b/dChatServer/PlayerContainer.cpp @@ -13,6 +13,7 @@ #include "dConfig.h" #include "MessageType/Chat.h" #include "TeamContainer.h" +#include "ChatMatchmaking.h" #include "ChatGuilds.h" #include "GuildManager.h" @@ -58,6 +59,9 @@ void PlayerContainer::InsertPlayer(const ChatPackets::LoginSessionNotify& notify return; } + // Loaded into a world (a new one when changing zones): a lobby they waited in on their last world is left + ChatMatchmaking::PlayerLeftWorld(playerId); + auto isLogin = !m_Players.contains(playerId); auto& data = m_Players[playerId]; data = PlayerData();