From b05b84ff30e58093bbcf1804a0733f01ed77abdd Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 23:19:55 -0500 Subject: [PATCH] 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 --- dCommon/dEnums/MessageType/Master.h | 5 ++ dNet/master/CDClientReload.h | 50 +++++++++++++++++++ tests/dCommonTests/MessageIdPinTests.cpp | 3 +- .../dNetTests/MasterPacketsTests.cpp | 25 ++++++++++ 4 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 dNet/master/CDClientReload.h diff --git a/dCommon/dEnums/MessageType/Master.h b/dCommon/dEnums/MessageType/Master.h index 36e69cdaa..d11bd6e80 100644 --- a/dCommon/dEnums/MessageType/Master.h +++ b/dCommon/dEnums/MessageType/Master.h @@ -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, }; } diff --git a/dNet/master/CDClientReload.h b/dNet/master/CDClientReload.h new file mode 100644 index 000000000..94e63f96e --- /dev/null +++ b/dNet/master/CDClientReload.h @@ -0,0 +1,50 @@ +#ifndef __CDCLIENTRELOAD__H__ +#define __CDCLIENTRELOAD__H__ + +#include +#include + +#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__ diff --git a/tests/dCommonTests/MessageIdPinTests.cpp b/tests/dCommonTests/MessageIdPinTests.cpp index 0c77429ac..8206ba8fa 100644 --- a/tests/dCommonTests/MessageIdPinTests.cpp +++ b/tests/dCommonTests/MessageIdPinTests.cpp @@ -1763,7 +1763,7 @@ static_assert(static_cast(MessageType::Game::CAN_ITEMS_BE_REFORGED) == static_assert(static_cast(MessageType::Game::NOTIFY_CLIENT_RAIL_START_FAILED) == 1771); static_assert(static_cast(MessageType::Game::GET_IS_ON_RAIL) == 1772); -// MessageType::Master: 43 enumerators +// MessageType::Master: 44 enumerators static_assert(static_cast(MessageType::Master::REQUEST_ZONE_TRANSFER) == 1); static_assert(static_cast(MessageType::Master::REQUEST_ZONE_TRANSFER_RESPONSE) == 2); static_assert(static_cast(MessageType::Master::SERVER_INFO) == 3); @@ -1809,6 +1809,7 @@ static_assert(static_cast(MessageType::Master::CHAT_HANDOFF) == 42); static_assert(static_cast(MessageType::Master::CHAT_SERVER_READY) == 43); static_assert(static_cast(MessageType::Master::PROFILE_REQUEST) == 44); static_assert(static_cast(MessageType::Master::PROFILE_RESULT) == 45); +static_assert(static_cast(MessageType::Master::CDCLIENT_RELOAD) == 46); // MessageType::Server: 3 enumerators static_assert(static_cast(MessageType::Server::VERSION_CONFIRM) == 0); diff --git a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp index 5b1407e50..f5ff40482 100644 --- a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp @@ -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(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>{ { "../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; + } +}