Files
DarkflameServer/tests/dGameTests/dNetTests/MasterPacketsTests.cpp
Aaron Kimbrell 37cc844562 revert(master): the edit mode flag on a private instance
This reverts commit 4cf0909d. Nothing starts an edit mode instance any more. Its two
fixes that had nothing to do with edit mode come back in the next commit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 19:34:43 -06:00

723 lines
29 KiB
C++

#include "MasterPackets.h"
#include "master/CDClientReload.h"
#include "master/DashboardMessages.h"
#include "master/DataChanged.h"
#include "master/InstanceMigration.h"
#include "master/WorldFiles.h"
#include "master/MessageCapture.h"
#include "master/PlayerAction.h"
#include "master/UpdateStatus.h"
#include "PacketDispatcher.h"
#include "PacketTestUtils.h"
#include "Game.h"
#include "Logger.h"
#include "dGameMessagesTests/GameMessageTestUtils.h"
#include "Legacy/MasterPacketsLegacy.h"
#include <array>
#include <functional>
#include <limits>
#include <gtest/gtest.h>
using namespace PacketTestUtils;
using GameMessageTestUtils::RoundTrip;
using GameMessageTestUtils::ExpectTruncatedFails;
namespace {
const std::array<std::string, 4> g_Strings = { "", "localhost", "a password that is longer than fifty characters, which master cuts", "10.0.0.255" };
const std::array<uint64_t, 3> g_RequestIds = { 0, 1, 0x0102030405060708ULL };
const std::array<uint32_t, 3> g_Numbers = { 0, 1100, 0xFFFFFFFF };
PacketBytes StructPacket(const LUBitStream& msg) {
RakNet::BitStream bitStream;
msg.WritePacket(bitStream);
return FromBitStream(bitStream);
}
PacketBytes Written(const std::function<void(RakNet::BitStream&)>& write) {
RakNet::BitStream bitStream;
write(bitStream);
return FromBitStream(bitStream);
}
// A stream positioned after the header of msg's packet
void LoadPayload(RakNet::BitStream& bitStream, const LUBitStream& msg) {
msg.WritePacket(bitStream);
bitStream.IgnoreBytes(8);
}
// The old way the dashboard and migration structs were sent: the header, then Serialize
template<typename T>
void ExpectHeaderThenSerialize(const T& msg, MessageType::Master id) {
RakNet::BitStream old;
LUBitStream(ServiceType::MASTER, id).WriteHeader(old);
msg.Serialize(old);
EXPECT_PACKET_EQ(FromBitStream(old), StructPacket(msg));
RoundTrip(msg);
}
}
TEST(MasterPacketsTests, ZoneTransferMatchesLegacy) {
Stamps someStamps;
someStamps.list.emplace_back(eStamps::PASSPORT_AUTH_START, 0, 1700000000);
someStamps.list.emplace_back(eStamps::PASSPORT_AUTH_WORLD_PACKET_RECEIVED, 1100, 1700000001);
for (const auto& stamps : { Stamps{}, someStamps })
for (const auto requestID : g_RequestIds) {
for (const bool mythranShift : { false, true }) {
for (const auto zone : g_Numbers) {
MasterPackets::RequestZoneTransfer request;
request.requestID = requestID;
request.mythranShift = mythranShift;
request.zoneID = zone;
request.cloneID = zone ^ 0x5555;
request.stamps = stamps;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::SendZoneTransferRequest(b, requestID, mythranShift, zone, zone ^ 0x5555, stamps); }), StructPacket(request));
RakNet::BitStream stream; LoadPayload(stream, request);
const auto legacy = LegacyMaster::ReadZoneTransferRequest(stream);
const auto copy = RoundTrip(request);
EXPECT_EQ(legacy.requestID, copy.requestID);
EXPECT_EQ(legacy.mythranShift, copy.mythranShift);
EXPECT_EQ(legacy.zoneID, copy.zoneID);
EXPECT_EQ(legacy.zoneClone, copy.cloneID);
EXPECT_EQ(legacy.stamps.size(), copy.stamps.size());
for (const auto& ip : g_Strings) {
for (const uint32_t port : { 0u, 2001u, 70000u }) {
MasterPackets::RequestZoneTransferResponse response;
response.requestID = requestID;
response.mythranShift = mythranShift;
response.zoneID = zone;
response.zoneInstance = zone / 2;
response.zoneClone = zone / 3;
response.serverPort = static_cast<uint16_t>(port);
response.serverIP = LUString(ip, 255);
response.stamps = stamps;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::SendZoneTransferResponse(b, requestID, mythranShift, zone, zone / 2, zone / 3, ip, port, stamps); }), StructPacket(response));
RakNet::BitStream responseStream; LoadPayload(responseStream, response);
const auto legacyResponse = LegacyMaster::ReadZoneTransferResponse(responseStream);
const auto responseCopy = RoundTrip(response);
EXPECT_EQ(legacyResponse.requestID, responseCopy.requestID);
EXPECT_EQ(legacyResponse.mythranShift, responseCopy.mythranShift > 0);
EXPECT_EQ(legacyResponse.zoneInstance, responseCopy.zoneInstance);
EXPECT_EQ(legacyResponse.serverPort, responseCopy.serverPort);
EXPECT_EQ(legacyResponse.serverIP, responseCopy.serverIP.string);
EXPECT_EQ(legacyResponse.stamps.size(), responseCopy.stamps.size());
}
}
}
}
}
}
// Senders without the login stamps (older servers) still read, with no stamps; a cut off fixed part does not
TEST(MasterPacketsTests, ZoneTransferStampsAreOptional) {
RakNet::BitStream noStamps;
noStamps.Write<uint64_t>(5);
noStamps.Write<uint8_t>(1);
noStamps.Write<uint32_t>(1100);
noStamps.Write<uint32_t>(0);
MasterPackets::RequestZoneTransfer request;
ASSERT_TRUE(request.Deserialize(noStamps));
EXPECT_EQ(request.requestID, 5u);
EXPECT_TRUE(request.stamps.empty());
MasterPackets::RequestZoneTransfer full;
full.requestID = 5;
RakNet::BitStream all;
full.Serialize(all);
for (uint32_t bits = 0; bits < 8 * (8 + 1 + 4 + 4); bits++) {
RakNet::BitStream prefix;
prefix.WriteBits(all.GetData(), bits, false);
MasterPackets::RequestZoneTransfer copy;
EXPECT_FALSE(copy.Deserialize(prefix)) << bits;
}
}
TEST(MasterPacketsTests, ServerInfoMatchesLegacy) {
for (const auto type : { ServiceType::AUTH, ServiceType::CHAT, ServiceType::WORLD, ServiceType::DASHBOARD }) {
for (const auto& ip : g_Strings) {
if (ip.size() > 33) continue; // LUString(33)
for (const int instance : { 0, 7, -1 }) {
MasterPackets::ServerInfo info;
info.port = 2001;
info.zoneID = 1100;
info.instanceID = static_cast<uint32_t>(instance);
info.serverType = type;
info.ip = LUString(ip);
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::SendServerInfo(b, 2001, 1100u, instance, type, ip); }), StructPacket(info));
RakNet::BitStream stream; LoadPayload(stream, info);
const auto legacy = LegacyMaster::ReadServerInfo(stream);
const auto copy = RoundTrip(info);
EXPECT_EQ(legacy.theirPort, copy.port);
EXPECT_EQ(legacy.theirInstanceID, copy.instanceID);
EXPECT_EQ(legacy.theirServerType, copy.serverType);
EXPECT_EQ(legacy.theirIP, copy.ip.string);
ExpectTruncatedFails(info);
}
}
MasterPackets::ServerInfo offline;
offline.serverType = type;
offline.ip = LUString("offline");
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteServerOffline(b, type); }), StructPacket(offline));
}
}
TEST(MasterPacketsTests, SessionKeysMatchLegacy) {
for (const auto& key : { LUString(""), LUString("0123456789abcdef0123456789abcdef"), LUString("ffffffffffffffffffffffffffffffff") }) {
for (const auto& name : g_Strings) {
if (name.size() > 33) continue;
MasterPackets::SetSessionKey set;
set.sessionKey = key;
set.username = LUString(name);
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteSessionKey(b, MessageType::Master::SET_SESSION_KEY, key, LUString(name)); }), StructPacket(set));
RoundTrip(set);
ExpectTruncatedFails(set);
MasterPackets::NewSessionAlert alert;
alert.sessionKey = key;
alert.username = LUString(name);
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteSessionKey(b, MessageType::Master::NEW_SESSION_ALERT, key, LUString(name)); }), StructPacket(alert));
RoundTrip(alert);
MasterPackets::SessionKeyResponse response;
response.sessionKey = key;
response.username = LUWString(name);
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteSessionKeyResponse(b, key, LUWString(name)); }), StructPacket(response));
RoundTrip(response);
ExpectTruncatedFails(response);
MasterPackets::RequestSessionKey request;
request.username = LUWString(name);
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteRequestSessionKey(b, LUWString(name)); }), StructPacket(request));
RoundTrip(request);
ExpectTruncatedFails(request);
}
}
}
TEST(MasterPacketsTests, PrivateZonesMatchLegacy) {
for (const auto& password : g_Strings) {
MasterPackets::CreatePrivateZone create;
create.zoneID = 1150;
create.cloneID = 0xAABBCCDD;
create.password = password;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::SendZoneCreatePrivate(b, 1150, 0xAABBCCDD, password); }), StructPacket(create));
RakNet::BitStream stream; LoadPayload(stream, create);
const auto legacy = LegacyMaster::ReadCreatePrivateZone(stream);
RakNet::BitStream stream2; LoadPayload(stream2, create);
MasterPackets::CreatePrivateZone read;
ASSERT_TRUE(read.Deserialize(stream2));
EXPECT_EQ(legacy.first.first, read.zoneID);
EXPECT_EQ(legacy.first.second, read.cloneID);
EXPECT_EQ(legacy.second, read.password); // both cut at 50 characters
if (password.size() <= 50) RoundTrip(create);
for (const bool mythranShift : { false, true }) {
MasterPackets::RequestPrivateZone request;
request.requestID = 0x0102030405060708ULL;
request.mythranShift = mythranShift;
request.password = password;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::SendZoneRequestPrivate(b, 0x0102030405060708ULL, mythranShift, password); }), StructPacket(request));
RakNet::BitStream requestStream; LoadPayload(requestStream, request);
const auto legacyRequest = LegacyMaster::ReadRequestPrivateZone(requestStream);
RakNet::BitStream requestStream2; LoadPayload(requestStream2, request);
MasterPackets::RequestPrivateZone readRequest;
ASSERT_TRUE(readRequest.Deserialize(requestStream2));
EXPECT_EQ(legacyRequest.first.first, readRequest.requestID);
EXPECT_EQ(legacyRequest.first.second, readRequest.mythranShift);
EXPECT_EQ(legacyRequest.second, readRequest.password);
if (password.size() <= 50) RoundTrip(request);
}
}
}
TEST(MasterPacketsTests, WorldStateMatchesLegacy) {
for (const LWOMAPID zone : { static_cast<LWOMAPID>(0), static_cast<LWOMAPID>(1100), static_cast<LWOMAPID>(0xFFFF) }) {
for (const LWOINSTANCEID instance : { static_cast<LWOINSTANCEID>(0), static_cast<LWOINSTANCEID>(3), static_cast<LWOINSTANCEID>(0xFFFF) }) {
MasterPackets::WorldReady ready;
ready.zoneID = zone;
ready.instanceID = instance;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::SendWorldReady(b, zone, instance); }), StructPacket(ready));
RoundTrip(ready);
ExpectTruncatedFails(ready);
MasterPackets::PlayerAdded added;
added.zoneID = zone;
added.instanceID = instance;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WritePlayerCount(b, MessageType::Master::PLAYER_ADDED, zone, instance); }), StructPacket(added));
RoundTrip(added);
ExpectTruncatedFails(added);
MasterPackets::PlayerRemoved removed;
removed.zoneID = zone;
removed.instanceID = instance;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WritePlayerCount(b, MessageType::Master::PLAYER_REMOVED, zone, instance); }), StructPacket(removed));
RoundTrip(removed);
MasterPackets::WorldShutDown shutDown;
shutDown.zoneID = zone;
shutDown.instanceID = instance;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteWorldShutDown(b, zone, instance); }), StructPacket(shutDown));
RoundTrip(shutDown);
for (const bool isPrivate : { false, true }) {
MasterPackets::WorldReadyInfo info;
info.zoneID = zone;
info.instanceID = instance;
info.cloneID = 0xDEADBEEF;
info.ip = LUString("127.0.0.1");
info.port = 3000;
info.isPrivate = isPrivate ? 1 : 0;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteWorldReadyInfo(b, zone, instance, 0xDEADBEEF, "127.0.0.1", 3000, isPrivate); }), StructPacket(info));
RakNet::BitStream stream; LoadPayload(stream, info);
const auto legacy = LegacyMaster::ReadWorldReadyInfo(stream);
const auto copy = RoundTrip(info);
EXPECT_EQ(legacy.cloneID, copy.cloneID);
EXPECT_EQ(legacy.ip, copy.ip.string);
EXPECT_EQ(legacy.isPrivate, copy.isPrivate != 0);
ExpectTruncatedFails(info);
}
}
}
for (const int zone : { 0, 1100, -1 }) {
MasterPackets::PrepZone prep;
prep.zoneID = zone;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::TellMasterToPrepZone(b, zone); }), StructPacket(prep));
RoundTrip(prep);
ExpectTruncatedFails(prep);
}
{
// A property's clone rides after the zone; a prep without one reads as clone 0
MasterPackets::PrepZone prep;
prep.zoneID = 1251;
prep.cloneID = 2290;
RakNet::BitStream stream;
prep.Serialize(stream);
MasterPackets::PrepZone read;
ASSERT_TRUE(read.Deserialize(stream));
EXPECT_EQ(read.zoneID, 1251);
EXPECT_EQ(read.cloneID, 2290u);
RakNet::BitStream plain;
plain.Write<int32_t>(1100);
ASSERT_TRUE(read.Deserialize(plain));
EXPECT_EQ(read.cloneID, 0u);
}
for (const auto requestID : g_RequestIds) {
MasterPackets::AffirmTransferRequest request;
request.requestID = requestID;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteAffirmTransfer(b, MessageType::Master::AFFIRM_TRANSFER_REQUEST, requestID); }), StructPacket(request));
RoundTrip(request);
ExpectTruncatedFails(request);
MasterPackets::AffirmTransferResponse response;
response.requestID = requestID;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteAffirmTransfer(b, MessageType::Master::AFFIRM_TRANSFER_RESPONSE, requestID); }), StructPacket(response));
RoundTrip(response);
}
for (const auto zone : g_Numbers) {
MasterPackets::InstanceShutdown shutdown;
shutdown.zoneID = zone;
shutdown.instanceID = zone / 7;
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) { LegacyMaster::WriteInstanceShutdown(b, zone, zone / 7); }), StructPacket(shutdown));
RoundTrip(shutdown);
ExpectTruncatedFails(shutdown);
}
}
TEST(MasterPacketsTests, EmptyMessagesMatchLegacy) {
EXPECT_PACKET_EQ(Written([](RakNet::BitStream& b) { LegacyMaster::WriteEmpty(b, MessageType::Master::SHUTDOWN); }), StructPacket(MasterPackets::Shutdown()));
EXPECT_PACKET_EQ(Written([](RakNet::BitStream& b) { LegacyMaster::WriteEmpty(b, MessageType::Master::SHUTDOWN_RESPONSE); }), StructPacket(MasterPackets::ShutdownResponse()));
EXPECT_PACKET_EQ(Written([](RakNet::BitStream& b) { LegacyMaster::WriteEmpty(b, MessageType::Master::SHUTDOWN_UNIVERSE); }), StructPacket(MasterPackets::ShutdownUniverse()));
EXPECT_PACKET_EQ(Written([](RakNet::BitStream& b) { LegacyMaster::WriteEmpty(b, MessageType::Master::REQUEST_SERVER_LIST); }), StructPacket(MasterPackets::RequestServerList()));
EXPECT_PACKET_EQ(Written([](RakNet::BitStream& b) { LegacyMaster::WriteEmpty(b, MessageType::Master::CONFIG_RELOAD); }), StructPacket(MasterPackets::ConfigReload()));
EXPECT_PACKET_EQ(Written([](RakNet::BitStream& b) { LegacyMaster::WriteEmpty(b, MessageType::Master::DASHBOARD_SHUTDOWN); }), StructPacket(MasterPackets::DashboardShutdown()));
}
TEST(MasterPacketsTests, Golden) {
MasterPackets::PlayerAdded added;
added.zoneID = 1100;
added.instanceID = 2;
// 53 | MASTER (6) | PLAYER_ADDED (7) | pad | u16 1100 | u16 2
EXPECT_PACKET_EQ(FromHex("53 06 00 07 00 00 00 00 4c 04 02 00"), StructPacket(added));
MasterPackets::AffirmTransferRequest affirm;
affirm.requestID = 0x0102030405060708ULL;
// AFFIRM_TRANSFER_REQUEST (17) | u64
EXPECT_PACKET_EQ(FromHex("53 06 00 11 00 00 00 00 08 07 06 05 04 03 02 01"), StructPacket(affirm));
}
TEST(MasterPacketsTests, ServerListMatchesLegacy) {
for (size_t count = 0; count <= 3; count++) {
std::vector<LegacyMaster::ServerListInstance> instances;
MasterPackets::ServerListResponse response;
response.authOnline = 1;
response.chatOnline = count % 2;
for (size_t i = 0; i < count; i++) {
LegacyMaster::ServerListInstance instance{ static_cast<LWOMAPID>(1000 + i), static_cast<LWOINSTANCEID>(i), static_cast<LWOCLONEID>(i * 7), static_cast<uint32_t>(i * 3), "10.0.0." + std::to_string(i), static_cast<uint32_t>(3000 + i), i == 2 };
instances.push_back(instance);
auto& entry = response.instances.emplace_back();
entry.mapID = instance.mapID;
entry.instanceID = instance.instanceID;
entry.cloneID = instance.cloneID;
entry.players = instance.players;
entry.ip = LUString(instance.ip);
entry.port = instance.port;
entry.isPrivate = instance.isPrivate ? 1 : 0;
entry.state = static_cast<MasterPackets::ServerListResponse::eState>(i % 3);
entry.outdated = i == 1 ? 1 : 0;
}
response.ugcEnabled = 1;
response.ugcOnline = count % 2;
response.ugcPid = 4242 + count;
for (size_t i = 0; i < count; i++) {
auto& endpoint = response.endpoints.emplace_back();
endpoint.type = i == 0 ? ServiceType::AUTH : ServiceType::WORLD;
endpoint.zoneID = static_cast<uint32_t>(1000 + i);
endpoint.instanceID = static_cast<uint32_t>(i);
endpoint.ip = LUString("10.0.0." + std::to_string(i));
endpoint.port = static_cast<uint32_t>(3000 + i);
endpoint.host = LUString(i == 2 ? "192.168.1.20" : "localhost");
}
// The legacy list, then the UGC server's state, each world's state and where every server listens
EXPECT_PACKET_EQ(Written([&](RakNet::BitStream& b) {
LegacyMaster::WriteServerList(b, true, count % 2, instances);
b.Write<uint8_t>(1);
b.Write<uint8_t>(count % 2);
b.Write<uint32_t>(4242 + count);
for (size_t i = 0; i < count; i++) b.Write<uint8_t>(static_cast<uint8_t>(i % 3)); // each world's state
b.Write<uint32_t>(count);
for (size_t i = 0; i < count; i++) {
b.Write(i == 0 ? ServiceType::AUTH : ServiceType::WORLD);
b.Write<uint32_t>(1000 + i);
b.Write<uint32_t>(i);
b.Write(LUString("10.0.0." + std::to_string(i)));
b.Write<uint32_t>(3000 + i);
b.Write(LUString(i == 2 ? "192.168.1.20" : "localhost"));
}
for (size_t i = 0; i < count; i++) b.Write<uint8_t>(i == 1 ? 1 : 0); // each world's outdated flag
}), StructPacket(response));
RakNet::BitStream stream; LoadPayload(stream, response);
const auto legacy = LegacyMaster::ReadServerList(stream);
const auto copy = RoundTrip(response);
ASSERT_EQ(legacy.instances.size(), copy.instances.size());
for (size_t i = 0; i < count; i++) {
EXPECT_EQ(legacy.instances[i].mapID, copy.instances[i].mapID);
EXPECT_EQ(legacy.instances[i].players, copy.instances[i].players);
EXPECT_EQ(legacy.instances[i].ip, copy.instances[i].ip.string);
EXPECT_EQ(legacy.instances[i].isPrivate, copy.instances[i].isPrivate != 0);
EXPECT_EQ(static_cast<size_t>(copy.instances[i].state), i % 3);
EXPECT_EQ(copy.instances[i].outdated, i == 1 ? 1 : 0);
}
EXPECT_EQ(copy.ugcEnabled, 1);
EXPECT_EQ(copy.ugcOnline, count % 2);
EXPECT_EQ(copy.ugcPid, 4242 + count);
ASSERT_EQ(copy.endpoints.size(), count);
for (size_t i = 0; i < count; i++) {
EXPECT_EQ(copy.endpoints[i].type, i == 0 ? ServiceType::AUTH : ServiceType::WORLD);
EXPECT_EQ(copy.endpoints[i].zoneID, 1000 + i);
EXPECT_EQ(copy.endpoints[i].port, 3000 + i);
EXPECT_EQ(copy.endpoints[i].host.string, i == 2 ? "192.168.1.20" : "localhost");
}
// Cut short anywhere it fails, except exactly before the outdated flags (an older master's list), which reads
RakNet::BitStream full;
response.Serialize(full);
const auto olderMaster = full.GetNumberOfBitsUsed() - static_cast<uint32_t>(count) * 8;
for (uint32_t bits = 0; bits < full.GetNumberOfBitsUsed(); bits++) {
RakNet::BitStream prefix;
prefix.WriteBits(full.GetData(), bits, false);
MasterPackets::ServerListResponse cut;
EXPECT_EQ(cut.Deserialize(prefix), bits == olderMaster && count > 0) << "read from only " << bits << " bits";
}
}
}
// The dashboard and migration structs kept their payloads; they now carry their own header
TEST(MasterPacketsTests, DashboardAndMigrationStructsKeepTheirBytes) {
PlayerActionRequest action;
action.requestId = 7;
action.action = ePlayerAction::CHAT_MESSAGE;
action.text = "hello";
action.name = "[Discord] Bob";
ExpectHeaderThenSerialize(action, MessageType::Master::PLAYER_ACTION);
PlayerActionResult result;
result.requestId = 7;
result.affected = 3;
result.timedOut = true;
ExpectHeaderThenSerialize(result, MessageType::Master::PLAYER_ACTION_RESULT);
DataChanged changed;
changed.entries.push_back({ "characters", 42 });
ExpectHeaderThenSerialize(changed, MessageType::Master::DATA_CHANGED);
UgcModelsMade made;
made.blueprintIds = { 1152921510436831123, 42 };
ExpectHeaderThenSerialize(made, MessageType::Master::UGC_MODELS_MADE);
PlayerPositions positions;
positions.zoneId = 1100;
positions.players.push_back({ 42, 1.0f, 2.0f, 3.0f });
ExpectHeaderThenSerialize(positions, MessageType::Master::PLAYER_POSITIONS);
Announcement announcement;
announcement.title = "Title";
announcement.message = "Message";
announcement.zones = { 1100, 1200 };
ExpectHeaderThenSerialize(announcement, MessageType::Master::ANNOUNCE);
MessageCaptureControl control;
control.captureId = 3;
control.seconds = 60;
control.only = { 1, 2 };
ExpectHeaderThenSerialize(control, MessageType::Master::MESSAGE_CAPTURE_CONTROL);
MessageCaptureData data;
data.captureId = 3;
data.entries.resize(1);
data.entries[0].payload = "abc";
ExpectHeaderThenSerialize(data, MessageType::Master::MESSAGE_CAPTURE_DATA);
InstanceMigrationRequest migrate;
migrate.requestId = 9;
migrate.requestedBy = "GM";
ExpectHeaderThenSerialize(migrate, MessageType::Master::INSTANCE_MIGRATE);
MigratePlayersOrder order;
order.targetIp = "127.0.0.1";
ExpectHeaderThenSerialize(order, MessageType::Master::MIGRATE_PLAYERS);
MigrationStatus status;
status.message = "Moving players";
ExpectHeaderThenSerialize(status, MessageType::Master::MIGRATE_STATUS);
CarriedPlayerState state;
state.characterId = 42;
ExpectHeaderThenSerialize(state, MessageType::Master::MIGRATE_PLAYER_STATE);
}
TEST(MasterPacketsTests, DispatcherRoutesAndDropsBadPackets) {
// Dropped packets are logged
auto* const previousLogger = Game::logger;
Game::logger = new Logger("./testing.log", true, true);
PacketDispatcher<MessageType::Master> handlers;
int calls = 0;
MasterPackets::PlayerAdded seen;
handlers.On<MasterPackets::PlayerAdded>(MessageType::Master::PLAYER_ADDED, [&](const MasterPackets::PlayerAdded& msg, const SystemAddress&) { calls++; seen = msg; });
MasterPackets::PlayerAdded added;
added.zoneID = 1100;
added.instanceID = 4;
RakNet::BitStream good;
added.WritePacket(good);
Packet packet{};
packet.data = good.GetData();
packet.length = good.GetNumberOfBytesUsed();
EXPECT_TRUE(handlers.Dispatch(&packet, ServiceType::MASTER));
EXPECT_EQ(calls, 1);
EXPECT_EQ(seen.zoneID, 1100);
EXPECT_EQ(seen.instanceID, 4);
// Wrong service: not ours
EXPECT_FALSE(handlers.Dispatch(&packet, ServiceType::CHAT));
// Truncated: handled (dropped), the handler isn't called
packet.length -= 1;
EXPECT_TRUE(handlers.Dispatch(&packet, ServiceType::MASTER));
EXPECT_EQ(calls, 1);
// No handler for this ID
MasterPackets::PlayerRemoved removed;
RakNet::BitStream other;
removed.WritePacket(other);
packet.data = other.GetData();
packet.length = other.GetNumberOfBytesUsed();
EXPECT_FALSE(handlers.Dispatch(&packet, ServiceType::MASTER));
delete Game::logger;
Game::logger = previousLogger;
}
// UGC server -> master -> worlds: a u16 count, then each blueprint id
TEST(MasterPacketsTests, UgcModelsMadeBytes) {
UgcModelsMade made;
made.blueprintIds = { 0x0102030405060708, 9 };
EXPECT_PACKET_EQ(FromHex("53 06 00 25 00 00 00 00 02 00 08 07 06 05 04 03 02 01 09 00 00 00 00 00 00 00"), StructPacket(made));
RakNet::BitStream tooMany;
tooMany.Write<uint16_t>(UgcModelsMade::MAX_MODELS + 1);
UgcModelsMade read;
EXPECT_FALSE(read.Deserialize(tooMany));
RakNet::BitStream truncated;
truncated.Write<uint16_t>(2);
truncated.Write<LWOOBJID>(1);
EXPECT_FALSE(read.Deserialize(truncated));
}
TEST(MasterPacketsTests, CDClientReload) {
CDClientReload reload;
reload.fdb = "cdclient-0123456789abcdef.fdb";
reload.sqlite = "CDServer-0123456789abcdef.sqlite";
ExpectHeaderThenSerialize(reload, MessageType::Master::CDCLIENT_RELOAD);
EXPECT_FALSE(reload.IsRequest());
CDClientReload request;
request.requesterId = 42;
ExpectHeaderThenSerialize(request, MessageType::Master::CDCLIENT_RELOAD);
EXPECT_TRUE(request.IsRequest());
// A name that leaves resServer, or only one of the two names, is refused
for (const auto& [fdb, sqlite] : std::vector<std::pair<std::string, std::string>>{ { "../cdclient.fdb", "CDServer.sqlite" }, { "cdclient-1.fdb", "" } }) {
CDClientReload bad;
bad.fdb = fdb;
bad.sqlite = sqlite;
RakNet::BitStream stream;
bad.Serialize(stream);
CDClientReload read;
EXPECT_FALSE(read.Deserialize(stream)) << fdb;
}
}
TEST(MasterPacketsTests, WorldFilesReport) {
WorldFilesReport report;
report.zoneId = 1200;
report.instanceId = 3;
report.cloneId = 0;
report.files.push_back({ ZoneFileLog::eKind::ZONE, false, 1234, 0x0123456789abcdefULL, "/srv/res/maps/nimbusstation/nd_nimbus_station.luz" });
report.files.push_back({ ZoneFileLog::eKind::SCENE, true, 99, 7, "maps/nimbusstation/nd_scene.lvl" });
report.files.push_back({ ZoneFileLog::eKind::NAVMESH, false, 1ULL << 33, 0xffffffffffffffffULL, "C:/server/navmeshes/1200.bin" });
ExpectHeaderThenSerialize(report, MessageType::Master::WORLD_FILES);
RakNet::BitStream stream;
report.Serialize(stream);
WorldFilesReport read;
ASSERT_TRUE(read.Deserialize(stream));
EXPECT_EQ(read.zoneId, 1200u);
EXPECT_EQ(read.instanceId, 3u);
EXPECT_EQ(read.files, report.files);
// An empty path or an unknown kind is refused
for (const auto& bad : { ZoneFileLog::Entry{ ZoneFileLog::eKind::ZONE, false, 1, 1, "" },
ZoneFileLog::Entry{ static_cast<ZoneFileLog::eKind>(200), false, 1, 1, "x.luz" } }) {
WorldFilesReport badReport;
badReport.files.push_back(bad);
RakNet::BitStream badStream;
badReport.Serialize(badStream);
WorldFilesReport badRead;
EXPECT_FALSE(badRead.Deserialize(badStream));
}
RakNet::BitStream truncated;
report.Serialize(truncated);
RakNet::BitStream cut(truncated.GetData(), truncated.GetNumberOfBytesUsed() - 3, false);
WorldFilesReport cutRead;
EXPECT_FALSE(cutRead.Deserialize(cut));
}
TEST(MasterPacketsTests, WorldReloadRequest) {
WorldReloadRequest request;
request.zoneId = 1100;
request.warnSeconds = 0;
request.requesterId = 42;
request.requestedBy = "a GM";
ExpectHeaderThenSerialize(request, MessageType::Master::WORLD_RELOAD);
RakNet::BitStream stream;
request.Serialize(stream);
WorldReloadRequest read;
ASSERT_TRUE(read.Deserialize(stream));
EXPECT_EQ(read.zoneId, 1100u);
EXPECT_EQ(read.warnSeconds, 0);
EXPECT_EQ(read.requesterId, 42);
EXPECT_EQ(read.requestedBy, "a GM");
}
TEST(MasterPacketsTests, WorldFilesStatus) {
WorldFilesStatus status;
status.watching = true;
status.watchSeconds = 5;
status.seamless = false;
WorldFilesStatus::Zone zone;
zone.zoneId = 1100;
WorldFilesStatus::File file;
file.disk = { ZoneFileLog::eKind::TERRAIN, false, 5, 6, "/srv/res/maps/x.raw" };
file.hashed = true;
file.changed = true;
zone.files.push_back(file);
file.disk.path = "/srv/res/maps/y.raw";
file.missing = true;
file.changed = false;
zone.files.push_back(file);
zone.instances.push_back({ 1, 0, 12, true, false });
zone.instances.push_back({ 2, 0, 0, false, true });
zone.message = "Reloading 1 instance(s)";
status.zones.push_back(zone);
status.zones.push_back({});
ExpectHeaderThenSerialize(status, MessageType::Master::WORLD_FILES_STATUS);
RakNet::BitStream stream;
status.Serialize(stream);
WorldFilesStatus read;
ASSERT_TRUE(read.Deserialize(stream));
EXPECT_TRUE(read.watching);
EXPECT_EQ(read.watchSeconds, 5);
EXPECT_EQ(read.zones, status.zones);
}
TEST(MasterPacketsTests, UpdateStatus) {
UpdateStatus status;
status.state = 2;
status.summary = "Update available: release v3.1.0 (this server is running release v3.0.0)";
ExpectHeaderThenSerialize(status, MessageType::Master::UPDATE_STATUS);
RakNet::BitStream stream;
status.Serialize(stream);
UpdateStatus read;
ASSERT_TRUE(read.Deserialize(stream));
EXPECT_EQ(read.state, 2);
EXPECT_EQ(read.summary, status.summary);
// Longer than the cap: cut on the way out, refused on the way in
status.summary.assign(UpdateStatus::MAX_SUMMARY + 10, 'x');
RakNet::BitStream cut;
status.Serialize(cut);
ASSERT_TRUE(read.Deserialize(cut));
EXPECT_EQ(read.summary.size(), UpdateStatus::MAX_SUMMARY);
RakNet::BitStream tooLong;
tooLong.Write<uint8_t>(1);
tooLong.Write<uint16_t>(UpdateStatus::MAX_SUMMARY + 1);
EXPECT_FALSE(read.Deserialize(tooLong));
}
// A master without the outdated flags (written after the endpoints) still reads: nothing is outdated
TEST(MasterPacketsTests, ServerListWithoutOutdatedFlags) {
MasterPackets::ServerListResponse response;
auto& entry = response.instances.emplace_back();
entry.mapID = 1150;
entry.cloneID = 42;
entry.outdated = 1;
RakNet::BitStream full;
response.Serialize(full);
// Drop the last byte (the one flag)
RakNet::BitStream older;
older.Write(reinterpret_cast<const char*>(full.GetData()), static_cast<unsigned int>(full.GetNumberOfBytesUsed() - 1));
MasterPackets::ServerListResponse read;
ASSERT_TRUE(read.Deserialize(older));
ASSERT_EQ(read.instances.size(), 1u);
EXPECT_EQ(read.instances[0].outdated, 0);
RakNet::BitStream again;
response.Serialize(again);
MasterPackets::ServerListResponse withFlag;
ASSERT_TRUE(withFlag.Deserialize(again));
EXPECT_EQ(withFlag.instances[0].outdated, 1);
}