diff --git a/dDashboardServer/routes/WorldReloadRoutes.cpp b/dDashboardServer/routes/WorldReloadRoutes.cpp index f135b267e..6763980ef 100644 --- a/dDashboardServer/routes/WorldReloadRoutes.cpp +++ b/dDashboardServer/routes/WorldReloadRoutes.cpp @@ -34,7 +34,7 @@ namespace { nlohmann::json instances = nlohmann::json::array(); for (const auto& instance : zone.instances) { instances.push_back({ {"instance", instance.instanceId}, {"clone", instance.cloneId}, {"players", instance.players}, - {"stale", instance.stale}, {"reloading", instance.reloading} }); + {"stale", instance.stale}, {"reloading", instance.reloading}, {"outdated", instance.outdated} }); } zones.push_back({ {"zone", zone.zoneId}, {"zoneName", GameText::ZoneName(zone.zoneId)}, {"changed", changed}, {"files", files}, {"instances", instances}, {"message", zone.message} }); diff --git a/dMasterServer/WorldFileWatch.h b/dMasterServer/WorldFileWatch.h index 83e0050e1..5cd5adf39 100644 --- a/dMasterServer/WorldFileWatch.h +++ b/dMasterServer/WorldFileWatch.h @@ -216,6 +216,9 @@ namespace WorldFileWatch { START_THEN_STOP, // nobody there, but the zone always has an instance: start a new one, stop this one STOP, // nobody there: stop it (a new instance starts when someone goes there) SKIP, + // A property with players: never moved (building in progress isn't saved). It takes nobody new, its players + // are told an update is waiting, and it stops once everyone left (OutdatedInstances.h) + KEEP_UNTIL_EMPTY, }; inline const char* ActionName(eAction action) { @@ -224,6 +227,7 @@ namespace WorldFileWatch { case eAction::START_THEN_STOP: return "start a new one, then stop it"; case eAction::STOP: return "stop"; case eAction::SKIP: return "skip"; + case eAction::KEEP_UNTIL_EMPTY: return "keep until everyone left"; } return "skip"; } @@ -235,8 +239,8 @@ namespace WorldFileWatch { }; /** - * What to do with each instance wanted() picks: replace it when players are there, stop it when it is empty (a zone - * in keepZones gets one new public instance first). Character selection loads no zone; instances still starting + * What to do with each instance wanted() picks: replace it when players are there (a property is kept until they + * all left instead), stop it when it is empty (a zone in keepZones gets one new public instance first). Character selection loads no zone; instances still starting * load the files on disk now (and report them); instances shutting down or being emptied already are left alone. */ inline std::vector Choose(const std::vector& instances, @@ -250,7 +254,7 @@ namespace WorldFileWatch { else if (view.shuttingDown) choice.reason = "Already shutting down"; else if (view.draining) choice.reason = "Its players are already being moved"; else if (!view.ready) choice.reason = "Still starting; it loads the files on disk now"; - else if (view.players > 0) choice.action = eAction::REPLACE; + else if (view.players > 0) choice.action = view.cloneId != 0 ? eAction::KEEP_UNTIL_EMPTY : eAction::REPLACE; else choice.action = eAction::STOP; choices.push_back(std::move(choice)); } diff --git a/dMasterServer/WorldReloader.cpp b/dMasterServer/WorldReloader.cpp index a76226812..a94f35b3f 100644 --- a/dMasterServer/WorldReloader.cpp +++ b/dMasterServer/WorldReloader.cpp @@ -105,11 +105,23 @@ namespace { for (const auto& choice : choices) { const auto& view = choice.view; auto& counts = perZone[view.zoneId]; + if (choice.action != eAction::SKIP) { + // On the old files: nobody new goes there (a request starts an instance on the files on disk now) + Game::im->MarkOutdated([&view](const Instance& instance) { + return instance.GetMapID() == view.zoneId && instance.GetInstanceID() == view.instanceId; + }); + } switch (choice.action) { case eAction::SKIP: LOG("World reload (%s): zone %u instance %u left alone: %s", by.c_str(), view.zoneId, view.instanceId, choice.reason.c_str()); counts.second++; continue; + case eAction::KEEP_UNTIL_EMPTY: + LOG("World reload (%s): property zone %u clone %u instance %u keeps its %d player(s); it takes nobody new and stops once empty", + by.c_str(), view.zoneId, view.cloneId, view.instanceId, view.players); + counts.first++; + acted++; + continue; case eAction::STOP: case eAction::START_THEN_STOP: { const auto& instance = Game::im->FindInstanceWithPrivate(static_cast(view.zoneId), static_cast(view.instanceId)); @@ -141,9 +153,8 @@ namespace { request.requesterId = requesterId; request.requestedBy = ("world reload (" + by + ")").substr(0, InstanceMigrationRequest::MAX_BY); MigrationCoordinator::Options options; - // Properties, private instances and activity zones are moved too; a property is saved and frozen first + // Private instances and activity zones are moved too (properties never get here: KEEP_UNTIL_EMPTY) options.liveUpdate = true; - options.prepare = view.cloneId != 0; g_Migrations[request.requestId] = { view.zoneId, view.instanceId }; const auto refusal = MigrationCoordinator::Start(request, options); if (refusal != InstanceMigration::eRefusal::NONE) { @@ -159,7 +170,7 @@ namespace { } } for (const auto& [zone, counts] : perZone) { - std::string message = "Reload (" + by + "): " + std::to_string(counts.first) + " instance(s) replaced or stopped"; + std::string message = "Reload (" + by + "): " + std::to_string(counts.first) + " instance(s) replaced, stopped or left to empty"; if (counts.second) message += ", " + std::to_string(counts.second) + " left alone"; SetZoneMessage(zone, message); } @@ -224,6 +235,7 @@ namespace { row.players = instance->GetCurrentClientCount(); row.stale = g_Tracker.IsStale({ zoneId, row.instanceId }); row.reloading = instance->GetIsShuttingDown() || instance->GetIsDraining(); + row.outdated = instance->GetIsOutdated(); zone.instances.push_back(row); } if (const auto message = g_ZoneMessages.find(zoneId); message != g_ZoneMessages.end()) zone.message = message->second; diff --git a/dNet/master/WorldFiles.h b/dNet/master/WorldFiles.h index 721196b06..9c8abd629 100644 --- a/dNet/master/WorldFiles.h +++ b/dNet/master/WorldFiles.h @@ -127,6 +127,7 @@ struct WorldFilesStatus : public LUBitStream { 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; }; @@ -168,7 +169,7 @@ struct WorldFilesStatus : public LUBitStream { stream.Write(instance.instanceId); stream.Write(instance.cloneId); stream.Write(instance.players); - stream.Write((instance.stale ? 1 : 0) | (instance.reloading ? 2 : 0)); + stream.Write((instance.stale ? 1 : 0) | (instance.reloading ? 2 : 0) | (instance.outdated ? 4 : 0)); } InstanceMigration::WriteText(stream, zone.message, WorldFiles::MAX_MESSAGE); } @@ -201,6 +202,7 @@ struct WorldFilesStatus : public LUBitStream { 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; } diff --git a/tests/dCommonTests/WorldFileWatchTests.cpp b/tests/dCommonTests/WorldFileWatchTests.cpp index 8f1c0c314..01a303900 100644 --- a/tests/dCommonTests/WorldFileWatchTests.cpp +++ b/tests/dCommonTests/WorldFileWatchTests.cpp @@ -193,7 +193,7 @@ TEST(WorldFileWatchTests, ChooseReplacesBusyAndStopsEmptyInstances) { View(1100, 1, 5), View(1100, 2, 0), View(1200, 3, 2), - View(1150, 4, 1, 77), // a property keeps its clone: replaced like any other + View(1150, 4, 1, 77), // a property with players: never moved, kept until they left View(0, 5, 3), // character selection }; auto starting = View(1100, 6, 0); @@ -212,7 +212,7 @@ TEST(WorldFileWatchTests, ChooseReplacesBusyAndStopsEmptyInstances) { EXPECT_EQ(byInstance.size(), 7u); // 1200 wasn't wanted EXPECT_EQ(byInstance[1], eAction::REPLACE); EXPECT_EQ(byInstance[2], eAction::STOP); - EXPECT_EQ(byInstance[4], eAction::REPLACE); + EXPECT_EQ(byInstance[4], eAction::KEEP_UNTIL_EMPTY); EXPECT_EQ(byInstance[5], eAction::SKIP); EXPECT_EQ(byInstance[6], eAction::SKIP); EXPECT_EQ(byInstance[7], eAction::SKIP);