diff --git a/.github/workflows/host.yml b/.github/workflows/host.yml new file mode 100644 index 0000000..5960cca --- /dev/null +++ b/.github/workflows/host.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 4664a52..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -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 "$/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 -) diff --git a/README.md b/README.md index 46e7834..ac53b2c 100644 --- a/README.md +++ b/README.md @@ -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 cd server -dotnet build CommonwealthOnline.Server.csproj -c Release -dotnet run --project tests/CommonwealthOnline.Server.Tests.csproj -c Release -dotnet run --project CommonwealthOnline.Server.csproj -- serve --config commonwealth-server.json --interactive +./start.sh # Linux / macOS +start.bat # Windows ``` -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 -- session/control, player state, combat, world state and authority: reliable/ordered - -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 +```bash +dotnet run --project host/CommonwealthOnline.Host.csproj ``` -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 -- UDP 7777: GNS gameplay when enabled -- UDP 7778: LAN discovery -- 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). +> The Qt/C++ host and its CMake build were retired in favour of the Avalonia +> host. `SETUP.md` / `DEVELOPMENT.md` / `DEPLOYMENT.md` are from the Qt era and +> are pending a refresh. diff --git a/build.bat b/build.bat deleted file mode 100644 index f9862c3..0000000 --- a/build.bat +++ /dev/null @@ -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 diff --git a/check-setup.bat b/check-setup.bat deleted file mode 100644 index 2109b39..0000000 --- a/check-setup.bat +++ /dev/null @@ -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 diff --git a/cmake/stage_server.cmake b/cmake/stage_server.cmake deleted file mode 100644 index 2d590a5..0000000 --- a/cmake/stage_server.cmake +++ /dev/null @@ -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() diff --git a/deploy.bat b/deploy.bat deleted file mode 100644 index 8e01dbb..0000000 --- a/deploy.bat +++ /dev/null @@ -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 diff --git a/find-qt.bat b/find-qt.bat deleted file mode 100644 index cd4278f..0000000 --- a/find-qt.bat +++ /dev/null @@ -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 diff --git a/host/README.md b/host/README.md new file mode 100644 index 0000000..1968935 --- /dev/null +++ b/host/README.md @@ -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 +``` diff --git a/src/ConfigDialog.cpp b/src/ConfigDialog.cpp deleted file mode 100644 index ab37993..0000000 --- a/src/ConfigDialog.cpp +++ /dev/null @@ -1,264 +0,0 @@ -#include "ConfigDialog.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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(); -} diff --git a/src/ConfigDialog.h b/src/ConfigDialog.h deleted file mode 100644 index 496330c..0000000 --- a/src/ConfigDialog.h +++ /dev/null @@ -1,52 +0,0 @@ -#ifndef CONFIGDIALOG_H -#define CONFIGDIALOG_H - -#include -#include -#include -#include -#include -#include - -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 diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp deleted file mode 100644 index 985d3cf..0000000 --- a/src/MainWindow.cpp +++ /dev/null @@ -1,846 +0,0 @@ -#include "MainWindow.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -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(uptime))); - } else if (uptime < 3600.0) { - const int minutes = static_cast(uptime / 60.0); - const int seconds = static_cast(uptime) % 60; - uptimeLabel->setText(QStringLiteral("Uptime: %1m %2s").arg(minutes).arg(seconds)); - } else { - const int hours = static_cast(uptime / 3600.0); - const int minutes = (static_cast(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(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(); -} diff --git a/src/MainWindow.h b/src/MainWindow.h deleted file mode 100644 index 2e7f55d..0000000 --- a/src/MainWindow.h +++ /dev/null @@ -1,93 +0,0 @@ -#ifndef MAINWINDOW_H -#define MAINWINDOW_H - -#include -#include -#include -#include -#include -#include -#include -#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 diff --git a/src/ServerProcess.cpp b/src/ServerProcess.cpp deleted file mode 100644 index 85aacb1..0000000 --- a/src/ServerProcess.cpp +++ /dev/null @@ -1,313 +0,0 @@ -#include "ServerProcess.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -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(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); -} diff --git a/src/ServerProcess.h b/src/ServerProcess.h deleted file mode 100644 index 06de799..0000000 --- a/src/ServerProcess.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef SERVERPROCESS_H -#define SERVERPROCESS_H - -#include -#include -#include -#include -#include - -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 diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index d64d37d..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#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(); -} diff --git a/src/resources/app.rc b/src/resources/app.rc deleted file mode 100644 index ab5b359..0000000 --- a/src/resources/app.rc +++ /dev/null @@ -1 +0,0 @@ -IDI_ICON1 ICON "icons/app.ico" diff --git a/src/resources/icons/app.ico b/src/resources/icons/app.ico deleted file mode 100644 index 4833b88..0000000 --- a/src/resources/icons/app.ico +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/resources/resources.qrc b/src/resources/resources.qrc deleted file mode 100644 index 01e2230..0000000 --- a/src/resources/resources.qrc +++ /dev/null @@ -1,5 +0,0 @@ - - - icons/app.ico - -