diff --git a/.gitattributes b/.gitattributes index 09cbdbb..33e9be6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,9 @@ -# Normalize text files by default; platform-specific EOL below. * text=auto -# Shell / service / config (LF) +# LF text *.sh text eol=lf -*.py text eol=lf +*.cs text eol=lf +*.csproj text eol=lf *.service text eol=lf *.conf text eol=lf *.json text eol=lf @@ -20,7 +20,7 @@ *.hpp text eol=lf CMakeLists.txt text eol=lf -# Windows scripts (CRLF) +# Windows scripts *.bat text eol=crlf *.cmd text eol=crlf *.ps1 text eol=crlf diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml new file mode 100644 index 0000000..3eef26c --- /dev/null +++ b/.github/workflows/acceptance.yml @@ -0,0 +1,37 @@ +name: Acceptance (end-to-end TCP) + +on: + push: + paths: + - "server/**" + - ".github/workflows/acceptance.yml" + pull_request: + paths: + - "server/**" + - ".github/workflows/acceptance.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + acceptance: + name: End-to-end TCP acceptance + runs-on: [self-hosted, Linux, X64, co-server] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Enforce repository runtime policy + run: bash server/scripts/verify-no-legacy-runtime.sh + + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 + env: + DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet + with: + dotnet-version: "8.0.x" + + - name: Run end-to-end acceptance harness + working-directory: server + run: dotnet run --project acceptance/CommonwealthOnline.Server.Acceptance.csproj -c Release diff --git a/.github/workflows/csharp-server.yml b/.github/workflows/csharp-server.yml new file mode 100644 index 0000000..98c195e --- /dev/null +++ b/.github/workflows/csharp-server.yml @@ -0,0 +1,53 @@ +name: CSharp Server Gate + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-and-test: + name: Repository policy, C# build and test + runs-on: [self-hosted, Linux, X64, co-server] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Enforce repository runtime policy + run: bash server/scripts/verify-no-legacy-runtime.sh + + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 + env: + DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet + with: + dotnet-version: "8.0.x" + + - name: Build C# server + working-directory: server + run: dotnet build CommonwealthOnline.Server.csproj -c Release --nologo + + - name: Run C# server tests + working-directory: server + run: dotnet run --project tests/CommonwealthOnline.Server.Tests.csproj -c Release + + - name: Check Linux scripts + working-directory: server + run: | + chmod +x start.sh fix-port.sh scripts/linux_compat_checks.sh scripts/verify-no-legacy-runtime.sh + bash -n start.sh + bash -n fix-port.sh + bash -n scripts/linux_compat_checks.sh + bash -n scripts/verify-no-legacy-runtime.sh + bash scripts/linux_compat_checks.sh + + - name: Publish Linux server + working-directory: server + run: dotnet publish CommonwealthOnline.Server.csproj -c Release -r linux-x64 --self-contained false -o publish/linux-x64 --nologo + + - name: Verify published entrypoint + working-directory: server + run: test -f publish/linux-x64/CommonwealthOnline.Server.dll diff --git a/.github/workflows/gns-transport.yml b/.github/workflows/gns-transport.yml new file mode 100644 index 0000000..02692b5 --- /dev/null +++ b/.github/workflows/gns-transport.yml @@ -0,0 +1,55 @@ +name: GNS Transport Bridge + +on: + push: + paths: + - "server/native_transport/**" + - ".github/workflows/gns-transport.yml" + pull_request: + paths: + - "server/native_transport/**" + - ".github/workflows/gns-transport.yml" + +permissions: + contents: read + +jobs: + linux: + name: Linux native GNS bridge + runs-on: [self-hosted, Linux, X64, co-server] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - name: Verify build dependencies + run: | + missing=0 + for tool in cmake ninja protoc; do + command -v "$tool" >/dev/null 2>&1 || { echo "::error::missing build dependency: $tool"; missing=1; } + done + test -f /usr/include/openssl/ssl.h || { echo "::error::missing libssl-dev headers"; missing=1; } + test "$missing" -eq 0 + + - name: Fetch pinned GameNetworkingSockets + run: | + GNS_SHA=f4525e39ee10f6b45181fd92d01fee75f7d71756 + rm -rf /tmp/co-gns-src + git init /tmp/co-gns-src + git -C /tmp/co-gns-src remote add origin https://github.com/ValveSoftware/GameNetworkingSockets.git + git -C /tmp/co-gns-src fetch --depth 1 origin "$GNS_SHA" + git -C /tmp/co-gns-src checkout --detach FETCH_HEAD + test "$(git -C /tmp/co-gns-src rev-parse HEAD)" = "$GNS_SHA" + + - name: Build and run native bridge tests + run: | + cmake -S server/native_transport -B /tmp/co-server-gns-build -G Ninja \ + -DGNS_SOURCE_DIR=/tmp/co-gns-src \ + -DCMAKE_BUILD_TYPE=Release + cmake --build /tmp/co-server-gns-build -j2 + ctest --test-dir /tmp/co-server-gns-build --output-on-failure + + - name: Confirm shared bridge artifact + run: | + test -f /tmp/co-server-gns-build/libcommonwealth_online_gns_bridge.so + ldd /tmp/co-server-gns-build/libcommonwealth_online_gns_bridge.so diff --git a/.github/workflows/host.yml b/.github/workflows/host.yml new file mode 100644 index 0000000..babcabb --- /dev/null +++ b/.github/workflows/host.yml @@ -0,0 +1,33 @@ +name: Host GUI (Avalonia) + +on: + push: + paths: + - "host/**" + - ".github/workflows/host.yml" + pull_request: + paths: + - "host/**" + - ".github/workflows/host.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build Avalonia host + runs-on: [self-hosted, Linux, X64, co-server] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 + 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/.github/workflows/linux-compatibility.yml b/.github/workflows/linux-compatibility.yml deleted file mode 100644 index 6e6197b..0000000 --- a/.github/workflows/linux-compatibility.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: Linux Compatibility - -on: - push: - paths: - - "server/**" - - ".gitattributes" - - ".gitignore" - - ".github/workflows/linux-compatibility.yml" - pull_request: - paths: - - "server/**" - - ".gitattributes" - - ".gitignore" - - ".github/workflows/linux-compatibility.yml" - -jobs: - ubuntu: - name: Ubuntu dedicated server - runs-on: ubuntu-latest - defaults: - run: - working-directory: server - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install tools - run: | - sudo apt-get update - sudo apt-get install -y shellcheck - - - name: Verify start.sh LF and executable bit - run: | - python3 - <<'PY' - from pathlib import Path - data = Path("start.sh").read_bytes() - assert b"\r\n" not in data, "start.sh must use LF line endings" - PY - test -x start.sh - - - name: Syntax and shellcheck - run: | - bash -n start.sh - shellcheck start.sh - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Create clean venv and install server deps - run: | - python -m venv .venv - .venv/bin/python -m pip install --upgrade pip - .venv/bin/python -m pip install -r requirements-server.txt pytest - # Ensure GUI-only deps are not required for dedicated server - ! .venv/bin/python -c "import PySide6" 2>/dev/null - - - name: Compile and test - run: | - .venv/bin/python -m compileall -q . - .venv/bin/python -m pytest -q - .venv/bin/python test_npc_protocol.py - .venv/bin/python test_combat_protocol.py - - arch: - name: Arch Linux container - runs-on: ubuntu-latest - container: - image: archlinux:latest - defaults: - run: - working-directory: server - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install packages - run: | - pacman -Syu --noconfirm python python-pip shellcheck - - - name: Verify start.sh LF and executable bit - run: | - python - <<'PY' - from pathlib import Path - data = Path("start.sh").read_bytes() - assert b"\r\n" not in data, "start.sh must use LF line endings" - PY - test -x start.sh - - - name: Syntax and shellcheck - run: | - bash -n start.sh - shellcheck start.sh - - - name: Create clean venv and install server deps - run: | - python -m venv .venv - .venv/bin/python -m pip install --upgrade pip - .venv/bin/python -m pip install -r requirements-server.txt pytest - - - name: Compile and test - run: | - .venv/bin/python -m compileall -q . - .venv/bin/python -m pytest -q - .venv/bin/python test_npc_protocol.py - .venv/bin/python test_combat_protocol.py diff --git a/.github/workflows/open-gitea-pr.yml b/.github/workflows/open-gitea-pr.yml new file mode 100644 index 0000000..78ff0d5 --- /dev/null +++ b/.github/workflows/open-gitea-pr.yml @@ -0,0 +1,85 @@ +name: Open Gitea PR on merge to main + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + open-gitea-pr: + runs-on: [self-hosted, Linux, X64, co-server-sync] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Push main to Gitea and open a pull request + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GITEA_HOST: git.zambazosmedia.group + GITEA_USER: nomad + GITEA_REPO: Commonwealth-Online/Commonwealth-Online-Server + SYNC_BRANCH: sync/from-github + run: | + set -euo pipefail + umask 077 + + if [ -z "${GITEA_TOKEN:-}" ]; then + echo "::error::Missing GITEA_TOKEN repository secret." + exit 1 + fi + + askpass="${RUNNER_TEMP}/gitea-askpass-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.sh" + header_file="${RUNNER_TEMP}/gitea-header-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" + response_file="${RUNNER_TEMP}/gitea-pr-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}.json" + + cleanup() { + git remote remove gitea >/dev/null 2>&1 || true + rm -f -- "$askpass" "$header_file" "$response_file" + } + trap cleanup EXIT + + cat > "$askpass" <<'EOF' + #!/usr/bin/env bash + case "$1" in + *Username*) printf '%s\n' "${GITEA_USER:?}" ;; + *Password*) printf '%s\n' "${GITEA_TOKEN:?}" ;; + *) exit 1 ;; + esac + EOF + chmod 700 "$askpass" + printf 'Authorization: token %s\n' "$GITEA_TOKEN" > "$header_file" + chmod 600 "$header_file" + + export GIT_ASKPASS="$askpass" + export GIT_TERMINAL_PROMPT=0 + + git config user.name "github-sync" + git config user.email "github-sync@users.noreply.github.com" + git remote remove gitea >/dev/null 2>&1 || true + git remote add gitea "https://${GITEA_HOST}/${GITEA_REPO}.git" + git -c credential.helper= -c credential.useHttpPath=true \ + push -f gitea "HEAD:refs/heads/${SYNC_BRANCH}" + + http_code=$(curl -sS -o "$response_file" -w "%{http_code}" -X POST \ + "https://${GITEA_HOST}/api/v1/repos/${GITEA_REPO}/pulls" \ + -H "@${header_file}" \ + -H "Content-Type: application/json" \ + -d "{\"title\":\"Sync from GitHub main\",\"head\":\"${SYNC_BRANCH}\",\"base\":\"main\",\"body\":\"GitHub main was updated. Review and merge to land it on Gitea.\"}") + + echo "Gitea pulls API returned HTTP ${http_code}" + cat "$response_file" || true + echo + + if [ "$http_code" = "201" ]; then + echo "Opened a new Gitea pull request." + elif [ "$http_code" = "409" ] || grep -qiE "already exist|issue_exist" "$response_file"; then + echo "A Gitea PR from ${SYNC_BRANCH} is already open; it now has the latest commits." + else + echo "::error::Unexpected Gitea response (${http_code})." + exit 1 + fi diff --git a/.github/workflows/publish-host.yml b/.github/workflows/publish-host.yml new file mode 100644 index 0000000..e770328 --- /dev/null +++ b/.github/workflows/publish-host.yml @@ -0,0 +1,49 @@ +name: Publish Host (Avalonia) + +on: + workflow_dispatch: + push: + tags: + - "host-v*" + +permissions: + contents: read + +jobs: + publish: + name: Publish ${{ matrix.rid }} + runs-on: [self-hosted, Linux, X64, co-server] + strategy: + fail-fast: false + matrix: + rid: [win-x64, linux-x64] + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 + env: + DOTNET_INSTALL_DIR: ${{ runner.tool_cache }}/dotnet + with: + dotnet-version: "8.0.x" + + - name: Publish single-file self-contained + run: > + dotnet publish host/CommonwealthOnline.Host.csproj + -c Release + -r ${{ matrix.rid }} + --self-contained true + -p:PublishSingleFile=true + -p:IncludeNativeLibrariesForSelfExtract=true + -p:DebugType=none + -o out/${{ matrix.rid }} + --nologo + + - name: Upload artifact + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: CommonwealthOnline.Host-${{ matrix.rid }} + path: out/${{ matrix.rid }}/ + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore index 3225677..326a716 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ # Build outputs build/ out/ +publish/ +server/bin/ +server/obj/ +server/publish/ +server/tests/bin/ +server/tests/obj/ +server/acceptance/bin/ +server/acceptance/obj/ cmake-build-*/ *.exe *.dll @@ -8,6 +16,8 @@ cmake-build-*/ *.obj *.o *.a +*.so +*.dylib *.pdb *.ilk *.exp @@ -34,22 +44,22 @@ ui_*.h *.rcc qrc_*.cpp -# Python (source + staged server copies) -__pycache__/ -*.py[cod] -*$py.class -.pytest_cache/ -.mypy_cache/ -.ruff_cache/ -.venv/ -.venv-ci/ -venv/ -env/ +# .NET generated +*.deps.json +*.runtimeconfig.json +TestResults/ # Local / runtime artifacts *.log *.tmp *.bak logs/ +bans.json +server/bans.json +.admin-token +server/.admin-token .DS_Store Thumbs.db + +host/bin/ +host/obj/ diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index 637a87c..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,72 +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 -) - -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 -) - -# Windows-specific settings -if(WIN32) - set_target_properties(CommonwealthOnlineHost PROPERTIES - WIN32_EXECUTABLE ON - VS_DPI_AWARE "ON" - ) -endif() - -# Set output directory -set_target_properties(CommonwealthOnlineHost PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" -) - -# Stage the Python relay server next to the Host GUI executable so -# ServerProcess can find consumer_server_cli.py at runtime. -# Preserve a local commonwealth-server.json if the host already edited it. -set(CO_SERVER_SOURCE_DIR "${CMAKE_SOURCE_DIR}/server") -set(CO_SERVER_STAGE_DIR "$/server") -set(CO_SERVER_STAGE_CONFIG "${CO_SERVER_STAGE_DIR}/commonwealth-server.json") -if(NOT EXISTS "${CO_SERVER_SOURCE_DIR}/consumer_server_cli.py") - message(FATAL_ERROR - "Bundled server source not found at ${CO_SERVER_SOURCE_DIR}/consumer_server_cli.py") -endif() -add_custom_command(TARGET CommonwealthOnlineHost POST_BUILD - COMMAND ${CMAKE_COMMAND} -E make_directory "${CO_SERVER_STAGE_DIR}" - COMMAND ${CMAKE_COMMAND} - -DCO_SERVER_SOURCE_DIR=${CO_SERVER_SOURCE_DIR} - -DCO_SERVER_STAGE_DIR=${CO_SERVER_STAGE_DIR} - -DCO_SERVER_STAGE_CONFIG=${CO_SERVER_STAGE_CONFIG} - -P "${CMAKE_SOURCE_DIR}/cmake/stage_server.cmake" - COMMENT "Copying Python server next to CommonwealthOnlineHost.exe" -) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md index 9886247..3be31bf 100644 --- a/DEPLOYMENT.md +++ b/DEPLOYMENT.md @@ -1,237 +1,81 @@ -# Commonwealth Online GUI - Deployment Guide +# Deployment -## Quick Start - Running the GUI +## Dedicated Linux server -### First Time Setup (Deploy Qt DLLs) - -The executable needs Qt6 runtime libraries. Deploy them once: +Publish a self-contained server: ```bash -cd host-gui -deploy.bat +cd server +dotnet publish CommonwealthOnline.Server.csproj \ + -c Release \ + -r linux-x64 \ + --self-contained true \ + -p:PublishSingleFile=true \ + -o publish/linux-x64 ``` -This copies all required Qt6 DLLs and plugins to the Release folder. +Copy the published files, `commonwealth-server.json`, and the native GNS bridge when GNS is enabled to the target host. -### Running the Application - -After deployment, simply: +Start: ```bash -# Double-click this file: -build/bin/Release/CommonwealthOnlineHost.exe - -# Or run from command line: -cd build/bin/Release -CommonwealthOnlineHost.exe +./CommonwealthOnline.Server serve --config commonwealth-server.json ``` ---- +A systemd example is provided at `server/commonwealth-online.service.example`. -## Distribution +## Windows dedicated server -To distribute the application to other machines: - -### Option 1: Copy Entire Folder (Easiest) - -``` -Release/ -├── CommonwealthOnlineHost.exe -├── Qt6Core.dll -├── Qt6Gui.dll -├── Qt6Widgets.dll -├── Qt6Network.dll -├── Qt6Concurrent.dll -├── Qt6DBus.dll -├── Qt6Xml.dll -└── plugins/ - ├── platforms/ - ├── styles/ - └── imageformats/ +```bat +dotnet publish server\CommonwealthOnline.Server.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o server\publish\win-x64 ``` -Just zip and send this folder - everything needed is included. +Run: -### Option 2: Use Qt Deployment Tool (Advanced) - -Qt provides `windeployqt.exe` for automatic deployment: - -```bash -C:\Qt\6.11.1\msvc2022_64\bin\windeployqt.exe build/bin/Release/CommonwealthOnlineHost.exe +```bat +CommonwealthOnline.Server.exe serve --config commonwealth-server.json ``` ---- +## Qt Host package -## Requirements for Users +Run: -Recipients of the `.exe` need **only**: -- Windows 10 or later -- Python 3.9+ (for running the relay server) -- No Visual Studio or Qt installation needed - ---- - -## Troubleshooting - -### "Entry point not found" error - -The DLLs weren't deployed. Run: -```bash -deploy.bat -``` - -### "The procedure entry point... could not be located" - -Likely a missing or incompatible DLL. Try redeploying: -```bash -# Clean old DLLs -del build\bin\Release\*.dll - -# Redeploy -deploy.bat -``` - -### Application window doesn't appear - -The GUI may have launched but is hidden. Check: -1. Task Manager for `CommonwealthOnlineHost.exe` process -2. Try running from command line to see error messages -3. Ensure Python and server directory are accessible - ---- - -## Development Build vs Release Build - -**For Development:** -```bash -# Just build (DLLs not deployed) -build.bat -``` - -**For Distribution:** -```bash -# Build + deploy DLLs +```bat build.bat deploy.bat ``` ---- +CMake publishes the C# server self-contained and stages it under: -## Next Steps - -1. **Test Locally** - - Run `CommonwealthOnlineHost.exe` - - Click "Start Server" - - Run `fake_client.py` from server folder - - Verify client appears in table - -2. **Package for Release** - - Run `deploy.bat` - - Zip `build/bin/Release/` folder - - Distribute to users - -3. **Create Installer** (Future Enhancement) - - Use NSIS or WiX to create `.msi` installer - - Automatically handles DLL deployment - - Adds Start Menu shortcuts - - Enables uninstall - ---- - -## File Locations - -``` -host-gui/ -├── CMakeLists.txt # Build configuration -├── build.bat # Compile application -├── deploy.bat # Deploy Qt DLLs ← Run this after build.bat -├── check-setup.bat # Verify prerequisites -├── find-qt.bat # Locate Qt6 installation -├── README.md # User guide -├── SETUP.md # Setup instructions -├── src/ # Source code -└── build/ - └── bin/ - └── Release/ - ├── CommonwealthOnlineHost.exe - ├── Qt6*.dll # Runtime libraries - └── plugins/ # Qt plugins +```text +build\bin\Release\server\ ``` ---- +The staging and deployment checks reject prohibited legacy runtime artifacts before packaging succeeds. -## Creating a Portable Distribution +Package the entire `build\bin\Release` directory so the Qt runtime, plugins and bundled server remain together. -### Windows Host GUI (unchanged) +## Runtime state -To create a self-contained package anyone can run: +Preserve these files across upgrades: -```bash -# Build the application from the repository root -build.bat +- `commonwealth-server.json` +- `bans.json` +- `.admin-token` -# Deploy DLLs -deploy.bat +The CMake staging step preserves those files when replacing the server binaries. -# Create distribution package -# (build.bat / deploy.bat already stage server\ next to the exe) -mkdir Commonwealth-Online-Host -xcopy /I /E build\bin\Release Commonwealth-Online-Host\ -copy README.md Commonwealth-Online-Host\README.txt +## Network -# Zip and distribute -# Send Commonwealth-Online-Host.zip to users -``` +Default ports: -Users extract and run `CommonwealthOnlineHost.exe` - no setup needed! -Target machines still need Python 3.9+ on PATH for the bundled relay server. +- TCP 7777: gameplay compatibility +- UDP 7777: GameNetworkingSockets when enabled +- UDP 7778: LAN discovery +- TCP 127.0.0.1:7779: authenticated admin control -### Linux dedicated server packaging +Do not expose the admin port to the network. -Prefer a `.tar.gz` archive of the `server/` directory so the executable bit on `start.sh` is retained. +## GNS bridge -Do **not** include: - -- `server/.venv/` -- `server/__pycache__/` -- `server/logs/` -- local runtime files such as operator-specific `bans.json` unless intentional - -Example: - -```bash -tar --exclude='.venv' --exclude='__pycache__' --exclude='logs' \ - --exclude='*.pyc' -czf commonwealth-online-server-linux.tar.gz -C server . -``` - -If you also ship a ZIP archive, document that some extraction tools may drop Unix executable permissions. Recipients can recover with: - -```bash -sed -i 's/\r$//' start.sh -chmod +x start.sh -./start.sh -``` - -Windows Host GUI packaging remains ZIP-based and is unchanged by the Linux packaging guidance above. - ---- - -## Support - -If users encounter issues: - -1. Ensure `deployment completed successfully` (see output) -2. Verify Python is installed and in PATH -3. Check that `server\` exists next to the executable (contains `consumer_server_cli.py`) -4. Run from command line to see detailed error messages - ---- - -## Performance Notes - -- Startup time: <1 second -- Memory usage: ~80-150 MB -- CPU: Minimal (event-driven) -- DLL size: ~100-150 MB total (but only loaded once) - -Deploy once, run forever! 🚀 +The native bridge is platform-specific. Set `gns_bridge_path` when it is not installed beside the server. GNS startup is fail-loud when enabled and the bridge cannot be loaded. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 1d2717f..4edf387 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -1,118 +1,50 @@ -# Qt GUI Host Development +# Development -This directory contains the native C++ Qt6 GUI application for hosting Commonwealth Online servers. +## Server -## Quick Start for Developers +The dedicated server is a C#/.NET project at `server/CommonwealthOnline.Server.csproj`. -### Prerequisites +Core files: -1. **Qt6.4+** - - Download from https://www.qt.io/download-open-source - - Install to default location (C:\Qt\6.4.0) or adjust `build.bat` +- `AuthoritativeServer.cs`: sessions, authoritative gameplay state, security, interest filtering +- `ProtocolCore.cs`: JSON codec, transport policy, snapshot sequencing/envelope +- `ProtocolValidation.cs`: Protocol V2 packet validation +- `Domain.cs`: sessions, bans, NPC authority, world presets +- `TcpTransport.cs`: newline-framed TCP compatibility transport +- `GnsTransport.cs`: C# wrapper over the native GameNetworkingSockets bridge +- `AdminDiscovery.cs`: authenticated localhost admin channel and LAN discovery +- `Program.cs`: CLI, interactive server, management commands and synthetic load command -2. **Visual Studio 2022** - - Install C++ development tools - - Required for MSVC compiler - -3. **CMake 3.20+** - - Download from https://cmake.org/download - -4. **Python 3.9+** - - Required for running the relay server subprocess - - Add to PATH - -### Building +Build and test: ```bash -cd host-gui -build.bat +cd server +dotnet build CommonwealthOnline.Server.csproj -c Release +dotnet run --project tests/CommonwealthOnline.Server.Tests.csproj -c Release ``` -Or manually: +The test harness uses in-memory connections for authoritative behavior and contains no external test-framework package dependency. -```bash -cd host-gui -mkdir build -cd build -cmake .. -G "Visual Studio 17 2022" -cmake --build . --config Release -``` +## Native GNS bridge -### Running +`server/native_transport` remains C++. Keep it transport-only. Do not move validation, identity, interest filtering, NPC authority, combat, world state or caching into the bridge. -```bash -.\build\bin\Release\CommonwealthOnlineHost.exe -``` +The C ABI is the boundary consumed from `GnsTransport.cs`. -## Project Structure +## Qt Host -- `CMakeLists.txt` - Qt6 build configuration -- `src/main.cpp` - Application entry point -- `src/MainWindow.h/cpp` - Main UI window -- `src/ServerProcess.h/cpp` - Subprocess manager for relay -- `src/resources/` - Icons and resources -- `build.bat` - Windows build script -- `README.md` - User documentation +The Qt host remains C++. `ServerProcess` launches the published C# server and communicates through the authenticated localhost admin channel. It must not invoke a scripting runtime. -## Architecture +CMake publishes a self-contained server as part of the Host GUI post-build step. -### MainWindow -- Handles all UI elements (buttons, tables, labels, text areas) -- Manages server start/stop through ServerProcess -- Updates stats and client list in real-time -- Displays logs with timestamps +## Protocol changes -### ServerProcess -- Spawns Python relay server as subprocess -- Captures stdout/stderr in real-time -- Parses log output -- Handles process lifecycle (start, stop, errors) +Protocol behavior is intentionally independent from transport. If adding a packet type: -### Communication -- Uses `QProcess` for subprocess management -- Parses CLI output for stats and client data -- Emits Qt signals for UI updates +1. Define validation and normalization in C#. +2. Decide its delivery policy in `TransportPolicy`. +3. Default new control/state traffic to reliable/ordered. +4. Use unreliable/sequenced only for latest-wins snapshot families. +5. Add tests before changing the client. -## Design Decisions - -1. **Subprocess Architecture**: Server runs in separate process so GUI can restart/crash without affecting active connections - -2. **Python Relay**: Uses existing Python CLI to avoid duplicating networking logic in C++ - -3. **Live Logs**: Captures and displays all server output for debugging and transparency - -4. **Minimal Dependencies**: Qt6 core only, no additional frameworks or heavy dependencies - -5. **Dark Theme**: Fallout 4-inspired styling with amber/green accents matches game aesthetic - -## Performance - -- **Startup**: <1 second (just Qt6 initialization) -- **Memory**: ~50-100 MB baseline -- **Executable Size**: ~15-20 MB (with Qt6 DLLs included) -- **CPU**: Minimal, only updates on events - -## Next Steps - -- [ ] Implement stats fetching via CLI JSON output -- [ ] Add admin command buttons (set time/weather) -- [ ] Config file editor panel -- [ ] System tray icon -- [ ] Settings panel -- [ ] Player kick/ban interface -- [ ] Logging export functionality -- [ ] Performance profiling and optimization - -## Troubleshooting - -### CMake can't find Qt6 -Ensure Qt6 path is set in `build.bat` or CMake cache. - -### Build fails with MSVC errors -Check that Visual Studio 2022 with C++ tools is installed. - -### Python not found at runtime -Ensure Python 3.9+ is installed and in PATH. Restart the GUI or set `PYTHON` environment variable. - -### Application window appears but doesn't respond -Check console output or run from command line to see error messages. +Never cache/replay discrete action events, accept stale NPC authority epochs, accept stale snapshot sequences, or bypass server-owned identity and interest validation. diff --git a/README.md b/README.md index bfa32ae..ac53b2c 100644 --- a/README.md +++ b/README.md @@ -1,178 +1,42 @@ -# Commonwealth Online - Qt GUI Host +# Commonwealth Online — Server & Host -Production-ready Qt6 GUI application for hosting Commonwealth Online servers on Windows. +The dedicated server and its host GUI for Commonwealth Online (Fallout 4 +multiplayer). Everything now builds on one .NET 8 toolchain. -**Looking for the CLI only?** You do not need to build this repo. Download the [`server/`](server/) folder and follow [server/README.md](server/README.md) — run `start.bat` (Windows) or `./start.sh` (Linux / macOS). Python 3.9+ is all you need. +## Components -This repository is for building the **Host GUI**. The GUI wraps the same Python server from `server/` in a native Windows UI. +- **[`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. -## Linux dedicated server - -The dedicated server lives in [`server/`](server/) and is the supported way to host on Ubuntu, Debian, Arch Linux, CachyOS, Fedora, and similar distributions. +## Quick start — server ```bash cd server -# Ubuntu/Debian: sudo apt install python3 python3-venv -# Arch/CachyOS: sudo pacman -S --needed python -# Fedora: sudo dnf install python3 -chmod +x start.sh -./start.sh +./start.sh # Linux / macOS +start.bat # Windows ``` -Important notes: +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. -- `./start.sh` creates a local `.venv` and never uses `--break-system-packages` -- Do not run `sudo ./start.sh` -- Fish shell users do not need to activate the virtual environment -- 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 should join -- See [server/README.md](server/README.md) for systemd, journalctl, CRLF recovery, and admin-CLI usage - -## Quick Start (GUI) - -**First time? Follow the [Setup Instructions](SETUP.md)** - -### Building +## Quick start — host GUI ```bash -# Check prerequisites -check-setup.bat - -# Build -build.bat +dotnet run --project host/CommonwealthOnline.Host.csproj ``` -Output: `build\bin\Release\CommonwealthOnlineHost.exe` - -## Features - -- **Start/Stop Server**: Compact toolbar controls with a live status indicator -- **Server Settings**: Edit name, description, bind address, port, max players, and log level from Settings -- **Status Strip**: Bind address, LAN, clients, uptime, and packet counters at a glance -- **Client List**: Live table of connected clients and connection details -- **Server Logs**: Resizable log pane with timestamps -- **Utility UI**: Light admin-tool layout (toolbar, status strip, splitter panes) -- **Auto-detection**: Automatically finds Python and server directory -- **Subprocess Management**: Server runs in separate process; GUI crash doesn't kill server - -## Requirements - -- Windows 10 or later -- **Qt 6.4+** (install from https://www.qt.io/download-open-source) -- **Visual Studio 2022** with C++ tools -- **CMake 3.20+** -- **Python 3.9+** (for running the relay server) - -## Setup Issues? - -See [SETUP.md](SETUP.md) for detailed instructions and troubleshooting. - -## Building - -### Quick Build +## Build ```bash -check-setup.bat # Verify prerequisites -build.bat # Auto-detect Qt6 and build +dotnet build server/CommonwealthOnline.Server.csproj -c Release +dotnet build host/CommonwealthOnline.Host.csproj -c Release ``` -### From Visual Studio - -1. Open this repository folder in Visual Studio 2022 -2. Wait for CMake to auto-configure -3. Build → Build All -4. Run the executable - -### From Command Line (Manual) - -```bash -mkdir build -cd build -cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="C:\Qt\6.8.0\msvc2022_64" -cmake --build . --config Release -``` - -## First Run - -1. Double-click `CommonwealthOnlineHost.exe` -2. Click **Start** -3. Server will bind to 0.0.0.0:7777 (configurable in commonwealth-server.json) -4. View real-time logs and connected clients - -## Configuration - -Use **Settings** in the Host GUI to edit settings, or edit `commonwealth-server.json` in the server folder: - -- `host`: Bind address -- `port`: Server port -- `server_name`: Display name (shown in LAN browser) -- `server_description`: Short description (optional) -- `max_players`: Max player count -- `log_verbosity`: debug/info/warning/error - -Saved changes apply the next time the server is started. If the server is already running, stop and start it again. - -## Layout - -``` -├── CMakeLists.txt # Build configuration -├── build.bat # Auto-build script -├── check-setup.bat # Verify prerequisites -├── SETUP.md # Setup instructions -├── README.md # This file -├── DEVELOPMENT.md # Developer guide -├── IMPLEMENTATION.md # Architecture docs -├── src/ -│ ├── main.cpp # Application entry point -│ ├── MainWindow.h/cpp # Main window UI and logic -│ ├── ConfigDialog.h/cpp # Server settings editor dialog -│ ├── ServerProcess.h/cpp # Subprocess manager for relay server -│ └── resources/ -│ ├── resources.qrc # Qt resource manifest -│ └── icons/ -│ └── app.ico # Application icon -├── server/ # Python relay server (CLI; no build needed) -└── build/ # Build output directory (after building) - └── bin/ - └── Release/ - └── CommonwealthOnlineHost.exe -``` - -## Architecture - -The GUI spawns the Python relay server (`consumer_server_cli.py`) as a subprocess and: -- Captures stdout/stderr for real-time logs -- Parses log output to extract stats and client info -- Provides UI for server control and monitoring -- Maintains server state even if GUI crashes - -See [IMPLEMENTATION.md](IMPLEMENTATION.md) for detailed architecture. - -## Troubleshooting - -**Qt6 not found?** -- Install Qt6 from https://www.qt.io/download-open-source -- Use default path: C:\Qt\6.8.0\msvc2022_64 -- Run `check-setup.bat` to verify - -**Visual Studio not found?** -- Install Visual Studio 2022 with C++ tools -- See [SETUP.md](SETUP.md) - -**CMake error?** -- Install CMake from https://cmake.org/download/ -- Select "Add CMake to PATH" during install - -**Build fails?** -- See [SETUP.md](SETUP.md) for manual build instructions -- Review [DEVELOPMENT.md](DEVELOPMENT.md) for developer notes - -## Future Enhancements - -- [x] Player kick/ban buttons (via localhost admin port) -- [x] Config file editor in GUI (Settings dialog) -- [ ] Admin commands (set time/weather directly from GUI) -- [ ] Ban reason dialog in GUI -- [ ] System tray icon with quick access -- [ ] Server history and logs export -- [ ] Installer (.msi or .exe wrapper) +> 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/SETUP.md b/SETUP.md index 857a6a6..e4c1504 100644 --- a/SETUP.md +++ b/SETUP.md @@ -1,231 +1,50 @@ -# Commonwealth Online Qt GUI - Setup Instructions +# Setup -## Quick Setup (3 Steps) +## Dedicated server development -### Step 1: Install Qt6 - -1. Download Qt6 from: **https://www.qt.io/download-open-source** -2. Run the installer and select: - - ✓ Qt 6.8 (or latest 6.x) - - ✓ MSVC 2022 64-bit component - - Install to default location: `C:\Qt\6.8.0\` - -3. After installation, verify the path exists: - ``` - C:\Qt\6.8.0\msvc2022_64\lib\cmake\Qt6 - ``` - -### Step 2: Verify Prerequisites - -Run the setup checker: +Install the .NET 8 SDK. ```bash -cd host-gui -check-setup.bat +cd server +dotnet build CommonwealthOnline.Server.csproj -c Release +dotnet run --project tests/CommonwealthOnline.Server.Tests.csproj -c Release ``` -This verifies: -- ✓ CMake 3.20+ -- ✓ Visual Studio 2022 -- ✓ Qt6 installation - -### Step 3: Build +Run from source: ```bash -cd host-gui +dotnet run --project CommonwealthOnline.Server.csproj -- serve --config commonwealth-server.json --interactive +``` + +## Windows Qt Host development + +Required: + +- Visual Studio 2022 with C++ tools +- CMake 3.20+ +- Qt 6.4+ MSVC 2022 package +- .NET 8 SDK + +Run: + +```bat +check-setup.bat build.bat ``` -The script will: -1. Auto-detect Qt6 location -2. Configure CMake -3. Build Release executable -4. Output: `host-gui/build/bin/Release/CommonwealthOnlineHost.exe` +CMake builds the Qt host and publishes the bundled C# server as a self-contained Windows executable under the staged `server` directory beside the host. ---- +## GameNetworkingSockets -## Detailed Setup (If Quick Setup Fails) +The authoritative server is C#. Valve GameNetworkingSockets remains in the native C++ bridge under `server/native_transport`. -### Manual Qt6 Installation +A server with `enable_gns_transport: true` needs the appropriate bridge library beside the server or a valid `gns_bridge_path`. -1. **Download Qt Online Installer** - - Go to https://www.qt.io/download-open-source - - Download "Qt Online Installer for Windows" +## Ports -2. **Run Installer** - - Create Qt account (free) - - Select "Custom installation" - - Under "Qt 6.8.0" (or latest): - - ✓ MSVC 2022 64-bit - - ✓ Qt 5compat (optional) - - Under "Developer and Designer Tools": - - ✓ CMake (if not already installed) - - Click "Install" +- Gameplay TCP compatibility: TCP 7777 by default +- GNS gameplay: UDP 7777 by default when enabled +- LAN discovery: UDP 7778 +- Admin control: TCP 127.0.0.1:7779 by default -3. **Verify Installation** - - Check that this folder exists: - ``` - C:\Qt\6.8.0\msvc2022_64\ - ``` - - Should contain: - ``` - lib\cmake\Qt6\ - bin\ - include\ - plugins\ - ``` - -### Manual CMake Installation - -If CMake isn't found: - -1. Download from: https://cmake.org/download/ -2. Run installer -3. When prompted, select "Add CMake to PATH" -4. Restart your terminal/command prompt - -### Manual Visual Studio 2022 Installation - -If Visual Studio 2022 isn't found: - -1. Download from: https://visualstudio.microsoft.com/downloads/ -2. Run installer -3. Select "Desktop development with C++" -4. Install - ---- - -## Building Manually (If Scripts Fail) - -### Command Line Build - -```bash -cd host-gui -mkdir build -cd build - -# Configure (replace path with your Qt6 location) -cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="C:\Qt\6.8.0\msvc2022_64" - -# Build Release -cmake --build . --config Release -``` - -### Visual Studio IDE Build - -1. Open Visual Studio 2022 -2. File → Open → Folder -3. Select `host-gui` folder -4. Wait for CMake configuration -5. Build → Build All -6. Executable at: `host-gui/build/bin/Release/CommonwealthOnlineHost.exe` - ---- - -## Troubleshooting - -### "Qt6 not found" - -**Solution 1**: Edit `build.bat` and update Qt6 search paths -- Find the line with `PATHS_TO_CHECK` -- Add your Qt6 installation path - -**Solution 2**: Use manual CMake with explicit path -```bash -cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="C:\path\to\your\Qt6" -``` - -### "Visual Studio not found" - -Make sure Visual Studio 2022 is installed with C++ development tools: -1. Open Visual Studio Installer -2. Modify your installation -3. Select "Desktop development with C++" -4. Click "Modify" - -### "CMake not found" - -Add CMake to PATH: -1. Install CMake from cmake.org -2. Select "Add CMake to PATH" -3. Restart terminal - -### "Build fails with link errors" - -Ensure you're using the matching MSVC version: -- Qt6 installed for MSVC 2022 64-bit -- Building with "Visual Studio 17 2022" generator -- Using Release build configuration - ---- - -## Running the Application - -After successful build: - -```bash -cd host-gui -start build\bin\Release\CommonwealthOnlineHost.exe -``` - -Or double-click: -``` -host-gui\build\bin\Release\CommonwealthOnlineHost.exe -``` - ---- - -## Environment Setup (Advanced) - -If you want to set up your environment permanently: - -### Windows Environment Variables - -1. Press `Win + X`, select "System" -2. Click "Advanced system settings" -3. Click "Environment Variables" -4. Add or update: - ``` - CMAKE_PREFIX_PATH = C:\Qt\6.8.0\msvc2022_64 - ``` -5. Restart terminal - -Then you can just run: -```bash -cd host-gui -mkdir build -cd build -cmake .. -cmake --build . --config Release -``` - ---- - -## Next Steps After Build - -Once you have a successful build: - -1. **Test the Application** - - Launch `CommonwealthOnlineHost.exe` - - Click "Start Server" - - Verify logs appear - -2. **Connect Clients** - - Run fake_client from server directory - - Should see client in GUI table - -3. **Customize** - - Edit colors/styling in `MainWindow.cpp` - - Add admin buttons in `MainWindow.h/cpp` - - Extend ServerProcess for more features - ---- - -## Support - -If you encounter issues: - -1. Run `check-setup.bat` to verify prerequisites -2. Review output and error messages carefully -3. Check this troubleshooting guide -4. Try manual CMake command with explicit paths +The admin port must remain localhost-only. diff --git a/build.bat b/build.bat deleted file mode 100644 index 3661a60..0000000 --- a/build.bat +++ /dev/null @@ -1,132 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -echo. -echo ================================================================================ -echo Commonwealth Online - Qt GUI Build Script (with Qt6 Auto-Detection) -echo ================================================================================ -echo. - -REM Check if CMake is installed -cmake --version >nul 2>&1 -if errorlevel 1 ( - echo ERROR: CMake is not installed or not in PATH. - echo Please install CMake from https://cmake.org/download/ - echo Then add it to your PATH and restart this script. - pause - exit /b 1 -) - -REM Auto-detect Qt6 installation -echo Searching for Qt6 installation... -set QT6_PATH= - -REM Common Qt6 installation paths (check in order) -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,7) do ( - if exist "!PATHS_TO_CHECK[%%i]!\lib\cmake\Qt6" ( - set QT6_PATH=!PATHS_TO_CHECK[%%i]! - echo Found Qt6 at: !QT6_PATH! - goto found_qt6 - ) -) - -:found_qt6 -if "!QT6_PATH!"=="" ( - echo. - echo ERROR: Qt6 not found at common installation paths. - echo. - echo Please install Qt6 from https://www.qt.io/download-open-source - echo. - echo Common installation paths: - echo - C:\Qt\6.8.0\msvc2022_64 - echo - C:\Qt\6.7.0\msvc2022_64 - echo - C:\Qt\6.6.0\msvc2022_64 - echo. - echo Alternatively, manually set Qt6 path by editing this script - echo or run CMake manually with: - echo cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="C:\path\to\Qt6" - echo. - pause - exit /b 1 -) - -REM Create build directory -if not exist "build" ( - echo. - echo Creating build directory... - mkdir build -) - -cd build - -echo. -echo Configuring project with CMake... -echo CMake: %CMAKE_PREFIX_PATH% -echo Qt6: !QT6_PATH! -echo VS: Visual Studio 17 2022 -echo. - -cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="!QT6_PATH!" - -if errorlevel 1 ( - echo. - echo ERROR: CMake configuration failed. - echo. - echo Troubleshooting: - echo 1. Verify Qt6 is installed at: !QT6_PATH! - echo 2. Verify Visual Studio 2022 is installed with C++ tools - echo 3. Try running this script again - echo. - echo Manual configuration command: - echo cmake .. -G "Visual Studio 17 2022" -DCMAKE_PREFIX_PATH="!QT6_PATH!" - echo. - pause - exit /b 1 -) - -echo. -echo Configuration successful! -echo. -echo Building project (this may take a few minutes)... -echo. - -cmake --build . --config Release - -if errorlevel 1 ( - echo. - echo ERROR: Build failed. - echo. - echo Troubleshooting: - echo 1. Check that Visual Studio 2022 is installed - echo 2. Make sure C++ development tools are installed - echo 3. Try building again - echo. - pause - exit /b 1 -) - -echo. -echo ================================================================================ -echo Build successful! -echo ================================================================================ -echo. -echo Executable created at: -echo %cd%\bin\Release\CommonwealthOnlineHost.exe -echo. -echo The Python server\ folder is copied next to the exe by CMake post-build. -echo. -echo To run the application: -echo cd .. -echo start build\bin\Release\CommonwealthOnlineHost.exe -echo. -echo For a portable package, also run deploy.bat afterwards. -echo. -pause diff --git a/check-setup.bat b/check-setup.bat deleted file mode 100644 index 1307033..0000000 --- a/check-setup.bat +++ /dev/null @@ -1,66 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -echo. -echo ================================================================================ -echo Commonwealth Online - Qt GUI Setup Helper -echo ================================================================================ -echo. -echo This script will help you set up Qt6 for building the GUI. -echo. - -REM Check CMake -cmake --version >nul 2>&1 -if errorlevel 1 ( - echo [ERROR] CMake not found. Please install from https://cmake.org/download - pause - exit /b 1 -) -echo [OK] CMake found - -REM Check Visual Studio -if not exist "C:\Program Files\Microsoft Visual Studio\2022\Community" ( - if not exist "C:\Program Files\Microsoft Visual Studio\2022\Professional" ( - echo. - echo [ERROR] Visual Studio 2022 not found - echo Please install from https://visualstudio.microsoft.com/downloads/ - pause - exit /b 1 - ) -) -echo [OK] Visual Studio 2022 found - -REM Check Qt6 -set QT6_FOUND=0 - -if exist "C:\Qt\6.8.0\msvc2022_64\lib\cmake\Qt6" set QT6_FOUND=1 & set QT_PATH=C:\Qt\6.8.0\msvc2022_64 -if exist "C:\Qt\6.7.0\msvc2022_64\lib\cmake\Qt6" set QT6_FOUND=1 & set QT_PATH=C:\Qt\6.7.0\msvc2022_64 -if exist "C:\Qt\6.6.0\msvc2022_64\lib\cmake\Qt6" set QT6_FOUND=1 & set QT_PATH=C:\Qt\6.6.0\msvc2022_64 -if exist "C:\Qt\6.5.0\msvc2022_64\lib\cmake\Qt6" set QT6_FOUND=1 & set QT_PATH=C:\Qt\6.5.0\msvc2022_64 -if exist "C:\Qt\6.4.0\msvc2022_64\lib\cmake\Qt6" set QT6_FOUND=1 & set QT_PATH=C:\Qt\6.4.0\msvc2022_64 - -if %QT6_FOUND%==0 ( - echo. - echo [ERROR] Qt6 not found at standard locations - echo. - echo Qt6 installation required! Download from: https://www.qt.io/download-open-source - echo. - echo Installation paths checked: - echo - C:\Qt\6.8.0\msvc2022_64 - echo - C:\Qt\6.7.0\msvc2022_64 - echo - C:\Qt\6.6.0\msvc2022_64 - echo - C:\Qt\6.5.0\msvc2022_64 - echo - C:\Qt\6.4.0\msvc2022_64 - echo. - echo If Qt6 is installed elsewhere, manually edit build.bat - echo and update the Qt6 search paths. - echo. - pause - exit /b 1 -) - -echo [OK] Qt6 found at: %QT_PATH% -echo. -echo All prerequisites found! You can now run build.bat -echo. -pause diff --git a/cmake/stage_server.cmake b/cmake/stage_server.cmake deleted file mode 100644 index b826490..0000000 --- a/cmake/stage_server.cmake +++ /dev/null @@ -1,46 +0,0 @@ -# Stage server/ next to the Host GUI, preserving an existing local config. -if(NOT EXISTS "${CO_SERVER_SOURCE_DIR}") - message(FATAL_ERROR "Server source directory not found: ${CO_SERVER_SOURCE_DIR}") -endif() - -if(NOT EXISTS "${CO_SERVER_SOURCE_DIR}/consumer_server_cli.py") - message(FATAL_ERROR - "Server entrypoint not found: ${CO_SERVER_SOURCE_DIR}/consumer_server_cli.py") -endif() - -set(_preserve_config "") -if(EXISTS "${CO_SERVER_STAGE_CONFIG}") - file(READ "${CO_SERVER_STAGE_CONFIG}" _preserve_config) -endif() - -file(REMOVE_RECURSE "${CO_SERVER_STAGE_DIR}") -file(MAKE_DIRECTORY "${CO_SERVER_STAGE_DIR}") - -# Prefer a filtered copy so Python caches do not ship with the build. -file(GLOB _server_entries RELATIVE "${CO_SERVER_SOURCE_DIR}" "${CO_SERVER_SOURCE_DIR}/*") -foreach(_entry IN LISTS _server_entries) - if(_entry STREQUAL "__pycache__" - OR _entry STREQUAL ".pytest_cache" - OR _entry STREQUAL ".venv" - OR _entry STREQUAL "logs" - OR _entry STREQUAL "tests") - continue() - endif() - file(COPY "${CO_SERVER_SOURCE_DIR}/${_entry}" - DESTINATION "${CO_SERVER_STAGE_DIR}" - PATTERN "__pycache__" EXCLUDE - PATTERN "*.pyc" EXCLUDE - PATTERN ".pytest_cache" EXCLUDE - PATTERN ".venv" EXCLUDE - PATTERN "logs" EXCLUDE - ) -endforeach() - -if(NOT "${_preserve_config}" STREQUAL "") - file(WRITE "${CO_SERVER_STAGE_CONFIG}" "${_preserve_config}") -endif() - -if(NOT EXISTS "${CO_SERVER_STAGE_DIR}/consumer_server_cli.py") - message(FATAL_ERROR - "Failed to stage server entrypoint to ${CO_SERVER_STAGE_DIR}/consumer_server_cli.py") -endif() diff --git a/deploy.bat b/deploy.bat deleted file mode 100644 index 836da31..0000000 --- a/deploy.bat +++ /dev/null @@ -1,105 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -echo. -echo ================================================================================ -echo Commonwealth Online GUI - Qt6 DLL Deployment -echo ================================================================================ -echo. - -REM Find Qt6 installation (prefer the newest match; do not overwrite). -set QT6_PATH= - -if exist "C:\Qt\6.11.1\msvc2022_64\bin" ( - set QT6_PATH=C:\Qt\6.11.1\msvc2022_64 -) else if exist "C:\Qt\6.10.2\msvc2022_64\bin" ( - set QT6_PATH=C:\Qt\6.10.2\msvc2022_64 -) else 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. - echo Please ensure Qt6 is installed to one of these paths: - echo - C:\Qt\6.11.1\msvc2022_64 - echo - C:\Qt\6.10.2\msvc2022_64 - echo - C:\Qt\6.8.0\msvc2022_64 - pause - exit /b 1 -) - -echo Found Qt6 at: !QT6_PATH! -echo. -echo Deploying Qt6 DLLs to release directory... -echo. - -set DEPLOY_DIR=%cd%\build\bin\Release - -REM Required DLLs -set DLLs=^ - Qt6Core.dll ^ - Qt6Gui.dll ^ - Qt6Widgets.dll ^ - Qt6Network.dll ^ - Qt6Concurrent.dll ^ - Qt6DBus.dll ^ - Qt6Xml.dll - -REM Copy DLLs -for %%D in (%DLLs%) do ( - if exist "!QT6_PATH!\bin\%%D" ( - echo Copying %%D... - copy /Y "!QT6_PATH!\bin\%%D" "!DEPLOY_DIR!\%%D" >nul - ) else ( - echo WARNING: %%D not found (may not be required) - ) -) - -REM Copy plugins directory -if not exist "!DEPLOY_DIR!\plugins" mkdir "!DEPLOY_DIR!\plugins" - -echo Copying Qt plugins... -xcopy /Y /Q "!QT6_PATH!\plugins\platforms" "!DEPLOY_DIR!\plugins\platforms\" -xcopy /Y /Q "!QT6_PATH!\plugins\styles" "!DEPLOY_DIR!\plugins\styles\" 2>nul -xcopy /Y /Q "!QT6_PATH!\plugins\imageformats" "!DEPLOY_DIR!\plugins\imageformats\" 2>nul - -REM Stage Python relay server next to the executable (bundled from repo server\) -set SERVER_SRC=%cd%\server -set SERVER_DST=!DEPLOY_DIR!\server -set SERVER_CFG=!SERVER_DST!\commonwealth-server.json -set SERVER_CFG_BAK=%TEMP%\co-host-server-config.json -if exist "!SERVER_SRC!\consumer_server_cli.py" ( - echo Copying Python server from "!SERVER_SRC!"... - if exist "!SERVER_CFG!" copy /Y "!SERVER_CFG!" "!SERVER_CFG_BAK!" >nul - if not exist "!SERVER_DST!" mkdir "!SERVER_DST!" - robocopy "!SERVER_SRC!" "!SERVER_DST!" /E /XD __pycache__ .pytest_cache /XF *.pyc /NFL /NDL /NJH /NJS /nc /ns /np - if errorlevel 8 ( - echo WARNING: Failed to copy server directory from "!SERVER_SRC!" - ) else ( - if exist "!SERVER_CFG_BAK!" ( - copy /Y "!SERVER_CFG_BAK!" "!SERVER_CFG!" >nul - del /Q "!SERVER_CFG_BAK!" >nul 2>&1 - echo Preserved existing commonwealth-server.json - ) - echo Server directory staged at: !SERVER_DST! - ) -) else ( - echo WARNING: Server source not found at "!SERVER_SRC!" - echo Expected repo-local server\consumer_server_cli.py. - echo Rebuild with CMake post-build copy, or copy server\ manually. -) - -echo. -echo ================================================================================ -echo Deployment Complete! -echo ================================================================================ -echo. -echo Executable is now ready to run: -echo !DEPLOY_DIR!\CommonwealthOnlineHost.exe -echo. -echo You can now: -echo 1. Double-click CommonwealthOnlineHost.exe to run -echo 2. Or copy the entire Release folder to another machine -echo 3. Include all DLLs, plugins\, and server\ when distributing -echo. -pause 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/App.axaml b/host/App.axaml new file mode 100644 index 0000000..0483e48 --- /dev/null +++ b/host/App.axaml @@ -0,0 +1,213 @@ + + + + + + + #0A0E07 + #12180C + #151B0F + #1C2314 + #232C18 + #2C3720 + #43532F + #E6D28C + #F6ECBE + #D8D2BE + #8A9068 + #8BD05C + #3E5C28 + #C85450 + #5A2A28 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/host/App.axaml.cs b/host/App.axaml.cs new file mode 100644 index 0000000..96b355c --- /dev/null +++ b/host/App.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using CommonwealthOnline.Host.ViewModels; +using CommonwealthOnline.Host.Views; + +namespace CommonwealthOnline.Host; + +public partial class App : Application +{ + public override void Initialize() => AvaloniaXamlLoader.Load(this); + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/host/Assets/logo.png b/host/Assets/logo.png new file mode 100644 index 0000000..81be790 Binary files /dev/null and b/host/Assets/logo.png differ diff --git a/host/CommonwealthOnline.Host.csproj b/host/CommonwealthOnline.Host.csproj new file mode 100644 index 0000000..b15accb --- /dev/null +++ b/host/CommonwealthOnline.Host.csproj @@ -0,0 +1,26 @@ + + + + WinExe + net8.0 + enable + latest + true + false + CommonwealthOnline.Host + CommonwealthOnline.Host + + + + + + + + + + + + + + + diff --git a/host/Program.cs b/host/Program.cs new file mode 100644 index 0000000..30f4b76 --- /dev/null +++ b/host/Program.cs @@ -0,0 +1,16 @@ +using System; +using Avalonia; + +namespace CommonwealthOnline.Host; + +internal static class Program +{ + [STAThread] + public static void Main(string[] args) => + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); +} diff --git a/host/README.md b/host/README.md new file mode 100644 index 0000000..0d289c6 --- /dev/null +++ b/host/README.md @@ -0,0 +1,43 @@ +# 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 + +Self-contained, single-file builds that need no .NET runtime on the target. +The host uses reflection-based Avalonia bindings, so publish **untrimmed**. + +``` +dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r win-x64 \ + --self-contained true -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true -o out/win-x64 + +dotnet publish host/CommonwealthOnline.Host.csproj -c Release -r linux-x64 \ + --self-contained true -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true -o out/linux-x64 +``` + +CI builds both runtimes and uploads them as artifacts — run the +**Publish Host (Avalonia)** workflow (`.github/workflows/publish-host.yml`) +manually, or push a `host-v*` tag. diff --git a/host/Services/AdminClient.cs b/host/Services/AdminClient.cs new file mode 100644 index 0000000..07a6beb --- /dev/null +++ b/host/Services/AdminClient.cs @@ -0,0 +1,67 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading; +using System.Threading.Tasks; + +namespace CommonwealthOnline.Host.Services; + +// Speaks the server's token-authenticated admin protocol on 127.0.0.1:AdminPort: +// read .admin-token, send {..,"adminToken"}\n, read one newline-terminated JSON reply. +public sealed class AdminClient +{ + private const int MaxResponseBytes = 1_000_000; + + private readonly int _port; + private readonly string _tokenPath; + + public AdminClient(int adminPort, string tokenPath) + { + _port = adminPort; + _tokenPath = tokenPath; + } + + public async Task SendAsync(JsonObject request, CancellationToken ct = default) + { + var token = (await File.ReadAllTextAsync(_tokenPath, ct).ConfigureAwait(false)).Trim(); + var authenticated = (JsonObject)request.DeepClone(); + authenticated["adminToken"] = token; + var payload = JsonSerializer.SerializeToUtf8Bytes(authenticated); + + using var client = new TcpClient(); + await client.ConnectAsync(IPAddress.Loopback, _port, ct).ConfigureAwait(false); + var stream = client.GetStream(); + await stream.WriteAsync(payload, ct).ConfigureAwait(false); + await stream.WriteAsync(new byte[] { (byte)'\n' }, ct).ConfigureAwait(false); + + using var buffer = new MemoryStream(); + var one = new byte[1]; + while (buffer.Length < MaxResponseBytes) + { + var read = await stream.ReadAsync(one, ct).ConfigureAwait(false); + if (read == 0 || one[0] == (byte)'\n') + { + break; + } + + buffer.WriteByte(one[0]); + } + + return JsonNode.Parse(buffer.ToArray()) as JsonObject; + } + + public Task StatusAsync(CancellationToken ct = default) => + SendAsync(new JsonObject { ["cmd"] = "status" }, ct); + + public Task ClientsAsync(CancellationToken ct = default) => + SendAsync(new JsonObject { ["cmd"] = "clients" }, ct); + + public Task KickAsync(uint playerId, string reason, CancellationToken ct = default) => + SendAsync(new JsonObject { ["cmd"] = "kick", ["playerId"] = playerId, ["reason"] = reason }, ct); + + public Task BanAsync(uint playerId, string reason, CancellationToken ct = default) => + SendAsync(new JsonObject { ["cmd"] = "ban", ["playerId"] = playerId, ["reason"] = reason }, ct); +} diff --git a/host/Services/ServerConfig.cs b/host/Services/ServerConfig.cs new file mode 100644 index 0000000..95bb379 --- /dev/null +++ b/host/Services/ServerConfig.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CommonwealthOnline.Host.Services; + +// Mirrors CommonwealthOnline.Server Configuration. Keep the JSON shape aligned +// with the server's own serializer so a config saved here loads there. +public sealed class ServerConfig +{ + public string Host { get; set; } = "0.0.0.0"; + public int Port { get; set; } = 7777; + public string ServerName { get; set; } = "Commonwealth Online Server"; + public string ServerDescription { get; set; } = string.Empty; + public int MaxPlayers { get; set; } = 16; + public string LogVerbosity { get; set; } = "info"; + public int AdminPort { get; set; } = 7779; + public bool EnableGnsTransport { get; set; } + public string? GnsBridgePath { get; set; } + + private static readonly JsonSerializerOptions Options = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + public static ServerConfig Load(string path) + { + try + { + if (File.Exists(path)) + { + return JsonSerializer.Deserialize(File.ReadAllText(path), Options) + ?? new ServerConfig(); + } + } + catch (Exception) + { + // Fall back to defaults on unreadable/invalid config. + } + + return new ServerConfig(); + } + + public void Save(string path) => + File.WriteAllText(path, JsonSerializer.Serialize(this, Options)); +} diff --git a/host/Services/ServerController.cs b/host/Services/ServerController.cs new file mode 100644 index 0000000..f1ee490 --- /dev/null +++ b/host/Services/ServerController.cs @@ -0,0 +1,106 @@ +using System; +using System.Diagnostics; +using System.IO; + +namespace CommonwealthOnline.Host.Services; + +// Launches the CommonwealthOnline.Server process, preferring a published +// apphost, then a framework-dependent DLL, then a source-tree dotnet run. +public sealed class ServerController +{ + private Process? _process; + + public bool IsRunning => _process is { HasExited: false }; + + public event Action? LogReceived; + public event Action? RunningChanged; + + public void Start(string serverDir, string configPath) + { + if (IsRunning) + { + return; + } + + var startInfo = ResolveLaunch(serverDir, configPath); + var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true }; + process.OutputDataReceived += (_, e) => Emit(e.Data); + process.ErrorDataReceived += (_, e) => Emit(e.Data); + process.Exited += (_, _) => RunningChanged?.Invoke(false); + + _process = process; + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + RunningChanged?.Invoke(true); + } + + public void Stop() + { + if (_process is { HasExited: false } process) + { + try + { + process.Kill(entireProcessTree: true); + } + catch (Exception) + { + // Process already gone or not killable; RunningChanged fires on Exited. + } + } + } + + private void Emit(string? line) + { + if (!string.IsNullOrEmpty(line)) + { + LogReceived?.Invoke(line); + } + } + + private static ProcessStartInfo ResolveLaunch(string serverDir, string configPath) + { + var exeName = OperatingSystem.IsWindows() + ? "CommonwealthOnline.Server.exe" + : "CommonwealthOnline.Server"; + var apphost = Path.Combine(serverDir, exeName); + var dll = Path.Combine(serverDir, "CommonwealthOnline.Server.dll"); + var project = Path.Combine(serverDir, "CommonwealthOnline.Server.csproj"); + + var info = new ProcessStartInfo + { + WorkingDirectory = serverDir, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + if (File.Exists(apphost)) + { + info.FileName = apphost; + info.ArgumentList.Add("serve"); + } + else if (File.Exists(dll)) + { + info.FileName = "dotnet"; + info.ArgumentList.Add(dll); + info.ArgumentList.Add("serve"); + } + else + { + info.FileName = "dotnet"; + info.ArgumentList.Add("run"); + info.ArgumentList.Add("--project"); + info.ArgumentList.Add(project); + info.ArgumentList.Add("-c"); + info.ArgumentList.Add("Release"); + info.ArgumentList.Add("--"); + info.ArgumentList.Add("serve"); + } + + info.ArgumentList.Add("--config"); + info.ArgumentList.Add(configPath); + return info; + } +} diff --git a/host/ViewModels/MainWindowViewModel.cs b/host/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..7f3b25b --- /dev/null +++ b/host/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Avalonia.Threading; +using CommonwealthOnline.Host.Services; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace CommonwealthOnline.Host.ViewModels; + +public partial class MainWindowViewModel : ObservableObject +{ + private const int MaxLogLines = 2000; + + private readonly ServerController _controller = new(); + private readonly string _serverDir = Directory.GetCurrentDirectory(); + private readonly string _configPath = + Path.Combine(Directory.GetCurrentDirectory(), "commonwealth-server.json"); + private readonly DispatcherTimer _pollTimer; + private bool _polling; + + [ObservableProperty] private string _serverName; + [ObservableProperty] private string _host; + [ObservableProperty] private int _port; + [ObservableProperty] private int _maxPlayers; + [ObservableProperty] private int _adminPort; + [ObservableProperty] private string _logVerbosity; + [ObservableProperty] private bool _enableGnsTransport; + [ObservableProperty] private bool _isRunning; + [ObservableProperty] private string _statusText = "Stopped"; + [ObservableProperty] private string _statsText = string.Empty; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(KickCommand))] + [NotifyCanExecuteChangedFor(nameof(BanCommand))] + private PlayerRow? _selectedPlayer; + + public ObservableCollection Log { get; } = new(); + public ObservableCollection Players { get; } = new(); + + public string[] VerbosityOptions { get; } = { "error", "warning", "info", "debug" }; + + public MainWindowViewModel() + { + var config = ServerConfig.Load(_configPath); + _serverName = config.ServerName; + _host = config.Host; + _port = config.Port; + _maxPlayers = config.MaxPlayers; + _adminPort = config.AdminPort; + _logVerbosity = config.LogVerbosity; + _enableGnsTransport = config.EnableGnsTransport; + + _controller.LogReceived += line => + Dispatcher.UIThread.Post(() => Append(line)); + _controller.RunningChanged += running => + Dispatcher.UIThread.Post(() => + { + IsRunning = running; + StatusText = running ? "Running" : "Stopped"; + StartCommand.NotifyCanExecuteChanged(); + StopCommand.NotifyCanExecuteChanged(); + if (!running) + { + Players.Clear(); + StatsText = string.Empty; + } + }); + + _pollTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) }; + _pollTimer.Tick += async (_, _) => await PollAsync(); + _pollTimer.Start(); + } + + private AdminClient CreateAdminClient() + { + var tokenPath = Path.Combine(Path.GetDirectoryName(_configPath) ?? _serverDir, ".admin-token"); + return new AdminClient(AdminPort, tokenPath); + } + + private async Task PollAsync() + { + if (!IsRunning || _polling) + { + return; + } + + _polling = true; + try + { + var admin = CreateAdminClient(); + var clients = await admin.ClientsAsync().ConfigureAwait(true); + ApplyClients(clients); + + var status = await admin.StatusAsync().ConfigureAwait(true); + ApplyStatus(status); + } + catch (Exception) + { + // Server still starting, admin port not up yet, or token not written — ignore this tick. + } + finally + { + _polling = false; + } + } + + private void ApplyClients(JsonObject? response) + { + if (response?["clients"] is not JsonArray array) + { + return; + } + + var previouslySelected = SelectedPlayer?.PlayerId; + Players.Clear(); + foreach (var node in array) + { + if (node is not JsonObject client) + { + continue; + } + + Players.Add(new PlayerRow + { + PlayerId = (uint)(client["player_id"]?.GetValue() ?? 0), + Label = client["label"]?.GetValue() ?? string.Empty, + Address = client["address"]?.GetValue() ?? string.Empty, + PacketsReceived = client["packets_received"]?.GetValue() ?? 0, + PacketsSent = client["packets_sent"]?.GetValue() ?? 0, + }); + } + + if (previouslySelected is { } id) + { + foreach (var row in Players) + { + if (row.PlayerId == id) + { + SelectedPlayer = row; + break; + } + } + } + } + + private void ApplyStatus(JsonObject? response) + { + if (response is null) + { + return; + } + + var connected = response["connected_clients"]?.GetValue() ?? Players.Count; + var uptime = response["uptime_seconds"]?.GetValue() ?? 0; + StatsText = $"{connected}/{MaxPlayers} players · up {uptime}s"; + } + + private void Append(string line) + { + Log.Add(line); + while (Log.Count > MaxLogLines) + { + Log.RemoveAt(0); + } + } + + private ServerConfig CurrentConfig() => new() + { + ServerName = ServerName, + Host = Host, + Port = Port, + MaxPlayers = MaxPlayers, + AdminPort = AdminPort, + LogVerbosity = LogVerbosity, + EnableGnsTransport = EnableGnsTransport, + }; + + [RelayCommand] + private void Save() => CurrentConfig().Save(_configPath); + + [RelayCommand(CanExecute = nameof(CanStart))] + private void Start() + { + Save(); + Append($"[host] starting server on {Host}:{Port}..."); + _controller.Start(_serverDir, _configPath); + } + + private bool CanStart() => !IsRunning; + + [RelayCommand(CanExecute = nameof(CanStop))] + private void Stop() + { + Append("[host] stopping server..."); + _controller.Stop(); + } + + private bool CanStop() => IsRunning; + + [RelayCommand(CanExecute = nameof(CanActOnPlayer))] + private async Task Kick() + { + if (SelectedPlayer is not { } player) + { + return; + } + + await RunAdminAction(admin => admin.KickAsync(player.PlayerId, "Kicked by host"), + $"[host] kick #{player.PlayerId}").ConfigureAwait(true); + } + + [RelayCommand(CanExecute = nameof(CanActOnPlayer))] + private async Task Ban() + { + if (SelectedPlayer is not { } player) + { + return; + } + + await RunAdminAction(admin => admin.BanAsync(player.PlayerId, "Banned by host"), + $"[host] ban #{player.PlayerId}").ConfigureAwait(true); + } + + private bool CanActOnPlayer() => IsRunning && SelectedPlayer is not null; + + private async Task RunAdminAction(Func> action, string label) + { + try + { + var response = await action(CreateAdminClient()).ConfigureAwait(true); + var message = response?["message"]?.GetValue(); + Append(string.IsNullOrEmpty(message) ? $"{label} sent" : $"{label}: {message}"); + await PollAsync().ConfigureAwait(true); + } + catch (Exception ex) + { + Append($"{label} failed: {ex.Message}"); + } + } +} diff --git a/host/ViewModels/PlayerRow.cs b/host/ViewModels/PlayerRow.cs new file mode 100644 index 0000000..267747a --- /dev/null +++ b/host/ViewModels/PlayerRow.cs @@ -0,0 +1,13 @@ +namespace CommonwealthOnline.Host.ViewModels; + +public sealed class PlayerRow +{ + public uint PlayerId { get; init; } + public string Label { get; init; } = string.Empty; + public string Address { get; init; } = string.Empty; + public long PacketsReceived { get; init; } + public long PacketsSent { get; init; } + + public string Display => + $"#{PlayerId} {(string.IsNullOrEmpty(Label) ? "player" : Label)} {Address} ↓{PacketsReceived} ↑{PacketsSent}"; +} diff --git a/host/Views/MainWindow.axaml b/host/Views/MainWindow.axaml new file mode 100644 index 0000000..eba2e1b --- /dev/null +++ b/host/Views/MainWindow.axaml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +