From 4cf0909d52ad3935333d7ed8b235411eeca202ac Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 10 Nov 2026 17:34:13 -0600 Subject: [PATCH] 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 --- dMasterServer/InstanceManager.cpp | 5 ++-- dMasterServer/InstanceManager.h | 8 +++++- dMasterServer/MasterServer.cpp | 10 +++++-- dMasterServer/MigrationCoordinator.cpp | 4 +-- dMasterServer/Start.cpp | 7 ++--- dMasterServer/Start.h | 2 +- dNet/MasterPackets.cpp | 10 ++++++- dNet/MasterPackets.h | 3 +++ dNet/PacketFields.inc | 1 + dNet/ZoneInstanceManager.cpp | 3 ++- dNet/ZoneInstanceManager.h | 3 ++- .../dNetTests/MasterPacketsTests.cpp | 26 +++++++++++++++++++ 12 files changed, 68 insertions(+), 14 deletions(-) diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index d68f6e53f..f2c80905c 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -387,7 +387,7 @@ const InstancePtr& InstanceManager::FindInstanceWithPrivate(LWOMAPID mapID, LWOI 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); if (instance != nullptr) { @@ -408,9 +408,10 @@ const InstancePtr& InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLO uint32_t port = GetFreePort(); auto newInstance = make_unique(mExternalIP, port, mapID, ++m_LastInstanceID, cloneID, maxPlayers, maxPlayers, true, password); + newInstance->SetIsEditMode(editMode); //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)); if (m_OnInstancesChanged) m_OnInstancesChanged(); diff --git a/dMasterServer/InstanceManager.h b/dMasterServer/InstanceManager.h index f25a4129c..c9d2aa9d4 100644 --- a/dMasterServer/InstanceManager.h +++ b/dMasterServer/InstanceManager.h @@ -58,6 +58,10 @@ public: bool GetIsPrivate() const { return m_IsPrivate; } 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; } void SetIsReady(bool value) { m_Ready = value; } @@ -156,6 +160,7 @@ private: bool m_IsPrivate{}; std::string m_Password{}; + bool m_IsEditMode{}; bool m_Shutdown{}; @@ -194,7 +199,8 @@ public: const InstancePtr& FindInstance(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); void SetIsShuttingDown(bool value) { this->m_IsShuttingDown = value; }; void PruneUnreadyInstances(); diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index e7d60ad0c..e9f48e91e 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -893,12 +893,12 @@ namespace { void OnCreatePrivateZone(const CreatePrivateZone& request, const SystemAddress& sysAddr) { // 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) { LOG("No private zone was created for map %i/%i", request.zoneID, request.cloneID); 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) { @@ -915,6 +915,12 @@ namespace { 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(mythranShift), sysAddr, {} }); + return; + } + const auto& zone = instance->GetZoneID(); RequestZoneTransferResponse response; diff --git a/dMasterServer/MigrationCoordinator.cpp b/dMasterServer/MigrationCoordinator.cpp index d60291607..cf7900ffa 100644 --- a/dMasterServer/MigrationCoordinator.cpp +++ b/dMasterServer/MigrationCoordinator.cpp @@ -171,11 +171,11 @@ void MigrationCoordinator::SetObserver(std::functionCreatePrivateInstance(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; } const auto& started = Game::im->StartNewInstance(source.GetMapID(), source.GetCloneID()); diff --git a/dMasterServer/Start.cpp b/dMasterServer/Start.cpp index 024abb005..136a98f29 100644 --- a/dMasterServer/Start.cpp +++ b/dMasterServer/Start.cpp @@ -192,13 +192,13 @@ uint32_t StartUgcServer() { 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"; #ifdef _WIN32 world_path.replace_extension(".exe"); 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" -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_info = PROCESS_INFORMATION{}; @@ -226,7 +226,8 @@ uint32_t StartWorldServer(LWOMAPID mapID, uint16_t port, LWOINSTANCEID lastInsta "-port", std::to_string(port).c_str(), "-instance", std::to_string(lastInstanceID).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 perror("exec"); _exit(127); diff --git a/dMasterServer/Start.h b/dMasterServer/Start.h index 08c02096c..f56b3251f 100644 --- a/dMasterServer/Start.h +++ b/dMasterServer/Start.h @@ -5,4 +5,4 @@ uint32_t StartAuthServer(); uint32_t StartChatServer(); uint32_t StartDashboardServer(); 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); diff --git a/dNet/MasterPackets.cpp b/dNet/MasterPackets.cpp index 42279ac7d..c0c5fd4ca 100644 --- a/dNet/MasterPackets.cpp +++ b/dNet/MasterPackets.cpp @@ -16,7 +16,12 @@ namespace { uint32_t length{}; VALIDATE_READ(bitStream.Read(length)); value.resize(std::min(length, maxLength)); - return value.empty() || bitStream.ReadBits(reinterpret_cast(value.data()), BYTES_TO_BITS(value.size()), true); + if (!value.empty() && !bitStream.ReadBits(reinterpret_cast(value.data()), BYTES_TO_BITS(value.size()), true)) return false; + const uint32_t rest = length - static_cast(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(cloneID); BitStreamUtils::WriteLengthPrefixed(bitStream, password); + if (editMode != 0) bitStream.Write(editMode); } bool CreatePrivateZone::Deserialize(RakNet::BitStream& bitStream) { VALIDATE_READ(bitStream.Read(zoneID)); VALIDATE_READ(bitStream.Read(cloneID)); VALIDATE_READ(ReadCappedString(bitStream, password, MAX_PASSWORD_LENGTH)); + editMode = 0; + if (bitStream.GetNumberOfUnreadBits() >= 8) VALIDATE_READ(bitStream.Read(editMode)); return true; } diff --git a/dNet/MasterPackets.h b/dNet/MasterPackets.h index b08c0957b..605c47cd4 100644 --- a/dNet/MasterPackets.h +++ b/dNet/MasterPackets.h @@ -146,6 +146,9 @@ namespace MasterPackets { uint32_t zoneID{}; uint32_t cloneID{}; 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) {} void Serialize(RakNet::BitStream& bitStream) const override; diff --git a/dNet/PacketFields.inc b/dNet/PacketFields.inc index c7903f44f..c3cd114c5 100644 --- a/dNet/PacketFields.inc +++ b/dNet/PacketFields.inc @@ -1009,6 +1009,7 @@ json ToJson(const MasterPackets::CreatePrivateZone& m) { json j = json::object(); j["zoneID"] = ToJson(m.zoneID); j["cloneID"] = ToJson(m.cloneID); + j["editMode"] = ToJson(m.editMode); return j; } json ToJson(const MasterPackets::RequestPrivateZone& m) { diff --git a/dNet/ZoneInstanceManager.cpp b/dNet/ZoneInstanceManager.cpp index aca3e2356..d1cea1add 100644 --- a/dNet/ZoneInstanceManager.cpp +++ b/dNet/ZoneInstanceManager.cpp @@ -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; request.zoneID = zoneID; request.cloneID = zoneClone; request.password = password; + request.editMode = editMode ? 1 : 0; MasterPackets::SendToMaster(request, server); } diff --git a/dNet/ZoneInstanceManager.h b/dNet/ZoneInstanceManager.h index a12b47435..e7ff62b1f 100644 --- a/dNet/ZoneInstanceManager.h +++ b/dNet/ZoneInstanceManager.h @@ -62,7 +62,8 @@ public: */ 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); }; diff --git a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp index 6ddcdb6b0..b420cf32b 100644 --- a/tests/dGameTests/dNetTests/MasterPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/MasterPacketsTests.cpp @@ -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) { for (const LWOMAPID zone : { static_cast(0), static_cast(1100), static_cast(0xFFFF) }) { for (const LWOINSTANCEID instance : { static_cast(0), static_cast(3), static_cast(0xFFFF) }) {