mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat(chat): activity lobby state machine for matchmaking across worlds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
165
dChatServer/Matchmaking.cpp
Normal file
165
dChatServer/Matchmaking.cpp
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
#include "Matchmaking.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <ranges>
|
||||||
|
|
||||||
|
#include "GeneralUtils.h"
|
||||||
|
#include "LDFFormat.h"
|
||||||
|
|
||||||
|
namespace Matchmaking {
|
||||||
|
uint32_t ActivitySettings::Capacity() const {
|
||||||
|
return static_cast<uint32_t>(std::max(maxTeamSize == 1 ? maxTeams : maxTeamSize, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t ActivitySettings::Minimum() const {
|
||||||
|
return static_cast<uint32_t>(std::max(maxTeamSize == 1 ? minTeams : minTeamSize, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string PlayerText(const LWOOBJID player) {
|
||||||
|
return LDFData<LWOOBJID>(u"player", player).GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// The racing car the client names in its MatchRequest ("droppedItem=13:<id>", its text); live showed it to the
|
||||||
|
// lobby as an object ID ("droppedItem=9:<id>") before the player's line
|
||||||
|
std::string DroppedItemText(const std::string& choices) {
|
||||||
|
for (const auto& line : GeneralUtils::SplitString(choices, '\n')) {
|
||||||
|
const auto equals = line.find('=');
|
||||||
|
if (equals == std::string::npos || line.substr(0, equals) != "droppedItem") continue;
|
||||||
|
const auto colon = line.find(':', equals);
|
||||||
|
if (colon == std::string::npos) continue;
|
||||||
|
const auto item = GeneralUtils::TryParse<LWOOBJID>(line.substr(colon + 1));
|
||||||
|
if (!item) continue;
|
||||||
|
return LDFData<LWOOBJID>(u"droppedItem", *item).GetString() + "\n";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The name as a wide string (type 0), as live sent it
|
||||||
|
std::string PlayerAddedText(const LobbyPlayer& player) {
|
||||||
|
return DroppedItemText(player.choices) + PlayerText(player.id) + "\n" + LDFData<std::u16string>(u"playerName", GeneralUtils::UTF8ToUTF16(player.name)).GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string TimeText(const float seconds) {
|
||||||
|
return LDFData<float>(u"time", seconds).GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Lobbies::Broadcast(const Lobby& lobby, const eMatchUpdate type, const std::string& data, std::vector<Update>& out) {
|
||||||
|
for (const auto& player : lobby.players) out.push_back({ player.id, type, data });
|
||||||
|
}
|
||||||
|
|
||||||
|
const Lobby* Lobbies::Find(const LWOOBJID player) const {
|
||||||
|
for (const auto& lobby : m_Lobbies | std::views::values) {
|
||||||
|
for (const auto& lobbyPlayer : lobby.players) {
|
||||||
|
if (lobbyPlayer.id == player) return &lobby;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Lobby* Lobbies::FindMutable(const LWOOBJID player) {
|
||||||
|
return const_cast<Lobby*>(Find(player));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Lobbies::Join(const LWOOBJID player, const std::string& name, const std::string& choices, const ActivitySettings& settings, std::vector<Update>& out) {
|
||||||
|
if (const auto* current = Find(player)) {
|
||||||
|
if (current->settings.activityID == settings.activityID && current->settings.instanceMapID == settings.instanceMapID) return false;
|
||||||
|
// Waiting for something else: that lobby loses them
|
||||||
|
Leave(player, out);
|
||||||
|
}
|
||||||
|
|
||||||
|
Lobby* lobby = nullptr;
|
||||||
|
for (auto& candidate : m_Lobbies | std::views::values) {
|
||||||
|
if (candidate.settings.activityID == settings.activityID && candidate.settings.instanceMapID == settings.instanceMapID
|
||||||
|
&& candidate.players.size() < candidate.settings.Capacity()) {
|
||||||
|
lobby = &candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!lobby) {
|
||||||
|
lobby = &m_Lobbies[m_NextLobbyID++];
|
||||||
|
lobby->settings = settings;
|
||||||
|
lobby->timer = settings.waitTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LobbyPlayer joining{ player, name, choices, false };
|
||||||
|
const auto joinedText = PlayerAddedText(joining);
|
||||||
|
|
||||||
|
// The joining player: themselves first, then everyone already waiting and whether they are ready (live order)
|
||||||
|
out.push_back({ player, eMatchUpdate::PLAYER_ADDED, joinedText });
|
||||||
|
for (const auto& waiting : lobby->players) {
|
||||||
|
out.push_back({ player, eMatchUpdate::PLAYER_ADDED, PlayerAddedText(waiting) });
|
||||||
|
if (waiting.ready) out.push_back({ player, eMatchUpdate::PLAYER_READY, PlayerText(waiting.id) });
|
||||||
|
}
|
||||||
|
// Everyone already waiting: the new player
|
||||||
|
Broadcast(*lobby, eMatchUpdate::PLAYER_ADDED, joinedText, out);
|
||||||
|
|
||||||
|
lobby->players.push_back(joining);
|
||||||
|
|
||||||
|
// A countdown already running: the new player gets the time left
|
||||||
|
if (lobby->counting) out.push_back({ player, eMatchUpdate::PHASE_WAIT_READY, TimeText(lobby->timer) });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Lobbies::Leave(const LWOOBJID player, std::vector<Update>& out) {
|
||||||
|
for (auto it = m_Lobbies.begin(); it != m_Lobbies.end(); ++it) {
|
||||||
|
auto& players = it->second.players;
|
||||||
|
const auto found = std::ranges::find(players, player, &LobbyPlayer::id);
|
||||||
|
if (found == players.end()) continue;
|
||||||
|
|
||||||
|
// Everyone in the lobby, the one leaving included
|
||||||
|
Broadcast(it->second, eMatchUpdate::PLAYER_REMOVED, PlayerText(player), out);
|
||||||
|
players.erase(found);
|
||||||
|
if (players.empty()) m_Lobbies.erase(it);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Lobbies::SetReady(const LWOOBJID player, const bool ready, std::vector<Update>& out) {
|
||||||
|
auto* lobby = FindMutable(player);
|
||||||
|
if (!lobby) return false;
|
||||||
|
|
||||||
|
std::ranges::find(lobby->players, player, &LobbyPlayer::id)->ready = ready;
|
||||||
|
Broadcast(*lobby, ready ? eMatchUpdate::PLAYER_READY : eMatchUpdate::PLAYER_NOT_READY, PlayerText(player), out);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Match> Lobbies::Tick(const float deltaTime, std::vector<Update>& out) {
|
||||||
|
std::vector<Match> matches;
|
||||||
|
for (auto it = m_Lobbies.begin(); it != m_Lobbies.end();) {
|
||||||
|
auto& lobby = it->second;
|
||||||
|
if (lobby.players.empty()) {
|
||||||
|
it = m_Lobbies.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The countdown runs only while the lobby has enough players; it doesn't start over when one leaves
|
||||||
|
if (lobby.players.size() >= lobby.settings.Minimum()) {
|
||||||
|
if (!lobby.counting) {
|
||||||
|
lobby.counting = true;
|
||||||
|
Broadcast(lobby, eMatchUpdate::PHASE_WAIT_READY, TimeText(lobby.timer), out);
|
||||||
|
}
|
||||||
|
lobby.timer -= deltaTime;
|
||||||
|
|
||||||
|
// Everyone ready: only the start delay is left
|
||||||
|
const bool allReady = std::ranges::all_of(lobby.players, &LobbyPlayer::ready);
|
||||||
|
if (allReady && lobby.timer > lobby.settings.startDelay) {
|
||||||
|
lobby.timer = lobby.settings.startDelay;
|
||||||
|
Broadcast(lobby, eMatchUpdate::PHASE_WAIT_START, TimeText(lobby.timer), out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lobby.timer <= 0.0f) {
|
||||||
|
Match match{ lobby.settings, {} };
|
||||||
|
for (const auto& player : lobby.players) match.players.push_back(player.id);
|
||||||
|
matches.push_back(std::move(match));
|
||||||
|
it = m_Lobbies.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
dChatServer/Matchmaking.h
Normal file
99
dChatServer/Matchmaking.h
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#ifndef MATCHMAKING_H
|
||||||
|
#define MATCHMAKING_H
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "dCommonVars.h"
|
||||||
|
#include "eMatchUpdate.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity lobbies of every world, kept by the chat server (docs/Matchmaking.md). Players on different instances of a
|
||||||
|
* zone who join the same activity wait in the same lobby and are sent to the same activity instance.
|
||||||
|
*
|
||||||
|
* Only the state machine: no network, no game data. What it does is handed back as MatchUpdates to send to clients
|
||||||
|
* and Matches to start; ChatMatchmaking sends them.
|
||||||
|
*/
|
||||||
|
namespace Matchmaking {
|
||||||
|
// What the world read from the activity (CDClient Activities, with the world's own overrides)
|
||||||
|
struct ActivitySettings {
|
||||||
|
int32_t activityID{};
|
||||||
|
uint32_t instanceMapID{};
|
||||||
|
int32_t minTeams{};
|
||||||
|
int32_t maxTeams{};
|
||||||
|
int32_t minTeamSize{};
|
||||||
|
int32_t maxTeamSize{};
|
||||||
|
float waitTime{}; // seconds
|
||||||
|
float startDelay{}; // seconds
|
||||||
|
|
||||||
|
// How many players a lobby holds: the team size, or the number of teams when every team is one player
|
||||||
|
uint32_t Capacity() const;
|
||||||
|
// How many players a lobby needs before its countdown runs
|
||||||
|
uint32_t Minimum() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A game message MatchUpdate for one client
|
||||||
|
struct Update {
|
||||||
|
LWOOBJID to{};
|
||||||
|
eMatchUpdate type{};
|
||||||
|
std::string data; // name-value text
|
||||||
|
};
|
||||||
|
|
||||||
|
// A lobby whose countdown ran out: these players go to one new instance of the activity's zone
|
||||||
|
struct Match {
|
||||||
|
ActivitySettings settings;
|
||||||
|
std::vector<LWOOBJID> players; // in the order they joined
|
||||||
|
};
|
||||||
|
|
||||||
|
struct LobbyPlayer {
|
||||||
|
LWOOBJID id{};
|
||||||
|
std::string name;
|
||||||
|
std::string choices; // the client's name-value text, shown to the others with the player
|
||||||
|
bool ready{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Lobby {
|
||||||
|
ActivitySettings settings;
|
||||||
|
std::vector<LobbyPlayer> players;
|
||||||
|
float timer{}; // seconds left
|
||||||
|
bool counting{}; // the lobby reached its minimum and everyone was told the countdown
|
||||||
|
};
|
||||||
|
|
||||||
|
class Lobbies {
|
||||||
|
public:
|
||||||
|
// A player joins the activity: they go into the first lobby of it with room, or a new one.
|
||||||
|
// Returns false when they are already waiting for this activity (nothing changes).
|
||||||
|
bool Join(LWOOBJID player, const std::string& name, const std::string& choices, const ActivitySettings& settings, std::vector<Update>& out);
|
||||||
|
|
||||||
|
// The player leaves the lobby they are in. Returns false when they are in none.
|
||||||
|
bool Leave(LWOOBJID player, std::vector<Update>& out);
|
||||||
|
|
||||||
|
// The player is ready (or not). Returns false when they are in no lobby.
|
||||||
|
bool SetReady(LWOOBJID player, bool ready, std::vector<Update>& out);
|
||||||
|
|
||||||
|
// Runs the countdowns; returns the lobbies that start now (they are removed)
|
||||||
|
std::vector<Match> Tick(float deltaTime, std::vector<Update>& out);
|
||||||
|
|
||||||
|
bool IsWaiting(LWOOBJID player) const { return Find(player) != nullptr; }
|
||||||
|
const Lobby* Find(LWOOBJID player) const;
|
||||||
|
const std::map<uint64_t, Lobby>& GetLobbies() const { return m_Lobbies; }
|
||||||
|
void Clear() { m_Lobbies.clear(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
Lobby* FindMutable(LWOOBJID player);
|
||||||
|
static void Broadcast(const Lobby& lobby, eMatchUpdate type, const std::string& data, std::vector<Update>& out);
|
||||||
|
|
||||||
|
// In the order they were made, so a joining player fills the oldest lobby first
|
||||||
|
std::map<uint64_t, Lobby> m_Lobbies;
|
||||||
|
uint64_t m_NextLobbyID = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The name-value texts of the updates, as live sent them
|
||||||
|
std::string PlayerText(LWOOBJID player);
|
||||||
|
std::string PlayerAddedText(const LobbyPlayer& player);
|
||||||
|
std::string TimeText(float seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // MATCHMAKING_H
|
||||||
@@ -4,6 +4,7 @@ set(DGAMETEST_SOURCES
|
|||||||
"LUBitStreamTests.cpp"
|
"LUBitStreamTests.cpp"
|
||||||
"MailIdPinTests.cpp"
|
"MailIdPinTests.cpp"
|
||||||
"MailPacketTests.cpp"
|
"MailPacketTests.cpp"
|
||||||
|
"MatchmakingTests.cpp"
|
||||||
"EconomyLedgerTests.cpp"
|
"EconomyLedgerTests.cpp"
|
||||||
"PlayerReportsLimitTests.cpp"
|
"PlayerReportsLimitTests.cpp"
|
||||||
"SlashCommandPermissionTests.cpp"
|
"SlashCommandPermissionTests.cpp"
|
||||||
|
|||||||
292
tests/dGameTests/MatchmakingTests.cpp
Normal file
292
tests/dGameTests/MatchmakingTests.cpp
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
#include "Matchmaking.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
// The chat server's activity lobbies (docs/Matchmaking.md)
|
||||||
|
|
||||||
|
using namespace Matchmaking;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr LWOOBJID A = 0x1000000000000001LL;
|
||||||
|
constexpr LWOOBJID B = 0x1000000000000002LL;
|
||||||
|
constexpr LWOOBJID C = 0x1000000000000003LL;
|
||||||
|
constexpr LWOOBJID D = 0x1000000000000004LL;
|
||||||
|
constexpr LWOOBJID E = 0x1000000000000005LL;
|
||||||
|
|
||||||
|
// Avant Gardens survival: one team of 1 to 4, a minute to wait, 3 seconds once everyone is ready
|
||||||
|
ActivitySettings Survival() {
|
||||||
|
ActivitySettings settings;
|
||||||
|
settings.activityID = 5;
|
||||||
|
settings.instanceMapID = 1101;
|
||||||
|
settings.minTeams = 1;
|
||||||
|
settings.maxTeams = 1;
|
||||||
|
settings.minTeamSize = 2;
|
||||||
|
settings.maxTeamSize = 4;
|
||||||
|
settings.waitTime = 60.0f;
|
||||||
|
settings.startDelay = 3.0f;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A race: up to 6 teams of one, at least 2
|
||||||
|
ActivitySettings Race() {
|
||||||
|
ActivitySettings settings;
|
||||||
|
settings.activityID = 42;
|
||||||
|
settings.instanceMapID = 1203;
|
||||||
|
settings.minTeams = 2;
|
||||||
|
settings.maxTeams = 6;
|
||||||
|
settings.minTeamSize = 1;
|
||||||
|
settings.maxTeamSize = 1;
|
||||||
|
settings.waitTime = 60.0f;
|
||||||
|
settings.startDelay = 3.0f;
|
||||||
|
return settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Update> For(const std::vector<Update>& updates, LWOOBJID player) {
|
||||||
|
std::vector<Update> result;
|
||||||
|
for (const auto& update : updates) if (update.to == player) result.push_back(update);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Count(const std::vector<Update>& updates, eMatchUpdate type) {
|
||||||
|
return std::ranges::count(updates, type, &Update::type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, SettingsCapacityAndMinimum) {
|
||||||
|
EXPECT_EQ(Survival().Capacity(), 4u);
|
||||||
|
EXPECT_EQ(Survival().Minimum(), 2u);
|
||||||
|
EXPECT_EQ(Race().Capacity(), 6u);
|
||||||
|
EXPECT_EQ(Race().Minimum(), 2u);
|
||||||
|
ActivitySettings empty;
|
||||||
|
EXPECT_EQ(empty.Capacity(), 1u);
|
||||||
|
EXPECT_EQ(empty.Minimum(), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, UpdateTextsMatchLiveCaptures) {
|
||||||
|
EXPECT_EQ(PlayerText(1152921510436607007LL), "player=9:1152921510436607007");
|
||||||
|
EXPECT_EQ(PlayerAddedText({ 1152921510436607007LL, "GruntMonkey", "", false }), "player=9:1152921510436607007\nplayerName=0:GruntMonkey");
|
||||||
|
// The racing car the client chose comes first, as an object ID
|
||||||
|
EXPECT_EQ(PlayerAddedText({ 1152921510436607007LL, "GruntMonkey", "droppedItem=13:1152921510659010409", false }),
|
||||||
|
"droppedItem=9:1152921510659010409\nplayer=9:1152921510436607007\nplayerName=0:GruntMonkey");
|
||||||
|
EXPECT_EQ(PlayerAddedText({ 1, "x", "somethingElse=0:1\ndroppedItem=13:notanumber", false }), "player=9:1\nplayerName=0:x");
|
||||||
|
EXPECT_EQ(TimeText(3.0f).rfind("time=3:", 0), 0u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, JoinSendsTheJoinerThemselvesFirstThenTheOthers) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
ASSERT_TRUE(lobbies.Join(A, "Alpha", "", Survival(), out));
|
||||||
|
ASSERT_EQ(out.size(), 1u);
|
||||||
|
EXPECT_EQ(out[0].to, A);
|
||||||
|
EXPECT_EQ(out[0].type, eMatchUpdate::PLAYER_ADDED);
|
||||||
|
EXPECT_EQ(out[0].data, "player=9:" + std::to_string(A) + "\nplayerName=0:Alpha");
|
||||||
|
|
||||||
|
out.clear();
|
||||||
|
ASSERT_TRUE(lobbies.SetReady(A, true, out));
|
||||||
|
out.clear();
|
||||||
|
ASSERT_TRUE(lobbies.Join(B, "Bravo", "", Survival(), out));
|
||||||
|
// B: B added, A added, A ready. A: B added.
|
||||||
|
const auto toB = For(out, B);
|
||||||
|
ASSERT_EQ(toB.size(), 3u);
|
||||||
|
EXPECT_EQ(toB[0].type, eMatchUpdate::PLAYER_ADDED);
|
||||||
|
EXPECT_NE(toB[0].data.find("Bravo"), std::string::npos);
|
||||||
|
EXPECT_EQ(toB[1].type, eMatchUpdate::PLAYER_ADDED);
|
||||||
|
EXPECT_NE(toB[1].data.find("Alpha"), std::string::npos);
|
||||||
|
EXPECT_EQ(toB[2].type, eMatchUpdate::PLAYER_READY);
|
||||||
|
EXPECT_EQ(toB[2].data, PlayerText(A));
|
||||||
|
const auto toA = For(out, A);
|
||||||
|
ASSERT_EQ(toA.size(), 1u);
|
||||||
|
EXPECT_EQ(toA[0].type, eMatchUpdate::PLAYER_ADDED);
|
||||||
|
EXPECT_NE(toA[0].data.find("Bravo"), std::string::npos);
|
||||||
|
|
||||||
|
// Joining again changes nothing
|
||||||
|
out.clear();
|
||||||
|
EXPECT_FALSE(lobbies.Join(B, "Bravo", "", Survival(), out));
|
||||||
|
EXPECT_TRUE(out.empty());
|
||||||
|
EXPECT_EQ(lobbies.GetLobbies().size(), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, CountdownStartsAtTheMinimumAndLateJoinersGetTheTimeLeft) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
out.clear();
|
||||||
|
// Below the minimum nothing runs
|
||||||
|
EXPECT_TRUE(lobbies.Tick(30.0f, out).empty());
|
||||||
|
EXPECT_TRUE(out.empty());
|
||||||
|
EXPECT_FLOAT_EQ(lobbies.Find(A)->timer, 60.0f);
|
||||||
|
|
||||||
|
lobbies.Join(B, "Bravo", "", Survival(), out);
|
||||||
|
EXPECT_EQ(Count(out, eMatchUpdate::PHASE_WAIT_READY), 0u);
|
||||||
|
out.clear();
|
||||||
|
EXPECT_TRUE(lobbies.Tick(10.0f, out).empty());
|
||||||
|
// Everyone is told the countdown once
|
||||||
|
EXPECT_EQ(Count(out, eMatchUpdate::PHASE_WAIT_READY), 2u);
|
||||||
|
EXPECT_EQ(For(out, A)[0].data, TimeText(60.0f));
|
||||||
|
EXPECT_FLOAT_EQ(lobbies.Find(A)->timer, 50.0f);
|
||||||
|
out.clear();
|
||||||
|
lobbies.Tick(1.0f, out);
|
||||||
|
EXPECT_TRUE(out.empty());
|
||||||
|
|
||||||
|
lobbies.Join(C, "Charlie", "", Survival(), out);
|
||||||
|
const auto toC = For(out, C);
|
||||||
|
ASSERT_FALSE(toC.empty());
|
||||||
|
EXPECT_EQ(toC.back().type, eMatchUpdate::PHASE_WAIT_READY);
|
||||||
|
EXPECT_EQ(toC.back().data, TimeText(49.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, TimerRunsOutIntoOneMatchWithEveryoneInJoinOrder) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.Join(B, "Bravo", "", Survival(), out);
|
||||||
|
lobbies.Join(C, "Charlie", "", Survival(), out);
|
||||||
|
EXPECT_TRUE(lobbies.Tick(59.0f, out).empty());
|
||||||
|
const auto matches = lobbies.Tick(1.5f, out);
|
||||||
|
ASSERT_EQ(matches.size(), 1u);
|
||||||
|
EXPECT_EQ(matches[0].players, (std::vector<LWOOBJID>{ A, B, C }));
|
||||||
|
EXPECT_EQ(matches[0].settings.instanceMapID, 1101u);
|
||||||
|
EXPECT_TRUE(lobbies.GetLobbies().empty());
|
||||||
|
EXPECT_FALSE(lobbies.IsWaiting(A));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, EveryoneReadyCutsTheTimerToTheStartDelay) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.Join(B, "Bravo", "", Survival(), out);
|
||||||
|
lobbies.Tick(1.0f, out);
|
||||||
|
lobbies.SetReady(A, true, out);
|
||||||
|
out.clear();
|
||||||
|
lobbies.SetReady(B, true, out);
|
||||||
|
EXPECT_EQ(Count(out, eMatchUpdate::PLAYER_READY), 2u);
|
||||||
|
out.clear();
|
||||||
|
EXPECT_TRUE(lobbies.Tick(1.0f, out).empty());
|
||||||
|
EXPECT_EQ(Count(out, eMatchUpdate::PHASE_WAIT_START), 2u);
|
||||||
|
EXPECT_EQ(For(out, A)[0].data, TimeText(3.0f));
|
||||||
|
EXPECT_FLOAT_EQ(lobbies.Find(A)->timer, 3.0f);
|
||||||
|
EXPECT_TRUE(lobbies.Tick(2.0f, out).empty());
|
||||||
|
EXPECT_EQ(lobbies.Tick(1.0f, out).size(), 1u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, ReadyBelowTheMinimumDoesNotCutTheTimer) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.SetReady(A, true, out);
|
||||||
|
lobbies.Tick(1.0f, out);
|
||||||
|
EXPECT_FLOAT_EQ(lobbies.Find(A)->timer, 60.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, UnreadyIsBroadcast) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.Join(B, "Bravo", "", Survival(), out);
|
||||||
|
out.clear();
|
||||||
|
lobbies.SetReady(B, false, out);
|
||||||
|
EXPECT_EQ(Count(out, eMatchUpdate::PLAYER_NOT_READY), 2u);
|
||||||
|
EXPECT_FALSE(lobbies.SetReady(C, true, out));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, LeaveTellsEveryoneIncludingTheLeaverAndEmptyLobbiesGo) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.Join(B, "Bravo", "", Survival(), out);
|
||||||
|
out.clear();
|
||||||
|
ASSERT_TRUE(lobbies.Leave(B, out));
|
||||||
|
ASSERT_EQ(out.size(), 2u);
|
||||||
|
EXPECT_EQ(Count(out, eMatchUpdate::PLAYER_REMOVED), 2u);
|
||||||
|
EXPECT_EQ(out[0].data, PlayerText(B));
|
||||||
|
EXPECT_FALSE(lobbies.IsWaiting(B));
|
||||||
|
EXPECT_FALSE(lobbies.Leave(B, out));
|
||||||
|
ASSERT_TRUE(lobbies.Leave(A, out));
|
||||||
|
EXPECT_TRUE(lobbies.GetLobbies().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, CountdownPausesBelowTheMinimumWithoutStartingOver) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.Join(B, "Bravo", "", Survival(), out);
|
||||||
|
lobbies.Tick(20.0f, out);
|
||||||
|
lobbies.Leave(B, out);
|
||||||
|
lobbies.Tick(20.0f, out);
|
||||||
|
EXPECT_FLOAT_EQ(lobbies.Find(A)->timer, 40.0f);
|
||||||
|
out.clear();
|
||||||
|
lobbies.Join(C, "Charlie", "", Survival(), out);
|
||||||
|
EXPECT_EQ(For(out, C).back().data, TimeText(40.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, FullLobbiesOverflowIntoANewOne) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
for (const auto player : { A, B, C, D }) lobbies.Join(player, "p", "", Survival(), out);
|
||||||
|
EXPECT_EQ(lobbies.GetLobbies().size(), 1u);
|
||||||
|
lobbies.Join(E, "e", "", Survival(), out);
|
||||||
|
EXPECT_EQ(lobbies.GetLobbies().size(), 2u);
|
||||||
|
EXPECT_NE(lobbies.Find(A), lobbies.Find(E));
|
||||||
|
|
||||||
|
// A place in the first lobby is taken before the second one
|
||||||
|
lobbies.Leave(B, out);
|
||||||
|
constexpr LWOOBJID F = 0x1000000000000006LL;
|
||||||
|
lobbies.Join(F, "f", "", Survival(), out);
|
||||||
|
EXPECT_EQ(lobbies.Find(F), lobbies.Find(A));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, RacesHoldSixSingleTeams) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
for (LWOOBJID player = 1; player <= 6; player++) lobbies.Join(player, "p", "", Race(), out);
|
||||||
|
EXPECT_EQ(lobbies.GetLobbies().size(), 1u);
|
||||||
|
lobbies.Join(7, "p", "", Race(), out);
|
||||||
|
EXPECT_EQ(lobbies.GetLobbies().size(), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, DifferentActivitiesAndZonesWaitApart) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.Join(B, "Bravo", "", Race(), out);
|
||||||
|
auto otherZone = Survival();
|
||||||
|
otherZone.instanceMapID = 1102;
|
||||||
|
lobbies.Join(C, "Charlie", "", otherZone, out);
|
||||||
|
EXPECT_EQ(lobbies.GetLobbies().size(), 3u);
|
||||||
|
|
||||||
|
// Joining another activity leaves the first lobby
|
||||||
|
out.clear();
|
||||||
|
ASSERT_TRUE(lobbies.Join(A, "Alpha", "", Race(), out));
|
||||||
|
EXPECT_EQ(Count(out, eMatchUpdate::PLAYER_REMOVED), 1u);
|
||||||
|
EXPECT_EQ(lobbies.Find(A), lobbies.Find(B));
|
||||||
|
EXPECT_EQ(lobbies.GetLobbies().size(), 2u);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, SoloActivityStartsAfterTheWaitAlone) {
|
||||||
|
// Solo racing (the world's override): one player is enough
|
||||||
|
auto solo = Race();
|
||||||
|
solo.minTeams = 1;
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", solo, out);
|
||||||
|
lobbies.SetReady(A, true, out);
|
||||||
|
EXPECT_TRUE(lobbies.Tick(0.5f, out).empty());
|
||||||
|
EXPECT_FLOAT_EQ(lobbies.Find(A)->timer, 3.0f);
|
||||||
|
const auto matches = lobbies.Tick(3.0f, out);
|
||||||
|
ASSERT_EQ(matches.size(), 1u);
|
||||||
|
EXPECT_EQ(matches[0].players, std::vector<LWOOBJID>{ A });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MatchmakingTests, SeveralLobbiesStartInTheSameTick) {
|
||||||
|
Lobbies lobbies;
|
||||||
|
std::vector<Update> out;
|
||||||
|
lobbies.Join(A, "Alpha", "", Survival(), out);
|
||||||
|
lobbies.Join(B, "Bravo", "", Survival(), out);
|
||||||
|
lobbies.Join(C, "Charlie", "", Race(), out);
|
||||||
|
lobbies.Join(D, "Delta", "", Race(), out);
|
||||||
|
const auto matches = lobbies.Tick(61.0f, out);
|
||||||
|
EXPECT_EQ(matches.size(), 2u);
|
||||||
|
EXPECT_TRUE(lobbies.GetLobbies().empty());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user