mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
feat: worlds starting up on the dashboard; the prestarted worlds are a setting
The dashboard shows worlds master has launched but that aren't connected yet (Starting) and those shutting down, on the main page's world list and in /api/worlds (state: starting|stopping), without a shutdown button. Master's server list now carries each world's state (appended after the UGC fields, one byte per world) and master pushes the list to the dashboard whenever a world is launched, becomes ready, is told to shut down or goes away, instead of the dashboard only seeing it on its 30 second poll. Starting worlds are kept apart from the running ones, so counts, events and shutdown requests still only see running worlds. prestart_worlds (masterconfig.ini, zone ids) lists the worlds master starts when prestart_servers is on, instead of the hardcoded character select (0) and Venture Explorer (1000), which stay the default when it is missing or empty. It is on the Settings page as a zone list (restart only), shown when prestart_servers is on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,7 @@ namespace ServerState {
|
|||||||
bool g_UgcEnabled{};
|
bool g_UgcEnabled{};
|
||||||
uint32_t g_UgcPid{};
|
uint32_t g_UgcPid{};
|
||||||
std::vector<WorldInstanceInfo> g_WorldInstances{};
|
std::vector<WorldInstanceInfo> g_WorldInstances{};
|
||||||
|
std::vector<WorldInstanceInfo> g_PendingWorlds{};
|
||||||
std::mutex g_StatusMutex{};
|
std::mutex g_StatusMutex{};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +209,7 @@ namespace {
|
|||||||
ServerState::g_UgcPid = list.ugcPid;
|
ServerState::g_UgcPid = list.ugcPid;
|
||||||
|
|
||||||
ServerState::g_WorldInstances.clear();
|
ServerState::g_WorldInstances.clear();
|
||||||
|
ServerState::g_PendingWorlds.clear();
|
||||||
for (const auto& instance : list.instances) {
|
for (const auto& instance : list.instances) {
|
||||||
WorldInstanceInfo info;
|
WorldInstanceInfo info;
|
||||||
info.mapID = instance.mapID;
|
info.mapID = instance.mapID;
|
||||||
@@ -219,7 +221,13 @@ namespace {
|
|||||||
info.isPrivate = instance.isPrivate != 0;
|
info.isPrivate = instance.isPrivate != 0;
|
||||||
info.zoneName = GetZoneDisplayName(info.mapID);
|
info.zoneName = GetZoneDisplayName(info.mapID);
|
||||||
AddPropertyDetails(info);
|
AddPropertyDetails(info);
|
||||||
ServerState::g_WorldInstances.push_back(info);
|
using eState = MasterPackets::ServerListResponse::eState;
|
||||||
|
if (instance.state == eState::READY) {
|
||||||
|
ServerState::g_WorldInstances.push_back(info);
|
||||||
|
} else {
|
||||||
|
info.state = instance.state == eState::STARTING ? "starting" : "stopping";
|
||||||
|
ServerState::g_PendingWorlds.push_back(info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Received server list: auth=%s chat=%s ugc=%s worlds=%u",
|
LOG_DEBUG("Received server list: auth=%s chat=%s ugc=%s worlds=%u",
|
||||||
@@ -275,6 +283,8 @@ namespace {
|
|||||||
instances.push_back(info);
|
instances.push_back(info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::erase_if(ServerState::g_PendingWorlds, [&](const WorldInstanceInfo& w) { return w.mapID == zoneID && w.instanceID == instanceID; });
|
||||||
|
|
||||||
LOG("World ready: zone %i instance %i", zoneID, instanceID);
|
LOG("World ready: zone %i instance %i", zoneID, instanceID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ namespace LiveWorld {
|
|||||||
JsonSuccess(reply, { {"message", "Shutting it down"} });
|
JsonSuccess(reply, { {"message", "Shutting it down"} });
|
||||||
});
|
});
|
||||||
|
|
||||||
Route(eHTTPMethod::GET, "/api/worlds", Perm("players_view"), "Every running world with its instance and clone IDs, address, players and, for property instances, the property and owner",
|
Route(eHTTPMethod::GET, "/api/worlds", Perm("players_view"), "Every world with its instance and clone IDs, address, players and, for property instances, the property and owner; worlds still starting or shutting down have state: starting|stopping",
|
||||||
[](HTTPReply& reply, const HTTPContext&) {
|
[](HTTPReply& reply, const HTTPContext&) {
|
||||||
nlohmann::json worlds = nlohmann::json::array();
|
nlohmann::json worlds = nlohmann::json::array();
|
||||||
std::lock_guard lock(ServerState::g_StatusMutex);
|
std::lock_guard lock(ServerState::g_StatusMutex);
|
||||||
@@ -254,6 +254,12 @@ namespace LiveWorld {
|
|||||||
{"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName},
|
{"isPrivate", w.isPrivate}, {"ip", w.ip}, {"port", w.port}, {"propertyId", w.propertyId}, {"propertyName", w.propertyName},
|
||||||
{"ownerId", w.ownerId}, {"ownerName", w.ownerName} });
|
{"ownerId", w.ownerId}, {"ownerName", w.ownerName} });
|
||||||
}
|
}
|
||||||
|
// Launched but not connected yet, or shutting down
|
||||||
|
for (const auto& w : ServerState::g_PendingWorlds) {
|
||||||
|
worlds.push_back({ {"mapID", w.mapID}, {"zoneName", w.zoneName}, {"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} });
|
||||||
|
}
|
||||||
JsonSuccess(reply, { {"worlds", worlds} });
|
JsonSuccess(reply, { {"worlds", worlds} });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ struct WorldInstanceInfo {
|
|||||||
std::string propertyName{};
|
std::string propertyName{};
|
||||||
std::string ownerId{};
|
std::string ownerId{};
|
||||||
std::string ownerName{};
|
std::string ownerName{};
|
||||||
|
// "starting" (launched, not connected yet) or "stopping"; worlds that are up are in g_WorldInstances instead
|
||||||
|
std::string state{};
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace ServerState {
|
namespace ServerState {
|
||||||
@@ -49,6 +51,8 @@ namespace ServerState {
|
|||||||
extern bool g_UgcEnabled;
|
extern bool g_UgcEnabled;
|
||||||
extern uint32_t g_UgcPid;
|
extern uint32_t g_UgcPid;
|
||||||
extern std::vector<WorldInstanceInfo> g_WorldInstances;
|
extern std::vector<WorldInstanceInfo> g_WorldInstances;
|
||||||
|
// Worlds master launched that aren't connected yet, or that are shutting down (kept apart: not running)
|
||||||
|
extern std::vector<WorldInstanceInfo> g_PendingWorlds;
|
||||||
extern std::mutex g_StatusMutex;
|
extern std::mutex g_StatusMutex;
|
||||||
|
|
||||||
inline nlohmann::json GetServerStateJson() {
|
inline nlohmann::json GetServerStateJson() {
|
||||||
@@ -77,6 +81,12 @@ namespace ServerState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data["startingWorlds"] = nlohmann::json::array();
|
||||||
|
for (const auto& world : g_PendingWorlds) {
|
||||||
|
data["startingWorlds"].push_back({ {"mapID", world.mapID}, {"instanceID", world.instanceID}, {"cloneID", world.cloneID},
|
||||||
|
{"zoneName", world.zoneName}, {"state", world.state} });
|
||||||
|
}
|
||||||
|
|
||||||
data["stats"]["onlinePlayers"] = totalOnlinePlayers;
|
data["stats"]["onlinePlayers"] = totalOnlinePlayers;
|
||||||
data["stats"]["worlds"] = g_WorldInstances.size();
|
data["stats"]["worlds"] = g_WorldInstances.size();
|
||||||
return data;
|
return data;
|
||||||
@@ -108,6 +118,13 @@ namespace ServerState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
state["worlds"] = std::move(worlds);
|
state["worlds"] = std::move(worlds);
|
||||||
|
// Starting and stopping worlds without clone IDs (a property instance only says it's a property)
|
||||||
|
nlohmann::json pending = nlohmann::json::array();
|
||||||
|
for (const auto& world : state.value("startingWorlds", nlohmann::json::array())) {
|
||||||
|
pending.push_back({ {"mapID", world.value("mapID", 0u)}, {"instanceID", world.value("instanceID", 0u)}, {"zoneName", world.value("zoneName", "")},
|
||||||
|
{"state", world.value("state", "")}, {"property", world.value("cloneID", 0u) != 0} });
|
||||||
|
}
|
||||||
|
state["startingWorlds"] = std::move(pending);
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ namespace {
|
|||||||
|
|
||||||
c.AddSection("Startup");
|
c.AddSection("Startup");
|
||||||
c.Add(Bool(MASTER, "prestart_servers", "Start auth, chat and char servers", "Master starts the other servers itself.", true, true));
|
c.Add(Bool(MASTER, "prestart_servers", "Start auth, chat and char servers", "Master starts the other servers itself.", true, true));
|
||||||
|
{
|
||||||
|
auto worlds = List(MASTER, "prestart_worlds", "Worlds started with master", "Started when master starts, before anyone asks for them. Empty: character select (0) and Venture Explorer (1000).", eListOf::ZONE, "0,1000");
|
||||||
|
worlds.restart = true;
|
||||||
|
c.Add(When(std::move(worlds), MASTER, "prestart_servers", { "1" }));
|
||||||
|
}
|
||||||
c.Add(Bool(MASTER, "enable_dashboard", "Start the web dashboard", "", false, true));
|
c.Add(Bool(MASTER, "enable_dashboard", "Start the web dashboard", "", false, true));
|
||||||
c.Add(Bool(SHARED, "skip_account_creation", "Skip the first-account prompt", "For non-interactive setups: master doesn't ask for an account when there are none.", false, true));
|
c.Add(Bool(SHARED, "skip_account_creation", "Skip the first-account prompt", "For non-interactive setups: master doesn't ask for an account when there are none.", false, true));
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,17 @@ $(function () {
|
|||||||
|
|
||||||
// Staff get the detailed list (address, which property a clone is); everyone else the basic one from the status
|
// Staff get the detailed list (address, which property a clone is); everyone else the basic one from the status
|
||||||
var detailedWorlds = DASH.can('players_view');
|
var detailedWorlds = DASH.can('players_view');
|
||||||
|
// Worlds master launched that haven't connected yet, and those shutting down
|
||||||
|
function stateBadge(row) {
|
||||||
|
if (row.state === 'starting') return ' ' + fmt.badge('Starting', 'info');
|
||||||
|
if (row.state === 'stopping') return ' ' + fmt.badge('Shutting down', 'secondary');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
function withPending(data) {
|
||||||
|
return (data.worlds || []).concat((data.startingWorlds || []).map(function (w) {
|
||||||
|
return { zoneName: w.zoneName, mapID: w.mapID, instanceID: w.property ? '' : w.instanceID, cloneID: w.cloneID || 0, players: 0, state: w.state };
|
||||||
|
}));
|
||||||
|
}
|
||||||
var worlds = $('#worldInstancesTable').DataTable({
|
var worlds = $('#worldInstancesTable').DataTable({
|
||||||
paging: false,
|
paging: false,
|
||||||
searching: false,
|
searching: false,
|
||||||
@@ -133,7 +144,7 @@ $(function () {
|
|||||||
{ data: 'zoneName', render: function (d, t, row) {
|
{ data: 'zoneName', render: function (d, t, row) {
|
||||||
// Staff who read chat can open this world on the Chat Log: its chat, players and stats
|
// Staff who read chat can open this world on the Chat Log: its chat, players and stats
|
||||||
var name = DASH.can('chat_view') && row.mapID ? fmt.link('/chat_log#world=' + row.mapID + ':' + row.instanceID, d) : esc(d);
|
var name = DASH.can('chat_view') && row.mapID ? fmt.link('/chat_log#world=' + row.mapID + ':' + row.instanceID, d) : esc(d);
|
||||||
return name + ' <span class="text-body-secondary">(' + esc(row.mapID) + ')</span>'; } },
|
return name + ' <span class="text-body-secondary">(' + esc(row.mapID) + ')</span>' + stateBadge(row); } },
|
||||||
// Without players_view, property instances come merged per zone ({properties: n}) and without clone IDs
|
// Without players_view, property instances come merged per zone ({properties: n}) and without clone IDs
|
||||||
{ data: 'instanceID', render: function (d, t, row) { return row.properties ? esc(row.properties) + ' propert' + (row.properties === 1 ? 'y' : 'ies') : esc(d); } }
|
{ data: 'instanceID', render: function (d, t, row) { return row.properties ? esc(row.properties) + ' propert' + (row.properties === 1 ? 'y' : 'ies') : esc(d); } }
|
||||||
].concat(detailedWorlds ? [{ data: 'cloneID' }, { data: 'propertyName', defaultContent: '', render: function (d, t, row) {
|
].concat(detailedWorlds ? [{ data: 'cloneID' }, { data: 'propertyName', defaultContent: '', render: function (d, t, row) {
|
||||||
@@ -142,6 +153,7 @@ $(function () {
|
|||||||
} }] : []).concat([
|
} }] : []).concat([
|
||||||
{ data: 'players', render: function (d) { return fmt.badge(d, 'primary'); } }
|
{ data: 'players', render: function (d) { return fmt.badge(d, 'primary'); } }
|
||||||
]).concat(detailedWorlds ? [{ data: 'isPrivate', render: function (d) { return d ? fmt.badge('Private', 'warning') : fmt.badge('Public', 'success'); } }, { data: 'port', defaultContent: '', render: function (d, t, row) { return '<code class="small">' + esc((row.ip || '') + ':' + d) + '</code>'; } }] : []).concat(DASH.can('worlds_manage') ? [{ data: null, orderable: false, className: 'text-end', render: function (d, t, row) {
|
]).concat(detailedWorlds ? [{ data: 'isPrivate', render: function (d) { return d ? fmt.badge('Private', 'warning') : fmt.badge('Public', 'success'); } }, { data: 'port', defaultContent: '', render: function (d, t, row) { return '<code class="small">' + esc((row.ip || '') + ':' + d) + '</code>'; } }] : []).concat(DASH.can('worlds_manage') ? [{ data: null, orderable: false, className: 'text-end', render: function (d, t, row) {
|
||||||
|
if (row.state) return '';
|
||||||
return '<button class="btn btn-sm btn-outline-danger text-nowrap" data-shutdown-zone="' + esc(row.mapID) + '" data-shutdown-instance="' + esc(row.instanceID) + '">Shut down</button>';
|
return '<button class="btn btn-sm btn-outline-danger text-nowrap" data-shutdown-zone="' + esc(row.mapID) + '" data-shutdown-instance="' + esc(row.instanceID) + '">Shut down</button>';
|
||||||
} }] : []),
|
} }] : []),
|
||||||
language: { emptyTable: 'No active world instances' }
|
language: { emptyTable: 'No active world instances' }
|
||||||
@@ -211,7 +223,7 @@ $(function () {
|
|||||||
if (data.worlds) {
|
if (data.worlds) {
|
||||||
setText('world-count', data.stats && data.stats.worlds !== undefined ? data.stats.worlds : data.worlds.length);
|
setText('world-count', data.stats && data.stats.worlds !== undefined ? data.stats.worlds : data.worlds.length);
|
||||||
if (detailedWorlds) loadDetailedWorlds();
|
if (detailedWorlds) loadDetailedWorlds();
|
||||||
else worlds.clear().rows.add(data.worlds).draw(false);
|
else worlds.clear().rows.add(withPending(data)).draw(false);
|
||||||
}
|
}
|
||||||
if (data.stats) {
|
if (data.stats) {
|
||||||
setText('online-players', data.stats.onlinePlayers);
|
setText('online-players', data.stats.onlinePlayers);
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const InstancePtr& InstanceManager::CreateInstance(LWOMAPID mapID, LWOCLONEID cl
|
|||||||
StartWorldServer(mapID, port, m_LastInstanceID, maxPlayers, cloneID);
|
StartWorldServer(mapID, port, m_LastInstanceID, maxPlayers, cloneID);
|
||||||
|
|
||||||
m_Instances.push_back(std::move(newInstance));
|
m_Instances.push_back(std::move(newInstance));
|
||||||
|
if (m_OnInstancesChanged) m_OnInstancesChanged();
|
||||||
|
|
||||||
if (m_Instances.back()) {
|
if (m_Instances.back()) {
|
||||||
LOG("Created new instance: %i/%i/%i with min/max %i/%i", mapID, m_LastInstanceID, cloneID, softCap, maxPlayers);
|
LOG("Created new instance: %i/%i/%i with min/max %i/%i", mapID, m_LastInstanceID, cloneID, softCap, maxPlayers);
|
||||||
@@ -146,6 +147,7 @@ void InstanceManager::AddInstance(InstancePtr& instance) {
|
|||||||
if (instance == nullptr) return;
|
if (instance == nullptr) return;
|
||||||
|
|
||||||
m_Instances.push_back(std::move(instance));
|
m_Instances.push_back(std::move(instance));
|
||||||
|
if (m_OnInstancesChanged) m_OnInstancesChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InstanceManager::RemoveInstance(const InstancePtr& instance) {
|
void InstanceManager::RemoveInstance(const InstancePtr& instance) {
|
||||||
@@ -156,6 +158,7 @@ void InstanceManager::RemoveInstance(const InstancePtr& instance) {
|
|||||||
if (!Game::ShouldShutdown()) RedirectPendingRequests(instance);
|
if (!Game::ShouldShutdown()) RedirectPendingRequests(instance);
|
||||||
|
|
||||||
m_Instances.erase(m_Instances.begin() + i);
|
m_Instances.erase(m_Instances.begin() + i);
|
||||||
|
if (m_OnInstancesChanged) m_OnInstancesChanged();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -164,6 +167,7 @@ void InstanceManager::RemoveInstance(const InstancePtr& instance) {
|
|||||||
|
|
||||||
void InstanceManager::ReadyInstance(const InstancePtr& instance) {
|
void InstanceManager::ReadyInstance(const InstancePtr& instance) {
|
||||||
instance->SetIsReady(true);
|
instance->SetIsReady(true);
|
||||||
|
if (m_OnInstancesChanged) m_OnInstancesChanged();
|
||||||
|
|
||||||
auto& pending = instance->GetPendingRequests();
|
auto& pending = instance->GetPendingRequests();
|
||||||
|
|
||||||
@@ -288,6 +292,7 @@ const InstancePtr& InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLO
|
|||||||
StartWorldServer(mapID, port, m_LastInstanceID, maxPlayers, cloneID);
|
StartWorldServer(mapID, port, m_LastInstanceID, maxPlayers, cloneID);
|
||||||
|
|
||||||
m_Instances.push_back(std::move(newInstance));
|
m_Instances.push_back(std::move(newInstance));
|
||||||
|
if (m_OnInstancesChanged) m_OnInstancesChanged();
|
||||||
|
|
||||||
if (m_Instances.back()) return m_Instances.back();
|
if (m_Instances.back()) return m_Instances.back();
|
||||||
else LOG("Failed to create a new instance!");
|
else LOG("Failed to create a new instance!");
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include "SpareBackoff.h"
|
#include "SpareBackoff.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -138,6 +140,9 @@ public:
|
|||||||
|
|
||||||
const std::vector<InstancePtr>& GetInstances() const;
|
const std::vector<InstancePtr>& GetInstances() const;
|
||||||
void AddInstance(InstancePtr& instance);
|
void AddInstance(InstancePtr& instance);
|
||||||
|
|
||||||
|
// Called on the main thread whenever a world is launched, becomes ready or is removed (for the dashboard's list)
|
||||||
|
void SetOnInstancesChanged(std::function<void()> callback) { m_OnInstancesChanged = std::move(callback); }
|
||||||
void RemoveInstance(const InstancePtr& instance);
|
void RemoveInstance(const InstancePtr& instance);
|
||||||
|
|
||||||
void ReadyInstance(const InstancePtr& instance);
|
void ReadyInstance(const InstancePtr& instance);
|
||||||
@@ -187,4 +192,5 @@ private:
|
|||||||
int GetSoftCap(LWOMAPID mapID);
|
int GetSoftCap(LWOMAPID mapID);
|
||||||
int GetHardCap(LWOMAPID mapID);
|
int GetHardCap(LWOMAPID mapID);
|
||||||
const InstancePtr& CreateInstance(LWOMAPID mapID, LWOCLONEID cloneID);
|
const InstancePtr& CreateInstance(LWOMAPID mapID, LWOCLONEID cloneID);
|
||||||
|
std::function<void()> m_OnInstancesChanged;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,6 +88,38 @@ SystemAddress ugcServerMasterPeerSysAddr;
|
|||||||
uint32_t ugcServerPid = 0;
|
uint32_t ugcServerPid = 0;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
// Every server for the dashboard: auth, chat, the UGC server and the worlds, including those launched but not
|
||||||
|
// connected yet (starting) and those shutting down
|
||||||
|
MasterPackets::ServerListResponse BuildServerList() {
|
||||||
|
using eState = MasterPackets::ServerListResponse::eState;
|
||||||
|
MasterPackets::ServerListResponse response;
|
||||||
|
response.authOnline = authServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0;
|
||||||
|
response.chatOnline = chatServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0;
|
||||||
|
response.ugcEnabled = Game::config->GetValue("enable_ugc_server") == "1" ? 1 : 0;
|
||||||
|
response.ugcOnline = ugcServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0;
|
||||||
|
response.ugcPid = ugcServerPid;
|
||||||
|
if (!Game::im) return response;
|
||||||
|
for (const auto& inst : Game::im->GetInstances()) {
|
||||||
|
if (!inst || inst->GetShutdownComplete()) continue;
|
||||||
|
auto& entry = response.instances.emplace_back();
|
||||||
|
entry.mapID = inst->GetMapID();
|
||||||
|
entry.instanceID = inst->GetInstanceID();
|
||||||
|
entry.cloneID = inst->GetCloneID();
|
||||||
|
entry.players = static_cast<uint32_t>(inst->GetCurrentClientCount());
|
||||||
|
entry.ip = LUString(inst->GetIP());
|
||||||
|
entry.port = inst->GetPort();
|
||||||
|
entry.isPrivate = inst->GetIsPrivate() ? 1 : 0;
|
||||||
|
entry.state = inst->GetIsShuttingDown() ? eState::STOPPING : inst->GetIsReady() ? eState::READY : eState::STARTING;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tells the dashboard at once when a world is launched, connects or goes away (it also asks every 30 seconds)
|
||||||
|
void PushServerListToDashboard() {
|
||||||
|
if (dashboardServerMasterPeerSysAddr == UNASSIGNED_SYSTEM_ADDRESS) return;
|
||||||
|
MasterPackets::SendTo(dashboardServerMasterPeerSysAddr, BuildServerList());
|
||||||
|
}
|
||||||
|
|
||||||
// Dashboard player actions waiting for world servers to answer
|
// Dashboard player actions waiting for world servers to answer
|
||||||
struct PendingPlayerAction {
|
struct PendingPlayerAction {
|
||||||
ePlayerAction action{};
|
ePlayerAction action{};
|
||||||
@@ -418,6 +450,7 @@ int main(int argc, char** argv) {
|
|||||||
|
|
||||||
//Create additional objects here:
|
//Create additional objects here:
|
||||||
Game::im = new InstanceManager(Game::server->GetIP());
|
Game::im = new InstanceManager(Game::server->GetIP());
|
||||||
|
Game::im->SetOnInstancesChanged(PushServerListToDashboard);
|
||||||
|
|
||||||
//Get CDClient initial information
|
//Get CDClient initial information
|
||||||
try {
|
try {
|
||||||
@@ -442,8 +475,17 @@ int main(int argc, char** argv) {
|
|||||||
if (Game::config->GetValue("prestart_servers") != "0") {
|
if (Game::config->GetValue("prestart_servers") != "0") {
|
||||||
StartChatServer();
|
StartChatServer();
|
||||||
|
|
||||||
Game::im->GetInstance(0, false, 0);
|
// The worlds started with master (prestart_worlds, zone ids; missing or empty: character select and Venture Explorer)
|
||||||
Game::im->GetInstance(1000, false, 0);
|
for (auto part : GeneralUtils::SplitString(Game::config->GetValue("prestart_worlds", "0,1000"), ',')) {
|
||||||
|
std::erase_if(part, [](const char c) { return std::isspace(static_cast<unsigned char>(c)); });
|
||||||
|
if (part.empty()) continue;
|
||||||
|
const auto zoneId = GeneralUtils::TryParse<LWOMAPID>(part);
|
||||||
|
if (!zoneId) {
|
||||||
|
LOG("prestart_worlds: '%s' isn't a zone id; skipped", part.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Game::im->GetInstance(*zoneId, false, 0);
|
||||||
|
}
|
||||||
StartAuthServer();
|
StartAuthServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -804,6 +846,7 @@ namespace {
|
|||||||
|
|
||||||
LOG("Got shutdown response from zone %i clone %i instance %i port %i", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance->GetPort());
|
LOG("Got shutdown response from zone %i clone %i instance %i port %i", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance->GetPort());
|
||||||
instance->SetIsShuttingDown(true);
|
instance->SetIsShuttingDown(true);
|
||||||
|
PushServerListToDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnShutdownUniverse(const ShutdownUniverse& request, const SystemAddress& sysAddr) {
|
void OnShutdownUniverse(const ShutdownUniverse& request, const SystemAddress& sysAddr) {
|
||||||
@@ -937,25 +980,7 @@ namespace {
|
|||||||
void OnRequestServerList(const RequestServerList& request, const SystemAddress& sysAddr) {
|
void OnRequestServerList(const RequestServerList& request, const SystemAddress& sysAddr) {
|
||||||
LOG("Dashboard requested server list");
|
LOG("Dashboard requested server list");
|
||||||
|
|
||||||
ServerListResponse response;
|
const auto response = BuildServerList();
|
||||||
response.authOnline = authServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0;
|
|
||||||
response.chatOnline = chatServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0;
|
|
||||||
response.ugcEnabled = Game::config->GetValue("enable_ugc_server") == "1" ? 1 : 0;
|
|
||||||
response.ugcOnline = ugcServerMasterPeerSysAddr != UNASSIGNED_SYSTEM_ADDRESS ? 1 : 0;
|
|
||||||
response.ugcPid = ugcServerPid;
|
|
||||||
|
|
||||||
for (const auto& inst : Game::im->GetInstances()) {
|
|
||||||
if (!inst || !inst->GetIsReady() || inst->GetIsShuttingDown()) continue;
|
|
||||||
auto& entry = response.instances.emplace_back();
|
|
||||||
entry.mapID = inst->GetMapID();
|
|
||||||
entry.instanceID = inst->GetInstanceID();
|
|
||||||
entry.cloneID = inst->GetCloneID();
|
|
||||||
entry.players = static_cast<uint32_t>(inst->GetCurrentClientCount());
|
|
||||||
entry.ip = LUString(inst->GetIP());
|
|
||||||
entry.port = inst->GetPort();
|
|
||||||
entry.isPrivate = inst->GetIsPrivate() ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
MasterPackets::SendTo(sysAddr, response);
|
MasterPackets::SendTo(sysAddr, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,6 +244,7 @@ namespace MasterPackets {
|
|||||||
bitStream.Write(ugcEnabled);
|
bitStream.Write(ugcEnabled);
|
||||||
bitStream.Write(ugcOnline);
|
bitStream.Write(ugcOnline);
|
||||||
bitStream.Write(ugcPid);
|
bitStream.Write(ugcPid);
|
||||||
|
for (const auto& instance : instances) bitStream.Write(static_cast<uint8_t>(instance.state));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ServerListResponse::Deserialize(RakNet::BitStream& bitStream) {
|
bool ServerListResponse::Deserialize(RakNet::BitStream& bitStream) {
|
||||||
@@ -265,6 +266,12 @@ namespace MasterPackets {
|
|||||||
VALIDATE_READ(bitStream.Read(ugcEnabled));
|
VALIDATE_READ(bitStream.Read(ugcEnabled));
|
||||||
VALIDATE_READ(bitStream.Read(ugcOnline));
|
VALIDATE_READ(bitStream.Read(ugcOnline));
|
||||||
VALIDATE_READ(bitStream.Read(ugcPid));
|
VALIDATE_READ(bitStream.Read(ugcPid));
|
||||||
|
for (auto& instance : instances) {
|
||||||
|
uint8_t state{};
|
||||||
|
VALIDATE_READ(bitStream.Read(state));
|
||||||
|
if (state > static_cast<uint8_t>(eState::STOPPING)) return false;
|
||||||
|
instance.state = static_cast<eState>(state);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -249,6 +249,9 @@ namespace MasterPackets {
|
|||||||
// More than this many worlds is a broken packet
|
// More than this many worlds is a broken packet
|
||||||
static constexpr uint32_t MAX_INSTANCES = 10000;
|
static constexpr uint32_t MAX_INSTANCES = 10000;
|
||||||
|
|
||||||
|
// Where a world is: connected, launched but not connected yet, or shutting down
|
||||||
|
enum class eState : uint8_t { READY = 0, STARTING = 1, STOPPING = 2 };
|
||||||
|
|
||||||
struct Instance {
|
struct Instance {
|
||||||
LWOMAPID mapID{};
|
LWOMAPID mapID{};
|
||||||
LWOINSTANCEID instanceID{};
|
LWOINSTANCEID instanceID{};
|
||||||
@@ -257,6 +260,7 @@ namespace MasterPackets {
|
|||||||
LUString ip{};
|
LUString ip{};
|
||||||
uint32_t port{};
|
uint32_t port{};
|
||||||
uint8_t isPrivate{};
|
uint8_t isPrivate{};
|
||||||
|
eState state{ eState::READY }; // written after the UGC fields, one byte per instance
|
||||||
};
|
};
|
||||||
|
|
||||||
uint8_t authOnline{};
|
uint8_t authOnline{};
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ world_port_start=3000
|
|||||||
# 0 or 1, should autostart auth, chat, and char servers
|
# 0 or 1, should autostart auth, chat, and char servers
|
||||||
prestart_servers=1
|
prestart_servers=1
|
||||||
|
|
||||||
|
# The worlds (zone ids, comma separated) started with master when prestart_servers is on. Empty: 0,1000
|
||||||
|
prestart_worlds=0,1000
|
||||||
|
|
||||||
master_password=3.25DARKFLAME1
|
master_password=3.25DARKFLAME1
|
||||||
|
|
||||||
# Enable the web dashboard (0 = disabled, 1 = enabled)
|
# Enable the web dashboard (0 = disabled, 1 = enabled)
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ TEST(MasterPacketsTests, ServerListMatchesLegacy) {
|
|||||||
entry.ip = LUString(instance.ip);
|
entry.ip = LUString(instance.ip);
|
||||||
entry.port = instance.port;
|
entry.port = instance.port;
|
||||||
entry.isPrivate = instance.isPrivate ? 1 : 0;
|
entry.isPrivate = instance.isPrivate ? 1 : 0;
|
||||||
|
entry.state = static_cast<MasterPackets::ServerListResponse::eState>(i % 3);
|
||||||
}
|
}
|
||||||
response.ugcEnabled = 1;
|
response.ugcEnabled = 1;
|
||||||
response.ugcOnline = count % 2;
|
response.ugcOnline = count % 2;
|
||||||
@@ -362,6 +363,7 @@ TEST(MasterPacketsTests, ServerListMatchesLegacy) {
|
|||||||
b.Write<uint8_t>(1);
|
b.Write<uint8_t>(1);
|
||||||
b.Write<uint8_t>(count % 2);
|
b.Write<uint8_t>(count % 2);
|
||||||
b.Write<uint32_t>(4242 + count);
|
b.Write<uint32_t>(4242 + count);
|
||||||
|
for (size_t i = 0; i < count; i++) b.Write<uint8_t>(static_cast<uint8_t>(i % 3)); // each world's state
|
||||||
}), StructPacket(response));
|
}), StructPacket(response));
|
||||||
|
|
||||||
RakNet::BitStream stream; LoadPayload(stream, response);
|
RakNet::BitStream stream; LoadPayload(stream, response);
|
||||||
@@ -373,6 +375,7 @@ TEST(MasterPacketsTests, ServerListMatchesLegacy) {
|
|||||||
EXPECT_EQ(legacy.instances[i].players, copy.instances[i].players);
|
EXPECT_EQ(legacy.instances[i].players, copy.instances[i].players);
|
||||||
EXPECT_EQ(legacy.instances[i].ip, copy.instances[i].ip.string);
|
EXPECT_EQ(legacy.instances[i].ip, copy.instances[i].ip.string);
|
||||||
EXPECT_EQ(legacy.instances[i].isPrivate, copy.instances[i].isPrivate != 0);
|
EXPECT_EQ(legacy.instances[i].isPrivate, copy.instances[i].isPrivate != 0);
|
||||||
|
EXPECT_EQ(static_cast<size_t>(copy.instances[i].state), i % 3);
|
||||||
}
|
}
|
||||||
EXPECT_EQ(copy.ugcEnabled, 1);
|
EXPECT_EQ(copy.ugcEnabled, 1);
|
||||||
EXPECT_EQ(copy.ugcOnline, count % 2);
|
EXPECT_EQ(copy.ugcOnline, count % 2);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ namespace ServerState {
|
|||||||
bool g_UgcEnabled{};
|
bool g_UgcEnabled{};
|
||||||
uint32_t g_UgcPid{};
|
uint32_t g_UgcPid{};
|
||||||
std::vector<WorldInstanceInfo> g_WorldInstances{};
|
std::vector<WorldInstanceInfo> g_WorldInstances{};
|
||||||
|
std::vector<WorldInstanceInfo> g_PendingWorlds{};
|
||||||
std::mutex g_StatusMutex{};
|
std::mutex g_StatusMutex{};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user