feat(dashboard): draining (old version) badge; properties update when empty

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-30 04:03:22 -05:00
parent d71e44f3e1
commit 4c93c8fea7
5 changed files with 18 additions and 7 deletions

View File

@@ -229,6 +229,7 @@ namespace {
info.ip = instance.ip.string;
info.port = instance.port;
info.isPrivate = instance.isPrivate != 0;
info.outdated = instance.outdated != 0;
info.zoneName = GameText::ZoneName(info.mapID);
AddPropertyDetails(info);
using eState = MasterPackets::ServerListResponse::eState;

View File

@@ -253,13 +253,13 @@ namespace LiveWorld {
for (const auto& w : ServerState::g_WorldInstances) {
worlds.push_back({ {"mapID", w.mapID}, {"zoneName", GameText::ZoneName(w.mapID)}, {"instanceID", w.instanceID}, {"cloneID", w.cloneID}, {"players", w.players},
{"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName},
{"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state} });
{"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state}, {"outdated", w.outdated} });
}
// Launched but not connected yet, or shutting down
for (const auto& w : ServerState::g_PendingWorlds) {
worlds.push_back({ {"mapID", w.mapID}, {"zoneName", GameText::ZoneName(w.mapID)}, {"instanceID", w.instanceID}, {"cloneID", w.cloneID}, {"players", 0},
{"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName},
{"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state} });
{"ownerId", w.ownerId}, {"ownerName", w.ownerName}, {"state", w.state}, {"outdated", w.outdated} });
}
JsonSuccess(reply, { {"worlds", worlds} });
});

View File

@@ -42,6 +42,8 @@ struct WorldInstanceInfo {
// "starting" (launched, not connected yet) or "stopping" (in g_PendingWorlds), or "draining" for a world that is up
// but whose players are being moved to a new instance (in g_WorldInstances); empty for one that is just up
std::string state{};
// On an old binary or old zone files (a live update or world reload): takes nobody new and stops once empty
bool outdated{false};
};
// Where a server listens and the machine it runs on, from master's server list
@@ -88,7 +90,8 @@ namespace ServerState {
{"players", world.players},
{"isPrivate", world.isPrivate},
{"zoneName", world.zoneName},
{"state", world.state}
{"state", world.state},
{"outdated", world.outdated}
});
}

View File

@@ -60,8 +60,9 @@
<p class="small text-body-secondary">Moves everything onto the server binaries on disk now, without a restart: the UGC server finishes its jobs
and restarts, auth restarts (logins pause for a few seconds), chat hands its teams over and restarts, every world instance is
replaced by a new one and its players are moved there (the game's Mythran maintenance warning, then a short loading screen), and
the dashboard restarts last. Properties are saved before their new instance loads them; races and minigames finish first.
Master keeps running: updating master itself takes a normal restart.</p>
the dashboard restarts last. Races and minigames finish first. <strong>Properties are not moved</strong> (building in progress
isn't saved): their players are told an update is waiting, and a property updates once everyone has left it. Instances on the old
build take nobody new. Master keeps running: updating master itself takes a normal restart.</p>
<div class="d-flex flex-wrap gap-2 align-items-center mb-2" id="liveUpdateForm">
<div class="input-group input-group-sm" style="max-width: 20rem">
<span class="input-group-text">Warn players</span>
@@ -214,6 +215,9 @@ $(function () {
if (row.state === 'starting') return ' ' + fmt.badge('Starting', 'info');
if (row.state === 'stopping') return ' ' + fmt.badge('Shutting down', 'secondary');
if (row.state === 'draining') return ' ' + fmt.badge('Moving players', 'warning');
// Old binary or old zone files: nobody new goes there; it stops once the players still there have left
if (row.outdated) return ' ' + fmt.badge('Draining (old version)', 'warning') + ' <span class="small text-body-secondary text-nowrap">' +
Number(row.players || 0).toLocaleString() + ' player(s) still there</span>';
return '';
}
// Zone names in the viewer's language (the live status carries the server's)

View File

@@ -90,7 +90,9 @@
<div class="card-body">
<p class="small text-body-secondary">The zone files each running world loaded (<code>.luz</code>, <code>.lvl</code>, <code>.lutriggers</code>, terrain,
navmesh). When one changes on disk, the master server replaces the instances that loaded the old version with new ones and moves their
players over. <strong>Reload</strong> does the same for every instance of a zone now. Files read from the client's packs are not watched.</p>
players over. <strong>Reload</strong> does the same for every instance of a zone now. <strong>Properties are not moved</strong> (building in
progress isn't saved): an instance of a property on old files takes nobody new, its players are told an update is waiting, and it updates
once everyone has left. Files read from the client's packs are not watched.</p>
<div id="worldFilesZones"><p class="text-body-secondary mb-0">Waiting for the master server&hellip;</p></div>
</div>
</div>
@@ -257,7 +259,8 @@
(z.changed ? ' ' + fmt.badge('changed on disk', 'warning') : '') + '</h6>' +
'<button type="button" class="btn btn-outline-primary btn-sm" data-reload-zone="' + esc(z.zone) + '">Reload</button></div>';
var instances = '<div class="small mb-1">' + z.instances.map(function (i) {
var badges = (i.stale ? ' ' + fmt.badge('old files', 'warning') : '') + (i.reloading ? ' ' + fmt.badge('being replaced', 'info') : '');
var badges = (i.stale ? ' ' + fmt.badge('old files', 'warning') : '') + (i.reloading ? ' ' + fmt.badge('being replaced', 'info') :
i.outdated ? ' ' + fmt.badge(i.clone ? 'updates when empty' : 'draining (old version)', 'secondary') : '');
return '<span class="me-3 text-nowrap">Instance ' + esc(i.instance) + (i.clone ? ' (clone ' + esc(i.clone) + ')' : '') + ', ' +
nf.format(i.players) + ' player(s)' + badges + '</span>';
}).join('') + '</div>';