Sync from GitHub main #1

Open
nomad wants to merge 145 commits from sync/from-github into main
19 changed files with 89 additions and 2067 deletions
Showing only changes of commit 0accc17b66 - Show all commits
+30
View File
@@ -0,0 +1,30 @@
name: Host GUI (Avalonia)
on:
push:
paths:
- "host/**"
- ".github/workflows/host.yml"
pull_request:
paths:
- "host/**"
- ".github/workflows/host.yml"
workflow_dispatch:
jobs:
build:
name: Build Avalonia host
runs-on: [self-hosted, Linux, X64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
# The runner user cannot write to system /usr/share/dotnet; install the
# pinned SDK into a runner-writable, cached path instead.
env:
DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet
with:
dotnet-version: "8.0.x"
- name: Build host
run: dotnet build host/CommonwealthOnline.Host.csproj -c Release --nologo
-68
View File
@@ -1,68 +0,0 @@
cmake_minimum_required(VERSION 3.20)
project(CommonwealthOnlineHost)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
find_package(Qt6 COMPONENTS Core Gui Widgets Network Concurrent REQUIRED)
find_program(DOTNET_EXECUTABLE dotnet REQUIRED)
set(PROJECT_SOURCES
src/main.cpp
src/MainWindow.h
src/MainWindow.cpp
src/ConfigDialog.h
src/ConfigDialog.cpp
src/ServerProcess.h
src/ServerProcess.cpp
src/resources/resources.qrc
)
add_executable(CommonwealthOnlineHost ${PROJECT_SOURCES})
target_link_libraries(CommonwealthOnlineHost Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Concurrent)
if(WIN32)
set_target_properties(CommonwealthOnlineHost PROPERTIES WIN32_EXECUTABLE ON VS_DPI_AWARE "ON")
set(CO_SERVER_RID "win-x64")
set(CO_SERVER_EXECUTABLE_SUFFIX ".exe")
else()
set(CO_SERVER_RID "linux-x64")
set(CO_SERVER_EXECUTABLE_SUFFIX "")
endif()
set_target_properties(CommonwealthOnlineHost PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
set(CO_SERVER_SOURCE_DIR "${CMAKE_SOURCE_DIR}/server")
set(CO_SERVER_PROJECT "${CO_SERVER_SOURCE_DIR}/CommonwealthOnline.Server.csproj")
set(CO_SERVER_PUBLISH_DIR "${CMAKE_BINARY_DIR}/server-publish/${CO_SERVER_RID}")
set(CO_SERVER_STAGE_DIR "$<TARGET_FILE_DIR:CommonwealthOnlineHost>/server")
set(CO_SERVER_STAGE_CONFIG "${CO_SERVER_STAGE_DIR}/commonwealth-server.json")
if(NOT EXISTS "${CO_SERVER_PROJECT}")
message(FATAL_ERROR "Bundled C# server project not found at ${CO_SERVER_PROJECT}")
endif()
add_custom_command(TARGET CommonwealthOnlineHost POST_BUILD
COMMAND ${CMAKE_COMMAND} -E rm -rf "${CO_SERVER_PUBLISH_DIR}"
COMMAND ${DOTNET_EXECUTABLE} publish "${CO_SERVER_PROJECT}"
-c Release
-r ${CO_SERVER_RID}
--self-contained true
-p:PublishSingleFile=true
-p:DebugType=None
-p:DebugSymbols=false
-o "${CO_SERVER_PUBLISH_DIR}"
--nologo
COMMAND ${CMAKE_COMMAND}
-DCO_SERVER_SOURCE_DIR=${CO_SERVER_SOURCE_DIR}
-DCO_SERVER_PUBLISH_DIR=${CO_SERVER_PUBLISH_DIR}
-DCO_SERVER_STAGE_DIR=${CO_SERVER_STAGE_DIR}
-DCO_SERVER_STAGE_CONFIG=${CO_SERVER_STAGE_CONFIG}
-DCO_SERVER_EXECUTABLE_SUFFIX=${CO_SERVER_EXECUTABLE_SUFFIX}
-P "${CMAKE_SOURCE_DIR}/cmake/stage_server.cmake"
COMMENT "Publishing and staging C# Commonwealth Online server"
VERBATIM
)
+28 -41
View File
@@ -1,55 +1,42 @@
# Commonwealth Online Server and Qt Host # Commonwealth Online Server & Host
This repository contains the Commonwealth Online authoritative dedicated server and the Qt Host GUI. The dedicated server and its host GUI for Commonwealth Online (Fallout 4
multiplayer). Everything now builds on one .NET 8 toolchain.
The dedicated server is C#/.NET. Valve GameNetworkingSockets remains behind the small native C++ bridge in `server/native_transport/`. ## Components
## Server - **[`server/`](server/README.md)** — the dedicated relay server
(`CommonwealthOnline.Server`, a .NET 8 console app). Run it with
`server/start.sh` / `server/start.bat`, or `CommonwealthOnline.Server serve`.
- **[`host/`](host/README.md)** — the **Avalonia** cross-platform host GUI:
start/stop the server, edit config, watch the log, and manage players
(kick/ban). Replaces the former Qt/C++ host.
## Quick start — server
```bash ```bash
cd server cd server
dotnet build CommonwealthOnline.Server.csproj -c Release ./start.sh # Linux / macOS
dotnet run --project tests/CommonwealthOnline.Server.Tests.csproj -c Release start.bat # Windows
dotnet run --project CommonwealthOnline.Server.csproj -- serve --config commonwealth-server.json --interactive
``` ```
The server owns Protocol V2 admission, server-owned player IDs, packet validation, movement validation, interest filtering, durable player state, scoped NPC authority and epochs, combat routing, world state, bans, rate limits, LAN discovery and localhost administration. Needs the .NET 8 runtime (or the SDK for a source checkout). Allow TCP `7777`
(and optionally UDP `7778` for LAN discovery) through the firewall. `0.0.0.0`
is a bind address, not the address players join.
Transport policy remains: ## Quick start — host GUI
- `transform`, `npcState`: unreliable/sequenced under GNS ```bash
- session/control, player state, combat, world state and authority: reliable/ordered dotnet run --project host/CommonwealthOnline.Host.csproj
TCP compatibility keeps newline framing inside the TCP transport only. GNS is message-oriented and uses the `COG2` snapshot sequence envelope for latest-wins snapshots.
## Native GNS bridge
`server/native_transport` stays C++ and owns only GNS listen/connection/message mechanics and endpoint lookup. C# loads the existing C ABI directly.
## Qt Host GUI
The Qt Host remains native C++. It launches the published `CommonwealthOnline.Server` process and uses the authenticated localhost admin channel for stats, clients, kicks and bans.
Build requirements on Windows:
- Visual Studio 2022 C++ tools
- CMake 3.20+
- Qt 6.4+
- .NET 8 SDK
```bat
check-setup.bat
build.bat
deploy.bat
``` ```
CMake publishes the C# server self-contained and stages it under the Host GUI `server` directory. Packaged users do not need a separate .NET runtime. ## Build
## Default ports ```bash
dotnet build server/CommonwealthOnline.Server.csproj -c Release
dotnet build host/CommonwealthOnline.Host.csproj -c Release
```
- TCP 7777: gameplay compatibility > The Qt/C++ host and its CMake build were retired in favour of the Avalonia
- UDP 7777: GNS gameplay when enabled > host. `SETUP.md` / `DEVELOPMENT.md` / `DEPLOYMENT.md` are from the Qt era and
- UDP 7778: LAN discovery > are pending a refresh.
- TCP 127.0.0.1:7779: authenticated admin control
See [server/README.md](server/README.md), [SETUP.md](SETUP.md), [DEVELOPMENT.md](DEVELOPMENT.md), and [DEPLOYMENT.md](DEPLOYMENT.md).
-55
View File
@@ -1,55 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo.
echo ================================================================================
echo Commonwealth Online - Qt Host and C# Server Build
echo ================================================================================
echo.
cmake --version >nul 2>&1 || (
echo ERROR: CMake is not installed or not in PATH.
exit /b 1
)
dotnet --version >nul 2>&1 || (
echo ERROR: .NET 8 SDK is not installed or dotnet is not in PATH.
exit /b 1
)
set QT6_PATH=
set PATHS_TO_CHECK[0]=C:\Qt\6.11.1\msvc2022_64
set PATHS_TO_CHECK[1]=C:\Qt\6.10.2\msvc2022_64
set PATHS_TO_CHECK[2]=C:\Qt\6.8.0\msvc2022_64
set PATHS_TO_CHECK[3]=C:\Qt\6.7.0\msvc2022_64
set PATHS_TO_CHECK[4]=C:\Qt\6.6.0\msvc2022_64
set PATHS_TO_CHECK[5]=C:\Qt\6.5.0\msvc2022_64
set PATHS_TO_CHECK[6]=C:\Qt\6.4.0\msvc2022_64
for /l %%i in (0,1,6) do (
if exist "!PATHS_TO_CHECK[%%i]!\lib\cmake\Qt6" if "!QT6_PATH!"=="" set QT6_PATH=!PATHS_TO_CHECK[%%i]!
)
if "!QT6_PATH!"=="" (
echo ERROR: Qt6 not found at standard C:\Qt paths.
exit /b 1
)
echo Qt6: !QT6_PATH!
echo .NET:
dotnet --version
dotnet build server\CommonwealthOnline.Server.csproj -c Release --nologo || exit /b 1
dotnet run --project server\tests\CommonwealthOnline.Server.Tests.csproj -c Release || exit /b 1
if not exist build mkdir build
pushd build
cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="!QT6_PATH!" || (popd & exit /b 1)
cmake --build . --config Release || (popd & exit /b 1)
popd
echo.
echo ================================================================================
echo Build successful
echo ================================================================================
echo Host GUI: build\bin\Release\CommonwealthOnlineHost.exe
echo CMake also published and staged the self-contained C# server beside the host.
exit /b 0
-56
View File
@@ -1,56 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo.
echo ================================================================================
echo Commonwealth Online - Qt Host and C# Server Setup Check
echo ================================================================================
echo.
cmake --version >nul 2>&1
if errorlevel 1 (
echo [ERROR] CMake not found.
exit /b 1
)
echo [OK] CMake found
dotnet --version >nul 2>&1
if errorlevel 1 (
echo [ERROR] .NET 8 SDK not found or dotnet is not in PATH.
exit /b 1
)
for /f "tokens=*" %%V in ('dotnet --version') do set DOTNET_VERSION=%%V
echo [OK] .NET SDK found: !DOTNET_VERSION!
if not exist "C:\Program Files\Microsoft Visual Studio\2022\Community" if not exist "C:\Program Files\Microsoft Visual Studio\2022\Professional" if not exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise" (
echo [ERROR] Visual Studio 2022 not found.
exit /b 1
)
echo [OK] Visual Studio 2022 found
set QT6_FOUND=0
set QT_PATH=
for %%Q in (
"C:\Qt\6.11.1\msvc2022_64"
"C:\Qt\6.10.2\msvc2022_64"
"C:\Qt\6.8.0\msvc2022_64"
"C:\Qt\6.7.0\msvc2022_64"
"C:\Qt\6.6.0\msvc2022_64"
"C:\Qt\6.5.0\msvc2022_64"
"C:\Qt\6.4.0\msvc2022_64"
) do (
if exist "%%~Q\lib\cmake\Qt6" if !QT6_FOUND!==0 (
set QT6_FOUND=1
set QT_PATH=%%~Q
)
)
if %QT6_FOUND%==0 (
echo [ERROR] Qt6 MSVC 2022 package not found at the standard C:\Qt locations.
exit /b 1
)
echo [OK] Qt6 found at: %QT_PATH%
echo.
echo All prerequisites found. build.bat will build the Qt host and publish the bundled C# server.
exit /b 0
-69
View File
@@ -1,69 +0,0 @@
if(NOT EXISTS "${CO_SERVER_PUBLISH_DIR}")
message(FATAL_ERROR "Published C# server directory not found: ${CO_SERVER_PUBLISH_DIR}")
endif()
if(NOT EXISTS "${CO_SERVER_PUBLISH_DIR}/CommonwealthOnline.Server${CO_SERVER_EXECUTABLE_SUFFIX}")
message(FATAL_ERROR "Published C# server entrypoint not found: ${CO_SERVER_PUBLISH_DIR}/CommonwealthOnline.Server${CO_SERVER_EXECUTABLE_SUFFIX}")
endif()
set(_preserve_config "")
if(EXISTS "${CO_SERVER_STAGE_CONFIG}")
file(READ "${CO_SERVER_STAGE_CONFIG}" _preserve_config)
endif()
set(_stage_bans "${CO_SERVER_STAGE_DIR}/bans.json")
set(_preserve_bans "")
if(EXISTS "${_stage_bans}")
file(READ "${_stage_bans}" _preserve_bans)
endif()
set(_stage_admin_token "${CO_SERVER_STAGE_DIR}/.admin-token")
set(_preserve_admin_token "")
if(EXISTS "${_stage_admin_token}")
file(READ "${_stage_admin_token}" _preserve_admin_token)
endif()
file(REMOVE_RECURSE "${CO_SERVER_STAGE_DIR}")
file(MAKE_DIRECTORY "${CO_SERVER_STAGE_DIR}")
file(COPY "${CO_SERVER_PUBLISH_DIR}/" DESTINATION "${CO_SERVER_STAGE_DIR}")
if(NOT "${_preserve_config}" STREQUAL "")
file(WRITE "${CO_SERVER_STAGE_CONFIG}" "${_preserve_config}")
elseif(EXISTS "${CO_SERVER_SOURCE_DIR}/commonwealth-server.json")
file(COPY "${CO_SERVER_SOURCE_DIR}/commonwealth-server.json" DESTINATION "${CO_SERVER_STAGE_DIR}")
endif()
if(NOT "${_preserve_bans}" STREQUAL "")
file(WRITE "${_stage_bans}" "${_preserve_bans}")
endif()
if(NOT "${_preserve_admin_token}" STREQUAL "")
file(WRITE "${_stage_admin_token}" "${_preserve_admin_token}")
endif()
file(GLOB_RECURSE _prohibited_legacy_files
LIST_DIRECTORIES false
"${CO_SERVER_STAGE_DIR}/*.py"
"${CO_SERVER_STAGE_DIR}/*.pyw"
"${CO_SERVER_STAGE_DIR}/*.pyi"
"${CO_SERVER_STAGE_DIR}/*.pyc"
"${CO_SERVER_STAGE_DIR}/*.pyo"
"${CO_SERVER_STAGE_DIR}/*.whl"
"${CO_SERVER_STAGE_DIR}/*.egg"
"${CO_SERVER_STAGE_DIR}/requirements*.txt"
"${CO_SERVER_STAGE_DIR}/Pipfile"
"${CO_SERVER_STAGE_DIR}/Pipfile.lock"
"${CO_SERVER_STAGE_DIR}/pyproject.toml"
"${CO_SERVER_STAGE_DIR}/poetry.lock"
"${CO_SERVER_STAGE_DIR}/setup.py"
"${CO_SERVER_STAGE_DIR}/setup.cfg"
"${CO_SERVER_STAGE_DIR}/tox.ini"
)
if(_prohibited_legacy_files)
string(JOIN "\n " _prohibited_list ${_prohibited_legacy_files})
message(FATAL_ERROR "Prohibited legacy runtime artifacts were staged:\n ${_prohibited_list}")
endif()
if(NOT EXISTS "${CO_SERVER_STAGE_DIR}/CommonwealthOnline.Server${CO_SERVER_EXECUTABLE_SUFFIX}")
message(FATAL_ERROR "Failed to stage C# server entrypoint")
endif()
-57
View File
@@ -1,57 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo.
echo ================================================================================
echo Commonwealth Online - Qt Host Deployment
echo ================================================================================
echo.
set QT6_PATH=
if exist "C:\Qt\6.11.1\msvc2022_64\bin" set QT6_PATH=C:\Qt\6.11.1\msvc2022_64
if "!QT6_PATH!"=="" if exist "C:\Qt\6.10.2\msvc2022_64\bin" set QT6_PATH=C:\Qt\6.10.2\msvc2022_64
if "!QT6_PATH!"=="" if exist "C:\Qt\6.8.0\msvc2022_64\bin" set QT6_PATH=C:\Qt\6.8.0\msvc2022_64
if "!QT6_PATH!"=="" (
echo ERROR: Could not find Qt6 installation.
exit /b 1
)
set DEPLOY_DIR=%cd%\build\bin\Release
if not exist "!DEPLOY_DIR!\CommonwealthOnlineHost.exe" (
echo ERROR: Host executable not found. Run build.bat first.
exit /b 1
)
set DLLS=Qt6Core.dll Qt6Gui.dll Qt6Widgets.dll Qt6Network.dll Qt6Concurrent.dll Qt6DBus.dll Qt6Xml.dll
for %%D in (%DLLS%) do (
if exist "!QT6_PATH!\bin\%%D" copy /Y "!QT6_PATH!\bin\%%D" "!DEPLOY_DIR!\%%D" >nul
)
if not exist "!DEPLOY_DIR!\plugins" mkdir "!DEPLOY_DIR!\plugins"
xcopy /Y /Q /I "!QT6_PATH!\plugins\platforms" "!DEPLOY_DIR!\plugins\platforms\" >nul
xcopy /Y /Q /I "!QT6_PATH!\plugins\styles" "!DEPLOY_DIR!\plugins\styles\" >nul 2>&1
xcopy /Y /Q /I "!QT6_PATH!\plugins\imageformats" "!DEPLOY_DIR!\plugins\imageformats\" >nul 2>&1
set SERVER_DIR=!DEPLOY_DIR!\server
if not exist "!SERVER_DIR!\CommonwealthOnline.Server.exe" (
echo ERROR: Self-contained C# server is not staged beside the Host GUI.
echo Run build.bat. CMake publishes the server during the Host GUI build.
exit /b 1
)
set PROHIBITED_FOUND=0
for /r "!SERVER_DIR!" %%F in (*.py *.pyw *.pyi *.pyc *.pyo *.whl *.egg requirements*.txt Pipfile Pipfile.lock pyproject.toml poetry.lock setup.py setup.cfg tox.ini) do (
if exist "%%F" (
echo ERROR: Prohibited legacy runtime artifact found in staged package: %%F
set PROHIBITED_FOUND=1
)
)
if "!PROHIBITED_FOUND!"=="1" exit /b 1
echo.
echo Deployment complete:
echo !DEPLOY_DIR!\CommonwealthOnlineHost.exe
echo !SERVER_DIR!\CommonwealthOnline.Server.exe
echo.
echo Copy the entire Release directory when distributing. The bundled server is self-contained.
exit /b 0
-69
View File
@@ -1,69 +0,0 @@
@echo off
setlocal enabledelayedexpansion
echo.
echo ================================================================================
echo Qt Installation Finder
echo ================================================================================
echo.
REM Check common locations
echo Searching for Qt6 installation...
echo.
set FOUND=0
echo Checking C:\Qt...
if exist "C:\Qt" (
echo Found C:\Qt directory
dir /b "C:\Qt" | findstr /R "^[0-9]"
set FOUND=1
)
echo.
echo Checking C:\Program Files...
if exist "C:\Program Files\Qt" (
echo Found C:\Program Files\Qt
dir /b "C:\Program Files\Qt"
set FOUND=1
)
echo.
echo Checking C:\Program Files ^(x86^)...
if exist "C:\Program Files (x86)\Qt" (
echo Found C:\Program Files (x86)\Qt
dir /b "C:\Program Files (x86)\Qt"
set FOUND=1
)
echo.
echo Checking AppData...
if exist "%APPDATA%\Qt" (
echo Found %APPDATA%\Qt
dir /b "%APPDATA%\Qt"
set FOUND=1
)
if %FOUND%==0 (
echo.
echo [WARNING] Qt6 not found in common locations!
echo.
echo Please try one of the following:
echo.
echo 1. Install Qt6 from https://www.qt.io/download-open-source
echo Use default installation path: C:\Qt\6.8.0\
echo.
echo 2. If Qt is installed elsewhere, manually edit build.bat
echo and add your Qt path to the PATHS_TO_CHECK list
echo.
echo 3. Or run CMake manually with explicit path:
echo cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="path\to\your\Qt"
echo.
pause
exit /b 1
)
echo.
echo Installation search complete!
echo.
pause
+31
View File
@@ -0,0 +1,31 @@
# Commonwealth Online — Server Host (Avalonia)
Cross-platform C# GUI (.NET 8 + Avalonia) for running a Commonwealth Online
dedicated server. Replaces the former Qt/C++ host, so the whole project now
builds on one `dotnet` toolchain.
## Run
```
dotnet run --project host/CommonwealthOnline.Host.csproj
```
Launch it from (or point its working directory at) a folder that holds the
server — a published `CommonwealthOnline.Server` executable, the framework
`CommonwealthOnline.Server.dll`, or a source checkout — alongside
`commonwealth-server.json`.
## Features
- Edit and save `commonwealth-server.json` (host, port, name, max players,
admin port, log verbosity, GNS toggle).
- Start / Stop the server and stream its output to a live log.
- Live player list with **Kick** / **Ban**, driven by the server's
token-authenticated admin port.
## Publish
```
dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r win-x64 --self-contained false
dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r linux-x64 --self-contained false
```
-264
View File
@@ -1,264 +0,0 @@
#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
@@ -1,52 +0,0 @@
#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
@@ -1,846 +0,0 @@
#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 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 server 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
@@ -1,93 +0,0 @@
#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
-313
View File
@@ -1,313 +0,0 @@
#include "ServerProcess.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHostAddress>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
namespace {
constexpr int kDefaultAdminPort = 7779;
constexpr int kAdminTimeoutMs = 3000;
constexpr auto kAdminTokenFileName = ".admin-token";
QString appHostName() {
#ifdef Q_OS_WIN
return QStringLiteral("CommonwealthOnline.Server.exe");
#else
return QStringLiteral("CommonwealthOnline.Server");
#endif
}
}
ServerProcess::ServerProcess(QObject *parent)
: QObject(parent)
, process(nullptr)
, running(false)
, m_adminPort(kDefaultAdminPort)
, adminFailCount(0)
{
serverDir = findServerDirectory();
resolveServerLaunch();
}
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(QStringLiteral("Server is already running"));
return;
}
if (serverDir.isEmpty()) {
emit error(QStringLiteral("C# server directory not found"));
return;
}
if (serverProgram.isEmpty() && !resolveServerLaunch()) {
emit error(QStringLiteral("CommonwealthOnline.Server executable was not found. Build or publish the .NET server first."));
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 = serverPrefixArguments;
arguments << QStringLiteral("serve") << QStringLiteral("--config") << configPath;
process->setWorkingDirectory(serverDir);
process->start(serverProgram, arguments);
}
void ServerProcess::stop() {
if (!process) return;
if (process->state() == QProcess::Running) {
process->terminate();
if (!process->waitForFinished(3000)) {
process->kill();
process->waitForFinished();
}
}
process->deleteLater();
process = nullptr;
running = false;
adminFailCount = 0;
}
QJsonObject ServerProcess::sendAdminCommand(const QJsonObject &request) {
if (serverDir.isEmpty()) {
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Server directory is unavailable for admin authentication")}};
}
QFile tokenFile(QDir(serverDir).absoluteFilePath(QString::fromLatin1(kAdminTokenFileName)));
if (!tokenFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin authentication token is not available yet")}};
}
const QByteArray token = tokenFile.readAll().trimmed();
if (token.isEmpty()) {
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin authentication token is empty")}};
}
QJsonObject authenticatedRequest = request;
authenticatedRequest.insert(QStringLiteral("adminToken"), QString::fromUtf8(token));
QTcpSocket socket;
socket.connectToHost(QStringLiteral("127.0.0.1"), static_cast<quint16>(m_adminPort));
if (!socket.waitForConnected(kAdminTimeoutMs)) {
return {{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(authenticatedRequest).toJson(QJsonDocument::Compact) + '\n';
if (socket.write(payload) < 0 || !socket.waitForBytesWritten(kAdminTimeoutMs)) {
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Failed to send admin command")}};
}
QByteArray buffer;
while (!buffer.contains('\n')) {
if (!socket.waitForReadyRead(kAdminTimeoutMs)) {
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Timed out waiting for admin response")}};
}
buffer += socket.readAll();
if (buffer.size() > 64 * 1024) {
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin response exceeded maximum size")}};
}
}
const QByteArray line = buffer.left(buffer.indexOf('\n'));
QJsonParseError parseError{};
const QJsonDocument doc = QJsonDocument::fromJson(line, &parseError);
if (parseError.error != QJsonParseError::NoError || !doc.isObject()) {
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Invalid admin response JSON")}};
}
return doc.object();
}
void ServerProcess::fetchStats() {
if (!running) return;
const QJsonObject statsResponse = sendAdminCommand({{QStringLiteral("cmd"), QStringLiteral("stats")}});
if (statsResponse.value(QStringLiteral("ok")).toBool()) {
adminFailCount = 0;
emit statsUpdated(statsResponse.value(QStringLiteral("data")).toObject());
} else {
++adminFailCount;
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({{QStringLiteral("cmd"), QStringLiteral("clients")}});
if (clientsResponse.value(QStringLiteral("ok")).toBool()) {
emit clientsUpdated(clientsResponse.value(QStringLiteral("data")).toObject().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) {
const QJsonObject response = sendAdminCommand({
{QStringLiteral("cmd"), QStringLiteral("kick")},
{QStringLiteral("playerId"), playerId},
{QStringLiteral("reason"), reason}
});
const bool ok = response.value(QStringLiteral("ok")).toBool();
emit adminCommandFinished(ok, ok ? response.value(QStringLiteral("message")).toString(QStringLiteral("Player kicked."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Kick failed.")));
return ok;
}
bool ServerProcess::banPlayer(int playerId, const QString &reason) {
const QJsonObject response = sendAdminCommand({
{QStringLiteral("cmd"), QStringLiteral("ban")},
{QStringLiteral("playerId"), playerId},
{QStringLiteral("reason"), reason}
});
const bool ok = response.value(QStringLiteral("ok")).toBool();
emit adminCommandFinished(ok, ok ? response.value(QStringLiteral("message")).toString(QStringLiteral("Player banned."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Ban failed.")));
if (ok) fetchStats();
return ok;
}
bool ServerProcess::unbanIp(const QString &ip) {
const QJsonObject response = sendAdminCommand({
{QStringLiteral("cmd"), QStringLiteral("unban")},
{QStringLiteral("ip"), ip}
});
const bool ok = response.value(QStringLiteral("ok")).toBool();
emit adminCommandFinished(ok, ok ? response.value(QStringLiteral("message")).toString(QStringLiteral("IP unbanned."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Unban failed.")));
return ok;
}
QJsonArray ServerProcess::listBans() {
const QJsonObject response = sendAdminCommand({{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(QStringLiteral("Server exited with code %1").arg(exitCode));
else emit error(QStringLiteral("Server process crashed"));
}
void ServerProcess::onProcessError(QProcess::ProcessError processError) {
QString message;
switch (processError) {
case QProcess::FailedToStart: message = QStringLiteral("Failed to start CommonwealthOnline.Server"); break;
case QProcess::Crashed: message = QStringLiteral("Server process crashed"); break;
case QProcess::Timedout: message = QStringLiteral("Server process timed out"); break;
default: message = QStringLiteral("Unknown server process error"); break;
}
emit error(message);
}
void ServerProcess::onReadyReadStandardOutput() {
if (!process) return;
outputBuffer += process->readAllStandardOutput();
while (outputBuffer.contains('\n')) {
const int newlinePos = outputBuffer.indexOf('\n');
QString line = outputBuffer.left(newlinePos).trimmed();
outputBuffer = outputBuffer.mid(newlinePos + 1);
if (!line.isEmpty()) {
parseLogLine(line);
emit logMessage(line);
}
}
}
void ServerProcess::onReadyReadStandardError() {
if (!process) return;
const QString value = QString::fromUtf8(process->readAllStandardError()).trimmed();
if (!value.isEmpty()) emit logMessage(QStringLiteral("[STDERR] ") + value);
}
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("/CommonwealthOnline.Server.csproj")) ||
QFileInfo::exists(candidate + QStringLiteral("/CommonwealthOnline.Server.dll")) ||
QFileInfo::exists(candidate + QLatin1Char('/') + appHostName())) {
return QFileInfo(candidate).absoluteFilePath();
}
if (!dir.cdUp()) break;
}
return {};
}
bool ServerProcess::resolveServerLaunch() {
serverProgram.clear();
serverPrefixArguments.clear();
if (serverDir.isEmpty()) return false;
const QDir dir(serverDir);
const QStringList appHostCandidates = {
dir.absoluteFilePath(appHostName()),
dir.absoluteFilePath(QStringLiteral("publish/") + appHostName()),
dir.absoluteFilePath(QStringLiteral("bin/Release/net8.0/") + appHostName())
};
for (const QString &candidate : appHostCandidates) {
if (QFileInfo::exists(candidate)) {
serverProgram = QFileInfo(candidate).absoluteFilePath();
return true;
}
}
const QStringList dllCandidates = {
dir.absoluteFilePath(QStringLiteral("CommonwealthOnline.Server.dll")),
dir.absoluteFilePath(QStringLiteral("publish/CommonwealthOnline.Server.dll")),
dir.absoluteFilePath(QStringLiteral("bin/Release/net8.0/CommonwealthOnline.Server.dll"))
};
QString dllPath;
for (const QString &candidate : dllCandidates) {
if (QFileInfo::exists(candidate)) { dllPath = QFileInfo(candidate).absoluteFilePath(); break; }
}
if (dllPath.isEmpty()) return false;
QProcess probe;
probe.start(QStringLiteral("dotnet"), {QStringLiteral("--version")});
if (!probe.waitForFinished(3000) || probe.exitStatus() != QProcess::NormalExit || probe.exitCode() != 0) return false;
serverProgram = QStringLiteral("dotnet");
serverPrefixArguments << dllPath;
return true;
}
void ServerProcess::parseLogLine(const QString &line) {
Q_UNUSED(line);
}
-61
View File
@@ -1,61 +0,0 @@
#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 findServerDirectory();
bool resolveServerLaunch();
void parseLogLine(const QString &line);
QJsonObject sendAdminCommand(const QJsonObject &request);
QProcess *process;
QString serverDir;
QString serverProgram;
QStringList serverPrefixArguments;
bool running;
QString outputBuffer;
int m_adminPort;
int adminFailCount;
};
#endif
-16
View File
@@ -1,16 +0,0 @@
#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
@@ -1 +0,0 @@
IDI_ICON1 ICON "icons/app.ico"
-1
View File
@@ -1 +0,0 @@
<!-- Placeholder for app.ico - Qt will handle resource loading gracefully if missing -->
-5
View File
@@ -1,5 +0,0 @@
<RCC>
<qresource prefix="/">
<file>icons/app.ico</file>
</qresource>
</RCC>