feat(chat): keep activity lobbies of every world and start their instances through master

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-30 04:11:59 -05:00
parent 4a7cac4f35
commit 2ae086d933
5 changed files with 180 additions and 1 deletions

View File

@@ -1,8 +1,10 @@
set(DCHATSERVER_SOURCES set(DCHATSERVER_SOURCES
"ChatIgnoreList.cpp" "ChatIgnoreList.cpp"
"ChatMatchmaking.cpp"
"ChatPacketHandler.cpp" "ChatPacketHandler.cpp"
"ChatGuilds.cpp" "ChatGuilds.cpp"
"GuildManager.cpp" "GuildManager.cpp"
"Matchmaking.cpp"
"PlayerContainer.cpp" "PlayerContainer.cpp"
"TeamContainer.cpp" "TeamContainer.cpp"
) )

View File

@@ -0,0 +1,126 @@
#include "ChatMatchmaking.h"
#include <map>
#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<Matchmaking::Update>& 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<SystemAddress, ChatPackets::MatchTransfer> perWorld;
std::vector<LWOOBJID> 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<Matchmaking::Update> 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<float>(request.waitTime) / 1000.0f;
settings.startDelay = static_cast<float>(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<Matchmaking::Update> updates;
if (g_Lobbies.Leave(playerID, updates)) SendUpdates(updates);
}
void Update(const float deltaTime) {
if (g_Lobbies.GetLobbies().empty()) return;
std::vector<Matchmaking::Update> 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<uint32_t>(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);
});
}
}

View File

@@ -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

View File

@@ -33,6 +33,7 @@
#include "ChatPackets.h" #include "ChatPackets.h"
#include "master/LiveUpdate.h" #include "master/LiveUpdate.h"
#include "MasterPackets.h" #include "MasterPackets.h"
#include "ChatMatchmaking.h"
#include "Game.h" #include "Game.h"
#include "Server.h" #include "Server.h"
@@ -169,6 +170,10 @@ int main(int argc, char** argv) {
Profiler::Scope scope("Player container update", Profiler::Phase::ENTITIES); Profiler::Scope scope("Player container update", Profiler::Phase::ENTITIES);
Game::playerContainer.Update(deltaTime); Game::playerContainer.Update(deltaTime);
} }
{
Profiler::Scope scope("Matchmaking", Profiler::Phase::ENTITIES);
ChatMatchmaking::Update(deltaTime);
}
//Check for packets here: //Check for packets here:
//ReceiveFromMaster also handles the master packets if needed; it hands back the ones for us. //ReceiveFromMaster also handles the master packets if needed; it hands back the ones for us.
@@ -283,7 +288,15 @@ namespace {
handlers.On<GuildKick>(Chat::GUILD_KICK, [](const GuildKick& kick, const SystemAddress&) { ChatGuilds::Get().Kick(kick.playerID, kick.kickedPlayer.GetAsString()); }); handlers.On<GuildKick>(Chat::GUILD_KICK, [](const GuildKick& kick, const SystemAddress&) { ChatGuilds::Get().Kick(kick.playerID, kick.kickedPlayer.GetAsString()); });
handlers.On<GuildSetRank>(Chat::GUILD_SET_RANK, [](const GuildSetRank& rank, const SystemAddress&) { ChatGuilds::Get().SetRank(rank.playerID, rank.targetPlayer.GetAsString(), static_cast<eGuildRank>(rank.rank)); }); handlers.On<GuildSetRank>(Chat::GUILD_SET_RANK, [](const GuildSetRank& rank, const SystemAddress&) { ChatGuilds::Get().SetRank(rank.playerID, rank.targetPlayer.GetAsString(), static_cast<eGuildRank>(rank.rank)); });
handlers.On<GuildDisband>(Chat::GUILD_DISBAND, [](const GuildDisband& disband, const SystemAddress&) { ChatGuilds::Get().Disband(disband.playerID); }); handlers.On<GuildDisband>(Chat::GUILD_DISBAND, [](const GuildDisband& disband, const SystemAddress&) { ChatGuilds::Get().Disband(disband.playerID); });
handlers.On<UnexpectedDisconnect>(Chat::UNEXPECTED_DISCONNECT, [](const UnexpectedDisconnect& notify, const SystemAddress& sysAddr) { Game::playerContainer.ScheduleRemovePlayer(notify, sysAddr); }); handlers.On<UnexpectedDisconnect>(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<MatchRequest>(Chat::MATCH_REQUEST, ChatMatchmaking::HandleMatchRequest);
handlers.On<FindPlayerRequest>(Chat::WHO, ChatPacketHandler::HandleWho); handlers.On<FindPlayerRequest>(Chat::WHO, ChatPacketHandler::HandleWho);
handlers.On<ShowAllRequest>(Chat::SHOW_ALL, ChatPacketHandler::HandleShowAll); handlers.On<ShowAllRequest>(Chat::SHOW_ALL, ChatPacketHandler::HandleShowAll);
handlers.On<AchievementNotify>(Chat::ACHIEVEMENT_NOTIFY, ChatPacketHandler::OnAchievementNotify); handlers.On<AchievementNotify>(Chat::ACHIEVEMENT_NOTIFY, ChatPacketHandler::OnAchievementNotify);

View File

@@ -13,6 +13,7 @@
#include "dConfig.h" #include "dConfig.h"
#include "MessageType/Chat.h" #include "MessageType/Chat.h"
#include "TeamContainer.h" #include "TeamContainer.h"
#include "ChatMatchmaking.h"
#include "ChatGuilds.h" #include "ChatGuilds.h"
#include "GuildManager.h" #include "GuildManager.h"
@@ -58,6 +59,9 @@ void PlayerContainer::InsertPlayer(const ChatPackets::LoginSessionNotify& notify
return; 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 isLogin = !m_Players.contains(playerId);
auto& data = m_Players[playerId]; auto& data = m_Players[playerId];
data = PlayerData(); data = PlayerData();