mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
feat(net): WORLD_FILES, WORLD_RELOAD and WORLD_FILES_STATUS master messages
Appended at the end of MessageType::Master (47-49); the pin test is updated. A world sends its zone file list to master once it is ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -94,5 +94,13 @@ namespace MessageType {
|
||||
// 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,
|
||||
|
||||
// World hot reload (see WorldFiles.h, docs/WorldHotReload.md)
|
||||
// World -> master, once ready: the zone data files it loaded, with their sizes and hashes
|
||||
WORLD_FILES,
|
||||
// World (a GM's /reloadworld) or dashboard -> master: replace every instance of a zone with one on the files on disk
|
||||
WORLD_RELOAD,
|
||||
// Master -> dashboard: each running zone's files, whether they changed on disk, and which instances are stale
|
||||
WORLD_FILES_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
211
dNet/master/WorldFiles.h
Normal file
211
dNet/master/WorldFiles.h
Normal file
@@ -0,0 +1,211 @@
|
||||
#ifndef __WORLDFILES__H__
|
||||
#define __WORLDFILES__H__
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "BitStream.h"
|
||||
#include "BitStreamUtils.h"
|
||||
#include "InstanceMigration.h"
|
||||
#include "MessageType/Master.h"
|
||||
#include "ZoneFileLog.h"
|
||||
#include "dCommonVars.h"
|
||||
|
||||
/**
|
||||
* World hot reload (docs/WorldHotReload.md).
|
||||
*/
|
||||
namespace WorldFiles {
|
||||
constexpr uint16_t MAX_PATH = 1024;
|
||||
constexpr uint16_t MAX_FILES = 2048;
|
||||
constexpr uint16_t MAX_BY = 64;
|
||||
constexpr uint16_t MAX_MESSAGE = 256;
|
||||
|
||||
inline void WriteFile(RakNet::BitStream& stream, const ZoneFileLog::Entry& file) {
|
||||
stream.Write(static_cast<uint8_t>(file.kind));
|
||||
stream.Write<uint8_t>(file.packed ? 1 : 0);
|
||||
stream.Write(file.size);
|
||||
stream.Write(file.hash);
|
||||
InstanceMigration::WriteText(stream, file.path, MAX_PATH);
|
||||
}
|
||||
|
||||
inline bool ReadFile(RakNet::BitStream& stream, ZoneFileLog::Entry& file) {
|
||||
uint8_t kind{}, packed{};
|
||||
if (!stream.Read(kind) || !stream.Read(packed) || !stream.Read(file.size) || !stream.Read(file.hash)) return false;
|
||||
if (kind > static_cast<uint8_t>(ZoneFileLog::eKind::OTHER)) return false;
|
||||
file.kind = static_cast<ZoneFileLog::eKind>(kind);
|
||||
file.packed = packed != 0;
|
||||
return InstanceMigration::ReadText(stream, file.path, MAX_PATH) && !file.path.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WORLD_FILES (world -> master, once the world is ready): the zone data files it loaded (ZoneFileLog). Master takes
|
||||
* the zone, instance and clone from the instance it knows at that address; the IDs here are for the log.
|
||||
*/
|
||||
struct WorldFilesReport : public LUBitStream {
|
||||
WorldFilesReport() : LUBitStream(ServiceType::MASTER, MessageType::Master::WORLD_FILES) {}
|
||||
|
||||
uint32_t zoneId{};
|
||||
uint32_t instanceId{};
|
||||
uint32_t cloneId{};
|
||||
std::vector<ZoneFileLog::Entry> files;
|
||||
|
||||
void Serialize(RakNet::BitStream& stream) const override {
|
||||
stream.Write(zoneId);
|
||||
stream.Write(instanceId);
|
||||
stream.Write(cloneId);
|
||||
const auto count = static_cast<uint16_t>(std::min<size_t>(files.size(), WorldFiles::MAX_FILES));
|
||||
stream.Write(count);
|
||||
for (uint16_t i = 0; i < count; i++) WorldFiles::WriteFile(stream, files[i]);
|
||||
}
|
||||
|
||||
bool Deserialize(RakNet::BitStream& stream) override {
|
||||
uint16_t count{};
|
||||
if (!stream.Read(zoneId) || !stream.Read(instanceId) || !stream.Read(cloneId) || !stream.Read(count)) return false;
|
||||
if (count > WorldFiles::MAX_FILES) return false;
|
||||
files.assign(count, {});
|
||||
for (auto& file : files) {
|
||||
if (!WorldFiles::ReadFile(stream, file)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* WORLD_RELOAD (world for a GM's /reloadworld, or the dashboard -> master): replace every instance of zoneId with a
|
||||
* new one that loads the files on disk now. warnSeconds: how long players are warned first. requesterId is the
|
||||
* character who asked (told how each move goes), 0 for the dashboard.
|
||||
*/
|
||||
struct WorldReloadRequest : public LUBitStream {
|
||||
WorldReloadRequest() : LUBitStream(ServiceType::MASTER, MessageType::Master::WORLD_RELOAD) {}
|
||||
|
||||
static constexpr uint16_t DEFAULT_WARN_SECONDS = 10;
|
||||
|
||||
uint32_t zoneId{};
|
||||
uint16_t warnSeconds{ DEFAULT_WARN_SECONDS };
|
||||
LWOOBJID requesterId{};
|
||||
std::string requestedBy;
|
||||
|
||||
void Serialize(RakNet::BitStream& stream) const override {
|
||||
stream.Write(zoneId);
|
||||
stream.Write(std::min(warnSeconds, InstanceMigrationRequest::MAX_WARN_SECONDS));
|
||||
stream.Write(requesterId);
|
||||
InstanceMigration::WriteText(stream, requestedBy, WorldFiles::MAX_BY);
|
||||
}
|
||||
|
||||
bool Deserialize(RakNet::BitStream& stream) override {
|
||||
if (!stream.Read(zoneId) || !stream.Read(warnSeconds) || !stream.Read(requesterId)) return false;
|
||||
if (warnSeconds > InstanceMigrationRequest::MAX_WARN_SECONDS) return false;
|
||||
return InstanceMigration::ReadText(stream, requestedBy, WorldFiles::MAX_BY);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* WORLD_FILES_STATUS (master -> dashboard): every zone that runs, the files its instances loaded as they are on disk
|
||||
* now, and which instances loaded an older version. Sent when something changes and when the dashboard connects.
|
||||
*/
|
||||
struct WorldFilesStatus : public LUBitStream {
|
||||
WorldFilesStatus() : LUBitStream(ServiceType::MASTER, MessageType::Master::WORLD_FILES_STATUS) {}
|
||||
|
||||
static constexpr uint16_t MAX_ZONES = 1024;
|
||||
static constexpr uint16_t MAX_INSTANCES = 1024;
|
||||
|
||||
struct File {
|
||||
ZoneFileLog::Entry disk; // as master last read it (packed: as the world reported it)
|
||||
bool hashed{}; // master has read it (not yet: disk is what a world reported)
|
||||
bool missing{}; // gone from disk
|
||||
bool changed{}; // an instance of the zone loaded another version
|
||||
|
||||
bool operator==(const File& other) const = default;
|
||||
};
|
||||
|
||||
struct Instance {
|
||||
uint32_t instanceId{};
|
||||
uint32_t cloneId{};
|
||||
int32_t players{};
|
||||
bool stale{}; // loaded a version of a file that isn't on disk any more
|
||||
bool reloading{}; // being replaced, or shutting down
|
||||
|
||||
bool operator==(const Instance& other) const = default;
|
||||
};
|
||||
|
||||
struct Zone {
|
||||
uint32_t zoneId{};
|
||||
std::vector<File> files;
|
||||
std::vector<Instance> instances;
|
||||
std::string message; // the last reload of the zone
|
||||
|
||||
bool operator==(const Zone& other) const = default;
|
||||
};
|
||||
|
||||
bool watching{};
|
||||
uint16_t watchSeconds{};
|
||||
bool seamless{};
|
||||
std::vector<Zone> zones;
|
||||
|
||||
void Serialize(RakNet::BitStream& stream) const override {
|
||||
stream.Write<uint8_t>(watching ? 1 : 0);
|
||||
stream.Write(watchSeconds);
|
||||
stream.Write<uint8_t>(seamless ? 1 : 0);
|
||||
const auto zoneCount = static_cast<uint16_t>(std::min<size_t>(zones.size(), MAX_ZONES));
|
||||
stream.Write(zoneCount);
|
||||
for (uint16_t z = 0; z < zoneCount; z++) {
|
||||
const auto& zone = zones[z];
|
||||
stream.Write(zone.zoneId);
|
||||
const auto fileCount = static_cast<uint16_t>(std::min<size_t>(zone.files.size(), WorldFiles::MAX_FILES));
|
||||
stream.Write(fileCount);
|
||||
for (uint16_t f = 0; f < fileCount; f++) {
|
||||
const auto& file = zone.files[f];
|
||||
WorldFiles::WriteFile(stream, file.disk);
|
||||
stream.Write<uint8_t>((file.hashed ? 1 : 0) | (file.missing ? 2 : 0) | (file.changed ? 4 : 0));
|
||||
}
|
||||
const auto instanceCount = static_cast<uint16_t>(std::min<size_t>(zone.instances.size(), MAX_INSTANCES));
|
||||
stream.Write(instanceCount);
|
||||
for (uint16_t i = 0; i < instanceCount; i++) {
|
||||
const auto& instance = zone.instances[i];
|
||||
stream.Write(instance.instanceId);
|
||||
stream.Write(instance.cloneId);
|
||||
stream.Write(instance.players);
|
||||
stream.Write<uint8_t>((instance.stale ? 1 : 0) | (instance.reloading ? 2 : 0));
|
||||
}
|
||||
InstanceMigration::WriteText(stream, zone.message, WorldFiles::MAX_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
bool Deserialize(RakNet::BitStream& stream) override {
|
||||
uint8_t watchingValue{}, seamlessValue{};
|
||||
uint16_t zoneCount{};
|
||||
if (!stream.Read(watchingValue) || !stream.Read(watchSeconds) || !stream.Read(seamlessValue) || !stream.Read(zoneCount)) return false;
|
||||
if (zoneCount > MAX_ZONES) return false;
|
||||
watching = watchingValue != 0;
|
||||
seamless = seamlessValue != 0;
|
||||
zones.assign(zoneCount, {});
|
||||
for (auto& zone : zones) {
|
||||
uint16_t fileCount{};
|
||||
if (!stream.Read(zone.zoneId) || !stream.Read(fileCount) || fileCount > WorldFiles::MAX_FILES) return false;
|
||||
zone.files.assign(fileCount, {});
|
||||
for (auto& file : zone.files) {
|
||||
uint8_t flags{};
|
||||
if (!WorldFiles::ReadFile(stream, file.disk) || !stream.Read(flags)) return false;
|
||||
file.hashed = flags & 1;
|
||||
file.missing = flags & 2;
|
||||
file.changed = flags & 4;
|
||||
}
|
||||
uint16_t instanceCount{};
|
||||
if (!stream.Read(instanceCount) || instanceCount > MAX_INSTANCES) return false;
|
||||
zone.instances.assign(instanceCount, {});
|
||||
for (auto& instance : zone.instances) {
|
||||
uint8_t flags{};
|
||||
if (!stream.Read(instance.instanceId) || !stream.Read(instance.cloneId) || !stream.Read(instance.players) || !stream.Read(flags)) return false;
|
||||
instance.stale = flags & 1;
|
||||
instance.reloading = flags & 2;
|
||||
}
|
||||
if (!InstanceMigration::ReadText(stream, zone.message, WorldFiles::MAX_MESSAGE)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif //!__WORLDFILES__H__
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "BinaryPathFinder.h"
|
||||
#include "FdbSnapshot.h"
|
||||
#include "master/CDClientReload.h"
|
||||
#include "master/WorldFiles.h"
|
||||
#include "dPlatforms.h"
|
||||
|
||||
//RakNet includes:
|
||||
@@ -717,6 +718,14 @@ int main(int argc, char** argv) {
|
||||
worldReady.instanceID = static_cast<LWOINSTANCEID>(Game::server->GetInstanceID());
|
||||
MasterPackets::SendToMaster(worldReady);
|
||||
|
||||
// The zone data files this world loaded; master watches them (docs/WorldHotReload.md)
|
||||
WorldFilesReport files;
|
||||
files.zoneId = Game::server->GetZoneID();
|
||||
files.instanceId = Game::server->GetInstanceID();
|
||||
files.cloneId = g_CloneID;
|
||||
files.files = ZoneFileLog::Entries();
|
||||
MasterPackets::SendToMaster(files);
|
||||
|
||||
ready = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: 44 enumerators
|
||||
// MessageType::Master: 47 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);
|
||||
@@ -1810,6 +1810,9 @@ 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);
|
||||
static_assert(static_cast<int64_t>(MessageType::Master::WORLD_FILES) == 47);
|
||||
static_assert(static_cast<int64_t>(MessageType::Master::WORLD_RELOAD) == 48);
|
||||
static_assert(static_cast<int64_t>(MessageType::Master::WORLD_FILES_STATUS) == 49);
|
||||
|
||||
// MessageType::Server: 3 enumerators
|
||||
static_assert(static_cast<int64_t>(MessageType::Server::VERSION_CONFIRM) == 0);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#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 "PacketDispatcher.h"
|
||||
@@ -572,3 +573,89 @@ TEST(MasterPacketsTests, CDClientReload) {
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user