From 807446920f9971799acdc350ef84c783fe315f33 Mon Sep 17 00:00:00 2001 From: Alexander Zielonka Date: Sun, 19 Apr 2026 20:57:14 +0200 Subject: [PATCH] Add Space Engineers server support Integrates Space Engineers dedicated server (docker+wine on 192.168.2.78) into GSM: - config.json entry with docker runtime and log-based player detection - getSpaceEngineersPlayers() parses docker logs for connect/disconnect events - Extends type-check sites in rcon, autoshutdown, discordBot, servers routes - Frontend ServerCard/ServerDetail logo + address wiring Co-Authored-By: Claude Opus 4.7 (1M context) --- gsm-backend/config.json | 11 +++++++ gsm-backend/routes/servers.js | 8 ++--- gsm-backend/services/autoshutdown.js | 2 +- gsm-backend/services/discordBot.js | 5 +-- gsm-backend/services/rcon.js | 10 +++++- gsm-backend/services/ssh.js | 36 ++++++++++++++++++++++ gsm-frontend/src/components/ServerCard.jsx | 8 +++++ gsm-frontend/src/pages/ServerDetail.jsx | 1 + 8 files changed, 73 insertions(+), 8 deletions(-) diff --git a/gsm-backend/config.json b/gsm-backend/config.json index 907ee04..31d6544 100644 --- a/gsm-backend/config.json +++ b/gsm-backend/config.json @@ -91,6 +91,17 @@ "workDir": "/opt/openttd", "port": 3979 }, + { + "id": "spaceengineers", + "name": "Space Engineers", + "host": "192.168.2.78", + "type": "spaceengineers", + "runtime": "docker", + "maxRam": 12, + "containerName": "space-engineers-dedicated-docker-linux", + "workDir": "/opt/spaceengineers", + "port": 27016 + }, { "id": "hytale", "name": "Hytale", diff --git a/gsm-backend/routes/servers.js b/gsm-backend/routes/servers.js index 6e4af0e..b9dcd2f 100644 --- a/gsm-backend/routes/servers.js +++ b/gsm-backend/routes/servers.js @@ -349,8 +349,8 @@ router.get('/', optionalAuth, async (req, res) => { getCurrentMetrics(server.id).catch(() => ({ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0 })), - (server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, - (server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, + (server.rconPassword || server.type === 'hytale' || server.type === 'terraria' || server.type === 'spaceengineers') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, + (server.rconPassword || server.type === 'hytale' || server.type === 'terraria' || server.type === 'spaceengineers') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, getProcessUptime(server).catch(() => 0) ]); @@ -587,8 +587,8 @@ router.get('/:id', authenticateToken, rejectGuest, async (req, res) => { getCurrentMetrics(server.id).catch(() => ({ cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0 })), - (server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, - (server.rconPassword || server.type === 'hytale' || server.type === 'terraria') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, + (server.rconPassword || server.type === 'hytale' || server.type === 'terraria' || server.type === 'spaceengineers') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, + (server.rconPassword || server.type === 'hytale' || server.type === 'terraria' || server.type === 'spaceengineers') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, getProcessUptime(server).catch(() => 0) ]); diff --git a/gsm-backend/services/autoshutdown.js b/gsm-backend/services/autoshutdown.js index dd66c86..78efe02 100644 --- a/gsm-backend/services/autoshutdown.js +++ b/gsm-backend/services/autoshutdown.js @@ -47,7 +47,7 @@ async function checkServers() { // Get player count // Some servers use RCON, others use log parsing (like Hytale) let playerCount = 0; - if (server.rconPassword || server.type === 'hytale' || server.type === 'terraria') { + if (server.rconPassword || server.type === 'hytale' || server.type === 'terraria' || server.type === 'spaceengineers') { const players = await getPlayers(server); playerCount = players.online || 0; } diff --git a/gsm-backend/services/discordBot.js b/gsm-backend/services/discordBot.js index 1783349..8c249a4 100644 --- a/gsm-backend/services/discordBot.js +++ b/gsm-backend/services/discordBot.js @@ -23,7 +23,8 @@ const serverDisplay = { palworld: { name: 'Palworld', icon: '🦎', color: 0x00D4AA, address: 'palworld.zeasy.dev:8211' }, terraria: { name: 'Terraria', icon: '⚔️', color: 0x05C46B, address: 'terraria.zeasy.dev:7777' }, openttd: { name: 'OpenTTD', icon: '🚂', color: 0x1E90FF, address: 'openttd.zeasy.dev:3979' }, - hytale: { name: 'Hytale', icon: '🏰', color: 0x00BFFF, address: 'hytale.zeasy.dev:5520' } + hytale: { name: 'Hytale', icon: '🏰', color: 0x00BFFF, address: 'hytale.zeasy.dev:5520' }, + spaceengineers: { name: 'Space Engineers', icon: '🚀', color: 0xFFA500, address: 'spaceengineers.zeasy.dev:27016' } }; function loadConfig() { @@ -376,7 +377,7 @@ async function fetchServerStatuses() { let players = { online: 0, max: null }; let playerList = { players: [] }; - if (running && (server.rconPassword || server.type === 'hytale' || server.type === 'terraria')) { + if (running && (server.rconPassword || server.type === 'hytale' || server.type === 'terraria' || server.type === 'spaceengineers')) { try { players = await getPlayers(server); playerList = await getPlayerList(server); diff --git a/gsm-backend/services/rcon.js b/gsm-backend/services/rcon.js index 3de9aec..8995c9e 100644 --- a/gsm-backend/services/rcon.js +++ b/gsm-backend/services/rcon.js @@ -1,5 +1,5 @@ import { Rcon } from 'rcon-client'; -import { getHytalePlayers, getTerrariaPlayers } from './ssh.js'; +import { getHytalePlayers, getTerrariaPlayers, getSpaceEngineersPlayers } from './ssh.js'; const rconConnections = new Map(); const playerCache = new Map(); @@ -118,6 +118,10 @@ export async function getPlayers(server) { // Use log parsing for Terraria (no RCON support) const data = await getTerrariaPlayers(server); result = { online: data.online, max: 8 }; + } else if (server.type === 'spaceengineers') { + // Use docker log parsing for Space Engineers (no RCON support) + const data = await getSpaceEngineersPlayers(server); + result = { online: data.online, max: null }; } playerCache.set(cacheKey, { data: result, time: Date.now() }); @@ -188,6 +192,10 @@ export async function getPlayerList(server) { // Use log parsing for Terraria (no RCON support) const data = await getTerrariaPlayers(server); players = data.players || []; + } else if (server.type === 'spaceengineers') { + // Use docker log parsing for Space Engineers (no RCON support) + const data = await getSpaceEngineersPlayers(server); + players = data.players || []; } const result = { players }; diff --git a/gsm-backend/services/ssh.js b/gsm-backend/services/ssh.js index 239426b..c74c465 100644 --- a/gsm-backend/services/ssh.js +++ b/gsm-backend/services/ssh.js @@ -694,6 +694,42 @@ export async function getTerrariaPlayers(server) { } } +// ============ SPACE ENGINEERS FUNCTIONS ============ + +// Get Space Engineers players by parsing docker logs +export async function getSpaceEngineersPlayers(server) { + try { + const ssh = await getConnection(server.host, server.sshUser); + + // SE logs connect/disconnect lines through the wine-wrapped dedicated server. + // Typical patterns emitted by SpaceEngineersDedicated: + // "User joined" / "User connected" + // "User left" / "User disconnected" + // "Player '' connected" / "Player '' disconnected" + const result = await ssh.execCommand( + `docker logs --tail 2000 ${server.containerName} 2>&1 | ` + + `grep -iE "user .* (joined|connected|left|disconnected)|player '.*' (connected|disconnected)" | tail -200` + ); + + const players = new Map(); + const lines = result.stdout.split('\n').filter(l => l.trim()); + + for (const line of lines) { + let m; + if ((m = line.match(/Player '([^']+)' connected/i))) { players.set(m[1], true); continue; } + if ((m = line.match(/Player '([^']+)' disconnected/i))) { players.delete(m[1]); continue; } + if ((m = line.match(/User\s+(.+?)\s+(?:joined|connected)/i))) { players.set(m[1].trim(), true); continue; } + if ((m = line.match(/User\s+(.+?)\s+(?:left|disconnected)/i))) { players.delete(m[1].trim()); continue; } + } + + const playerList = Array.from(players.keys()); + return { online: playerList.length, players: playerList }; + } catch (err) { + console.error(`[SpaceEngineers] Error getting players:`, err.message); + return { online: 0, players: [] }; + } +} + // ============ HYTALE FUNCTIONS ============ const HYTALE_CONFIG_PATH = "/opt/hytale/Server/config.json"; const HYTALE_LOGS_PATH = "/opt/hytale/Server/logs"; diff --git a/gsm-frontend/src/components/ServerCard.jsx b/gsm-frontend/src/components/ServerCard.jsx index 8502c29..25e8359 100644 --- a/gsm-frontend/src/components/ServerCard.jsx +++ b/gsm-frontend/src/components/ServerCard.jsx @@ -60,6 +60,13 @@ const serverInfo = { links: [ { label: 'Website', url: 'https://hytale.com/' } ] + }, + spaceengineers: { + address: 'spaceengineers.zeasy.dev:27016', + logo: '/spaceengineers.png', + links: [ + { label: 'Steam', url: 'https://store.steampowered.com/app/244850/Space_Engineers/' } + ] } } @@ -73,6 +80,7 @@ const getServerInfo = (serverName) => { if (name.includes('terraria')) return serverInfo.terraria if (name.includes('openttd')) return serverInfo.openttd if (name.includes('hytale')) return serverInfo.hytale + if (name.includes('space engineers') || name.includes('spaceengineers')) return serverInfo.spaceengineers return null } diff --git a/gsm-frontend/src/pages/ServerDetail.jsx b/gsm-frontend/src/pages/ServerDetail.jsx index b950319..7161609 100644 --- a/gsm-frontend/src/pages/ServerDetail.jsx +++ b/gsm-frontend/src/pages/ServerDetail.jsx @@ -22,6 +22,7 @@ const getServerLogo = (serverName) => { if (name.includes("terraria")) return "/terraria.png" if (name.includes("openttd")) return "/openttd.png" if (name.includes("hytale")) return "/hytale.png" + if (name.includes("space engineers") || name.includes("spaceengineers")) return "/spaceengineers.png" return null } export default function ServerDetail() {