Files
DarkflameServer/dNet/master/WorldFiles.h
Aaron Kimbrell e714ff2f0a feat(master): the world file watch follows the override folder, and can hold a zone
For a file under maps/ master watches the file a world would read now: the override
when one is there, else the client's. An override that appears, changes or goes away
makes the instances that read another version stale, also for files from the packs.
The dashboard can hold a zone's automatic reloads (WORLD_RELOAD_HOLD, a new message
after the existing ones) until a reload of the zone is asked for. The status sent to
the dashboard counts what reloads did per zone.

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

250 lines
9.4 KiB
C++

#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_RELOAD_HOLD (dashboard -> master): while hold is set, master does not replace zoneId's instances by itself
* when their files change (a set of edited files is in place and its reload waits for someone to say so). A
* WORLD_RELOAD of the zone still reloads it, and ends the hold.
*/
struct WorldReloadHold : public LUBitStream {
WorldReloadHold() : LUBitStream(ServiceType::MASTER, MessageType::Master::WORLD_RELOAD_HOLD) {}
uint32_t zoneId{};
bool hold{};
void Serialize(RakNet::BitStream& stream) const override {
stream.Write(zoneId);
stream.Write<uint8_t>(hold ? 1 : 0);
}
bool Deserialize(RakNet::BitStream& stream) override {
uint8_t value{};
if (!stream.Read(zoneId) || !stream.Read(value)) return false;
hold = value != 0;
return true;
}
};
/**
* 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 outdated{}; // takes nobody new and stops once empty (a property kept until its players left)
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
// Since master started (or the zone last had no instance), so a reader can tell what one request did
uint32_t requests{}; // reloads asked for (WORLD_RELOAD)
uint32_t acted{}; // instances replaced, stopped or left to empty
uint32_t playersMoved{};
uint32_t failed{}; // instances that could not be replaced (message says why)
uint16_t pending{}; // instances being replaced right now
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) | (instance.outdated ? 4 : 0));
}
InstanceMigration::WriteText(stream, zone.message, WorldFiles::MAX_MESSAGE);
stream.Write(zone.requests);
stream.Write(zone.acted);
stream.Write(zone.playersMoved);
stream.Write(zone.failed);
stream.Write(zone.pending);
}
}
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;
instance.outdated = flags & 4;
}
if (!InstanceMigration::ReadText(stream, zone.message, WorldFiles::MAX_MESSAGE)) return false;
if (!stream.Read(zone.requests) || !stream.Read(zone.acted) || !stream.Read(zone.playersMoved) || !stream.Read(zone.failed) || !stream.Read(zone.pending)) return false;
}
return true;
}
};
#endif //!__WORLDFILES__H__