Add Space Engineers server support
All checks were successful
Deploy GSM / deploy (push) Successful in 34s
All checks were successful
Deploy GSM / deploy (push) Successful in 34s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,17 @@
|
|||||||
"workDir": "/opt/openttd",
|
"workDir": "/opt/openttd",
|
||||||
"port": 3979
|
"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",
|
"id": "hytale",
|
||||||
"name": "Hytale",
|
"name": "Hytale",
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ router.get('/', optionalAuth, async (req, res) => {
|
|||||||
getCurrentMetrics(server.id).catch(() => ({
|
getCurrentMetrics(server.id).catch(() => ({
|
||||||
cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0
|
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' || server.type === 'spaceengineers') ? 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') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] },
|
||||||
getProcessUptime(server).catch(() => 0)
|
getProcessUptime(server).catch(() => 0)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -587,8 +587,8 @@ router.get('/:id', authenticateToken, rejectGuest, async (req, res) => {
|
|||||||
getCurrentMetrics(server.id).catch(() => ({
|
getCurrentMetrics(server.id).catch(() => ({
|
||||||
cpu: 0, cpuCores: 1, memory: 0, memoryUsed: 0, memoryTotal: 0, uptime: 0
|
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' || server.type === 'spaceengineers') ? 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') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] },
|
||||||
getProcessUptime(server).catch(() => 0)
|
getProcessUptime(server).catch(() => 0)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async function checkServers() {
|
|||||||
// Get player count
|
// Get player count
|
||||||
// Some servers use RCON, others use log parsing (like Hytale)
|
// Some servers use RCON, others use log parsing (like Hytale)
|
||||||
let playerCount = 0;
|
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);
|
const players = await getPlayers(server);
|
||||||
playerCount = players.online || 0;
|
playerCount = players.online || 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const serverDisplay = {
|
|||||||
palworld: { name: 'Palworld', icon: '🦎', color: 0x00D4AA, address: 'palworld.zeasy.dev:8211' },
|
palworld: { name: 'Palworld', icon: '🦎', color: 0x00D4AA, address: 'palworld.zeasy.dev:8211' },
|
||||||
terraria: { name: 'Terraria', icon: '⚔️', color: 0x05C46B, address: 'terraria.zeasy.dev:7777' },
|
terraria: { name: 'Terraria', icon: '⚔️', color: 0x05C46B, address: 'terraria.zeasy.dev:7777' },
|
||||||
openttd: { name: 'OpenTTD', icon: '🚂', color: 0x1E90FF, address: 'openttd.zeasy.dev:3979' },
|
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() {
|
function loadConfig() {
|
||||||
@@ -376,7 +377,7 @@ async function fetchServerStatuses() {
|
|||||||
let players = { online: 0, max: null };
|
let players = { online: 0, max: null };
|
||||||
let playerList = { players: [] };
|
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 {
|
try {
|
||||||
players = await getPlayers(server);
|
players = await getPlayers(server);
|
||||||
playerList = await getPlayerList(server);
|
playerList = await getPlayerList(server);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Rcon } from 'rcon-client';
|
import { Rcon } from 'rcon-client';
|
||||||
import { getHytalePlayers, getTerrariaPlayers } from './ssh.js';
|
import { getHytalePlayers, getTerrariaPlayers, getSpaceEngineersPlayers } from './ssh.js';
|
||||||
|
|
||||||
const rconConnections = new Map();
|
const rconConnections = new Map();
|
||||||
const playerCache = new Map();
|
const playerCache = new Map();
|
||||||
@@ -118,6 +118,10 @@ export async function getPlayers(server) {
|
|||||||
// Use log parsing for Terraria (no RCON support)
|
// Use log parsing for Terraria (no RCON support)
|
||||||
const data = await getTerrariaPlayers(server);
|
const data = await getTerrariaPlayers(server);
|
||||||
result = { online: data.online, max: 8 };
|
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() });
|
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)
|
// Use log parsing for Terraria (no RCON support)
|
||||||
const data = await getTerrariaPlayers(server);
|
const data = await getTerrariaPlayers(server);
|
||||||
players = data.players || [];
|
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 };
|
const result = { players };
|
||||||
|
|||||||
@@ -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 <name> joined" / "User <name> connected"
|
||||||
|
// "User <name> left" / "User <name> disconnected"
|
||||||
|
// "Player '<name>' connected" / "Player '<name>' 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 ============
|
// ============ HYTALE FUNCTIONS ============
|
||||||
const HYTALE_CONFIG_PATH = "/opt/hytale/Server/config.json";
|
const HYTALE_CONFIG_PATH = "/opt/hytale/Server/config.json";
|
||||||
const HYTALE_LOGS_PATH = "/opt/hytale/Server/logs";
|
const HYTALE_LOGS_PATH = "/opt/hytale/Server/logs";
|
||||||
|
|||||||
@@ -60,6 +60,13 @@ const serverInfo = {
|
|||||||
links: [
|
links: [
|
||||||
{ label: 'Website', url: 'https://hytale.com/' }
|
{ 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('terraria')) return serverInfo.terraria
|
||||||
if (name.includes('openttd')) return serverInfo.openttd
|
if (name.includes('openttd')) return serverInfo.openttd
|
||||||
if (name.includes('hytale')) return serverInfo.hytale
|
if (name.includes('hytale')) return serverInfo.hytale
|
||||||
|
if (name.includes('space engineers') || name.includes('spaceengineers')) return serverInfo.spaceengineers
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ const getServerLogo = (serverName) => {
|
|||||||
if (name.includes("terraria")) return "/terraria.png"
|
if (name.includes("terraria")) return "/terraria.png"
|
||||||
if (name.includes("openttd")) return "/openttd.png"
|
if (name.includes("openttd")) return "/openttd.png"
|
||||||
if (name.includes("hytale")) return "/hytale.png"
|
if (name.includes("hytale")) return "/hytale.png"
|
||||||
|
if (name.includes("space engineers") || name.includes("spaceengineers")) return "/spaceengineers.png"
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
export default function ServerDetail() {
|
export default function ServerDetail() {
|
||||||
|
|||||||
Reference in New Issue
Block a user