added server

This commit is contained in:
2026-08-01 18:43:40 +12:00
commit 07cc2e20a3
46 changed files with 9310 additions and 0 deletions
+264
View File
@@ -0,0 +1,264 @@
#include "ConfigDialog.h"
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFile>
#include <QFileInfo>
#include <QDir>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMessageBox>
#include <QPushButton>
namespace {
constexpr int kServerNameMaxLength = 64;
constexpr int kServerDescriptionMaxLength = 256;
}
ConfigDialog::ConfigDialog(const QString &configPath, QWidget *parent)
: QDialog(parent)
, configPath(configPath)
{
setWindowTitle(QStringLiteral("Server Settings"));
setModal(true);
setMinimumWidth(480);
setupUI();
ServerConfigData data;
QString error;
if (QFileInfo::exists(configPath)) {
if (!loadFromFile(configPath, &data, &error)) {
QMessageBox::warning(this, QStringLiteral("Config"),
QStringLiteral("Could not load config; using defaults.\n%1").arg(error));
data = ServerConfigData{};
}
}
loadIntoForm(data);
}
ServerConfigData ConfigDialog::config() const {
ServerConfigData data;
data.serverName = serverNameEdit->text().trimmed();
data.serverDescription = descriptionEdit->toPlainText().trimmed();
data.host = hostEdit->text().trimmed();
data.port = portSpin->value();
data.adminPort = adminPortSpin->value();
data.maxPlayers = maxPlayersSpin->value();
data.logVerbosity = logVerbosityCombo->currentData().toString();
return data;
}
bool ConfigDialog::loadFromFile(const QString &configPath, ServerConfigData *out, QString *error) {
if (!out) {
if (error) {
*error = QStringLiteral("Output config pointer is null");
}
return false;
}
QFile file(configPath);
if (!file.open(QIODevice::ReadOnly)) {
if (error) {
*error = QStringLiteral("Could not open %1").arg(configPath);
}
return false;
}
QJsonParseError parseError;
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &parseError);
file.close();
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
if (error) {
*error = QStringLiteral("Invalid JSON: %1").arg(parseError.errorString());
}
return false;
}
const QJsonObject obj = doc.object();
out->host = obj.value(QStringLiteral("host")).toString(QStringLiteral("0.0.0.0"));
out->port = obj.value(QStringLiteral("port")).toInt(7777);
out->adminPort = obj.value(QStringLiteral("admin_port")).toInt(7779);
out->serverName = obj.value(QStringLiteral("server_name")).toString(QStringLiteral("Commonwealth Online Server"));
out->serverDescription = obj.value(QStringLiteral("server_description")).toString();
out->maxPlayers = obj.value(QStringLiteral("max_players")).toInt(16);
out->logVerbosity = obj.value(QStringLiteral("log_verbosity")).toString(QStringLiteral("info"));
return true;
}
bool ConfigDialog::saveToFile(const QString &configPath, const ServerConfigData &config, QString *error) {
QFileInfo info(configPath);
if (!info.dir().exists() && !QDir().mkpath(info.absolutePath())) {
if (error) {
*error = QStringLiteral("Could not create directory for %1").arg(configPath);
}
return false;
}
QJsonObject obj;
obj.insert(QStringLiteral("host"), config.host);
obj.insert(QStringLiteral("port"), config.port);
obj.insert(QStringLiteral("admin_port"), config.adminPort);
obj.insert(QStringLiteral("server_name"), config.serverName);
obj.insert(QStringLiteral("server_description"), config.serverDescription);
obj.insert(QStringLiteral("max_players"), config.maxPlayers);
obj.insert(QStringLiteral("log_verbosity"), config.logVerbosity);
QFile file(configPath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
if (error) {
*error = QStringLiteral("Could not write %1").arg(configPath);
}
return false;
}
file.write(QJsonDocument(obj).toJson(QJsonDocument::Indented));
file.close();
return true;
}
void ConfigDialog::setupUI() {
auto *mainLayout = new QVBoxLayout(this);
auto *form = new QFormLayout();
form->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
form->setFieldGrowthPolicy(QFormLayout::ExpandingFieldsGrow);
serverNameEdit = new QLineEdit();
serverNameEdit->setMaxLength(kServerNameMaxLength);
serverNameEdit->setPlaceholderText(QStringLiteral("Commonwealth Online Server"));
form->addRow(QStringLiteral("Server name"), serverNameEdit);
descriptionEdit = new QPlainTextEdit();
descriptionEdit->setPlaceholderText(QStringLiteral("Short description shown in the server browser"));
descriptionEdit->setMaximumHeight(80);
form->addRow(QStringLiteral("Description"), descriptionEdit);
hostEdit = new QLineEdit();
hostEdit->setPlaceholderText(QStringLiteral("0.0.0.0"));
form->addRow(QStringLiteral("Bind address"), hostEdit);
portSpin = new QSpinBox();
portSpin->setRange(1, 65535);
portSpin->setValue(7777);
form->addRow(QStringLiteral("Port"), portSpin);
adminPortSpin = new QSpinBox();
adminPortSpin->setRange(1, 65535);
adminPortSpin->setValue(7779);
form->addRow(QStringLiteral("Admin port (localhost)"), adminPortSpin);
maxPlayersSpin = new QSpinBox();
maxPlayersSpin->setRange(1, 128);
maxPlayersSpin->setValue(16);
form->addRow(QStringLiteral("Max players"), maxPlayersSpin);
logVerbosityCombo = new QComboBox();
logVerbosityCombo->addItem(QStringLiteral("Debug"), QStringLiteral("debug"));
logVerbosityCombo->addItem(QStringLiteral("Info"), QStringLiteral("info"));
logVerbosityCombo->addItem(QStringLiteral("Warning"), QStringLiteral("warning"));
logVerbosityCombo->addItem(QStringLiteral("Error"), QStringLiteral("error"));
form->addRow(QStringLiteral("Log level"), logVerbosityCombo);
mainLayout->addLayout(form);
pathLabel = new QLabel();
pathLabel->setWordWrap(true);
pathLabel->setStyleSheet(QStringLiteral("QLabel { color: #666666; font-size: 11px; }"));
pathLabel->setText(QStringLiteral("Config file: %1").arg(configPath));
mainLayout->addWidget(pathLabel);
auto *hint = new QLabel(QStringLiteral("Changes apply the next time the server is started."));
hint->setStyleSheet(QStringLiteral("QLabel { color: #555555; font-size: 11px; }"));
hint->setWordWrap(true);
mainLayout->addWidget(hint);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel);
buttons->button(QDialogButtonBox::Save)->setText(QStringLiteral("Save"));
connect(buttons, &QDialogButtonBox::accepted, this, &ConfigDialog::onAccepted);
connect(buttons, &QDialogButtonBox::rejected, this, &ConfigDialog::reject);
mainLayout->addWidget(buttons);
}
void ConfigDialog::loadIntoForm(const ServerConfigData &config) {
serverNameEdit->setText(config.serverName);
descriptionEdit->setPlainText(config.serverDescription);
hostEdit->setText(config.host);
portSpin->setValue(config.port > 0 ? config.port : 7777);
adminPortSpin->setValue(config.adminPort > 0 ? config.adminPort : 7779);
maxPlayersSpin->setValue(config.maxPlayers > 0 ? config.maxPlayers : 16);
const int idx = logVerbosityCombo->findData(config.logVerbosity.toLower());
logVerbosityCombo->setCurrentIndex(idx >= 0 ? idx : logVerbosityCombo->findData(QStringLiteral("info")));
}
bool ConfigDialog::validateForm(QString *error) const {
const ServerConfigData data = config();
if (data.serverName.isEmpty()) {
if (error) {
*error = QStringLiteral("Server name cannot be empty.");
}
return false;
}
if (data.serverName.size() > kServerNameMaxLength) {
if (error) {
*error = QStringLiteral("Server name must be at most %1 characters.").arg(kServerNameMaxLength);
}
return false;
}
if (data.serverDescription.size() > kServerDescriptionMaxLength) {
if (error) {
*error = QStringLiteral("Description must be at most %1 characters.").arg(kServerDescriptionMaxLength);
}
return false;
}
if (data.host.isEmpty()) {
if (error) {
*error = QStringLiteral("Bind address cannot be empty.");
}
return false;
}
if (data.port < 1 || data.port > 65535) {
if (error) {
*error = QStringLiteral("Port must be between 1 and 65535.");
}
return false;
}
if (data.adminPort < 1 || data.adminPort > 65535) {
if (error) {
*error = QStringLiteral("Admin port must be between 1 and 65535.");
}
return false;
}
if (data.adminPort == data.port) {
if (error) {
*error = QStringLiteral("Admin port must differ from the game port.");
}
return false;
}
if (data.maxPlayers < 1) {
if (error) {
*error = QStringLiteral("Max players must be at least 1.");
}
return false;
}
return true;
}
void ConfigDialog::onAccepted() {
QString error;
if (!validateForm(&error)) {
QMessageBox::warning(this, QStringLiteral("Invalid settings"), error);
return;
}
if (!saveToFile(configPath, config(), &error)) {
QMessageBox::critical(this, QStringLiteral("Save failed"), error);
return;
}
accept();
}
+52
View File
@@ -0,0 +1,52 @@
#ifndef CONFIGDIALOG_H
#define CONFIGDIALOG_H
#include <QDialog>
#include <QLineEdit>
#include <QPlainTextEdit>
#include <QSpinBox>
#include <QComboBox>
#include <QLabel>
struct ServerConfigData {
QString host = QStringLiteral("0.0.0.0");
int port = 7777;
int adminPort = 7779;
QString serverName = QStringLiteral("Commonwealth Online Server");
QString serverDescription;
int maxPlayers = 16;
QString logVerbosity = QStringLiteral("info");
};
class ConfigDialog : public QDialog {
Q_OBJECT
public:
explicit ConfigDialog(const QString &configPath, QWidget *parent = nullptr);
ServerConfigData config() const;
static bool loadFromFile(const QString &configPath, ServerConfigData *out, QString *error = nullptr);
static bool saveToFile(const QString &configPath, const ServerConfigData &config, QString *error = nullptr);
private slots:
void onAccepted();
private:
void setupUI();
void loadIntoForm(const ServerConfigData &config);
bool validateForm(QString *error) const;
QString configPath;
QLineEdit *serverNameEdit;
QPlainTextEdit *descriptionEdit;
QLineEdit *hostEdit;
QSpinBox *portSpin;
QSpinBox *adminPortSpin;
QSpinBox *maxPlayersSpin;
QComboBox *logVerbosityCombo;
QLabel *pathLabel;
};
#endif // CONFIGDIALOG_H
+846
View File
@@ -0,0 +1,846 @@
#include "MainWindow.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFrame>
#include <QStatusBar>
#include <QApplication>
#include <QDateTime>
#include <QJsonArray>
#include <QJsonObject>
#include <QHeaderView>
#include <QCloseEvent>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QStyle>
#include <QInputDialog>
#include <QTimer>
#include <QPalette>
#include <QColor>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, isServerRunning(false)
, serverName("Commonwealth Online Server")
, configFilePath("commonwealth-server.json")
{
setWindowTitle("Commonwealth Online — Server Host");
setWindowIcon(QIcon(":/icons/app.ico"));
setGeometry(100, 100, 960, 640);
setMinimumSize(780, 520);
setupUI();
setupStyles();
setupConnections();
setupTimer();
serverProcess = new ServerProcess(this);
connect(serverProcess, &ServerProcess::started, this, &MainWindow::onServerStarted);
connect(serverProcess, &ServerProcess::stopped, this, &MainWindow::onServerStopped);
connect(serverProcess, &ServerProcess::logMessage, this, &MainWindow::onServerLog);
connect(serverProcess, &ServerProcess::clientsUpdated, this, &MainWindow::onUpdateClients);
connect(serverProcess, &ServerProcess::statsUpdated, this, &MainWindow::onStatsUpdated);
connect(serverProcess, &ServerProcess::error, this, &MainWindow::onServerError);
connect(serverProcess, &ServerProcess::adminCommandFinished, this, &MainWindow::onAdminCommandFinished);
resolveConfigPath();
loadConfigIntoUi();
}
MainWindow::~MainWindow() {
if (isServerRunning) {
serverProcess->stop();
}
}
void MainWindow::setupUI() {
centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
auto *mainLayout = new QVBoxLayout(centralWidget);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// ===== TOOLBAR =====
auto *toolbar = new QFrame();
toolbar->setObjectName("toolBar");
auto *toolbarLayout = new QHBoxLayout(toolbar);
toolbarLayout->setContentsMargins(8, 6, 8, 6);
toolbarLayout->setSpacing(6);
serverNameLabel = new QLabel(serverName);
serverNameLabel->setObjectName("serverNameLabel");
startButton = new QPushButton("Start");
startButton->setObjectName("startButton");
startButton->setFixedWidth(72);
startButton->setToolTip("Start the relay server");
stopButton = new QPushButton("Stop");
stopButton->setObjectName("stopButton");
stopButton->setFixedWidth(72);
stopButton->setEnabled(false);
stopButton->setToolTip("Stop the relay server");
configButton = new QPushButton("Settings");
configButton->setFixedWidth(80);
configButton->setToolTip("Edit server settings");
statusDot = new QLabel();
statusDot->setObjectName("statusDot");
statusDot->setFixedSize(10, 10);
statusDot->setProperty("running", false);
statusIndicator = new QLabel("Stopped");
statusIndicator->setObjectName("statusText");
toolbarLayout->addWidget(startButton);
toolbarLayout->addWidget(stopButton);
toolbarLayout->addWidget(configButton);
toolbarLayout->addSpacing(12);
toolbarLayout->addWidget(serverNameLabel, 1);
toolbarLayout->addWidget(statusDot);
toolbarLayout->addWidget(statusIndicator);
mainLayout->addWidget(toolbar);
// ===== STATUS STRIP =====
auto *statusStrip = new QFrame();
statusStrip->setObjectName("statusStrip");
auto *statusLayout = new QHBoxLayout(statusStrip);
statusLayout->setContentsMargins(10, 5, 10, 5);
statusLayout->setSpacing(16);
bindAddressLabel = new QLabel("Bind: 0.0.0.0:7777");
lanAddressLabel = new QLabel("LAN: —");
clientsLabel = new QLabel("Clients: 0");
uptimeLabel = new QLabel("Uptime: 0s");
transformPacketsLabel = new QLabel("Transform: 0 / 0");
worldStatePacketsLabel = new QLabel("WorldState: 0 / 0");
for (QLabel *label : {bindAddressLabel, lanAddressLabel, clientsLabel, uptimeLabel,
transformPacketsLabel, worldStatePacketsLabel}) {
label->setObjectName("statLabel");
statusLayout->addWidget(label);
}
statusLayout->addStretch();
mainLayout->addWidget(statusStrip);
// ===== MAIN SPLITTER (clients + log) =====
mainSplitter = new QSplitter(Qt::Vertical);
mainSplitter->setObjectName("mainSplitter");
mainSplitter->setChildrenCollapsible(false);
auto *clientsPane = new QFrame();
clientsPane->setObjectName("pane");
auto *clientsLayout = new QVBoxLayout(clientsPane);
clientsLayout->setContentsMargins(8, 6, 8, 4);
clientsLayout->setSpacing(4);
auto *clientsHeaderRow = new QHBoxLayout();
clientsHeaderRow->setContentsMargins(0, 0, 0, 0);
clientsHeaderRow->setSpacing(8);
auto *clientsHeader = new QLabel("Connected Clients");
clientsHeader->setObjectName("paneHeader");
clientsHeaderRow->addWidget(clientsHeader, 1);
kickButton = new QPushButton(QStringLiteral("Kick"));
kickButton->setEnabled(false);
kickButton->setToolTip(QStringLiteral("Disconnect the selected player without banning"));
banButton = new QPushButton(QStringLiteral("Ban IP"));
banButton->setEnabled(false);
banButton->setToolTip(QStringLiteral("Disconnect and permanently ban the selected player's IP"));
bansButton = new QPushButton(QStringLiteral("Bans…"));
bansButton->setEnabled(false);
bansButton->setToolTip(QStringLiteral("View and unban banned IP addresses"));
clientsHeaderRow->addWidget(kickButton);
clientsHeaderRow->addWidget(banButton);
clientsHeaderRow->addWidget(bansButton);
clientsLayout->addLayout(clientsHeaderRow);
clientsTable = new QTableWidget();
clientsTable->setColumnCount(5);
clientsTable->setHorizontalHeaderLabels({"Player ID", "Address", "Label", "Connected", "Packets"});
clientsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
clientsTable->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
clientsTable->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
clientsTable->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
clientsTable->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
clientsTable->verticalHeader()->setVisible(false);
clientsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
clientsTable->setSelectionMode(QAbstractItemView::SingleSelection);
clientsTable->setAlternatingRowColors(true);
clientsTable->setShowGrid(false);
clientsTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
clientsTable->setFocusPolicy(Qt::StrongFocus);
clientsLayout->addWidget(clientsTable);
mainSplitter->addWidget(clientsPane);
auto *logPane = new QFrame();
logPane->setObjectName("pane");
auto *logLayout = new QVBoxLayout(logPane);
logLayout->setContentsMargins(8, 4, 8, 8);
logLayout->setSpacing(4);
auto *logHeader = new QLabel("Server Log");
logHeader->setObjectName("paneHeader");
logLayout->addWidget(logHeader);
logViewer = new QTextEdit();
logViewer->setReadOnly(true);
logViewer->setLineWrapMode(QTextEdit::NoWrap);
logViewer->setFont(QFont("Consolas", 9));
logViewer->setPlaceholderText("Server output appears here…");
logLayout->addWidget(logViewer);
mainSplitter->addWidget(logPane);
mainSplitter->setStretchFactor(0, 2);
mainSplitter->setStretchFactor(1, 3);
mainSplitter->setSizes({220, 320});
mainLayout->addWidget(mainSplitter, 1);
statusBar()->showMessage("Ready");
}
void MainWindow::setupStyles() {
// Force a light application palette so Windows dark mode cannot leave
// white text on the Host GUI's light backgrounds (status strip, dialogs).
QPalette lightPalette;
const QColor windowBg(243, 243, 243);
const QColor baseBg(255, 255, 255);
const QColor text(31, 31, 31);
const QColor muted(68, 68, 68);
const QColor disabled(153, 153, 153);
const QColor highlight(204, 228, 247);
lightPalette.setColor(QPalette::Window, windowBg);
lightPalette.setColor(QPalette::WindowText, text);
lightPalette.setColor(QPalette::Base, baseBg);
lightPalette.setColor(QPalette::AlternateBase, QColor(247, 247, 247));
lightPalette.setColor(QPalette::Text, text);
lightPalette.setColor(QPalette::Button, baseBg);
lightPalette.setColor(QPalette::ButtonText, text);
lightPalette.setColor(QPalette::BrightText, text);
lightPalette.setColor(QPalette::ToolTipBase, baseBg);
lightPalette.setColor(QPalette::ToolTipText, text);
lightPalette.setColor(QPalette::PlaceholderText, muted);
lightPalette.setColor(QPalette::Highlight, highlight);
lightPalette.setColor(QPalette::HighlightedText, text);
lightPalette.setColor(QPalette::Link, QColor(59, 121, 183));
lightPalette.setColor(QPalette::Disabled, QPalette::WindowText, disabled);
lightPalette.setColor(QPalette::Disabled, QPalette::Text, disabled);
lightPalette.setColor(QPalette::Disabled, QPalette::ButtonText, disabled);
qApp->setPalette(lightPalette);
const QString stylesheet = R"(
* {
font-family: "Segoe UI", "Segoe UI Variable", sans-serif;
font-size: 12px;
}
QMainWindow, QDialog, QInputDialog, QMessageBox {
background-color: #f3f3f3;
color: #1f1f1f;
}
QLabel {
color: #1f1f1f;
background-color: transparent;
}
QStatusBar {
background-color: #e8e8e8;
color: #444444;
border-top: 1px solid #c8c8c8;
}
QStatusBar QLabel {
color: #444444;
}
QFrame#toolBar {
background-color: #e8e8e8;
border-bottom: 1px solid #c0c0c0;
}
QFrame#statusStrip {
background-color: #ececec;
border-bottom: 1px solid #d0d0d0;
}
QFrame#statusStrip QLabel {
color: #1f1f1f;
}
QFrame#pane {
background-color: #f3f3f3;
border: none;
}
QLabel#serverNameLabel {
color: #1f1f1f;
font-weight: 600;
font-size: 13px;
}
QLabel#statusText {
color: #444444;
font-weight: 600;
min-width: 56px;
}
QLabel#statusDot {
background-color: #c44;
border: 1px solid #a33;
border-radius: 5px;
color: transparent;
}
QLabel#statusDot[running="true"] {
background-color: #2e8b4e;
border: 1px solid #246b3c;
}
QLabel#paneHeader {
color: #444444;
font-weight: 600;
font-size: 11px;
}
QLabel#statLabel {
color: #1f1f1f;
font-family: "Consolas", "Cascadia Mono", monospace;
font-size: 11px;
}
QInputDialog QLabel, QMessageBox QLabel {
color: #1f1f1f;
background-color: transparent;
}
QPushButton {
background-color: #ffffff;
color: #1f1f1f;
border: 1px solid #adadad;
border-radius: 2px;
padding: 4px 10px;
min-height: 24px;
}
QPushButton:hover {
background-color: #eef5fc;
border-color: #7aa7d4;
}
QPushButton:pressed {
background-color: #dceaf8;
}
QPushButton:disabled {
color: #999999;
background-color: #f0f0f0;
border-color: #d0d0d0;
}
QPushButton#startButton {
background-color: #2e8b4e;
color: #ffffff;
border-color: #246b3c;
font-weight: 600;
}
QPushButton#startButton:hover {
background-color: #359957;
}
QPushButton#startButton:pressed {
background-color: #246b3c;
}
QPushButton#startButton:disabled {
background-color: #9cbcab;
border-color: #8aa996;
color: #f0f0f0;
}
QPushButton#stopButton {
background-color: #c44;
color: #ffffff;
border-color: #a33;
font-weight: 600;
}
QPushButton#stopButton:hover {
background-color: #d25555;
}
QPushButton#stopButton:pressed {
background-color: #a33;
}
QPushButton#stopButton:disabled {
background-color: #d0a0a0;
border-color: #b88888;
color: #f0f0f0;
}
QTableWidget {
background-color: #ffffff;
alternate-background-color: #f7f7f7;
color: #1f1f1f;
border: 1px solid #c8c8c8;
gridline-color: #e6e6e6;
selection-background-color: #cce4f7;
selection-color: #1f1f1f;
}
QTableWidget::item {
padding: 2px 6px;
}
QHeaderView::section {
background-color: #ececec;
color: #333333;
padding: 4px 6px;
border: none;
border-right: 1px solid #d4d4d4;
border-bottom: 1px solid #c8c8c8;
font-weight: 600;
}
QTextEdit {
background-color: #ffffff;
color: #1a1a1a;
border: 1px solid #c8c8c8;
font-family: "Consolas", "Cascadia Mono", monospace;
font-size: 11px;
selection-background-color: #cce4f7;
selection-color: #1f1f1f;
}
QSplitter::handle:vertical {
background-color: #d0d0d0;
height: 3px;
margin: 0 8px;
}
QSplitter::handle:vertical:hover {
background-color: #7aa7d4;
}
QLineEdit, QPlainTextEdit, QSpinBox, QComboBox {
background-color: #ffffff;
color: #1f1f1f;
border: 1px solid #adadad;
border-radius: 2px;
padding: 3px 6px;
selection-background-color: #cce4f7;
selection-color: #1f1f1f;
}
QLineEdit:focus, QPlainTextEdit:focus, QSpinBox:focus, QComboBox:focus {
border-color: #3b79b7;
}
QComboBox::drop-down {
border: none;
width: 18px;
}
QComboBox QAbstractItemView {
background-color: #ffffff;
color: #1f1f1f;
selection-background-color: #cce4f7;
selection-color: #1f1f1f;
border: 1px solid #adadad;
}
QSpinBox::up-button, QSpinBox::down-button {
background-color: #ececec;
border: none;
width: 16px;
}
QDialogButtonBox QPushButton {
min-width: 72px;
}
)";
qApp->setStyle("Fusion");
qApp->setStyleSheet(stylesheet);
}
void MainWindow::setupConnections() {
connect(startButton, &QPushButton::clicked, this, &MainWindow::onStartServer);
connect(stopButton, &QPushButton::clicked, this, &MainWindow::onStopServer);
connect(configButton, &QPushButton::clicked, this, &MainWindow::onOpenConfig);
connect(kickButton, &QPushButton::clicked, this, &MainWindow::onKickSelectedClient);
connect(banButton, &QPushButton::clicked, this, &MainWindow::onBanSelectedClient);
connect(bansButton, &QPushButton::clicked, this, &MainWindow::onManageBans);
connect(clientsTable, &QTableWidget::itemSelectionChanged, this, &MainWindow::onClientSelectionChanged);
}
void MainWindow::resolveConfigPath() {
const QString serverDir = serverProcess ? serverProcess->serverDirectory() : QString();
if (!serverDir.isEmpty()) {
configFilePath = QDir(serverDir).absoluteFilePath(QStringLiteral("commonwealth-server.json"));
} else {
configFilePath = QDir(QCoreApplication::applicationDirPath())
.absoluteFilePath(QStringLiteral("commonwealth-server.json"));
}
}
void MainWindow::loadConfigIntoUi() {
ServerConfigData data;
QString error;
if (QFileInfo::exists(configFilePath) && ConfigDialog::loadFromFile(configFilePath, &data, &error)) {
applyConfigToUi(data);
} else {
applyConfigToUi(ServerConfigData{});
}
}
void MainWindow::applyConfigToUi(const ServerConfigData &config) {
serverName = config.serverName;
serverNameLabel->setText(serverName);
bindAddressLabel->setText(QStringLiteral("Bind: %1:%2").arg(config.host).arg(config.port));
if (serverProcess) {
serverProcess->setAdminPort(config.adminPort);
}
}
void MainWindow::setupTimer() {
statsTimer = new QTimer(this);
connect(statsTimer, &QTimer::timeout, this, &MainWindow::onUpdateStats);
}
void MainWindow::onStartServer() {
startButton->setEnabled(false);
statusBar()->showMessage("Starting server...");
loadConfigIntoUi();
serverProcess->start(configFilePath);
}
void MainWindow::onStopServer() {
stopButton->setEnabled(false);
statusBar()->showMessage("Stopping server...");
serverProcess->stop();
}
void MainWindow::onOpenConfig() {
resolveConfigPath();
if (serverProcess && serverProcess->serverDirectory().isEmpty()) {
QMessageBox::warning(
this,
QStringLiteral("Server Settings"),
QStringLiteral("Server directory not found. Settings cannot be saved until the Python server folder is available."));
return;
}
ConfigDialog dialog(configFilePath, this);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const ServerConfigData data = dialog.config();
applyConfigToUi(data);
addLogMessage(QStringLiteral("[GUI] Saved server settings to %1").arg(configFilePath));
if (isServerRunning) {
statusBar()->showMessage(QStringLiteral("Settings saved — restart the server to apply"));
QMessageBox::information(
this,
QStringLiteral("Settings saved"),
QStringLiteral("Settings were saved.\n\nStop and start the server for them to take effect."));
} else {
statusBar()->showMessage(QStringLiteral("Settings saved"));
}
}
void MainWindow::onServerStarted() {
isServerRunning = true;
updateServerStatus(true);
bansButton->setEnabled(true);
statsTimer->start(1000); // Update every second
statusBar()->showMessage("Server running");
addLogMessage("[GUI] Server started successfully");
// Give the Python admin port a moment to bind before the first poll.
QTimer *startupPoll = new QTimer(this);
startupPoll->setSingleShot(true);
connect(startupPoll, &QTimer::timeout, this, [this, startupPoll]() {
onUpdateStats();
startupPoll->deleteLater();
});
startupPoll->start(750);
}
void MainWindow::onServerStopped() {
isServerRunning = false;
updateServerStatus(false);
statsTimer->stop();
clientsTable->setRowCount(0);
bansButton->setEnabled(false);
onClientSelectionChanged();
clientsLabel->setText(QStringLiteral("Clients: 0"));
uptimeLabel->setText(QStringLiteral("Uptime: 0s"));
transformPacketsLabel->setText(QStringLiteral("Transform: 0 / 0"));
worldStatePacketsLabel->setText(QStringLiteral("WorldState: 0 / 0"));
lanAddressLabel->setText(QStringLiteral("LAN: —"));
statusBar()->showMessage("Server stopped");
addLogMessage("[GUI] Server stopped");
}
void MainWindow::updateServerStatus(bool running) {
statusDot->setProperty("running", running);
statusDot->style()->unpolish(statusDot);
statusDot->style()->polish(statusDot);
if (running) {
statusIndicator->setText("Running");
startButton->setEnabled(false);
stopButton->setEnabled(true);
} else {
statusIndicator->setText("Stopped");
startButton->setEnabled(true);
stopButton->setEnabled(false);
}
}
void MainWindow::onServerLog(const QString &message) {
addLogMessage(message);
}
void MainWindow::addLogMessage(const QString &message) {
QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss");
logViewer->append(QString("[%1] %2").arg(timestamp, message));
// Auto-scroll to bottom
QTextCursor cursor = logViewer->textCursor();
cursor.movePosition(QTextCursor::End);
logViewer->setTextCursor(cursor);
}
void MainWindow::onUpdateStats() {
if (serverProcess) {
serverProcess->fetchStats();
}
}
void MainWindow::onStatsUpdated(const QJsonObject &stats) {
applyStatsToUi(stats);
}
void MainWindow::applyStatsToUi(const QJsonObject &stats) {
const int connected = stats.value(QStringLiteral("connected_clients")).toInt();
clientsLabel->setText(QStringLiteral("Clients: %1").arg(connected));
const double uptime = stats.value(QStringLiteral("uptime_seconds")).toDouble();
if (uptime < 60.0) {
uptimeLabel->setText(QStringLiteral("Uptime: %1s").arg(static_cast<int>(uptime)));
} else if (uptime < 3600.0) {
const int minutes = static_cast<int>(uptime / 60.0);
const int seconds = static_cast<int>(uptime) % 60;
uptimeLabel->setText(QStringLiteral("Uptime: %1m %2s").arg(minutes).arg(seconds));
} else {
const int hours = static_cast<int>(uptime / 3600.0);
const int minutes = (static_cast<int>(uptime) % 3600) / 60;
uptimeLabel->setText(QStringLiteral("Uptime: %1h %2m").arg(hours).arg(minutes));
}
transformPacketsLabel->setText(
QStringLiteral("Transform: %1 / %2")
.arg(stats.value(QStringLiteral("transform_packets_received")).toInt())
.arg(stats.value(QStringLiteral("transform_packets_broadcast")).toInt()));
worldStatePacketsLabel->setText(
QStringLiteral("WorldState: %1 / %2")
.arg(stats.value(QStringLiteral("world_state_packets_received")).toInt())
.arg(stats.value(QStringLiteral("world_state_packets_broadcast")).toInt()));
const QString host = stats.value(QStringLiteral("host")).toString();
const QString port = stats.value(QStringLiteral("port")).toString();
if (!host.isEmpty() && !port.isEmpty()) {
bindAddressLabel->setText(QStringLiteral("Bind: %1:%2").arg(host, port));
}
// LAN addresses are not currently in the stats snapshot; leave placeholder.
}
void MainWindow::onUpdateClients(const QJsonArray &clients) {
const int selectedRow = clientsTable->currentRow();
int selectedPlayerId = -1;
if (selectedRow >= 0 && clientsTable->item(selectedRow, 0)) {
selectedPlayerId = clientsTable->item(selectedRow, 0)->text().toInt();
}
clientsTable->setRowCount(0);
int restoreRow = -1;
for (int i = 0; i < clients.size(); ++i) {
QJsonObject client = clients[i].toObject();
int row = clientsTable->rowCount();
clientsTable->insertRow(row);
const int playerId = client.value(QStringLiteral("player_id")).toInt();
QString connectedText = QStringLiteral("?");
const QJsonValue connectedValue = client.value(QStringLiteral("connected_at"));
if (connectedValue.isDouble()) {
connectedText = QDateTime::fromSecsSinceEpoch(
static_cast<qint64>(connectedValue.toDouble())).toString(QStringLiteral("HH:mm:ss"));
} else if (connectedValue.isString()) {
connectedText = connectedValue.toString();
}
clientsTable->setItem(row, 0, new QTableWidgetItem(QString::number(playerId)));
clientsTable->setItem(row, 1, new QTableWidgetItem(client.value(QStringLiteral("address")).toString()));
clientsTable->setItem(row, 2, new QTableWidgetItem(client.value(QStringLiteral("label")).toString()));
clientsTable->setItem(row, 3, new QTableWidgetItem(connectedText));
clientsTable->setItem(row, 4, new QTableWidgetItem(QString::number(client.value(QStringLiteral("packets_sent")).toInt())));
if (playerId == selectedPlayerId) {
restoreRow = row;
}
}
if (restoreRow >= 0) {
clientsTable->selectRow(restoreRow);
}
onClientSelectionChanged();
}
void MainWindow::onClientSelectionChanged() {
const bool hasSelection = isServerRunning && clientsTable->currentRow() >= 0;
kickButton->setEnabled(hasSelection);
banButton->setEnabled(hasSelection);
}
void MainWindow::onKickSelectedClient() {
const int row = clientsTable->currentRow();
if (row < 0 || !serverProcess) {
return;
}
const int playerId = clientsTable->item(row, 0)->text().toInt();
const QString address = clientsTable->item(row, 1)->text();
const auto result = QMessageBox::question(
this,
QStringLiteral("Kick player"),
QStringLiteral("Kick player %1 (%2)? They can reconnect.").arg(playerId).arg(address));
if (result != QMessageBox::Yes) {
return;
}
serverProcess->kickPlayer(playerId);
}
void MainWindow::onBanSelectedClient() {
const int row = clientsTable->currentRow();
if (row < 0 || !serverProcess) {
return;
}
const int playerId = clientsTable->item(row, 0)->text().toInt();
const QString address = clientsTable->item(row, 1)->text();
const QString ip = address.section(QLatin1Char(':'), 0, 0);
const auto result = QMessageBox::question(
this,
QStringLiteral("Ban IP"),
QStringLiteral(
"Ban IP %1 (player %2)?\n\n"
"They will be disconnected and cannot reconnect until unbanned.")
.arg(ip)
.arg(playerId));
if (result != QMessageBox::Yes) {
return;
}
serverProcess->banPlayer(playerId);
}
void MainWindow::onManageBans() {
if (!serverProcess || !isServerRunning) {
return;
}
const QJsonArray bans = serverProcess->listBans();
if (bans.isEmpty()) {
QMessageBox::information(
this,
QStringLiteral("Banned IPs"),
QStringLiteral("No IPs are currently banned."));
return;
}
QStringList lines;
QStringList ips;
for (const QJsonValue &value : bans) {
const QJsonObject entry = value.toObject();
const QString ip = entry.value(QStringLiteral("ip")).toString();
if (ip.isEmpty()) {
continue;
}
ips.append(ip);
const QString reason = entry.value(QStringLiteral("reason")).toString();
lines.append(reason.isEmpty() ? ip : QStringLiteral("%1 — %2").arg(ip, reason));
}
if (ips.isEmpty()) {
QMessageBox::information(
this,
QStringLiteral("Banned IPs"),
QStringLiteral("No IPs are currently banned."));
return;
}
bool ok = false;
const QString chosen = QInputDialog::getItem(
this,
QStringLiteral("Banned IPs"),
QStringLiteral("Select an IP to unban:"),
lines,
0,
false,
&ok);
if (!ok || chosen.isEmpty()) {
return;
}
const int index = lines.indexOf(chosen);
if (index < 0 || index >= ips.size()) {
return;
}
const QString ip = ips.at(index);
const auto confirm = QMessageBox::question(
this,
QStringLiteral("Unban IP"),
QStringLiteral("Remove %1 from the ban list?").arg(ip));
if (confirm != QMessageBox::Yes) {
return;
}
serverProcess->unbanIp(ip);
}
void MainWindow::onAdminCommandFinished(bool ok, const QString &message) {
if (ok) {
addLogMessage(QStringLiteral("[ADMIN] %1").arg(message));
statusBar()->showMessage(message);
if (serverProcess) {
serverProcess->fetchStats();
}
} else {
addLogMessage(QStringLiteral("[ADMIN ERROR] %1").arg(message));
statusBar()->showMessage(QStringLiteral("Admin error: %1").arg(message));
QMessageBox::warning(this, QStringLiteral("Admin command failed"), message);
}
}
void MainWindow::onServerError(const QString &error) {
addLogMessage(QString("[ERROR] %1").arg(error));
statusBar()->showMessage(QString("Error: %1").arg(error));
}
void MainWindow::closeEvent(QCloseEvent *event) {
if (isServerRunning) {
serverProcess->stop();
}
event->accept();
}
+93
View File
@@ -0,0 +1,93 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QLabel>
#include <QPushButton>
#include <QTableWidget>
#include <QTextEdit>
#include <QSplitter>
#include <QTimer>
#include "ServerProcess.h"
#include "ConfigDialog.h"
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
protected:
void closeEvent(QCloseEvent *event) override;
private slots:
void onStartServer();
void onStopServer();
void onOpenConfig();
void onServerStarted();
void onServerStopped();
void onServerLog(const QString &message);
void onUpdateStats();
void onUpdateClients(const QJsonArray &clients);
void onServerError(const QString &error);
void onKickSelectedClient();
void onBanSelectedClient();
void onManageBans();
void onClientSelectionChanged();
void onAdminCommandFinished(bool ok, const QString &message);
void onStatsUpdated(const QJsonObject &stats);
private:
void setupUI();
void setupStyles();
void setupConnections();
void setupTimer();
void updateServerStatus(bool running);
void addLogMessage(const QString &message);
void applyStatsToUi(const QJsonObject &stats);
void resolveConfigPath();
void loadConfigIntoUi();
void applyConfigToUi(const ServerConfigData &config);
// UI Components
QWidget *centralWidget;
// Toolbar
QLabel *serverNameLabel;
QLabel *statusDot;
QLabel *statusIndicator;
QPushButton *startButton;
QPushButton *stopButton;
QPushButton *configButton;
// Status strip
QLabel *uptimeLabel;
QLabel *clientsLabel;
QLabel *transformPacketsLabel;
QLabel *worldStatePacketsLabel;
QLabel *bindAddressLabel;
QLabel *lanAddressLabel;
// Main panes
QSplitter *mainSplitter;
QTableWidget *clientsTable;
QPushButton *kickButton;
QPushButton *banButton;
QPushButton *bansButton;
QTextEdit *logViewer;
// Server Process
ServerProcess *serverProcess;
// Timer for stats updates
QTimer *statsTimer;
// State
bool isServerRunning;
QString serverName;
QString configFilePath;
};
#endif // MAINWINDOW_H
+336
View File
@@ -0,0 +1,336 @@
#include "ServerProcess.h"
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDir>
#include <QFileInfo>
#include <QHostAddress>
namespace {
constexpr int kDefaultAdminPort = 7779;
constexpr int kAdminTimeoutMs = 3000;
}
ServerProcess::ServerProcess(QObject *parent)
: QObject(parent)
, process(nullptr)
, running(false)
, m_adminPort(kDefaultAdminPort)
, adminFailCount(0)
{
pythonPath = findPythonExecutable();
serverDir = findServerDirectory();
}
ServerProcess::~ServerProcess() {
stop();
}
void ServerProcess::setAdminPort(int port) {
m_adminPort = (port > 0 && port <= 65535) ? port : kDefaultAdminPort;
}
int ServerProcess::adminPort() const {
return m_adminPort;
}
void ServerProcess::start(const QString &configPath) {
if (running) {
emit error("Server is already running");
return;
}
if (pythonPath.isEmpty()) {
emit error("Python not found. Please install Python 3.9+ and add it to PATH");
return;
}
if (serverDir.isEmpty()) {
emit error("Server directory not found");
return;
}
process = new QProcess(this);
connect(process, SIGNAL(finished(int, QProcess::ExitStatus)),
this, SLOT(onProcessFinished(int, QProcess::ExitStatus)));
connect(process, SIGNAL(error(QProcess::ProcessError)),
this, SLOT(onProcessError(QProcess::ProcessError)));
connect(process, SIGNAL(readyReadStandardOutput()),
this, SLOT(onReadyReadStandardOutput()));
connect(process, SIGNAL(readyReadStandardError()),
this, SLOT(onReadyReadStandardError()));
connect(process, SIGNAL(started()),
this, SLOT(onProcessStarted()));
QStringList arguments;
arguments << "consumer_server_cli.py" << "serve" << "--config" << configPath;
process->setWorkingDirectory(serverDir);
process->start(pythonPath, arguments);
}
void ServerProcess::stop() {
if (!process) return;
if (process->state() == QProcess::Running) {
process->terminate();
if (!process->waitForFinished(3000)) {
process->kill();
process->waitForFinished();
}
}
running = false;
adminFailCount = 0;
}
QJsonObject ServerProcess::sendAdminCommand(const QJsonObject &request) {
QTcpSocket socket;
// Force IPv4 loopback — matches server admin bind on 127.0.0.1.
socket.connectToHost(QStringLiteral("127.0.0.1"), static_cast<quint16>(m_adminPort));
if (!socket.waitForConnected(kAdminTimeoutMs)) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"),
QStringLiteral("Could not connect to admin port 127.0.0.1:%1 (%2)")
.arg(m_adminPort)
.arg(socket.errorString())}
};
}
const QByteArray payload = QJsonDocument(request).toJson(QJsonDocument::Compact) + '\n';
if (socket.write(payload) < 0 || !socket.waitForBytesWritten(kAdminTimeoutMs)) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Failed to send admin command")}
};
}
QByteArray buffer;
while (!buffer.contains('\n')) {
if (!socket.waitForReadyRead(kAdminTimeoutMs)) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Timed out waiting for admin response")}
};
}
buffer += socket.readAll();
}
const int newline = buffer.indexOf('\n');
const QByteArray line = buffer.left(newline);
QJsonParseError parseError{};
const QJsonDocument doc = QJsonDocument::fromJson(line, &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Invalid admin response JSON")}
};
}
return doc.object();
}
void ServerProcess::fetchStats() {
if (!running) return;
const QJsonObject statsResponse = sendAdminCommand(QJsonObject{{QStringLiteral("cmd"), QStringLiteral("stats")}});
if (statsResponse.value(QStringLiteral("ok")).toBool()) {
adminFailCount = 0;
emit statsUpdated(statsResponse.value(QStringLiteral("data")).toObject());
} else {
++adminFailCount;
// Avoid spamming the log every second while the admin port is still starting.
if (adminFailCount == 3 || adminFailCount == 10 || (adminFailCount % 30) == 0) {
emit logMessage(QStringLiteral("[ADMIN] %1")
.arg(statsResponse.value(QStringLiteral("error"))
.toString(QStringLiteral("Admin stats request failed."))));
}
return;
}
const QJsonObject clientsResponse = sendAdminCommand(QJsonObject{{QStringLiteral("cmd"), QStringLiteral("clients")}});
if (clientsResponse.value(QStringLiteral("ok")).toBool()) {
const QJsonObject data = clientsResponse.value(QStringLiteral("data")).toObject();
emit clientsUpdated(data.value(QStringLiteral("clients")).toArray());
} else {
emit logMessage(QStringLiteral("[ADMIN] %1")
.arg(clientsResponse.value(QStringLiteral("error"))
.toString(QStringLiteral("Admin clients request failed."))));
}
}
bool ServerProcess::kickPlayer(int playerId, const QString &reason) {
QJsonObject request{
{QStringLiteral("cmd"), QStringLiteral("kick")},
{QStringLiteral("playerId"), playerId},
{QStringLiteral("reason"), reason}
};
const QJsonObject response = sendAdminCommand(request);
const bool ok = response.value(QStringLiteral("ok")).toBool();
const QString message = ok
? response.value(QStringLiteral("message")).toString(QStringLiteral("Player kicked."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Kick failed."));
emit adminCommandFinished(ok, message);
return ok;
}
bool ServerProcess::banPlayer(int playerId, const QString &reason) {
QJsonObject request{
{QStringLiteral("cmd"), QStringLiteral("ban")},
{QStringLiteral("playerId"), playerId},
{QStringLiteral("reason"), reason}
};
const QJsonObject response = sendAdminCommand(request);
const bool ok = response.value(QStringLiteral("ok")).toBool();
const QString message = ok
? response.value(QStringLiteral("message")).toString(QStringLiteral("Player banned."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Ban failed."));
emit adminCommandFinished(ok, message);
if (ok) {
fetchStats();
}
return ok;
}
bool ServerProcess::unbanIp(const QString &ip) {
QJsonObject request{
{QStringLiteral("cmd"), QStringLiteral("unban")},
{QStringLiteral("ip"), ip}
};
const QJsonObject response = sendAdminCommand(request);
const bool ok = response.value(QStringLiteral("ok")).toBool();
const QString message = ok
? response.value(QStringLiteral("message")).toString(QStringLiteral("IP unbanned."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Unban failed."));
emit adminCommandFinished(ok, message);
return ok;
}
QJsonArray ServerProcess::listBans() {
const QJsonObject response = sendAdminCommand(QJsonObject{{QStringLiteral("cmd"), QStringLiteral("bans")}});
if (!response.value(QStringLiteral("ok")).toBool()) {
emit adminCommandFinished(
false,
response.value(QStringLiteral("error")).toString(QStringLiteral("Could not list bans.")));
return {};
}
return response.value(QStringLiteral("data")).toObject().value(QStringLiteral("bans")).toArray();
}
bool ServerProcess::isRunning() const {
return running && process && process->state() == QProcess::Running;
}
QString ServerProcess::serverDirectory() const {
return serverDir;
}
void ServerProcess::onProcessStarted() {
running = true;
adminFailCount = 0;
emit started();
}
void ServerProcess::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) {
running = false;
adminFailCount = 0;
emit stopped();
if (exitStatus == QProcess::NormalExit) {
emit logMessage(QString("Server exited with code %1").arg(exitCode));
} else {
emit error("Server process crashed");
}
}
void ServerProcess::onProcessError(QProcess::ProcessError error) {
QString errorString;
switch (error) {
case QProcess::FailedToStart:
errorString = "Failed to start Python process";
break;
case QProcess::Crashed:
errorString = "Server process crashed";
break;
case QProcess::Timedout:
errorString = "Server process timed out";
break;
default:
errorString = "Unknown process error";
}
emit this->error(errorString);
}
void ServerProcess::onReadyReadStandardOutput() {
if (!process) return;
outputBuffer += process->readAllStandardOutput();
while (outputBuffer.contains('\n')) {
int newlinePos = outputBuffer.indexOf('\n');
QString line = outputBuffer.left(newlinePos);
outputBuffer = outputBuffer.mid(newlinePos + 1);
if (!line.isEmpty()) {
line = line.trimmed();
parseLogLine(line);
emit logMessage(line);
}
}
}
void ServerProcess::onReadyReadStandardError() {
if (!process) return;
QString errorOutput = process->readAllStandardError();
emit logMessage("[STDERR] " + errorOutput);
}
QString ServerProcess::findPythonExecutable() {
QProcess proc;
proc.start("python", QStringList() << "--version");
if (proc.waitForFinished(2000)) {
return "python";
}
proc.start("python3", QStringList() << "--version");
if (proc.waitForFinished(2000)) {
return "python3";
}
return "";
}
QString ServerProcess::findServerDirectory() {
QDir dir(QCoreApplication::applicationDirPath());
for (int i = 0; i < 8; ++i) {
const QString candidate = dir.absoluteFilePath(QStringLiteral("server"));
if (QFileInfo::exists(candidate + QStringLiteral("/consumer_server_cli.py")) &&
QFileInfo::exists(candidate + QStringLiteral("/admin_server.py"))) {
return QFileInfo(candidate).absoluteFilePath();
}
if (!dir.cdUp()) {
break;
}
}
// Fallback: accept server dirs without admin_server.py so older layouts still launch,
// but prefer ones that include the admin channel when present.
dir = QDir(QCoreApplication::applicationDirPath());
for (int i = 0; i < 8; ++i) {
const QString candidate = dir.absoluteFilePath(QStringLiteral("server"));
if (QFileInfo::exists(candidate + QStringLiteral("/consumer_server_cli.py"))) {
return QFileInfo(candidate).absoluteFilePath();
}
if (!dir.cdUp()) {
break;
}
}
return QString();
}
void ServerProcess::parseLogLine(const QString &line) {
Q_UNUSED(line);
}
+60
View File
@@ -0,0 +1,60 @@
#ifndef SERVERPROCESS_H
#define SERVERPROCESS_H
#include <QObject>
#include <QProcess>
#include <QJsonArray>
#include <QJsonObject>
#include <QTcpSocket>
class ServerProcess : public QObject {
Q_OBJECT
public:
explicit ServerProcess(QObject *parent = nullptr);
~ServerProcess();
void start(const QString &configPath);
void stop();
void fetchStats();
bool kickPlayer(int playerId, const QString &reason = QString());
bool banPlayer(int playerId, const QString &reason = QString());
bool unbanIp(const QString &ip);
QJsonArray listBans();
bool isRunning() const;
QString serverDirectory() const;
void setAdminPort(int port);
int adminPort() const;
signals:
void started();
void stopped();
void logMessage(const QString &message);
void clientsUpdated(const QJsonArray &clients);
void statsUpdated(const QJsonObject &stats);
void error(const QString &message);
void adminCommandFinished(bool ok, const QString &message);
private slots:
void onProcessStarted();
void onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus);
void onProcessError(QProcess::ProcessError error);
void onReadyReadStandardOutput();
void onReadyReadStandardError();
private:
QString findPythonExecutable();
QString findServerDirectory();
void parseLogLine(const QString &line);
QJsonObject sendAdminCommand(const QJsonObject &request);
QProcess *process;
QString pythonPath;
QString serverDir;
bool running;
QString outputBuffer;
int m_adminPort;
int adminFailCount;
};
#endif // SERVERPROCESS_H
+16
View File
@@ -0,0 +1,16 @@
#include <QApplication>
#include "MainWindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
app.setApplicationName("Commonwealth Online Host");
app.setApplicationVersion("1.0.0");
app.setApplicationDisplayName("Commonwealth Online — Server Host");
MainWindow window;
window.show();
return app.exec();
}
+1
View File
@@ -0,0 +1 @@
IDI_ICON1 ICON "icons/app.ico"
+1
View File
@@ -0,0 +1 @@
<!-- Placeholder for app.ico - Qt will handle resource loading gracefully if missing -->
+5
View File
@@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>icons/app.ico</file>
</qresource>
</RCC>