feat(world-reload): keep properties until empty

Busy property instances on changed files are kept (KEEP_UNTIL_EMPTY) instead
of saved, frozen and moved. Every instance acted on is marked outdated; the
files status carries the flag (bit 4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-30 04:03:19 -05:00
parent 891f21daa9
commit d71e44f3e1
5 changed files with 28 additions and 10 deletions

View File

@@ -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<Choice> Choose(const std::vector<InstanceMigration::InstanceView>& 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));
}

View File

@@ -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<LWOMAPID>(view.zoneId), static_cast<LWOINSTANCEID>(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;