mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-11-14 07:47:13 +00:00
feat(master): an edit mode flag on a private instance, down to its world server
CreatePrivateZone carries editMode as one more byte, written only when set, so a plain private zone is sent as before. Master keeps it on the instance, starts the world server with -editmode 1, and gives a private instance's replacement (a reload or a live update) the same flag. Nothing sets it yet. A capped string now skips the characters it cut, so what follows it is read from the right place. A request to join a private instance that is still starting is answered once its world server is ready, instead of at once with an address nothing listens on yet. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -387,7 +387,7 @@ const InstancePtr& InstanceManager::FindInstanceWithPrivate(LWOMAPID mapID, LWOI
|
|||||||
return g_Empty;
|
return g_Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InstancePtr& InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID cloneID, const std::string& password) {
|
const InstancePtr& InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID cloneID, const std::string& password, const bool editMode) {
|
||||||
const auto& instance = FindPrivateInstance(password);
|
const auto& instance = FindPrivateInstance(password);
|
||||||
|
|
||||||
if (instance != nullptr) {
|
if (instance != nullptr) {
|
||||||
@@ -408,9 +408,10 @@ const InstancePtr& InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLO
|
|||||||
|
|
||||||
uint32_t port = GetFreePort();
|
uint32_t port = GetFreePort();
|
||||||
auto newInstance = make_unique<Instance>(mExternalIP, port, mapID, ++m_LastInstanceID, cloneID, maxPlayers, maxPlayers, true, password);
|
auto newInstance = make_unique<Instance>(mExternalIP, port, mapID, ++m_LastInstanceID, cloneID, maxPlayers, maxPlayers, true, password);
|
||||||
|
newInstance->SetIsEditMode(editMode);
|
||||||
|
|
||||||
//Start the actual process:
|
//Start the actual process:
|
||||||
StartWorldServer(mapID, port, m_LastInstanceID, maxPlayers, cloneID);
|
StartWorldServer(mapID, port, m_LastInstanceID, maxPlayers, cloneID, editMode);
|
||||||
|
|
||||||
m_Instances.push_back(std::move(newInstance));
|
m_Instances.push_back(std::move(newInstance));
|
||||||
if (m_OnInstancesChanged) m_OnInstancesChanged();
|
if (m_OnInstancesChanged) m_OnInstancesChanged();
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ public:
|
|||||||
|
|
||||||
bool GetIsPrivate() const { return m_IsPrivate; }
|
bool GetIsPrivate() const { return m_IsPrivate; }
|
||||||
const std::string& GetPassword() const { return m_Password; }
|
const std::string& GetPassword() const { return m_Password; }
|
||||||
|
// A GM's edit mode instance (docs/EditMode.md): private, and its world server constructs none of the zone's
|
||||||
|
// objects. Its replacement (a reload, a live update) is in edit mode too
|
||||||
|
bool GetIsEditMode() const { return m_IsEditMode; }
|
||||||
|
void SetIsEditMode(bool value) { m_IsEditMode = value; }
|
||||||
|
|
||||||
bool GetIsReady() const { return m_Ready; }
|
bool GetIsReady() const { return m_Ready; }
|
||||||
void SetIsReady(bool value) { m_Ready = value; }
|
void SetIsReady(bool value) { m_Ready = value; }
|
||||||
@@ -156,6 +160,7 @@ private:
|
|||||||
|
|
||||||
bool m_IsPrivate{};
|
bool m_IsPrivate{};
|
||||||
std::string m_Password{};
|
std::string m_Password{};
|
||||||
|
bool m_IsEditMode{};
|
||||||
|
|
||||||
bool m_Shutdown{};
|
bool m_Shutdown{};
|
||||||
|
|
||||||
@@ -194,7 +199,8 @@ public:
|
|||||||
const InstancePtr& FindInstance(LWOMAPID mapID, LWOINSTANCEID instanceID);
|
const InstancePtr& FindInstance(LWOMAPID mapID, LWOINSTANCEID instanceID);
|
||||||
const InstancePtr& FindInstanceWithPrivate(LWOMAPID mapID, LWOINSTANCEID instanceID);
|
const InstancePtr& FindInstanceWithPrivate(LWOMAPID mapID, LWOINSTANCEID instanceID);
|
||||||
|
|
||||||
const InstancePtr& CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID cloneID, const std::string& password);
|
// editMode: a GM's edit mode instance. An instance that has the password already is returned as it is
|
||||||
|
const InstancePtr& CreatePrivateInstance(LWOMAPID mapID, LWOCLONEID cloneID, const std::string& password, bool editMode = false);
|
||||||
const InstancePtr& FindPrivateInstance(const std::string& password);
|
const InstancePtr& FindPrivateInstance(const std::string& password);
|
||||||
void SetIsShuttingDown(bool value) { this->m_IsShuttingDown = value; };
|
void SetIsShuttingDown(bool value) { this->m_IsShuttingDown = value; };
|
||||||
void PruneUnreadyInstances();
|
void PruneUnreadyInstances();
|
||||||
|
|||||||
@@ -893,12 +893,12 @@ namespace {
|
|||||||
|
|
||||||
void OnCreatePrivateZone(const CreatePrivateZone& request, const SystemAddress& sysAddr) {
|
void OnCreatePrivateZone(const CreatePrivateZone& request, const SystemAddress& sysAddr) {
|
||||||
// Passwords were cut to 50 characters when read
|
// Passwords were cut to 50 characters when read
|
||||||
const auto& newInst = Game::im->CreatePrivateInstance(request.zoneID, request.cloneID, request.password.c_str());
|
const auto& newInst = Game::im->CreatePrivateInstance(request.zoneID, request.cloneID, request.password.c_str(), request.editMode != 0);
|
||||||
if (!newInst) {
|
if (!newInst) {
|
||||||
LOG("No private zone was created for map %i/%i", request.zoneID, request.cloneID);
|
LOG("No private zone was created for map %i/%i", request.zoneID, request.cloneID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LOG("Creating private zone %i/%i/%i", newInst->GetMapID(), newInst->GetCloneID(), newInst->GetInstanceID());
|
LOG("Creating private zone %i/%i/%i%s", newInst->GetMapID(), newInst->GetCloneID(), newInst->GetInstanceID(), newInst->GetIsEditMode() ? " in edit mode" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnRequestPrivateZone(const RequestPrivateZone& request, const SystemAddress& sysAddr) {
|
void OnRequestPrivateZone(const RequestPrivateZone& request, const SystemAddress& sysAddr) {
|
||||||
@@ -915,6 +915,12 @@ namespace {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Still starting (asked for right after it was created): answered when its world server is ready
|
||||||
|
if (!instance->GetIsReady()) {
|
||||||
|
instance->GetPendingRequests().push_back({ requestID, static_cast<bool>(mythranShift), sysAddr, {} });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& zone = instance->GetZoneID();
|
const auto& zone = instance->GetZoneID();
|
||||||
|
|
||||||
RequestZoneTransferResponse response;
|
RequestZoneTransferResponse response;
|
||||||
|
|||||||
@@ -171,11 +171,11 @@ void MigrationCoordinator::SetObserver(std::function<void(const MigrationStatus&
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// The new instance (REPLACE): a fresh world server from the binary on disk now, so an update takes effect. A private
|
// The new instance (REPLACE): a fresh world server from the binary on disk now, so an update takes effect. A private
|
||||||
// instance's replacement gets the same password; the source is draining already, so the password finds the new one.
|
// instance's replacement gets the same password and edit mode; the source is draining already, so the password finds the new one.
|
||||||
Instance* StartTarget(Instance& source) {
|
Instance* StartTarget(Instance& source) {
|
||||||
if (source.GetIsPrivate()) {
|
if (source.GetIsPrivate()) {
|
||||||
source.SetIsDraining(true);
|
source.SetIsDraining(true);
|
||||||
const auto& started = Game::im->CreatePrivateInstance(source.GetMapID(), source.GetCloneID(), source.GetPassword());
|
const auto& started = Game::im->CreatePrivateInstance(source.GetMapID(), source.GetCloneID(), source.GetPassword(), source.GetIsEditMode());
|
||||||
return started ? started.get() : nullptr;
|
return started ? started.get() : nullptr;
|
||||||
}
|
}
|
||||||
const auto& started = Game::im->StartNewInstance(source.GetMapID(), source.GetCloneID());
|
const auto& started = Game::im->StartNewInstance(source.GetMapID(), source.GetCloneID());
|
||||||
|
|||||||
@@ -192,13 +192,13 @@ uint32_t StartUgcServer() {
|
|||||||
return ugc_pid;
|
return ugc_pid;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID) {
|
uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID, const bool editMode) {
|
||||||
auto world_path = BinaryPathFinder::GetBinaryDir() / "WorldServer";
|
auto world_path = BinaryPathFinder::GetBinaryDir() / "WorldServer";
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
world_path.replace_extension(".exe");
|
world_path.replace_extension(".exe");
|
||||||
auto cmd = world_path.wstring() + L" -zone " + std::to_wstring(mapID) + L" -port " + std::to_wstring(port) +
|
auto cmd = world_path.wstring() + L" -zone " + std::to_wstring(mapID) + L" -port " + std::to_wstring(port) +
|
||||||
L" -instance " + std::to_wstring(lastInstanceID) + L" -maxclients " + std::to_wstring(maxPlayers) +
|
L" -instance " + std::to_wstring(lastInstanceID) + L" -maxclients " + std::to_wstring(maxPlayers) +
|
||||||
L" -clone " + std::to_wstring(cloneID);
|
L" -clone " + std::to_wstring(cloneID) + L" -editmode " + std::to_wstring(editMode ? 1 : 0);
|
||||||
|
|
||||||
auto world_startup = startup;
|
auto world_startup = startup;
|
||||||
auto world_info = PROCESS_INFORMATION{};
|
auto world_info = PROCESS_INFORMATION{};
|
||||||
@@ -226,7 +226,8 @@ uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInsta
|
|||||||
"-port", std::to_string(port).c_str(),
|
"-port", std::to_string(port).c_str(),
|
||||||
"-instance", std::to_string(lastInstanceID).c_str(),
|
"-instance", std::to_string(lastInstanceID).c_str(),
|
||||||
"-maxclients", std::to_string(maxPlayers).c_str(),
|
"-maxclients", std::to_string(maxPlayers).c_str(),
|
||||||
"-clone", std::to_string(cloneID).c_str(), nullptr);
|
"-clone", std::to_string(cloneID).c_str(),
|
||||||
|
"-editmode", editMode ? "1" : "0", nullptr);
|
||||||
// exec failed (e.g. the binary is being rebuilt): the child must not carry on as a second master
|
// exec failed (e.g. the binary is being rebuilt): the child must not carry on as a second master
|
||||||
perror("exec");
|
perror("exec");
|
||||||
_exit(127);
|
_exit(127);
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ uint32_t StartAuthServer();
|
|||||||
uint32_t StartChatServer();
|
uint32_t StartChatServer();
|
||||||
uint32_t StartDashboardServer();
|
uint32_t StartDashboardServer();
|
||||||
uint32_t StartUgcServer();
|
uint32_t StartUgcServer();
|
||||||
uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID);
|
uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInstanceID, int maxPlayers, LWOCLONEID cloneID, bool editMode = false);
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ namespace {
|
|||||||
uint32_t length{};
|
uint32_t length{};
|
||||||
VALIDATE_READ(bitStream.Read(length));
|
VALIDATE_READ(bitStream.Read(length));
|
||||||
value.resize(std::min(length, maxLength));
|
value.resize(std::min(length, maxLength));
|
||||||
return value.empty() || bitStream.ReadBits(reinterpret_cast<unsigned char*>(value.data()), BYTES_TO_BITS(value.size()), true);
|
if (!value.empty() && !bitStream.ReadBits(reinterpret_cast<unsigned char*>(value.data()), BYTES_TO_BITS(value.size()), true)) return false;
|
||||||
|
const uint32_t rest = length - static_cast<uint32_t>(value.size());
|
||||||
|
if (rest == 0) return true;
|
||||||
|
if (BITS_TO_BYTES(bitStream.GetNumberOfUnreadBits()) < rest) return false;
|
||||||
|
bitStream.IgnoreBits(BYTES_TO_BITS(rest));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,12 +152,15 @@ namespace MasterPackets {
|
|||||||
bitStream.Write(zoneID);
|
bitStream.Write(zoneID);
|
||||||
bitStream.Write(cloneID);
|
bitStream.Write(cloneID);
|
||||||
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, password);
|
BitStreamUtils::WriteLengthPrefixed<uint32_t>(bitStream, password);
|
||||||
|
if (editMode != 0) bitStream.Write(editMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CreatePrivateZone::Deserialize(RakNet::BitStream& bitStream) {
|
bool CreatePrivateZone::Deserialize(RakNet::BitStream& bitStream) {
|
||||||
VALIDATE_READ(bitStream.Read(zoneID));
|
VALIDATE_READ(bitStream.Read(zoneID));
|
||||||
VALIDATE_READ(bitStream.Read(cloneID));
|
VALIDATE_READ(bitStream.Read(cloneID));
|
||||||
VALIDATE_READ(ReadCappedString(bitStream, password, MAX_PASSWORD_LENGTH));
|
VALIDATE_READ(ReadCappedString(bitStream, password, MAX_PASSWORD_LENGTH));
|
||||||
|
editMode = 0;
|
||||||
|
if (bitStream.GetNumberOfUnreadBits() >= 8) VALIDATE_READ(bitStream.Read(editMode));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,9 @@ namespace MasterPackets {
|
|||||||
uint32_t zoneID{};
|
uint32_t zoneID{};
|
||||||
uint32_t cloneID{};
|
uint32_t cloneID{};
|
||||||
std::string password; // u32 length, then 1 byte per character
|
std::string password; // u32 length, then 1 byte per character
|
||||||
|
// A GM's edit mode: the instance's world server constructs none of the zone's objects (docs/EditMode.md). Written
|
||||||
|
// only when set, so a plain private zone stays as it always was
|
||||||
|
uint8_t editMode{};
|
||||||
|
|
||||||
CreatePrivateZone() : LUBitStream(ServiceType::MASTER, MessageType::Master::CREATE_PRIVATE_ZONE) {}
|
CreatePrivateZone() : LUBitStream(ServiceType::MASTER, MessageType::Master::CREATE_PRIVATE_ZONE) {}
|
||||||
void Serialize(RakNet::BitStream& bitStream) const override;
|
void Serialize(RakNet::BitStream& bitStream) const override;
|
||||||
|
|||||||
@@ -1009,6 +1009,7 @@ json ToJson(const MasterPackets::CreatePrivateZone& m) {
|
|||||||
json j = json::object();
|
json j = json::object();
|
||||||
j["zoneID"] = ToJson(m.zoneID);
|
j["zoneID"] = ToJson(m.zoneID);
|
||||||
j["cloneID"] = ToJson(m.cloneID);
|
j["cloneID"] = ToJson(m.cloneID);
|
||||||
|
j["editMode"] = ToJson(m.editMode);
|
||||||
return j;
|
return j;
|
||||||
}
|
}
|
||||||
json ToJson(const MasterPackets::RequestPrivateZone& m) {
|
json ToJson(const MasterPackets::RequestPrivateZone& m) {
|
||||||
|
|||||||
@@ -35,11 +35,12 @@ void ZoneInstanceManager::HandleRequestZoneTransferResponse(const MasterPackets:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ZoneInstanceManager::CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password) {
|
void ZoneInstanceManager::CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password, const bool editMode) {
|
||||||
MasterPackets::CreatePrivateZone request;
|
MasterPackets::CreatePrivateZone request;
|
||||||
request.zoneID = zoneID;
|
request.zoneID = zoneID;
|
||||||
request.cloneID = zoneClone;
|
request.cloneID = zoneClone;
|
||||||
request.password = password;
|
request.password = password;
|
||||||
|
request.editMode = editMode ? 1 : 0;
|
||||||
MasterPackets::SendToMaster(request, server);
|
MasterPackets::SendToMaster(request, server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ public:
|
|||||||
*/
|
*/
|
||||||
void HandleRequestZoneTransferResponse(const MasterPackets::RequestZoneTransferResponse& response);
|
void HandleRequestZoneTransferResponse(const MasterPackets::RequestZoneTransferResponse& response);
|
||||||
|
|
||||||
void CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password);
|
// editMode: a GM's edit mode instance, whose world server constructs none of the zone's objects
|
||||||
|
void CreatePrivateZone(dServer* server, uint32_t zoneID, uint32_t zoneClone, const std::string& password, bool editMode = false);
|
||||||
|
|
||||||
void RequestPrivateZone(dServer* server, bool mythranShift, const std::string& password, TransferCallback callback);
|
void RequestPrivateZone(dServer* server, bool mythranShift, const std::string& password, TransferCallback callback);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -237,6 +237,32 @@ TEST(MasterPacketsTests, PrivateZonesMatchLegacy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The edit mode flag of a private zone: one more byte, only when set, and read after a password of any length
|
||||||
|
TEST(MasterPacketsTests, CreatePrivateZoneEditMode) {
|
||||||
|
for (const auto& password : g_Strings) {
|
||||||
|
MasterPackets::CreatePrivateZone plain;
|
||||||
|
plain.zoneID = 1100;
|
||||||
|
plain.password = password;
|
||||||
|
MasterPackets::CreatePrivateZone edit = plain;
|
||||||
|
edit.editMode = 1;
|
||||||
|
|
||||||
|
RakNet::BitStream plainStream; LoadPayload(plainStream, plain);
|
||||||
|
RakNet::BitStream editStream; LoadPayload(editStream, edit);
|
||||||
|
EXPECT_EQ(editStream.GetNumberOfUnreadBits(), plainStream.GetNumberOfUnreadBits() + 8);
|
||||||
|
|
||||||
|
MasterPackets::CreatePrivateZone readPlain;
|
||||||
|
readPlain.editMode = 1;
|
||||||
|
ASSERT_TRUE(readPlain.Deserialize(plainStream));
|
||||||
|
EXPECT_EQ(readPlain.editMode, 0);
|
||||||
|
|
||||||
|
MasterPackets::CreatePrivateZone readEdit;
|
||||||
|
ASSERT_TRUE(readEdit.Deserialize(editStream));
|
||||||
|
EXPECT_EQ(readEdit.editMode, 1);
|
||||||
|
EXPECT_EQ(readEdit.zoneID, 1100u);
|
||||||
|
EXPECT_EQ(readEdit.password, password.substr(0, MasterPackets::CreatePrivateZone::MAX_PASSWORD_LENGTH));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MasterPacketsTests, WorldStateMatchesLegacy) {
|
TEST(MasterPacketsTests, WorldStateMatchesLegacy) {
|
||||||
for (const LWOMAPID zone : { static_cast<LWOMAPID>(0), static_cast<LWOMAPID>(1100), static_cast<LWOMAPID>(0xFFFF) }) {
|
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) }) {
|
for (const LWOINSTANCEID instance : { static_cast<LWOINSTANCEID>(0), static_cast<LWOINSTANCEID>(3), static_cast<LWOINSTANCEID>(0xFFFF) }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user