Files
DarkflameServer/tests/dGameTests/dNetTests/MasterPacketsTests.cpp
Aaron Kimbrell dce2037d9d feat: worlds starting up on the dashboard; the prestarted worlds are a setting
The dashboard shows worlds master has launched but that aren't connected
yet (Starting) and those shutting down, on the main page's world list
and in /api/worlds (state: starting|stopping), without a shutdown
button. Master's server list now carries each world's state (appended
after the UGC fields, one byte per world) and master pushes the list to
the dashboard whenever a world is launched, becomes ready, is told to
shut down or goes away, instead of the dashboard only seeing it on its
30 second poll. Starting worlds are kept apart from the running ones,
so counts, events and shutdown requests still only see running worlds.

prestart_worlds (masterconfig.ini, zone ids) lists the worlds master
starts when prestart_servers is on, instead of the hardcoded character
select (0) and Venture Explorer (1000), which stay the default when it
is missing or empty. It is on the Settings page as a zone list (restart
only), shown when prestart_servers is on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:14 -05:00

488 lines
20 KiB
C++

#include "MasterPackets.h"
#include "master/DashboardMessages.h"
#include "master/DataChanged.h"
#include "master/InstanceMigration.h"
#include "master/MessageCapture.h"
#include "master/PlayerAction.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 uint32_t key : { 0u, 12345u, 0xFFFFFFFFu }) {
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);
}
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);
}
response.ugcEnabled = 1;
response.ugcOnline = count % 2;
response.ugcPid = 4242 + count;
// The legacy list, then the UGC server's state
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
}), 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.ugcEnabled, 1);
EXPECT_EQ(copy.ugcOnline, count % 2);
EXPECT_EQ(copy.ugcPid, 4242 + count);
ExpectTruncatedFails(response);
}
}
// 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);
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;
}