diff --git a/CLAUDE.md b/CLAUDE.md index db52ed5..5cdf0f0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ The homelab consists of: - **V Rising Server (192.168.2.52)**: Dedicated server (LXC) - **Palworld Server (192.168.2.53)**: Dedicated server with systemd (LXC) - **Project Zomboid Server (10.0.30.66)**: Dedicated server (external VM) -- **Terraria Server (10.0.30.202)**: Vanilla server mit PM2 (external VM, VPN) +- **Terraria Server (10.0.30.202)**: tModLoader mit Calamity Mod, PM2 (external VM, VPN) - **Hytale Server (10.0.30.204)**: Dedicated server mit tmux (external VM, VPN) ## Key Technical Details @@ -61,11 +61,21 @@ ssh root@192.168.2.30 'curl -X POST http://localhost:3000/api/servers/discord/in **Server ohne RCON (Spielererkennung via Log-Parsing)**: - Hytale: Spieler werden über Server-Logs erkannt (`[World|*] Player joined` / `[PlayerSystems] Removing player`) +- Terraria: Spieler werden über PM2-Logs erkannt (`ist beigetreten` / `hat das Spiel verlassen`) - Bei neuen Servern ohne RCON: `server.type === 'serverid'` zu folgenden Dateien hinzufügen: - `services/autoshutdown.js` (Zeile ~50) - `services/discordBot.js` (fetchServerStatuses, Zeile ~379) - `routes/servers.js` (zwei Stellen, suche nach `rconPassword || server.type`) +## Deployment + +**WICHTIG: NIEMALS per SCP deployen!** Alle Änderungen am GSM-Code müssen: +1. Lokal committed werden +2. Auf GitHub gepusht werden +3. Das CI/CD-System deployt automatisch auf den Server + +Kein manuelles Kopieren von Dateien per SCP, rsync oder ähnlichem! + ## Language Note Documentation is written in German. diff --git a/gsm-backend/routes/servers.js b/gsm-backend/routes/servers.js index 694de90..8d65da3 100644 --- a/gsm-backend/routes/servers.js +++ b/gsm-backend/routes/servers.js @@ -348,8 +348,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') ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, - (server.rconPassword || server.type === 'hytale') ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, + (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: [] }, getProcessUptime(server).catch(() => 0) ]); @@ -586,8 +586,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 ? getPlayers(server).catch(() => ({ online: 0, max: null })) : { online: 0, max: null }, - server.rconPassword ? getPlayerList(server).catch(() => ({ players: [] })) : { players: [] }, + (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: [] }, getProcessUptime(server).catch(() => 0) ]); diff --git a/gsm-backend/server.js b/gsm-backend/server.js index f175d22..4018bd7 100644 --- a/gsm-backend/server.js +++ b/gsm-backend/server.js @@ -9,6 +9,16 @@ import { initDiscordBot } from './services/discordBot.js'; config(); +// Global error handlers to prevent crashes +process.on('uncaughtException', (err) => { + console.error('[FATAL] Uncaught Exception:', err.message); + console.error(err.stack); +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('[ERROR] Unhandled Promise Rejection:', reason); +}); + const app = express(); const PORT = process.env.PORT || 3000; diff --git a/gsm-backend/services/autoshutdown.js b/gsm-backend/services/autoshutdown.js index 5ede08b..dd66c86 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') { + if (server.rconPassword || server.type === 'hytale' || server.type === 'terraria') { const players = await getPlayers(server); playerCount = players.online || 0; } diff --git a/gsm-backend/services/discordBot.js b/gsm-backend/services/discordBot.js index 45d2e66..e32f191 100644 --- a/gsm-backend/services/discordBot.js +++ b/gsm-backend/services/discordBot.js @@ -376,7 +376,7 @@ async function fetchServerStatuses() { let players = { online: 0, max: null }; let playerList = { players: [] }; - if (running && (server.rconPassword || server.type === 'hytale')) { + if (running && (server.rconPassword || server.type === 'hytale' || server.type === 'terraria')) { try { players = await getPlayers(server); playerList = await getPlayerList(server); diff --git a/gsm-backend/services/rcon.js b/gsm-backend/services/rcon.js index ed8aba6..3de9aec 100644 --- a/gsm-backend/services/rcon.js +++ b/gsm-backend/services/rcon.js @@ -1,5 +1,5 @@ import { Rcon } from 'rcon-client'; -import { getHytalePlayers } from './ssh.js'; +import { getHytalePlayers, getTerrariaPlayers } from './ssh.js'; const rconConnections = new Map(); const playerCache = new Map(); @@ -114,6 +114,10 @@ export async function getPlayers(server) { // Use log parsing for Hytale (no RCON support) const data = await getHytalePlayers(server); result = { online: data.online, max: 32 }; + } else if (server.type === 'terraria') { + // Use log parsing for Terraria (no RCON support) + const data = await getTerrariaPlayers(server); + result = { online: data.online, max: 8 }; } playerCache.set(cacheKey, { data: result, time: Date.now() }); @@ -180,6 +184,10 @@ export async function getPlayerList(server) { // Use log parsing for Hytale (no RCON support) const data = await getHytalePlayers(server); players = data.players || []; + } else if (server.type === 'terraria') { + // Use log parsing for Terraria (no RCON support) + const data = await getTerrariaPlayers(server); + players = data.players || []; } const result = { players }; diff --git a/gsm-backend/services/ssh.js b/gsm-backend/services/ssh.js index 734c7fb..3273250 100644 --- a/gsm-backend/services/ssh.js +++ b/gsm-backend/services/ssh.js @@ -580,7 +580,7 @@ export async function writePalworldConfig(server, filename, content) { } // ============ TERRARIA CONFIG ============ -const TERRARIA_CONFIG_PATH = "/home/terraria/serverconfig.txt"; +const TERRARIA_CONFIG_PATH = "/home/terraria/tModLoader/serverconfig.txt"; export async function readTerrariaConfig(server) { const ssh = await getConnection(server.host, server.sshUser); @@ -598,7 +598,7 @@ export async function writeTerrariaConfig(server, content) { // Create backup const backupName = `serverconfig.txt.backup.${Date.now()}`; - await ssh.execCommand(`cp ${TERRARIA_CONFIG_PATH} /home/terraria/${backupName} 2>/dev/null || true`); + await ssh.execCommand(`cp ${TERRARIA_CONFIG_PATH} /home/terraria/tModLoader/${backupName} 2>/dev/null || true`); // Write file using sftp const sftp = await ssh.requestSFTP(); @@ -611,7 +611,7 @@ export async function writeTerrariaConfig(server, content) { }); // Clean up old backups (keep last 5) - await ssh.execCommand(`ls -t /home/terraria/serverconfig.txt.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`); + await ssh.execCommand(`ls -t /home/terraria/tModLoader/serverconfig.txt.backup.* 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true`); return true; } @@ -643,6 +643,57 @@ export async function writeOpenTTDConfig(server, content) { return true; } +// ============ TERRARIA FUNCTIONS ============ +// Get Terraria players by parsing PM2 logs +export async function getTerrariaPlayers(server) { + try { + const ssh = await getConnection(server.host, server.sshUser); + + // Get last 500 lines of PM2 logs + const nvmPrefix = "source ~/.nvm/nvm.sh && "; + const result = await ssh.execCommand(nvmPrefix + `pm2 logs ${server.serviceName} --lines 500 --nostream 2>/dev/null | grep -E "ist beigetreten|hat das Spiel verlassen|has joined|has left" | tail -100`); + + const players = new Map(); // PlayerName -> true + + const lines = result.stdout.split('\n').filter(l => l.trim()); + for (const line of lines) { + // German: "Lokführer ist beigetreten." / "Lokführer hat das Spiel verlassen." + // English: "PlayerName has joined." / "PlayerName has left." + + // Join patterns + const joinMatchDE = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+ist beigetreten\.?$/i); + const joinMatchEN = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+has joined\.?$/i); + + if (joinMatchDE) { + players.set(joinMatchDE[1].trim(), true); + continue; + } + if (joinMatchEN) { + players.set(joinMatchEN[1].trim(), true); + continue; + } + + // Leave patterns + const leaveMatchDE = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+hat das Spiel verlassen\.?$/i); + const leaveMatchEN = line.match(/^\d+\|[^\|]+\s*\|\s*(.+?)\s+has left\.?$/i); + + if (leaveMatchDE) { + players.delete(leaveMatchDE[1].trim()); + continue; + } + if (leaveMatchEN) { + players.delete(leaveMatchEN[1].trim()); + } + } + + const playerList = Array.from(players.keys()); + return { online: playerList.length, players: playerList }; + } catch (err) { + console.error(`[Terraria] 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";