feat(net): CDCLIENT_RELOAD master message

Appended at the end of MessageType::Master (46); the pin test is updated.
With names it tells a server which fdb copy and CDServer.sqlite to switch
to; without them it asks master to check the client's fdb now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-29 23:19:55 -05:00
parent 27a4564fec
commit b05b84ff30
4 changed files with 82 additions and 1 deletions

View File

@@ -89,5 +89,10 @@ namespace MessageType {
PROFILE_REQUEST,
// Any server -> master -> dashboard: a profiling session started, failed or finished with its scope tree
PROFILE_RESULT,
// CDClient reload (see CDClientReload.h, docs/CDClientFdb.md)
// Master -> every server: the client's cdclient.fdb changed; switch to the named copy and CDServer.sqlite.
// World or dashboard -> master (no names): check the client's fdb now
CDCLIENT_RELOAD,
};
}

View File

@@ -0,0 +1,50 @@
#ifndef __CDCLIENTRELOAD__H__
#define __CDCLIENTRELOAD__H__
#include <cstdint>
#include <string>
#include "BitStream.h"
#include "BitStreamUtils.h"
#include "InstanceMigration.h"
#include "MessageType/Master.h"
#include "dCommonVars.h"
/**
* CDCLIENT_RELOAD payload.
*
* Master -> every server: the client's cdclient.fdb changed. fdb and sqlite name the new copy and its CDServer.sqlite
* in resServer (FdbSnapshot.h); each server switches to them between frames.
*
* World or dashboard -> master, with no names: check the client's fdb now (a GM's /reloadcdclient or the dashboard).
* requesterId is the character who asked (told the result in chat), 0 for the dashboard.
*/
struct CDClientReload : public LUBitStream {
CDClientReload() : LUBitStream(ServiceType::MASTER, MessageType::Master::CDCLIENT_RELOAD) {}
static constexpr uint16_t MAX_NAME = 64;
LWOOBJID requesterId{};
std::string fdb;
std::string sqlite;
[[nodiscard]] bool IsRequest() const { return fdb.empty(); }
void Serialize(RakNet::BitStream& stream) const override {
stream.Write(requesterId);
InstanceMigration::WriteText(stream, fdb, MAX_NAME);
InstanceMigration::WriteText(stream, sqlite, MAX_NAME);
}
bool Deserialize(RakNet::BitStream& stream) override {
if (!stream.Read(requesterId)) return false;
if (!InstanceMigration::ReadText(stream, fdb, MAX_NAME) || !InstanceMigration::ReadText(stream, sqlite, MAX_NAME)) return false;
// Names in resServer only
for (const auto* name : { &fdb, &sqlite }) {
if (name->find_first_of("/\\") != std::string::npos || *name == "..") return false;
}
return fdb.empty() == sqlite.empty();
}
};
#endif //!__CDCLIENTRELOAD__H__

View File

@@ -1763,7 +1763,7 @@ static_assert(static_cast<int64_t>(MessageType::Game::CAN_ITEMS_BE_REFORGED) ==
static_assert(static_cast<int64_t>(MessageType::Game::NOTIFY_CLIENT_RAIL_START_FAILED) == 1771);
static_assert(static_cast<int64_t>(MessageType::Game::GET_IS_ON_RAIL) == 1772);
// MessageType::Master: 43 enumerators
// MessageType::Master: 44 enumerators
static_assert(static_cast<int64_t>(MessageType::Master::REQUEST_ZONE_TRANSFER) == 1);
static_assert(static_cast<int64_t>(MessageType::Master::REQUEST_ZONE_TRANSFER_RESPONSE) == 2);
static_assert(static_cast<int64_t>(MessageType::Master::SERVER_INFO) == 3);
@@ -1809,6 +1809,7 @@ static_assert(static_cast<int64_t>(MessageType::Master::CHAT_HANDOFF) == 42);
static_assert(static_cast<int64_t>(MessageType::Master::CHAT_SERVER_READY) == 43);
static_assert(static_cast<int64_t>(MessageType::Master::PROFILE_REQUEST) == 44);
static_assert(static_cast<int64_t>(MessageType::Master::PROFILE_RESULT) == 45);
static_assert(static_cast<int64_t>(MessageType::Master::CDCLIENT_RELOAD) == 46);
// MessageType::Server: 3 enumerators
static_assert(static_cast<int64_t>(MessageType::Server::VERSION_CONFIRM) == 0);

View File

@@ -1,4 +1,5 @@
#include "MasterPackets.h"
#include "master/CDClientReload.h"
#include "master/DashboardMessages.h"
#include "master/DataChanged.h"
#include "master/InstanceMigration.h"
@@ -522,3 +523,27 @@ TEST(MasterPacketsTests, UgcModelsMadeBytes) {
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;
}
}